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