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