| 1 | //! The app's own settings: how it starts, what closing the window does, |
| 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`), |
| 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. |
| 45 | /// The runner's own updates follow it too (runner/mod.rs). |
| 46 | pub auto_update: bool, |
| 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>, |
| 54 | } |
| 55 | |
| 56 | impl Default for Settings { |
| 57 | fn default() -> Self { |
| 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 | } |
| 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, |
| 99 | /// The runner's state, diagnosis and recent log. |
| 100 | pub runner_view: crate::runner::View, |
| 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), |
| 114 | runner_view: crate::runner::view(app), |
| 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 | } |
| 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 | } |
| 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); |
| 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")); |
| 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 | } |