Skip to content

Commit

Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version.

Check for updates… in the Help menu or the tray opens the settings window at Updates, which checks and says where this copy stands: up to date and when it last heard back, a version downloaded with Restart now beside it, or that g1t.sh could not be reached. A notification alone was easy to miss, and nothing at all with the system's notifications off. A download the background finishes shows there too, and the Help menu gains Restart to update, as the tray has.

Claude-Session:
https://claude.ai/code/session_01MMRPpook51KPMQQRurb5kC
Claudeagent and Claudeagentcommitted Parent044136aBrowse files
11 files+186−360/11 viewed
+16−0
141141 select:hover {
142142 border-color: #3a3a44;
143143 }
144+.setting > .buttons {
145+ flex-direction: row;
146+ flex: none;
147+ gap: 0.5rem;
148+}
149+button.primary {
150+ background: var(--accent);
151+ border-color: var(--accent);
152+ color: var(--accent-ink);
153+}
154+button.primary:hover {
155+ filter: brightness(1.06);
156+}
157+button[hidden] {
158+ display: none;
159+}
144160 button:disabled {
145161 opacity: 0.6;
146162 }
+6−3
7878 <span><strong>Download updates by themselves</strong><span class="muted">Each one installs when you restart or quit g1t, never in the middle of something.</span></span>
7979 <input type="checkbox" role="switch" id="autoUpdate" />
8080 </label>
81− <div class="setting">
82− <span><strong id="version">g1t</strong><span class="muted" id="update-status">&nbsp;</span></span>
83− <button type="button" id="check">Check for updates</button>
81+ <div class="setting" id="updates-row">
82+ <span><strong id="version">g1t</strong><span class="muted" id="update-status" aria-live="polite">&nbsp;</span></span>
83+ <span class="buttons">
84+ <button type="button" id="restart" class="primary" hidden>Restart now</button>
85+ <button type="button" id="check">Check for updates</button>
86+ </span>
8487 </div>
8588 </section>
8689
+65−7
5353 zoom.value = String(view.zoom);
5454 if (zoom.value === "") zoom.value = "1";
5555 byId("version").textContent = "g1t " + view.version;
56− if (view.updateReady) byId("update-status").textContent = "Version " + view.updateReady + " is downloaded. Restart g1t to use it.";
56+ if (!checking) updateStatus(view);
5757 byId("server").textContent = hostOf(view.server);
5858 }
5959
8484 });
8585 });
8686
87− byId("check").addEventListener("click", function () {
87+ // Updates: where this copy stands, said in the window, so a check from
88+ // the menu or the tray is answered here and cannot be missed.
89+ var checking = false;
90+
91+ function when(ms) {
92+ var date = new Date(ms);
93+ var today = new Date().toDateString() === date.toDateString();
94+ var time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
95+ return today ? "today at " + time : date.toLocaleDateString([], { month: "short", day: "numeric" }) + " at " + time;
96+ }
97+
98+ function ready(version) {
99+ byId("update-status").textContent = "Version " + version + " is downloaded and installs when g1t restarts.";
100+ byId("restart").hidden = false;
101+ }
102+
103+ function updateStatus(view) {
104+ if (view.updateReady) return ready(view.updateReady);
105+ byId("restart").hidden = true;
106+ byId("update-status").textContent = view.checkedAt
107+ ? "Up to date. Last checked " + when(view.checkedAt) + "."
108+ : view.autoUpdate
109+ ? "Not checked yet since g1t started; it checks on its own shortly."
110+ : "Checking on its own is off.";
111+ }
112+
113+ function check() {
114+ if (checking) return;
115+ checking = true;
88116 var button = byId("check");
89117 var status = byId("update-status");
90118 button.disabled = true;
91− status.textContent = "Checking…";
119+ status.textContent = "Checking, and downloading any new version…";
92120 invoke("check_for_updates")
93121 .then(function (answer) {
94− if (answer === "current") status.textContent = "You have the newest version.";
95− else if (answer.indexOf("ready:") === 0) status.textContent = "Version " + answer.slice(6) + " is downloaded. Restart g1t to use it.";
96− else status.textContent = answer;
122+ if (answer.indexOf("ready:") === 0) ready(answer.slice(6));
123+ else status.textContent = "Up to date. Last checked " + when(Date.now()) + ".";
97124 })
98125 .catch(function (error) {
99− status.textContent = "Could not check: " + (typeof error === "string" ? error : "try again later.");
126+ var text = typeof error === "string" ? error : "";
127+ status.textContent = /sending request|connect|dns|timed out/i.test(text) || !text
128+ ? "Could not reach g1t.sh to check. Check your connection and try again."
129+ : "Could not check: " + text;
100130 })
101131 .then(function () {
132+ checking = false;
102133 button.disabled = false;
103134 });
135+ }
136+
137+ byId("check").addEventListener("click", check);
138+ byId("restart").addEventListener("click", function () {
139+ byId("restart").disabled = true;
140+ byId("update-status").textContent = "Installing…";
141+ invoke("restart_to_update").catch(function (error) {
142+ byId("restart").disabled = false;
143+ fail(error);
144+ });
104145 });
105146
147+ // The menu's or tray's "Check for updates…" with this window open, and a
148+ // download finishing in the background (src/settings.rs).
149+ var events = window.__TAURI__ && window.__TAURI__.event;
150+ if (events && typeof events.listen === "function") {
151+ events.listen("g1t-check-updates", check).catch(function () {});
152+ events
153+ .listen("g1t-update-ready", function (event) {
154+ if (!checking && typeof event.payload === "string") ready(event.payload);
155+ })
156+ .catch(function () {});
157+ }
158+
106159 byId("change-server").addEventListener("click", function () {
107160 invoke("change_server").catch(fail);
108161 });
119172 .then(function (view) {
120173 platform(view.platform);
121174 render(view);
175+ // Opened by "Check for updates…" in a menu.
176+ if (new URLSearchParams(location.search).has("check")) {
177+ byId("updates-row").scrollIntoView({ block: "center" });
178+ check();
179+ }
122180 })
123181 .catch(fail);
124182 })();
+11−1
131131 }
132132
133133 /// Looks for an update now: `current`, `ready:<version>`, or an error.
134+/// The settings window shows the answer itself; anyone else asking is also
135+/// told with a notification.
134136 #[tauri::command]
135137 pub async fn check_for_updates(app: AppHandle, webview: Webview) -> Result<String, String> {
136138 from_either(&app, &webview)?;
137− updates::check(&app, true).await
139+ updates::check(&app, webview.label() != settings::SETTINGS).await
140+}
141+
142+/// The settings window's "Restart now": installs the downloaded update and starts it.
143+#[tauri::command]
144+pub fn restart_to_update(app: AppHandle, webview: Webview) -> Result<(), String> {
145+ from_settings(&webview)?;
146+ updates::restart(&app);
147+ Ok(())
138148 }
139149
140150 /// Opens the app's settings window: the site's account menu asks for it.
+2−0
3232 pub allowed: Mutex<Vec<String>>,
3333 /// An update downloaded and waiting to be installed, and its version.
3434 pub update: Mutex<Option<updates::Ready>>,
35+ /// When the updater last heard back from g1t.sh, in milliseconds since 1970.
36+ pub checked_at: Mutex<Option<u64>>,
3537 /// What the badge shows.
3638 pub waiting: Mutex<i64>,
3739 /// The app's own settings, as the settings window last left them.
+1−0
5757 bridge::save_settings,
5858 bridge::set_open_at_login,
5959 bridge::change_server,
60+ bridge::restart_to_update,
6061 ])
6162 .on_menu_event(|app, event| menu::handle(app, event.id().as_ref()))
6263 .setup(|app| {
+8−7
6868 true,
6969 &[&PredefinedMenuItem::minimize(app, None)?, &PredefinedMenuItem::maximize(app, None)?],
7070 )?;
71− let help = Submenu::with_items(app, "Help", true, &[&item("docs", "Desktop app guide", None)?, &item("status", "g1t.sh status", None)?, &separator()?, &check])?;
71+ let help = Submenu::with_items(
72+ app,
73+ "Help",
74+ true,
75+ &[&item("docs", "Desktop app guide", None)?, &item("status", "g1t.sh status", None)?, &separator()?, &check, &tray::restart_item(app)?],
76+ )?;
7277 // macOS keeps About in g1t's own menu; elsewhere it closes Help.
7378 #[cfg(not(target_os = "macos"))]
7479 {
136141 "open" => tray::show(app),
137142 "settings" => settings::open(app),
138143 "server" => tray::launcher(app, "change"),
139− "check" => {
140− let app = app.clone();
141− tauri::async_runtime::spawn(async move {
142− let _ = updates::check(&app, true).await;
143− });
144− }
144+ // The settings window asks, and shows the answer where it cannot be missed.
145+ "check" => settings::open_and_check(app),
145146 "restart" => updates::restart(app),
146147 "quit" => updates::quit(app),
147148 "close" => tray::close(app),
+25−2
77 //! through the autostart plugin, and read back from it.
88
99 use serde::{Deserialize, Serialize};
10−use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
10+use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
1111 use tauri_plugin_autostart::ManagerExt;
1212
1313 use crate::{WINDOW, config};
6969 pub platform: String,
7070 pub server: String,
7171 pub update_ready: Option<String>,
72+ /// When the app last asked for updates and heard back, in milliseconds since 1970.
73+ pub checked_at: Option<u64>,
7274 }
7375
7476 pub fn view(app: &AppHandle) -> View {
7981 platform: std::env::consts::OS.into(),
8082 server: config::server(app).to_string(),
8183 update_ready: crate::updates::ready(app),
84+ checked_at: crate::updates::checked_at(app),
8285 }
8386 }
8487
125128 std::env::args().any(|arg| arg == AT_LOGIN) && config::settings(app).start_hidden
126129 }
127130
131+/// What the settings window listens for: check for updates now.
132+pub const CHECK_EVENT: &str = "g1t-check-updates";
133+/// What it listens for: an update is downloaded, with its version.
134+pub const READY_EVENT: &str = "g1t-update-ready";
135+
128136 /// Opens the settings window, or brings it forward.
129137 pub fn open(app: &AppHandle) {
138+ show(app, false);
139+}
140+
141+/// "Check for updates…" in a menu: the settings window opens and checks,
142+/// and says what it found, the download included, in its Updates section.
143+pub fn open_and_check(app: &AppHandle) {
144+ show(app, true);
145+}
146+
147+fn show(app: &AppHandle, check: bool) {
130148 if let Some(window) = app.get_webview_window(SETTINGS) {
131149 let _ = window.unminimize();
132150 let _ = window.show();
133151 let _ = window.set_focus();
152+ if check {
153+ let _ = app.emit_to(SETTINGS, CHECK_EVENT, ());
154+ }
134155 return;
135156 }
136− let built = WebviewWindowBuilder::new(app, SETTINGS, WebviewUrl::App("settings.html".into()))
157+ // A window still loading has no listener yet: the address asks instead.
158+ let page = if check { "settings.html?check" } else { "settings.html" };
159+ let built = WebviewWindowBuilder::new(app, SETTINGS, WebviewUrl::App(page.into()))
137160 .title("g1t settings")
138161 .inner_size(560.0, 680.0)
139162 .min_inner_size(440.0, 420.0)
+14−7
44 //! back. Quit is here too. The menu's rows are handled with the app
55 //! menu's (menu.rs).
66
7−use std::sync::OnceLock;
7+use std::sync::Mutex;
88
99 use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
1010 use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
1414
1515 pub const TRAY: &str = "g1t";
1616
17−/// The menu row that restarts into a downloaded update: off until one is.
18−static RESTART: OnceLock<MenuItem<Wry>> = OnceLock::new();
17+/// The menu rows that restart into a downloaded update, the tray's and
18+/// the Help menu's: off until one is.
19+static RESTART: Mutex<Vec<MenuItem<Wry>>> = Mutex::new(Vec::new());
1920
21+/// A row that restarts into the update, kept so `update_ready` can turn it on.
22+pub fn restart_item(app: &AppHandle) -> tauri::Result<MenuItem<Wry>> {
23+ let item = MenuItem::with_id(app, "restart", "Restart to finish updating", false, None::<&str>)?;
24+ RESTART.lock().unwrap().push(item.clone());
25+ Ok(item)
26+}
27+
2028 pub fn install(app: &AppHandle) -> tauri::Result<()> {
2129 let open = MenuItem::with_id(app, "open", "Open g1t", true, None::<&str>)?;
2230 let check = MenuItem::with_id(app, "check", "Check for updates…", true, None::<&str>)?;
23− let restart = MenuItem::with_id(app, "restart", "Restart to finish updating", false, None::<&str>)?;
31+ let restart = restart_item(app)?;
2432 let settings = MenuItem::with_id(app, "settings", "Settings…", true, None::<&str>)?;
2533 let server = MenuItem::with_id(app, "server", "Use another g1t…", true, None::<&str>)?;
2634 let quit = MenuItem::with_id(app, "quit", "Quit g1t", true, None::<&str>)?;
3846 &quit,
3947 ],
4048 )?;
41− let _ = RESTART.set(restart);
4249 let mut tray = TrayIconBuilder::with_id(TRAY)
4350 .menu(&menu)
4451 .show_menu_on_left_click(false)
5562 Ok(())
5663 }
5764
58−/// The update is downloaded: the menu says so, and offers the restart.
65+/// The update is downloaded: the menus say so, and offer the restart.
5966 pub fn update_ready(version: &str) {
60− if let Some(item) = RESTART.get() {
67+ for item in RESTART.lock().unwrap().iter() {
6168 let _ = item.set_text(format!("Restart to update to {version}"));
6269 let _ = item.set_enabled(true);
6370 }
+18−4
88
99 use std::time::Duration;
1010
11−use tauri::AppHandle;
12−use tauri::Manager;
11+use tauri::{AppHandle, Emitter, Manager};
1312 use tauri_plugin_notification::NotificationExt;
1413 use tauri_plugin_updater::{Update, UpdaterExt};
1514
16−use crate::{config, tray};
15+use crate::{config, settings, tray};
1716
1817 /// An update downloaded and checked, waiting to be installed.
1918 pub struct Ready {
3938 });
4039 }
4140
41+/// When the updater last heard back, in milliseconds since 1970.
42+pub fn checked_at(app: &AppHandle) -> Option<u64> {
43+ *app.state::<config::State>().checked_at.lock().unwrap()
44+}
45+
46+fn heard_back(app: &AppHandle) {
47+ let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|since| since.as_millis() as u64).ok();
48+ *app.state::<config::State>().checked_at.lock().unwrap() = now;
49+}
50+
4251 /// The version downloaded and waiting, if one is.
4352 pub fn ready(app: &AppHandle) -> Option<String> {
4453 app.state::<config::State>().update.lock().unwrap().as_ref().map(|ready| ready.version.clone())
6069 }
6170 let updater = app.updater().map_err(|error| error.to_string())?;
6271 let update = match updater.check().await {
63− Ok(Some(update)) => update,
72+ Ok(Some(update)) => {
73+ heard_back(app);
74+ update
75+ }
6476 Ok(None) => {
77+ heard_back(app);
6578 if asked {
6679 tell(app, "g1t is up to date", &format!("You have version {}.", env!("CARGO_PKG_VERSION")));
6780 }
7992 Ok(bytes) => {
8093 *app.state::<config::State>().update.lock().unwrap() = Some(Ready { version: version.clone(), update, bytes });
8194 tray::update_ready(&version);
95+ let _ = app.emit_to(settings::SETTINGS, settings::READY_EVENT, version.clone());
8296 tell(app, "g1t is ready to update", &format!("Version {version} is downloaded. Restart g1t from the tray to use it."));
8397 Ok(format!("ready:{version}"))
8498 }
+20−5
8787 | **View** | **Reload** (Ctrl+R), **Actual size** (Ctrl+0), **Zoom in** (Ctrl+=), **Zoom out** (Ctrl+-), full screen |
8888 | **Go** | **Back** (Alt+Left, or ⌘[), **Forward** (Alt+Right, or ⌘]), **Home**, **Notifications** |
8989 | **Window** | Minimize and maximize |
90−| **Help** | **Desktop app guide** and **g1t.sh status**, in your browser; **Check for updates…**; **About g1t** |
90+| **Help** | **Desktop app guide** and **g1t.sh status**, in your browser; **Check for updates…**; **Restart to update**, once one is downloaded; **About g1t** |
9191
9292 On Windows and Linux, **Show the menu bar** in the settings hides it; the
9393 tray's **Settings…** and your account menu still open the settings to bring
113113 | **Zoom** | 100% | How large g1t is drawn, from 67% to 200%. The View menu changes it too. |
114114 | **Download updates by themselves** | On | Off, the app never checks on its own; **Check for updates** still does. |
115115
116−The settings window also shows the app's version, with **Check for
117−updates**, and the server it opens, with **Change…** to [use another
116+The settings window also shows the app's version and whether it is up to
117+date, with **Check for updates** (see [Updates](#updates)), and the server it opens, with **Change…** to [use another
118118 g1t](#your-own-g1t).
119119
120120 ## Links
138138 shortly after it starts and every six hours, and downloads one in the
139139 background. It never installs under you: a notification and the tray's
140140 **Restart to update** say it is ready, and the restart, or quitting,
141−installs it. **Check for updates…** in the tray or the Help menu asks now
142−and tells you the answer. To stop the app checking on its own, turn off
141+installs it.
142+
143+To see where your copy stands, choose **Check for updates…** in the Help
144+menu or the tray, or **Check for updates** in [the settings](#settings).
145+The menu and the tray open the settings window at its Updates section,
146+which checks and says the answer there:
147+
148+- **Up to date. Last checked today at 10:42.** You have the newest version.
149+- **Version 0.2.0 is downloaded and installs when g1t restarts.** A
150+ **Restart now** button beside it installs it and opens the new version,
151+ as **Restart to update** in the Help menu and the tray do.
152+- **Could not reach g1t.sh to check.** Try again once you are online.
153+
154+A check finds and downloads a new version in one go, so the answer can
155+take a moment on a slow connection. The automatic checks and yours are the
156+same check: a version the background found is shown as downloaded when
157+you look. To stop the app checking on its own, turn off
143158 **Download updates by themselves** in [the settings](#settings).
144159
145160 Every release is signed with g1t's key, whose public half is built into