Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,57 @@ fn open_url(url: &str) {
let _ = webbrowser::open(url);
}

// ------------------------------------------------------------- update check

/// (major, minor, patch, (pre-release kind, number)); a release is kind 3, above alpha/beta/rc.
type Ver = (u32, u32, u32, (u8, u32));

fn ver(v: &str) -> Option<Ver> {
let (core, pre) = match v.split_once('-') {
Some((c, p)) => (c, Some(p)),
None => (v, None),
};
let mut n = core.split('.');
let mut num = || n.next()?.parse().ok();
let nums = (num()?, num()?, num()?);
if n.next().is_some() {
return None; // 1.2.3.4 isn't a version we ship
}
let (name, n) = match pre {
Some(p) => p.split_once('.').unwrap_or((p, "0")),
None => return Some((nums.0, nums.1, nums.2, (3, 0))),
};
let kind = match name {
"alpha" => 0,
"beta" => 1,
"rc" => 2,
_ => return None,
};
Some((nums.0, nums.1, nums.2, (kind, n.parse().ok()?)))
}

/// True only when `latest` really is ahead: garbage on either side means no update.
fn is_newer(latest: &str, current: &str) -> bool {
match (ver(latest), ver(current)) {
(Some(l), Some(c)) => l > c,
_ => false,
}
}

/// The tag out of curl -I's `location:` header, without the leading "v".
fn latest_tag(headers: &str) -> Option<&str> {
let url = headers
.lines()
.rev()
.find(|l| l.trim_start().get(..9).is_some_and(|p| p.eq_ignore_ascii_case("location:")))?
.split_once(':')?
.1
.trim();
let tag = url.split("/tag/").nth(1)?.split('/').next()?;
let tag = tag.strip_prefix('v').unwrap_or(tag);
ver(tag).is_some().then_some(tag)
}

fn bug_report_url(st: &State) -> String {
fn line<T>(d: &Dev<T>, name: impl Fn(&T) -> String) -> String {
match d {
Expand Down Expand Up @@ -763,6 +814,11 @@ impl App {
if ui.add(egui::Button::new("🐞 Report a bug").min_size(vec2(ui.available_width(), 34.0))).clicked() {
open_url(&bug_report_url(st));
}
if let Some(latest) = &st.update
&& ui.small_button(format!("⬆ Rig Hub {latest} is out")).on_hover_text("Opens the release page").clicked()
{
open_url(&format!("{REPO}/releases/latest"));
}
if let Some(h) = st.headset.ok() {
let text = match h.battery {
audio::Battery::Level(n) => format!("🎧 {n}%"),
Expand Down Expand Up @@ -1413,6 +1469,48 @@ fn main() -> eframe::Result {
}
}

#[cfg(test)]
mod update_tests {
use super::{is_newer, latest_tag};

#[test]
fn newer_same_older() {
assert!(is_newer("0.1.1", "0.1.0"));
assert!(!is_newer("0.1.0", "0.1.0")); // same: no update
assert!(!is_newer("0.1.0", "0.1.1")); // older
assert!(is_newer("0.2.0", "0.1.9"));
assert!(is_newer("1.0.0", "0.9.9"));
}

#[test]
fn pre_releases_compare_numerically_and_below_their_release() {
assert!(is_newer("0.1.0-alpha.10", "0.1.0-alpha.3")); // numeric, not "10" < "3"
assert!(!is_newer("0.1.0-alpha.3", "0.1.0-alpha.10"));
assert!(is_newer("0.1.0-beta.1", "0.1.0-alpha.10"));
assert!(is_newer("0.1.0", "0.1.0-beta.1"));
assert!(is_newer("0.1.0", "0.1.0-alpha.3"));
assert!(is_newer("0.1.1-alpha.1", "0.1.0")); // newer core beats older pre-release
}

#[test]
fn garbage_means_no_update() {
assert!(!is_newer("", "0.1.0"));
assert!(!is_newer("banana", "0.1.0"));
assert!(!is_newer("0.1", "0.1.0"));
assert!(!is_newer("0.1.0-omega.5", "0.1.0-alpha.3")); // unknown pre-release kind
assert!(!is_newer("0.1.0", "banana")); // garbage current version: no update either
}

#[test]
fn parses_tag_from_curl_headers() {
let ok = "HTTP/2 302\r\ncontent-length: 0\r\nlocation: https://github.com/SusieeTheLinuxUser/rig-hub/releases/tag/v0.1.0-alpha.4\r\n\r\n";
assert_eq!(latest_tag(ok), Some("0.1.0-alpha.4"));
assert_eq!(latest_tag(""), None); // empty output (offline, curl missing)
assert_eq!(latest_tag("HTTP/2 404\r\ncontent-length: 0\r\n\r\n"), None);
assert_eq!(latest_tag("location: https://github.com/x/y/releases/tag/banana\r\n"), None);
}
}

#[cfg(all(test, unix))]
mod tests {
#[test]
Expand Down
39 changes: 35 additions & 4 deletions src/workers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ pub struct State {
pub mic: Dev<Mic>,
pub busy: Option<&'static str>,
pub toast: Option<Toast>,
/// newest release tag, when GitHub has one this build doesn't (checked once at startup)
pub update: Option<String>,
}

pub type Shared = Arc<Mutex<State>>;
Expand Down Expand Up @@ -199,12 +201,41 @@ pub fn spawn(st: Shared, ctx: Context) -> Workers {
std::thread::Builder::new().name(name.into()).spawn(move || f(st, ctx, rx)).unwrap();
tx
}
let cooler = go("cooler", &st, &ctx, cooler);
#[cfg(target_os = "linux")]
let mouse = go("mouse", &st, &ctx, mouse);
let headset = go("headset", &st, &ctx, headset);
let mic = go("mic", &st, &ctx, mic);
// one shot at startup; silent on any failure (curl missing, offline, odd output)
{
let (st, ctx) = (st.clone(), ctx.clone());
let _ = std::thread::Builder::new().name("update".into()).spawn(move || check_update(st, ctx));
}
Workers {
cooler: go("cooler", &st, &ctx, cooler),
cooler,
#[cfg(target_os = "linux")]
mouse: go("mouse", &st, &ctx, mouse),
headset: go("headset", &st, &ctx, headset),
mic: go("mic", &st, &ctx, mic),
mouse,
headset,
mic,
}
}

/// Ask GitHub where /releases/latest redirects and compare that tag with this build. No HTTP
/// client here: curl ships with Linux and Windows 10+. Not the API — it allows 60 req/hour.
fn check_update(st: Shared, ctx: Context) {
let mut curl = std::process::Command::new("curl");
curl.args(["-sSI", "--max-time", "10"]).arg(format!("{}/releases/latest", crate::REPO));
// Rig Hub has no console on Windows, so a console child would flash a black window.
#[cfg(windows)]
std::os::windows::process::CommandExt::creation_flags(&mut curl, 0x0800_0000); // CREATE_NO_WINDOW
let Ok(out) = curl.output() else {
return;
};
let headers = String::from_utf8_lossy(&out.stdout);
let Some(tag) = crate::latest_tag(&headers) else { return };
if crate::is_newer(tag, env!("CARGO_PKG_VERSION")) {
let tag = tag.to_string();
update(&st, &ctx, |s| s.update = Some(tag));
}
}

Expand Down
Loading