Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 1 | //! The app's own settings: how it starts, what closing the window does, |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 2 | //! how it tells the person something arrived, its title bar, zoom, |
| 3 | //! updates, and the runner. They live in their own window (`launcher/settings.html`), | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 4 | //! opened from the menu, the tray, or the site's account menu, and are |
| 5 | //! kept with the server in `desktop.json` (config.rs). Whether g1t opens | |
| 6 | //! when someone signs in to their computer is kept by the system itself, | |
| 7 | //! through the autostart plugin, and read back from it. | |
| 8 | ||
| 9 | use serde::{Deserialize, Serialize}; | |
| 10 | use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; | |
| 11 | use tauri_plugin_autostart::ManagerExt; | |
| 12 | ||
| 13 | use crate::config; | |
| 14 | ||
| 15 | /// The settings window's label. | |
| 16 | pub const SETTINGS: &str = "settings"; | |
| 17 | ||
| 18 | /// The argument the system starts g1t with at sign-in, so it can tell a | |
| 19 | /// start of its own from someone opening it. | |
| 20 | pub const AT_LOGIN: &str = "--at-login"; | |
| 21 | ||
| 22 | /// The zoom levels the View menu steps through. | |
| 23 | pub const ZOOMS: &[f64] = &[0.67, 0.75, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0]; | |
| 24 | ||
| 25 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 26 | #[serde(default, rename_all = "camelCase")] | |
| 27 | pub struct Settings { | |
| 28 | /// Opened at sign-in, g1t waits in the tray instead of opening its window. | |
| 29 | pub start_hidden: bool, | |
| 30 | /// Closing the window leaves g1t running in the tray, for notifications; | |
| 31 | /// otherwise closing it quits. | |
| 32 | pub close_to_tray: bool, | |
| 33 | /// Notifications from the system, for what the site says needs the person. | |
| 34 | pub notifications: bool, | |
| 35 | /// The taskbar icon flashes, or the dock icon bounces, with a notification. | |
| 36 | pub flash: bool, | |
| 37 | /// What needs the person, counted on the icon. | |
| 38 | pub badge: bool, | |
| 39 | /// The system's title bar, and on Windows and Linux its menu bar, in | |
| 40 | /// place of g1t's own (frame.rs). Takes effect when g1t restarts. | |
| 41 | pub system_title_bar: bool, | |
| 42 | /// The site's zoom, 1 for as it is. | |
| 43 | pub zoom: f64, | |
| 44 | /// Updates download by themselves; a person can still check from the menu. | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 45 | /// The runner's own updates follow it too (runner/mod.rs). |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 46 | pub auto_update: bool, |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 47 | /// Runs the workspace's self-hosted jobs on this computer while g1t is |
| 48 | /// open (runner/mod.rs). On unless someone turns it off. | |
| 49 | pub runner: bool, | |
| 50 | /// How the runner runs: sandboxed unless someone chooses otherwise. | |
| 51 | pub runner_backend: crate::runner::Backend, | |
| 52 | /// The workspace it runs for; the one the person is in when it first registers. | |
| 53 | pub runner_workspace: Option<String>, | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 54 | } |
| 55 | ||
| 56 | impl Default for Settings { | |
| 57 | fn default() -> Self { | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 58 | Self { |
| 59 | start_hidden: true, | |
| 60 | close_to_tray: true, | |
| 61 | notifications: true, | |
| 62 | flash: true, | |
| 63 | badge: true, | |
| 64 | system_title_bar: false, | |
| 65 | zoom: 1.0, | |
| 66 | auto_update: true, | |
| 67 | runner: true, | |
| 68 | runner_backend: crate::runner::Backend::Sandboxed, | |
| 69 | runner_workspace: None, | |
| 70 | } | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 71 | } |
| 72 | } | |
| 73 | ||
| 74 | impl Settings { | |
| 75 | /// A zoom the app offers, nearest to the one asked for. | |
| 76 | pub fn clamp_zoom(zoom: f64) -> f64 { | |
| 77 | ZOOMS.iter().copied().min_by(|a, b| (a - zoom).abs().total_cmp(&(b - zoom).abs())).unwrap_or(1.0) | |
| 78 | } | |
| 79 | } | |
| 80 | ||
| 81 | /// What the settings window shows: the settings, and what the app and the | |
| 82 | /// system know. | |
| 83 | #[derive(Serialize)] | |
| 84 | #[serde(rename_all = "camelCase")] | |
| 85 | pub struct View { | |
| 86 | #[serde(flatten)] | |
| 87 | pub settings: Settings, | |
| 88 | pub open_at_login: bool, | |
| 89 | pub version: String, | |
| 90 | pub platform: String, | |
| 91 | pub server: String, | |
| 92 | pub update_ready: Option<String>, | |
| 93 | /// When the app last asked for updates and heard back, in milliseconds since 1970. | |
| 94 | pub checked_at: Option<u64>, | |
| 95 | /// The title bar setting differs from the one the windows have: a restart applies it. | |
| 96 | pub title_bar_changed: bool, | |
| 97 | /// Whether this window draws g1t's own title bar, which the page then draws (settings.html). | |
| 98 | pub custom_frame: bool, | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 99 | /// The runner's state, diagnosis and recent log. |
| 100 | pub runner_view: crate::runner::View, | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 101 | } |
| 102 | ||
| 103 | pub fn view(app: &AppHandle) -> View { | |
| 104 | View { | |
| 105 | settings: config::settings(app), | |
| 106 | open_at_login: app.autolaunch().is_enabled().unwrap_or(false), | |
| 107 | version: env!("CARGO_PKG_VERSION").into(), | |
| 108 | platform: std::env::consts::OS.into(), | |
| 109 | server: config::server(app).to_string(), | |
| 110 | update_ready: crate::updates::ready(app), | |
| 111 | checked_at: crate::updates::checked_at(app), | |
| 112 | title_bar_changed: crate::frame::started(app) != crate::frame::custom(app), | |
| 113 | custom_frame: crate::frame::started(app), | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 114 | runner_view: crate::runner::view(app), |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 115 | } |
| 116 | } | |
| 117 | ||
| 118 | /// Keeps new settings and puts them to work at once. | |
| 119 | pub fn apply(app: &AppHandle, mut settings: Settings) -> Result<(), String> { | |
| 120 | settings.zoom = Settings::clamp_zoom(settings.zoom); | |
| 121 | let before = config::settings(app); | |
| 122 | config::set_settings(app, settings.clone())?; | |
| 123 | if let Some(site) = crate::frame::site(app).filter(|_| before.zoom != settings.zoom) { | |
| 124 | let _ = site.set_zoom(settings.zoom); | |
| 125 | } | |
| 126 | if before.badge != settings.badge { | |
| 127 | crate::badge::refresh(app); | |
| 128 | } | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 129 | if (before.runner, before.runner_backend, &before.runner_workspace) != (settings.runner, settings.runner_backend, &settings.runner_workspace) { |
| 130 | crate::runner::settings_changed(app); | |
| 131 | } | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 132 | Ok(()) |
| 133 | } | |
| 134 | ||
| 135 | /// Turns opening at sign-in on or off, with the system. | |
| 136 | pub fn open_at_login(app: &AppHandle, on: bool) -> Result<(), String> { | |
| 137 | let autolaunch = app.autolaunch(); | |
| 138 | let result = if on { autolaunch.enable() } else { autolaunch.disable() }; | |
| 139 | result.map_err(|error| error.to_string()) | |
| 140 | } | |
| 141 | ||
| 142 | /// Steps the zoom in (1), out (-1), or back to as it is (0). | |
| 143 | pub fn zoom(app: &AppHandle, step: i32) { | |
| 144 | let mut settings = config::settings(app); | |
| 145 | let at = ZOOMS.iter().position(|zoom| *zoom == Settings::clamp_zoom(settings.zoom)).unwrap_or(4); | |
| 146 | settings.zoom = match step { | |
| 147 | 0 => 1.0, | |
| 148 | 1 => ZOOMS[(at + 1).min(ZOOMS.len() - 1)], | |
| 149 | _ => ZOOMS[at.saturating_sub(1)], | |
| 150 | }; | |
| 151 | let _ = apply(app, settings); | |
| 152 | } | |
| 153 | ||
| 154 | /// Whether this launch is the system's, at sign-in, and g1t should wait in the tray. | |
| 155 | pub fn starts_hidden(app: &AppHandle) -> bool { | |
| 156 | std::env::args().any(|arg| arg == AT_LOGIN) && config::settings(app).start_hidden | |
| 157 | } | |
| 158 | ||
| 159 | /// What the settings window listens for: check for updates now. | |
| 160 | pub const CHECK_EVENT: &str = "g1t-check-updates"; | |
| 161 | /// What it listens for: an update is downloaded, with its version. | |
| 162 | pub const READY_EVENT: &str = "g1t-update-ready"; | |
| 163 | ||
| 164 | /// Opens the settings window, or brings it forward. | |
| 165 | pub fn open(app: &AppHandle) { | |
| 166 | show(app, false); | |
| 167 | } | |
| 168 | ||
| 169 | /// "Check for updates…" in a menu: the settings window opens and checks, | |
| 170 | /// and says what it found, the download included, in its Updates section. | |
| 171 | pub fn open_and_check(app: &AppHandle) { | |
| 172 | show(app, true); | |
| 173 | } | |
| 174 | ||
| 175 | fn show(app: &AppHandle, check: bool) { | |
| 176 | if let Some(window) = app.get_webview_window(SETTINGS) { | |
| 177 | let _ = window.unminimize(); | |
| 178 | let _ = window.show(); | |
| 179 | let _ = window.set_focus(); | |
| 180 | if check { | |
| 181 | let _ = app.emit_to(SETTINGS, CHECK_EVENT, ()); | |
| 182 | } | |
| 183 | return; | |
| 184 | } | |
| 185 | // A window still loading has no listener yet: the address asks instead. | |
| 186 | let page = if check { "settings.html?check" } else { "settings.html" }; | |
| 187 | let custom = crate::frame::started(app); | |
| 188 | let builder = WebviewWindowBuilder::new(app, SETTINGS, WebviewUrl::App(page.into())) | |
| 189 | .title("g1t settings"); | |
| 190 | let built = crate::frame::dialog(builder, custom) | |
| 191 | .inner_size(560.0, 680.0) | |
| 192 | .min_inner_size(440.0, 420.0) | |
| 193 | .maximizable(false) | |
| 194 | .minimizable(false) | |
| 195 | .center() | |
| 196 | // The page is bundled with the app; its links go to the browser (settings.js). | |
| 197 | .on_navigation(config::is_launcher) | |
| 198 | .build(); | |
| 199 | match built { | |
| 200 | Ok(window) => { | |
| 201 | let _ = window.set_focus(); | |
| 202 | } | |
| 203 | Err(error) => eprintln!("g1t: the settings window could not open: {error}"), | |
| 204 | } | |
| 205 | } | |
| 206 | ||
| 207 | #[cfg(test)] | |
| 208 | mod tests { | |
| 209 | use super::Settings; | |
| 210 | ||
| 211 | #[test] | |
| 212 | fn a_missing_setting_is_its_default() { | |
| 213 | let settings: Settings = serde_json::from_str(r#"{"closeToTray":false}"#).unwrap(); | |
| 214 | assert!(!settings.close_to_tray); | |
| 215 | assert!(settings.notifications); | |
| 216 | assert_eq!(settings.zoom, 1.0); | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 217 | // The runner is on, and sandboxed, for someone who never chose. |
| 218 | assert!(settings.runner); | |
| 219 | assert_eq!(settings.runner_backend, crate::runner::Backend::Sandboxed); | |
| 220 | let chosen: Settings = serde_json::from_str(r#"{"runner":false,"runnerBackend":"host","runnerWorkspace":"acme"}"#).unwrap(); | |
| 221 | assert!(!chosen.runner); | |
| 222 | assert_eq!(chosen.runner_backend, crate::runner::Backend::Host); | |
| 223 | assert_eq!(chosen.runner_workspace.as_deref(), Some("acme")); | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 224 | } |
| 225 | ||
| 226 | #[test] | |
| 227 | fn zoom_is_one_the_app_offers() { | |
| 228 | assert_eq!(Settings::clamp_zoom(1.07), 1.1); | |
| 229 | assert_eq!(Settings::clamp_zoom(9.0), 2.0); | |
| 230 | assert_eq!(Settings::clamp_zoom(0.1), 0.67); | |
| 231 | } | |
| 232 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.