Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
327 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/// The Runner window's label: the runner on its own, apart from the
18/// settings, opened from the title bar's chip, the tray or the menu
19/// (launcher/runner.html).
20pub const RUNNER: &str = "runner";
21
22/// The argument the system starts g1t with at sign-in, so it can tell a
23/// start of its own from someone opening it.
24pub const AT_LOGIN: &str = "--at-login";
25
26/// The zoom levels the View menu steps through.
27pub const ZOOMS: &[f64] = &[0.67, 0.75, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0];
28
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30#[serde(default, rename_all = "camelCase")]
31pub struct Settings {
32 /// Opened at sign-in, g1t waits in the tray instead of opening its window.
33 pub start_hidden: bool,
34 /// Closing the window leaves g1t running in the tray, for notifications;
35 /// otherwise closing it quits.
36 pub close_to_tray: bool,
37 /// Notifications from the system, for what the site says needs the person.
38 pub notifications: bool,
39 /// The taskbar icon flashes, or the dock icon bounces, with a notification.
40 pub flash: bool,
41 /// What needs the person, counted on the icon.
42 pub badge: bool,
43 /// The system's title bar, and on Windows and Linux its menu bar, in
44 /// place of g1t's own (frame.rs). Takes effect when g1t restarts.
45 pub system_title_bar: bool,
46 /// The site's zoom, 1 for as it is.
47 pub zoom: f64,
48 /// Updates download by themselves; a person can still check from the menu.
49 /// The runner's own updates follow it too (runner/mod.rs).
50 pub auto_update: bool,
51 /// Runs the workspace's self-hosted jobs on this computer while g1t is
52 /// open (runner/mod.rs). On unless someone turns it off.
53 pub runner: bool,
54 /// How the runner runs: sandboxed unless someone chooses otherwise.
55 pub runner_backend: crate::runner::Backend,
56 /// The workspace it runs for; the one the person is in when it first registers.
57 pub runner_workspace: Option<String>,
58 /// Sandboxed, each kind of job on or off on its own: Linux jobs can
59 /// run while Windows jobs are stopped. All on unless someone stops one.
60 pub runner_capabilities: RunnerCapabilities,
61}
62
63/// Each kind of job the sandbox runs, on or off (`runnerCapabilities` in
64/// desktop.json: `{"linux": true, "windows": false}`).
65#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(default)]
67pub struct RunnerCapabilities {
68 pub linux: bool,
69 pub windows: bool,
70 pub macos: bool,
71}
72
73impl Default for RunnerCapabilities {
74 fn default() -> Self {
75 Self { linux: true, windows: true, macos: true }
76 }
77}
78
79impl RunnerCapabilities {
80 /// Whether a kind of job is on; one this app doesn't know is.
81 pub fn on(&self, id: &str) -> bool {
82 match id {
83 "linux" => self.linux,
84 "windows" => self.windows,
85 "macos" => self.macos,
86 _ => true,
87 }
88 }
89
90 /// Turns a kind of job on or off: false for a kind this app doesn't know.
91 pub fn set(&mut self, id: &str, on: bool) -> bool {
92 let kind = match id {
93 "linux" => &mut self.linux,
94 "windows" => &mut self.windows,
95 "macos" => &mut self.macos,
96 _ => return false,
97 };
98 *kind = on;
99 true
100 }
101}
102
103impl Default for Settings {
104 fn default() -> Self {
105 Self {
106 start_hidden: true,
107 close_to_tray: true,
108 notifications: true,
109 flash: true,
110 badge: true,
111 system_title_bar: false,
112 zoom: 1.0,
113 auto_update: true,
114 runner: true,
115 runner_backend: crate::runner::Backend::Sandboxed,
116 runner_workspace: None,
117 runner_capabilities: RunnerCapabilities::default(),
118 }
119 }
120}
121
122impl Settings {
123 /// A zoom the app offers, nearest to the one asked for.
124 pub fn clamp_zoom(zoom: f64) -> f64 {
125 ZOOMS.iter().copied().min_by(|a, b| (a - zoom).abs().total_cmp(&(b - zoom).abs())).unwrap_or(1.0)
126 }
127}
128
129/// What the settings window shows: the settings, and what the app and the
130/// system know.
131#[derive(Serialize)]
132#[serde(rename_all = "camelCase")]
133pub struct View {
134 #[serde(flatten)]
135 pub settings: Settings,
136 pub open_at_login: bool,
137 pub version: String,
138 pub platform: String,
139 pub server: String,
140 pub update_ready: Option<String>,
141 /// When the app last asked for updates and heard back, in milliseconds since 1970.
142 pub checked_at: Option<u64>,
143 /// The title bar setting differs from the one the windows have: a restart applies it.
144 pub title_bar_changed: bool,
145 /// Whether this window draws g1t's own title bar, which the page then draws (settings.html).
146 pub custom_frame: bool,
147 /// The runner's state, diagnosis and recent log.
148 pub runner_view: crate::runner::View,
149}
150
151pub fn view(app: &AppHandle) -> View {
152 View {
153 settings: config::settings(app),
154 open_at_login: app.autolaunch().is_enabled().unwrap_or(false),
155 version: env!("CARGO_PKG_VERSION").into(),
156 platform: std::env::consts::OS.into(),
157 server: config::server(app).to_string(),
158 update_ready: crate::updates::ready(app),
159 checked_at: crate::updates::checked_at(app),
160 title_bar_changed: crate::frame::started(app) != crate::frame::custom(app),
161 custom_frame: crate::frame::started(app),
162 runner_view: crate::runner::view(app),
163 }
164}
165
166/// Keeps new settings and puts them to work at once.
167pub fn apply(app: &AppHandle, mut settings: Settings) -> Result<(), String> {
168 settings.zoom = Settings::clamp_zoom(settings.zoom);
169 let before = config::settings(app);
170 config::set_settings(app, settings.clone())?;
171 if let Some(site) = crate::frame::site(app).filter(|_| before.zoom != settings.zoom) {
172 let _ = site.set_zoom(settings.zoom);
173 }
174 if before.badge != settings.badge {
175 crate::badge::refresh(app);
176 }
177 let runner = |s: &Settings| (s.runner, s.runner_backend, s.runner_workspace.clone(), s.runner_capabilities.clone());
178 if runner(&before) != runner(&settings) {
179 crate::runner::settings_changed(app);
180 }
181 Ok(())
182}
183
184/// Turns opening at sign-in on or off, with the system.
185pub fn open_at_login(app: &AppHandle, on: bool) -> Result<(), String> {
186 let autolaunch = app.autolaunch();
187 let result = if on { autolaunch.enable() } else { autolaunch.disable() };
188 result.map_err(|error| error.to_string())
189}
190
191/// Steps the zoom in (1), out (-1), or back to as it is (0).
192pub fn zoom(app: &AppHandle, step: i32) {
193 let mut settings = config::settings(app);
194 let at = ZOOMS.iter().position(|zoom| *zoom == Settings::clamp_zoom(settings.zoom)).unwrap_or(4);
195 settings.zoom = match step {
196 0 => 1.0,
197 1 => ZOOMS[(at + 1).min(ZOOMS.len() - 1)],
198 _ => ZOOMS[at.saturating_sub(1)],
199 };
200 let _ = apply(app, settings);
201}
202
203/// Whether this launch is the system's, at sign-in, and g1t should wait in the tray.
204pub fn starts_hidden(app: &AppHandle) -> bool {
205 std::env::args().any(|arg| arg == AT_LOGIN) && config::settings(app).start_hidden
206}
207
208/// What the settings window listens for: check for updates now.
209pub const CHECK_EVENT: &str = "g1t-check-updates";
210/// What it listens for: an update is downloaded, with its version.
211pub const READY_EVENT: &str = "g1t-update-ready";
212
213/// Opens the settings window, or brings it forward.
214pub fn open(app: &AppHandle) {
215 show(app, false);
216}
217
218/// "Check for updates…" in a menu: the settings window opens and checks,
219/// and says what it found, the download included, in its Updates section.
220pub fn open_and_check(app: &AppHandle) {
221 show(app, true);
222}
223
224/// Opens the Runner window, or brings it forward.
225pub fn open_runner(app: &AppHandle) {
226 if !forward(app, RUNNER) {
227 made_aside(app, Window { label: RUNNER, page: "runner.html", title: "g1t runner", size: (720.0, 760.0) });
228 }
229}
230
231/// Brings a window that is open forward: false when it isn't.
232fn forward(app: &AppHandle, label: &str) -> bool {
233 let Some(window) = app.get_webview_window(label) else { return false };
234 let _ = window.unminimize();
235 let _ = window.show();
236 let _ = window.set_focus();
237 true
238}
239
240fn show(app: &AppHandle, check: bool) {
241 if forward(app, SETTINGS) {
242 if check {
243 let _ = app.emit_to(SETTINGS, CHECK_EVENT, ());
244 }
245 return;
246 }
247 // A window still loading has no listener yet: the address asks instead.
248 let page = if check { "settings.html?check" } else { "settings.html" };
249 made_aside(app, Window { label: SETTINGS, page, title: "g1t settings", size: (620.0, 720.0) });
250}
251
252/// A window of the app's own pages.
253struct Window {
254 label: &'static str,
255 page: &'static str,
256 title: &'static str,
257 size: (f64, f64),
258}
259
260/// Makes a window on a thread of its own, whoever asks. The app's windows
261/// open from synchronous commands too (the title bar's buttons, the site's
262/// account menu), which run on the main thread, and on Windows a window
263/// made there deadlocks: it appears, white, and its page never loads. From
264/// another thread, Tauri hands the making to the event loop and waits.
265fn made_aside(app: &AppHandle, window: Window) {
266 let app = app.clone();
267 let spawned = std::thread::Builder::new().name(format!("g1t-{}-window", window.label)).spawn(move || build(&app, &window));
268 if let Err(error) = spawned {
269 eprintln!("g1t: a window could not open: {error}");
270 }
271}
272
273fn build(app: &AppHandle, window: &Window) {
274 let custom = crate::frame::started(app);
275 let builder = WebviewWindowBuilder::new(app, window.label, WebviewUrl::App(window.page.into()))
276 .title(window.title);
277 let built = crate::frame::dialog(builder, custom)
278 .inner_size(window.size.0, window.size.1)
279 .min_inner_size(440.0, 420.0)
280 .maximizable(false)
281 .minimizable(false)
282 .center()
283 // The page is bundled with the app; its links go to the browser (settings.js).
284 .on_navigation(config::is_launcher)
285 .build();
286 match built {
287 Ok(window) => {
288 let _ = window.set_focus();
289 }
290 Err(error) => eprintln!("g1t: the {} window could not open: {error}", window.label),
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::Settings;
297
298 #[test]
299 fn a_missing_setting_is_its_default() {
300 let settings: Settings = serde_json::from_str(r#"{"closeToTray":false}"#).unwrap();
301 assert!(!settings.close_to_tray);
302 assert!(settings.notifications);
303 assert_eq!(settings.zoom, 1.0);
304 // The runner is on, and sandboxed, for someone who never chose.
305 assert!(settings.runner);
306 assert_eq!(settings.runner_backend, crate::runner::Backend::Sandboxed);
307 let chosen: Settings = serde_json::from_str(r#"{"runner":false,"runnerBackend":"host","runnerWorkspace":"acme"}"#).unwrap();
308 assert!(!chosen.runner);
309 assert_eq!(chosen.runner_backend, crate::runner::Backend::Host);
310 assert_eq!(chosen.runner_workspace.as_deref(), Some("acme"));
311 // Every kind of job is on until someone stops one; a kind left out is on.
312 assert!(chosen.runner_capabilities.on("linux") && chosen.runner_capabilities.on("windows"));
313 let one_off: Settings = serde_json::from_str(r#"{"runnerCapabilities":{"windows":false}}"#).unwrap();
314 assert!(one_off.runner_capabilities.on("linux"));
315 assert!(!one_off.runner_capabilities.on("windows"));
316 let mut kinds = one_off.runner_capabilities.clone();
317 assert!(kinds.set("windows", true) && kinds.on("windows"));
318 assert!(!kinds.set("plan9", false));
319 }
320
321 #[test]
322 fn zoom_is_one_the_app_offers() {
323 assert_eq!(Settings::clamp_zoom(1.07), 1.1);
324 assert_eq!(Settings::clamp_zoom(9.0), 2.0);
325 assert_eq!(Settings::clamp_zoom(0.1), 0.67);
326 }
327}