Skip to content
232 linesCodeBlameRaw
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
9use serde::{Deserialize, Serialize};
10use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
11use tauri_plugin_autostart::ManagerExt;
12
13use crate::config;
14
15/// The settings window's label.
16pub 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.
20pub const AT_LOGIN: &str = "--at-login";
21
22/// The zoom levels the View menu steps through.
23pub 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")]
27pub 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
56impl 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
74impl 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")]
85pub 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
103pub 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.
119pub 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.
136pub 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).
143pub 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.
155pub 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.
160pub const CHECK_EVENT: &str = "g1t-check-updates";
161/// What it listens for: an update is downloaded, with its version.
162pub const READY_EVENT: &str = "g1t-update-ready";
163
164/// Opens the settings window, or brings it forward.
165pub 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.
171pub fn open_and_check(app: &AppHandle) {
172 show(app, true);
173}
174
175fn 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)]
208mod 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}