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
17 changes: 12 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,15 @@ fn main() {
skill::maybe_auto_update_after_cli_upgrade();
}

// Quiet update-available notice. Skip during `hotdata update` itself so
// we don't talk over the updater's own output.
if !matches!(&cli.command, Some(Commands::Update)) {
update::maybe_print_update_notice();
}
// Kick off the update check in the background so it runs concurrently
// with the command. We join and print after the command finishes so the
// notice always appears at the bottom of the output. Skipped during
// `hotdata update` itself so it doesn't talk over the updater's output.
let update_handle = if !matches!(&cli.command, Some(Commands::Update)) {
update::spawn_update_check()
} else {
None
};

match cli.command {
None => {
Expand Down Expand Up @@ -1008,6 +1012,9 @@ fn main() {
Commands::Update => update::run_update(),
},
}

// Print update notice after command output (joined from background thread).
update::maybe_print_update_notice(update_handle);
}

/// Parse a database target like `airbnb.listings` or `airbnb.public.listings`
Expand Down
52 changes: 33 additions & 19 deletions src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,31 +120,45 @@ fn stderr_is_tty() -> bool {
std::io::stderr().is_terminal()
}

/// Print a one-line notice if a newer release exists. No-op when stderr
/// isn't a TTY, when --no-input is set, or when the cache says we're up
/// to date. Best-effort: network/cache errors are swallowed silently so
/// commands never fail because of the update check.
pub fn maybe_print_update_notice() {
if !stderr_is_tty() {
return;
}
if !util::is_interactive() {
return;
}
if std::env::var_os("HOTDATA_NO_UPDATE_CHECK").is_some() {
return;
fn should_check() -> bool {
stderr_is_tty()
&& util::is_interactive()
&& std::env::var_os("HOTDATA_NO_UPDATE_CHECK").is_none()
}

/// How long `maybe_print_update_notice` will wait for the background thread
/// before giving up. In practice the thread finishes well within this window
/// because `fetch_latest_version` has its own 5-second HTTP timeout and cache
/// hits resolve in microseconds.
const NOTICE_WAIT_MS: u64 = 6_000;

/// Spawn a background thread that checks for a newer release. Returns a
/// channel receiver that `maybe_print_update_notice` can poll after the
/// command runs. No-op (returns None) when stderr isn't a TTY, `--no-input`
/// is set, or `HOTDATA_NO_UPDATE_CHECK` is set.
pub fn spawn_update_check() -> Option<std::sync::mpsc::Receiver<Option<Version>>> {
if !should_check() {
return None;
}
let Some(latest) = cached_latest_if_newer() else {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(cached_latest_if_newer());
});
Some(rx)
}

/// Poll the receiver returned by `spawn_update_check` and print a one-line
/// notice if a newer release was found. Call this *after* the command has
/// produced its own output so the notice appears at the bottom.
pub fn maybe_print_update_notice(rx: Option<std::sync::mpsc::Receiver<Option<Version>>>) {
let Some(rx) = rx else { return };
let Ok(Some(latest)) = rx.recv_timeout(Duration::from_millis(NOTICE_WAIT_MS)) else {
return;
};
let how = match detect_install_method() {
InstallMethod::Homebrew => format!("Run: brew upgrade {HOMEBREW_FORMULA}"),
InstallMethod::Other => "Run: hotdata update".to_string(),
};
eprintln!(
"{}",
format!(
"A new version of hotdata is available (v{CURRENT_VERSION} → v{latest}). {how}"
"\nA new version of hotdata is available (v{CURRENT_VERSION} → v{latest}). Run: hotdata update"
)
.yellow()
);
Expand Down
Loading