Check for updates at every start; show "Update to X" as a title-bar pill (#1538)

Thanks @bherbruck.
This commit is contained in:
bherbruck authored and GitHub committed 2026-10-16 00:07:41 -04:00
1 parent 4d862cd76c
commit 6c1a898e3d
11 files changed
+268 -50

No files matched your search

+1
View File
@@ -1139,6 +1139,7 @@
"update.available": "SolidCraft {version} ist verfügbar",
"update.button": "Update",
"update.button_tip": "SolidCraft {version} ist verfügbar",
"update.button_version": "Auf {version} aktualisieren",
"update.check_automatically": "Automatisch nach Updates suchen",
"update.check_now": "Jetzt suchen",
"update.checking": "Suche läuft…",
+1
View File
@@ -1409,6 +1409,7 @@
"update.available": "SolidCraft {version} is available",
"update.button": "Update",
"update.button_tip": "SolidCraft {version} is available",
"update.button_version": "Update to {version}",
"update.check_automatically": "Check for updates automatically",
"update.check_now": "Check now",
"update.checking": "Checking…",
+1
View File
@@ -1235,6 +1235,7 @@
"update.available": "SolidCraft {version} est disponible",
"update.button": "Mettre à jour",
"update.button_tip": "SolidCraft {version} est disponible",
"update.button_version": "Mettre à jour vers {version}",
"update.check_automatically": "Rechercher automatiquement les mises à jour",
"update.check_now": "Vérifier maintenant",
"update.checking": "Vérification…",
+1
View File
@@ -1409,6 +1409,7 @@
"update.available": "SolidCraft {version} が利用できます",
"update.button": "アップデート",
"update.button_tip": "SolidCraft {version} が利用できます",
"update.button_version": "{version} にアップデート",
"update.check_automatically": "アップデートを自動的に確認",
"update.check_now": "今すぐ確認",
"update.checking": "確認中…",
+1
View File
@@ -1139,6 +1139,7 @@
"update.available": "SolidCraft {version} 사용 가능",
"update.button": "업데이트",
"update.button_tip": "SolidCraft {version} 사용 가능",
"update.button_version": "{version}(으)로 업데이트",
"update.check_automatically": "업데이트 자동 확인",
"update.check_now": "지금 확인",
"update.checking": "확인 중…",
+1
View File
@@ -1066,6 +1066,7 @@
"update.available": "Доступна версія SolidCraft {version}",
"update.button": "Оновити",
"update.button_tip": "Доступна версія SolidCraft {version}",
"update.button_version": "Оновити до {version}",
"update.check_automatically": "Автоматично перевіряти оновлення",
"update.check_now": "Перевірити зараз",
"update.checking": "Перевірка…",
+1
View File
@@ -1409,6 +1409,7 @@
"update.available": "SolidCraft {version} 已发布",
"update.button": "更新",
"update.button_tip": "SolidCraft {version} 已发布",
"update.button_version": "更新到 {version}",
"update.check_automatically": "自动检查更新",
"update.check_now": "立即检查",
"update.checking": "正在检查…",
+43 -18
View File
@@ -166,30 +166,55 @@ fn report_rect(bar: Rect, captions: f32, tabs_start: f32, label_width: f32) -> (
(Rect::from_min_size(pos2(right - width, bar.top() + 4.0), vec2(width, 26.0)), labeled)
}
/// An application-bar button for something on offer (Update, Install) at the right of `span` (the
/// bar's free space): an accent-framed `icon`, labeled with `text` when `labeled` and it fits.
/// None when even the icon alone doesn't fit: it never covers the bar's other buttons. `handle`
/// names it for scenarios.
pub(crate) fn offer_button(ui: &mut egui::Ui, span: Rect, labeled: bool, text: &str, icon: &str, handle: &str) -> Option<(Rect, egui::Response)> {
/// The height of an application-bar pill ([`offer_pill`]), a little under the bar's 26 px buttons.
pub(crate) const PILL_HEIGHT: f32 = 22.0;
/// An application-bar pill for something on offer (Update) at the right of `span` (the bar's free
/// space): accent-filled, with `icon` and the first of `labels` that fits (the icon alone when
/// none does or `labeled` is false). Every label but the last also leaves `keep` points of the
/// span free (for the document tabs). None when even the icon doesn't fit: it never covers the
/// bar's other buttons. `handle` names it for scenarios.
pub(crate) fn offer_pill(
ui: &mut egui::Ui,
span: Rect,
labeled: bool,
labels: &[&str],
keep: f32,
icon: &str,
handle: &str,
) -> Option<(Rect, egui::Response)> {
let t = Tokens::get();
let full = 30.0 + ui.fonts_mut(|f| f.layout_no_wrap(text.to_string(), FontId::proportional(13.0), t.app_bar_text).size().x) + 10.0;
let labeled = labeled && full <= span.width();
let width = if labeled { full } else { 28.0 };
let font = FontId::proportional(12.5);
let (icon_x, text_x, right_pad) = (16.0, 29.0, 11.0);
let last = labels.len().saturating_sub(1);
let label = labels.iter().enumerate().filter(|_| labeled).find_map(|(i, text)| {
let galley = ui.fonts_mut(|f| f.layout_no_wrap(text.to_string(), font.clone(), t.app_bar_text));
let width = text_x + galley.size().x + right_pad;
let room = if i < last { span.width() - keep } else { span.width() };
(width <= room).then_some((*text, width))
});
let width = label.map_or(PILL_HEIGHT + 4.0, |(_, w)| w);
if width > span.width() {
return None;
}
let rect = Rect::from_min_size(pos2(span.right() - width, span.top()), vec2(width, 26.0));
let response = ui.interact(rect, ui.id().with(("sc_offer_button", handle)), Sense::click());
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), text));
let rect = Rect::from_min_size(pos2(span.right() - width, span.center().y - PILL_HEIGHT / 2.0), vec2(width, PILL_HEIGHT));
let response = ui.interact(rect, ui.id().with(("sc_offer_pill", handle)), Sense::click());
let name = label.map(|(text, _)| text).or(labels.first().copied()).unwrap_or_default().to_string();
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &name));
crate::scenario::publish_handle(handle, rect.center());
crate::scenario::publish_rect(handle, rect);
let fill = if response.hovered() || response.has_focus() { t.hover } else { t.app_bar };
ui.painter().rect_filled(rect, 4.0, fill);
ui.painter().rect_stroke(rect, 4.0, Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
let glyph = Rect::from_center_size(pos2(rect.left() + 14.0, rect.center().y), vec2(18.0, 18.0));
icons::paint(ui.painter(), glyph, icon, t.accent, t.accent, t.app_bar);
if labeled {
ui.painter().text(pos2(rect.left() + 29.0, rect.center().y), Align2::LEFT_CENTER, text, FontId::proportional(13.0), t.app_bar_text);
let hot = response.hovered() || response.has_focus();
let fill = if hot { t.accent.lerp_to_gamma(t.app_bar_text, 0.18) } else { t.accent };
let radius = PILL_HEIGHT / 2.0;
ui.painter().rect_filled(rect, radius, fill);
if response.has_focus() {
ui.painter().rect_stroke(rect.expand(2.0), radius + 2.0, Stroke::new(1.0, t.app_bar_text), egui::StrokeKind::Outside);
}
let glyph_x = if label.is_some() { rect.left() + icon_x } else { rect.center().x };
let glyph = Rect::from_center_size(pos2(glyph_x, rect.center().y), vec2(16.0, 16.0));
icons::paint(ui.painter(), glyph, icon, t.app_bar_text, fill, t.app_bar_text);
if let Some((text, _)) = label {
ui.painter().text(pos2(rect.left() + text_x, rect.center().y), Align2::LEFT_CENTER, text, font, t.app_bar_text);
}
Some((rect, response))
}
+34 -15
View File
@@ -1,5 +1,5 @@
//! Update notifications (docs/updates.md). At start, and at most once a day after that, a
//! background thread asks GitHub for the latest stable release (`releases/latest`; nightlies and
//! Update notifications (docs/book/src/guide/updates.md). At every start, and once a day after
//! that while the app stays open, a background thread asks GitHub for the latest stable release (`releases/latest`; nightlies and
//! release candidates are never "latest"). When it is newer than this build, an Update button
//! appears in the application bar beside Report a bug; it opens a small window with the version,
//! the release notes and Download, Skip this version and Remind me later.
@@ -32,17 +32,19 @@ pub const RELEASES_PAGE: &str = "https://github.com/bherbruck/solidcraft/release
/// Release pages and assets must be on GitHub, under this repository.
const RELEASE_PREFIX: &str = "https://github.com/bherbruck/solidcraft/releases/";
/// Automatic checks: at most one a day.
/// While the app stays open, automatic checks run once a day (every start checks too).
pub const CHECK_INTERVAL_S: f64 = 24.0 * 3600.0;
/// Remind me later hides the button for three days.
pub const REMIND_DELAY_S: f64 = 3.0 * 24.0 * 3600.0;
/// Check now can't be pressed more often than this (GitHub allows 60 anonymous requests an hour).
pub const MANUAL_COOLDOWN_S: f64 = 30.0;
/// After a failed automatic check, try again after this long (an hour), not every start.
/// After a failed automatic check, the open app tries again after this long (an hour).
pub const RETRY_AFTER_FAILURE_S: f64 = 3600.0;
/// Caps on what a release response may hold (it comes from the network).
const MAX_NOTES: usize = 64 * 1024;
const MAX_ASSETS: usize = 400;
/// The room the Update pill's long label leaves for the document tabs.
const TAB_ROOM: f32 = 300.0;
/// The Update window's width, and its release notes' wrap width.
const WINDOW_W: f32 = 460.0;
const NOTES_W: f32 = WINDOW_W - 30.0;
@@ -407,7 +409,8 @@ impl Release {
pub struct Memo {
/// When the last check finished (successfully or not).
pub last_check: f64,
/// The next automatic check: a day after a successful one, an hour after a failed one.
/// The next automatic check while the app stays open: a day after a successful one, an hour
/// after a failed one. A new start doesn't wait for it.
pub next_check: f64,
/// GitHub's rate limit was reached: no request at all before this, not even Check now.
pub rate_limited_until: f64,
@@ -418,9 +421,13 @@ pub struct Memo {
}
impl Memo {
/// An automatic check is due (a clock set back by more than a day counts as due).
pub fn due(&self, now: f64) -> bool {
now >= self.rate_limited_until && (now >= self.next_check || self.next_check - now > CHECK_INTERVAL_S)
/// An automatic check is due. `launch`: no automatic check has run since SolidCraft started,
/// so one is due whatever the daily schedule says (someone who started yesterday's version
/// this morning hears about today's release at the next start). GitHub's rate limit always
/// holds. A clock set back by more than a day counts as due.
pub fn due(&self, now: f64, launch: bool) -> bool {
let limited = now < self.rate_limited_until && self.rate_limited_until - now <= CHECK_INTERVAL_S;
!limited && (launch || now >= self.next_check || self.next_check - now > CHECK_INTERVAL_S)
}
/// The Update button shows for `release`.
@@ -484,6 +491,10 @@ pub struct Updater {
/// The check in flight, and whether Check now started it.
rx: Option<(Receiver<Result<Release, FetchError>>, bool)>,
started: bool,
/// An automatic check has started since launch (until then one is due at once).
launch_checked: bool,
/// `SOLIDCRAFT_UPDATE_CHECK=0`: no checks at all.
off: bool,
/// When Check now was last pressed (Unix seconds).
last_manual: f64,
/// A release given by `SOLIDCRAFT_UPDATE_FEED` (a file), for tests and screenshots.
@@ -511,10 +522,11 @@ pub fn offered(app: &SolveApp) -> Option<&Release> {
/// Checks may run here: a release build with the host's HTTP client, or a test feed.
pub fn available(app: &mut SolveApp) -> bool {
app.updater.feed.is_some() || (app.services.http_get.is_some() && app.updater.kind().checks())
!app.updater.off && (app.updater.feed.is_some() || (app.services.http_get.is_some() && app.updater.kind().checks()))
}
/// Per frame: the first frame starts a due automatic check; a finished check is collected.
/// Per frame: the first frame of each launch starts an automatic check, the open app checks
/// again once a day (an hour after a failure), and a finished check is collected.
pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
poll_install(app);
// Quitting to restart or install was cancelled (a "save changes?" answered Cancel).
@@ -527,6 +539,7 @@ pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
{
install::clean_up_after_update(std::env::current_exe().ok().as_deref());
if std::env::var("SOLIDCRAFT_UPDATE_CHECK").is_ok_and(|v| v == "0") {
app.updater.off = true;
return;
}
app.updater.feed = std::env::var_os("SOLIDCRAFT_UPDATE_FEED").map(PathBuf::from);
@@ -534,7 +547,9 @@ pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
}
poll(app);
let now = unix_now();
if app.updater.rx.is_none() && app.preferences.check_updates && app.preferences.update.due(now) && available(app) {
let launch = !app.updater.launch_checked;
if app.updater.rx.is_none() && app.preferences.check_updates && app.preferences.update.due(now, launch) && available(app) {
app.updater.launch_checked = true;
start(app, ctx, false);
}
}
@@ -714,11 +729,15 @@ pub fn take_launch(app: &mut SolveApp) -> Option<(PathBuf, Vec<String>)> {
// ---- user interface ---------------------------------------------------------------------------
/// The application bar's Update button, at the right of `span`; returns its rect (None: not shown).
/// The application bar's Update pill ("Update to 0.14.0"), at the right of `span`: it opens and
/// closes the Update window. Returns its rect (None: not shown).
pub fn button(app: &mut SolveApp, ui: &mut egui::Ui, span: egui::Rect, labeled: bool) -> Option<egui::Rect> {
offered(app)?;
let (rect, response) = crate::toolbar::offer_button(ui, span, labeled, crate::i18n::tr("update.button"), "update", "titlebar:update")?;
let version = app.updater.release.as_ref().map(|r| r.version().to_string()).unwrap_or_default();
let version = offered(app)?.version().to_string();
// "Update to 0.14.0" while the document tabs keep room; a short "Update" when they would be
// squeezed; the icon alone in a narrow bar.
let full = crate::i18n::format("update.button_version", &[("version", &version)]);
let labels = [full.as_str(), crate::i18n::tr("update.button")];
let (rect, response) = crate::toolbar::offer_pill(ui, span, labeled, &labels, TAB_ROOM, "update", "titlebar:update")?;
if response.on_hover_text(crate::i18n::format("update.button_tip", &[("version", &version)])).clicked() {
app.updater.open = !app.updater.open;
}
+172 -10
View File
@@ -217,24 +217,186 @@ fn skip_and_remind_later() {
}
#[test]
fn checks_run_daily_and_back_off() {
fn every_launch_checks_and_the_open_app_checks_daily() {
let mut m = Memo::default();
let now = 1_800_000_000.0;
assert!(m.due(now), "the first start checks");
assert!(m.due(now, true), "the first start checks");
m.checked(now, &Ok(release()));
assert!(!m.due(now + 3600.0));
assert!(m.due(now + CHECK_INTERVAL_S));
assert!(m.due(now - 2.0 * CHECK_INTERVAL_S), "a clock set back doesn't block checks for days");
// Started this morning, checked then: the next start checks again at once.
assert!(m.due(now + 3600.0, true), "every start checks, whatever the daily schedule says");
assert!(m.due(now + 5.0, true));
// While it stays open: once a day.
assert!(!m.due(now + 3600.0, false));
assert!(m.due(now + CHECK_INTERVAL_S, false));
assert!(m.due(now - 2.0 * CHECK_INTERVAL_S, false), "a clock set back doesn't block checks for days");
// A failed check: the open app tries again an hour later; a new start checks anyway.
m.checked(now, &Err(FetchError::Failed("offline".into())));
assert!(!m.due(now + 60.0));
assert!(m.due(now + RETRY_AFTER_FAILURE_S));
assert!(!m.due(now + 60.0, false));
assert!(m.due(now + RETRY_AFTER_FAILURE_S, false));
assert!(m.due(now + 60.0, true));
}
#[test]
fn the_rate_limit_holds_at_launch_too() {
let mut m = Memo::default();
let now = 1_800_000_000.0;
m.checked(now, &Err(FetchError::RateLimited { retry_after_s: 1800 }));
assert!(!m.due(now + 1700.0));
assert!(m.due(now + 1800.0));
assert_eq!(m.rate_limited_until, now + 1800.0);
for launch in [false, true] {
assert!(!m.due(now + 1700.0, launch), "GitHub said to wait (launch: {launch})");
assert!(m.due(now + 1800.0, launch));
}
// Absurd waits are capped at a day.
m.checked(now, &Err(FetchError::RateLimited { retry_after_s: u64::MAX }));
assert!(m.due(now + CHECK_INTERVAL_S));
assert!(!m.due(now + 3600.0, true));
assert!(m.due(now + CHECK_INTERVAL_S, false));
// A clock set back more than a day doesn't keep the limit for days.
assert!(m.due(now - 2.0 * CHECK_INTERVAL_S, true));
}
/// A fresh folder for a test's feed file, removed with it.
struct Scratch(PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn scratch() -> Scratch {
static N: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("solidcraft-update-test-{}-{n}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
Scratch(dir)
}
/// A release one minor version past this build, as the `SOLIDCRAFT_UPDATE_FEED` file gives it.
fn next_release_feed(dir: &Path) -> (PathBuf, String) {
let mut v = parse_version(current_version()).unwrap();
v = semver::Version::new(v.major, v.minor + 1, 0);
let path = dir.join("latest.json");
let body = json!({
"tag_name": format!("v{v}"),
"name": format!("SolidCraft {v}"),
"body": "## What's new\n\n- Faster rebuilds.",
"html_url": format!("https://github.com/bherbruck/solidcraft/releases/tag/v{v}"),
"assets": []
});
std::fs::write(&path, body.to_string()).unwrap();
(path, v.to_string())
}
/// Frames until the check in flight has been collected.
fn wait_for_check(h: &mut crate::scenario::Harness) {
for _ in 0..500 {
h.frame();
if h.app.updater.rx.is_none() && h.app.updater.status != Status::Checking {
return;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
panic!("the update check didn't finish");
}
#[test]
fn a_new_launch_checks_even_when_the_last_check_was_today() {
let dir = scratch();
let (feed, version) = next_release_feed(&dir.0);
let mut h = crate::scenario::Harness::new();
// Yesterday's version was started and checked this morning: no check due until tomorrow.
let now = unix_now();
h.app.preferences.update.last_check = now - 3600.0;
h.app.preferences.update.next_check = now + CHECK_INTERVAL_S - 3600.0;
h.app.updater.feed = Some(feed);
h.frame();
assert_eq!(h.app.updater.status, Status::Checking, "the launch checks at once");
wait_for_check(&mut h);
assert_eq!(h.app.updater.release.as_ref().map(|r| r.version().to_string()), Some(version.clone()));
assert!(h.app.preferences.update.next_check > now + CHECK_INTERVAL_S - 60.0, "the next check is a day later");
// While the app stays open it doesn't ask again before then.
h.frames(5);
assert!(h.app.updater.rx.is_none() && h.app.updater.status == Status::Idle);
}
#[test]
fn turning_off_automatic_checks_stops_the_launch_check() {
let dir = scratch();
let (feed, _) = next_release_feed(&dir.0);
let mut h = crate::scenario::Harness::new();
h.app.preferences.check_updates = false;
h.app.updater.feed = Some(feed);
h.frames(3);
assert_eq!(h.app.updater.status, Status::Idle);
assert!(h.app.updater.release.is_none());
}
#[test]
fn the_title_bar_offers_the_new_version_and_opens_the_update_window() {
let dir = scratch();
let (feed, version) = next_release_feed(&dir.0);
let mut h = crate::scenario::Harness::new();
let report = crate::scenario::rect_of("titlebar:report").unwrap();
assert!(crate::scenario::rect_of("titlebar:update").is_none(), "nothing offered before a check");
h.app.updater.feed = Some(feed);
h.frame();
wait_for_check(&mut h);
h.frames(2);
// An accent pill reading "Update to <version>", just left of Report a bug, on the bar.
let label = format!("Update to {version}");
let at = h.widget(&label).unwrap_or_else(|| panic!("no `{label}` in the title bar"));
let pill = crate::scenario::rect_of("titlebar:update").unwrap();
assert!(pill.contains(egui::pos2(at[0] as f32, at[1] as f32)));
assert!(pill.right() <= report.left() && report.left() - pill.right() < 12.0, "beside Report a bug: {pill:?} {report:?}");
assert!((pill.center().y - report.center().y).abs() < 1.0 && pill.height() <= report.height());
assert_eq!(crate::scenario::rect_of("titlebar:report"), Some(report), "Report a bug stays where it was");
assert!(!h.app.updater.open, "no window pops up by itself");
assert!(crate::scenario::rect_of("update:window").is_none());
// Clicking it opens the Update window (not a modal: the bar stays usable).
h.call("ui.click", json!({"x": pill.center().x, "y": pill.center().y}));
h.frames(2);
assert!(h.app.updater.open);
assert!(h.widget("Skip this version").is_some() && h.widget("Remind me later").is_some());
// Remind me later hides the pill.
let later = h.widget("Remind me later").unwrap();
h.call("ui.click", json!({"x": later[0], "y": later[1]}));
h.frames(2);
assert!(!h.app.updater.open);
assert!(h.widget(&label).is_none(), "Remind me later hides it");
}
#[test]
fn the_pill_shortens_in_a_narrow_title_bar() {
let dir = scratch();
let (feed, version) = next_release_feed(&dir.0);
let mut h = crate::scenario::Harness::new();
h.app.updater.feed = Some(feed);
h.frame();
wait_for_check(&mut h);
let wide = crate::scenario::rect_of("titlebar:update").unwrap();
let label = format!("Update to {version}");
assert!(h.widget(&label).is_some());
// Narrower and narrower: the pill never grows, never reaches Report a bug, and its label gives
// way (to "Update", then the icon alone) before the document tabs run out of room.
let mut last = wide.width();
let mut shortened = false;
for width in [1000.0, 800.0, 700.0, 600.0, 520.0, 440.0] {
h.call("ui.resize", json!({"width": width, "height": 600}));
h.frames(3);
let report = crate::scenario::rect_of("titlebar:report").unwrap();
// Icon alone, it keeps the full text for screen readers.
let pill = crate::scenario::rect_of("titlebar:update").filter(|_| h.widget(&label).is_some() || h.widget("Update").is_some());
if let Some(p) = pill {
assert!(p.width() <= last + 0.5, "at {width}: {p:?} grew from {last}");
assert!(p.right() <= report.left(), "at {width}: {p:?} overlaps Report a bug {report:?}");
last = p.width();
}
shortened |= pill.is_none_or(|p| p.width() < wide.width());
if width == 800.0 {
assert!(h.widget("Update").is_some() && h.widget(&label).is_none(), "the short label keeps room for the tabs");
}
}
assert!(shortened, "the long label gives way in a narrow bar");
}
#[test]
+12 -7
View File
@@ -5,15 +5,20 @@ the check sends nothing about you or your designs.
## What you see
When a newer stable release exists, an **Update** button appears in the title bar next to
**Report a bug**. It opens a small window with the new version, its release notes and three
choices:
When a newer stable release exists, a small accent-coloured pill reading **Update to 0.14.0**
(the new version) appears in the title bar next to **Report a bug**. Nothing pops up by itself:
clicking the pill opens a small window with the new version, its release notes and three
choices. In a narrower window the pill shortens to **Update** so the document tabs keep their
room, then to its icon alone.
- **Download** opens the file that fits your installation (see the table below) in your
browser, or the release page when there isn't one.
- **Skip this version** hides the button until an even newer release comes out.
- **Skip this version** hides the pill until an even newer release comes out.
- **Remind me later** hides it for three days.
SolidCraft checks every time it starts, so a release that came out since you last opened it shows
up straight away, and again once a day while it stays open.
**Preferences ▸ General ▸ Updates** has **Check for updates automatically** (on by default) and
**Check now**, which checks right away and shows the window even for a version you skipped.
@@ -43,7 +48,7 @@ fails with a message, and **Download** is the way to update it.
## What is sent
One HTTPS request, at start and at most once a day while SolidCraft runs:
One HTTPS request each time SolidCraft starts, and once a day while it stays open:
```text
GET https://api.github.com/repos/bherbruck/solidcraft/releases/latest
@@ -60,9 +65,9 @@ sees the request like any other visit to the API (your IP address, the time); se
SolidCraft compares the answer with its own version on your computer.
- Offline, or GitHub doesn't answer within 15 seconds: nothing is shown, and the check is tried
again an hour later.
again an hour later (or at the next start, whichever comes first).
- GitHub's rate limit for anonymous requests (60 an hour per address) is respected: when GitHub
says to wait, SolidCraft waits that long, Check now included.
says to wait, SolidCraft waits that long, Check now and the check at start included.
- Turning off **Check for updates automatically** stops the checks; only **Check now** asks.
- Setting the environment variable `SOLIDCRAFT_UPDATE_CHECK=0` turns all checks off, for
example on managed machines.