Skip to content
173 linesCodeBlameRaw
1//! The g1t desktop app: g1t.sh, or a g1t of your own, in a window of its
2//! own. The window loads the live site, so every change to g1t reaches it
3//! the moment it is deployed and nothing here needs a new build for it;
4//! only what is native lives in this program: notifications from the
5//! system, a badge on the icon, a tray, a menu, `g1t://` links, the server
6//! to use, the app's own settings, updates that install themselves, and a
7//! self-hosted runner for the signed-in person (runner/mod.rs)
8//! (docs.g1t.sh/guides/desktop/).
9//!
10//! The site finds the app at `window.g1tDesktop` (bridge.js), the one
11//! contract between the two; what the page asks of it is answered by the
12//! commands in bridge.rs, each of which checks that the page asking is the
13//! configured server's.
14#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
15
16mod badge;
17mod bridge;
18mod config;
19mod devtools;
20mod frame;
21mod links;
22mod menu;
23mod navigation;
24mod runner;
25mod settings;
26mod tray;
27mod updates;
28
29use tauri::webview::{PageLoadEvent, WebviewBuilder};
30use tauri::{RunEvent, WebviewUrl, WindowEvent};
31use tauri_plugin_window_state::{StateFlags, WindowExt};
32
33/// What the window keeps between launches: its size, place, and whether it
34/// was maximized or full screen; not its frame.
35const KEPT: StateFlags = StateFlags::all().difference(StateFlags::DECORATIONS);
36
37/// The one window's label.
38pub const WINDOW: &str = "main";
39
40fn main() {
41 tauri::Builder::default()
42 // A second launch (a g1t:// link, or the icon clicked again) goes to
43 // the app already running: the link reaches links.rs through the
44 // deep-link plugin, and the window comes forward.
45 .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| tray::show(app)))
46 .plugin(tauri_plugin_deep_link::init())
47 .plugin(tauri_plugin_notification::init())
48 .plugin(tauri_plugin_opener::init())
49 .plugin(tauri_plugin_updater::Builder::new().build())
50 // The window's size and place come back in setup, without the plugin
51 // also showing it: at sign-in g1t may start in the tray. Whether it
52 // has the system's frame is the title bar setting's (frame.rs), never
53 // what an earlier run left: kept, it would put the frame back round
54 // g1t's own title bar.
55 .plugin(
56 tauri_plugin_window_state::Builder::new()
57 .with_state_flags(KEPT)
58 .skip_initial_state(WINDOW)
59 .with_denylist(&[settings::SETTINGS])
60 .build(),
61 )
62 // Opening at sign-in, which settings turn on; the argument tells that start from someone's.
63 .plugin(tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec![settings::AT_LOGIN])))
64 .manage(config::State::default())
65 .manage(frame::State::default())
66 .manage(runner::Shared::default())
67 .invoke_handler(tauri::generate_handler![
68 bridge::desktop_info,
69 bridge::notify,
70 bridge::set_badge,
71 bridge::open_url,
72 bridge::reachable,
73 bridge::set_server,
74 bridge::check_for_updates,
75 bridge::open_settings,
76 bridge::get_settings,
77 bridge::save_settings,
78 bridge::set_open_at_login,
79 bridge::change_server,
80 bridge::restart_to_update,
81 bridge::restart,
82 bridge::window_control,
83 bridge::window_menu,
84 bridge::title_bar_page,
85 bridge::title_bar_current,
86 bridge::title_bar_act,
87 bridge::runner_status,
88 bridge::runner_offer,
89 bridge::runner_register,
90 bridge::runner_control,
91 bridge::site_ready,
92 bridge::open_devtools,
93 ])
94 .on_menu_event(|app, event| menu::handle(app, event.id().as_ref()))
95 .setup(|app| {
96 let handle = app.handle().clone();
97 config::note_dev_server(&handle);
98 config::load(&handle);
99 config::allow_site(&handle)?;
100 let hidden = settings::starts_hidden(&handle);
101 let prefs = config::settings(&handle);
102 let custom = frame::started(&handle);
103 // The site's web view: the launcher first, which goes on to the server.
104 let site = WebviewBuilder::new(frame::SITE, WebviewUrl::App("index.html".into()))
105 .initialization_script(bridge::script(custom))
106 // The web inspector, as the Developer tools setting has it now (devtools.rs).
107 .devtools(devtools::started(&handle))
108 .on_navigation({
109 let handle = handle.clone();
110 move |url| navigation::allowed(&handle, url)
111 })
112 .on_page_load({
113 let handle = handle.clone();
114 move |_webview, payload| config::at(&handle, payload.url().clone(), matches!(payload.event(), PageLoadEvent::Started))
115 });
116 let builder = frame::main_window(app, custom)?;
117 // macOS has one menu bar, the app's; elsewhere the menu is the window's own.
118 #[cfg(target_os = "macos")]
119 app.set_menu(menu::build(&handle)?)?;
120 #[cfg(not(target_os = "macos"))]
121 let builder = builder.menu(menu::build(&handle)?);
122 let window = builder.build()?;
123 // Opened at sign-in, g1t waits in the tray; otherwise the window comes up where it was.
124 let _ = window.restore_state(KEPT - StateFlags::VISIBLE);
125 frame::fill(&window, custom, site)?;
126 frame::layout(&window);
127 #[cfg(not(target_os = "macos"))]
128 frame::menu_bar(&window, custom);
129 if !hidden {
130 let _ = window.show();
131 if let Some(site) = frame::site(&handle) {
132 let _ = site.set_focus();
133 }
134 }
135 if let Some(site) = frame::site(&handle).filter(|_| prefs.zoom != 1.0) {
136 let _ = site.set_zoom(prefs.zoom);
137 }
138 // Closing the window keeps g1t running, for notifications, unless
139 // settings say it quits. A new size gives the bar and the site theirs.
140 window.on_window_event({
141 let handle = handle.clone();
142 let window = window.clone();
143 move |event| match event {
144 WindowEvent::CloseRequested { api, .. } => {
145 api.prevent_close();
146 tray::close(&handle);
147 }
148 WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => frame::layout(&window),
149 _ => {}
150 }
151 });
152 tray::install(&handle)?;
153 links::install(&handle);
154 updates::schedule(handle.clone());
155 // The runner starts on a thread of its own, a few seconds after the window.
156 runner::install(&handle);
157 Ok(())
158 })
159 .build(tauri::generate_context!())
160 .expect("g1t could not start")
161 .run(|app, event| match event {
162 // The dock icon, clicked while the window is hidden.
163 #[cfg(target_os = "macos")]
164 RunEvent::Reopen { .. } => tray::show(app),
165 // No window open is not a reason to quit: the tray is still there.
166 RunEvent::ExitRequested { api, code: None, .. } => api.prevent_exit(),
167 // However g1t ends, the runner it started ends with it.
168 RunEvent::Exit => runner::shutdown(app),
169 _ => {
170 let _ = app;
171 }
172 });
173}