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