Skip to content
247 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

preparing for cloud work1//! What the site may ask of the app: the commands behind `window.g1tDesktop`
2//! (bridge.js), and the launcher's own.
3//!
4//! Every command checks who is asking. The site's commands answer only a
5//! page of the configured server; the launcher's only the page bundled
6//! with the app. A page from anywhere else gets nothing, whatever it
7//! loads.
8
9use serde::Serialize;
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.10use tauri::{AppHandle, Manager, UserAttentionType, Webview};
preparing for cloud work11use tauri_plugin_notification::NotificationExt;
12use tauri_plugin_opener::OpenerExt;
13use url::Url;
14
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.15use crate::settings::{self, Settings};
16use crate::{WINDOW, badge, config, updates};
preparing for cloud work17
18const SCRIPT: &str = include_str!("bridge.js");
19
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.20/// The bridge, run at the start of every page a window loads: `window` is
21/// its label, and `custom` whether it draws g1t's title bar (frame.rs).
22pub fn script(window: &str, custom: bool) -> String {
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.23 SCRIPT
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.24 .replace("__WINDOW__", window)
25 .replace("__FRAME__", if custom { "custom" } else { "system" })
26 .replace("\"__HEIGHT__\"", &crate::frame::HEIGHT.to_string())
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.27 .replace("__VERSION__", env!("CARGO_PKG_VERSION"))
28 .replace("__PLATFORM__", std::env::consts::OS)
29 .replace("\"__OUTSIDE__\"", &serde_json::to_string(crate::navigation::OUTSIDE).expect("a list of paths is JSON"))
preparing for cloud work30}
31
32/// What the site and the launcher learn about the app.
33#[derive(Serialize)]
34pub struct Info {
35 pub version: String,
36 pub platform: String,
37 pub server: String,
38 /// The version downloaded and waiting for a restart, if one is.
39 pub update_ready: Option<String>,
40}
41
42fn caller(webview: &Webview) -> Result<Url, String> {
43 webview.url().map_err(|error| error.to_string())
44}
45
46/// The page asking is one of the server's.
47fn from_site(app: &AppHandle, webview: &Webview) -> Result<(), String> {
48 let url = caller(webview)?;
49 if config::is_server(app, &url) { Ok(()) } else { Err("only g1t's own pages may ask that".into()) }
50}
51
52/// The page asking is the launcher.
53fn from_launcher(webview: &Webview) -> Result<(), String> {
54 let url = caller(webview)?;
55 if config::is_launcher(&url) { Ok(()) } else { Err("only the launcher may ask that".into()) }
56}
57
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.58/// The page asking is the settings window's, bundled with the app.
59fn from_settings(webview: &Webview) -> Result<(), String> {
60 from_launcher(webview)?;
61 if webview.label() == settings::SETTINGS { Ok(()) } else { Err("only the settings window may ask that".into()) }
62}
63
preparing for cloud work64fn from_either(app: &AppHandle, webview: &Webview) -> Result<(), String> {
65 from_site(app, webview).or_else(|_| from_launcher(webview))
66}
67
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.68/// The page asking is one the title bar is drawn on: the site's, the
69/// app's own, or GitHub's while signing in.
70fn from_frame(app: &AppHandle, webview: &Webview) -> Result<(), String> {
71 let url = caller(webview)?;
72 if config::is_server(app, &url) || config::is_launcher(&url) || crate::navigation::signing_in(&url) {
73 Ok(())
74 } else {
75 Err("only g1t's own pages may ask that".into())
76 }
77}
78
preparing for cloud work79#[tauri::command]
80pub fn desktop_info(app: AppHandle, webview: Webview) -> Result<Info, String> {
81 from_either(&app, &webview)?;
82 Ok(Info {
83 version: env!("CARGO_PKG_VERSION").into(),
84 platform: std::env::consts::OS.into(),
85 server: config::server(&app).to_string(),
86 update_ready: updates::ready(&app),
87 })
88}
89
90/// A notification from the system: the site asks for one when a message
91/// is for the person and the window is behind another.
92#[tauri::command]
93pub fn notify(app: AppHandle, webview: Webview, title: String, body: String) -> Result<(), String> {
94 from_site(&app, &webview)?;
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.95 let settings = config::settings(&app);
96 if let Some(window) = app.get_webview_window(WINDOW).filter(|window| settings.flash && !window.is_focused().unwrap_or(false)) {
97 let _ = window.request_user_attention(Some(UserAttentionType::Informational));
98 }
99 if !settings.notifications {
100 return Ok(());
101 }
preparing for cloud work102 app.notification().builder().title(title).body(body).show().map_err(|error| error.to_string())
103}
104
105/// What needs the person, on the icon and the tray; 0 clears it.
106#[tauri::command]
107pub fn set_badge(app: AppHandle, webview: Webview, count: i64) -> Result<(), String> {
108 from_site(&app, &webview)?;
109 badge::set(&app, count.max(0));
110 Ok(())
111}
112
113/// A link to somewhere else, opened in the browser the system uses.
114#[tauri::command]
115pub fn open_url(app: AppHandle, webview: Webview, url: String) -> Result<(), String> {
116 from_either(&app, &webview)?;
117 let parsed = Url::parse(&url).map_err(|error| error.to_string())?;
118 if !matches!(parsed.scheme(), "http" | "https" | "mailto") {
119 return Err(format!("{} links stay in the app", parsed.scheme()));
120 }
121 app.opener().open_url(parsed.as_str(), None::<&str>).map_err(|error| error.to_string())
122}
123
124/// Whether a server answers at all: the launcher asks before the window
125/// goes there, so a machine with no network sees a page of ours, not the
126/// web view's own error.
127#[tauri::command]
128pub async fn reachable(app: AppHandle, webview: Webview, url: Option<String>) -> Result<bool, String> {
129 from_launcher(&webview)?;
130 let target = match url {
131 Some(text) => config::parse(&text).ok_or("That is not a web address.")?,
132 None => config::server(&app),
133 };
134 let client = reqwest::Client::builder()
135 .timeout(std::time::Duration::from_secs(6))
136 .build()
137 .map_err(|error| error.to_string())?;
138 Ok(client.head(target).send().await.is_ok())
139}
140
141/// The launcher chose a server: it is kept, and the window goes there next.
142#[tauri::command]
143pub fn set_server(app: AppHandle, webview: Webview, url: String) -> Result<String, String> {
144 from_launcher(&webview)?;
145 config::set_server(&app, &url).map(|server| server.to_string())
146}
147
148/// Looks for an update now: `current`, `ready:<version>`, or an error.
Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version.149/// The settings window shows the answer itself; anyone else asking is also
150/// told with a notification.
preparing for cloud work151#[tauri::command]
152pub async fn check_for_updates(app: AppHandle, webview: Webview) -> Result<String, String> {
153 from_either(&app, &webview)?;
Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version.154 updates::check(&app, webview.label() != settings::SETTINGS).await
155}
156
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.157/// A title bar button: `minimize`, `maximize` (which toggles), `close`,
158/// `drag`, or `maximized` to ask. Answers whether the window is maximized.
159#[tauri::command]
160pub fn window_control(app: AppHandle, webview: Webview, action: String) -> Result<bool, String> {
161 from_frame(&app, &webview)?;
162 let window = app.get_webview_window(webview.label()).ok_or("no such window")?;
163 crate::frame::control(&app, &window, &action)
164}
165
166/// The title bar's menu button: the app's menu, at that place in the window.
167#[tauri::command]
168pub fn window_menu(app: AppHandle, webview: Webview, x: f64, y: f64) -> Result<(), String> {
169 from_frame(&app, &webview)?;
170 let window = app.get_webview_window(WINDOW).filter(|_| webview.label() == WINDOW).ok_or("only the main window has a menu")?;
171 crate::frame::menu(&window, x, y)
172}
173
174/// Restarts g1t, installing a downloaded update on the way: the settings
175/// window offers it after a change that needs one.
176#[tauri::command]
177pub fn restart(app: AppHandle, webview: Webview) -> Result<(), String> {
178 from_settings(&webview)?;
179 if updates::ready(&app).is_some() {
180 updates::restart(&app);
181 }
182 app.restart()
183}
184
Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version.185/// The settings window's "Restart now": installs the downloaded update and starts it.
186#[tauri::command]
187pub fn restart_to_update(app: AppHandle, webview: Webview) -> Result<(), String> {
188 from_settings(&webview)?;
189 updates::restart(&app);
190 Ok(())
preparing for cloud work191}
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.192
193/// Opens the app's settings window: the site's account menu asks for it.
194#[tauri::command]
195pub fn open_settings(app: AppHandle, webview: Webview) -> Result<(), String> {
196 from_either(&app, &webview)?;
197 settings::open(&app);
198 Ok(())
199}
200
201/// What the settings window shows.
202#[tauri::command]
203pub fn get_settings(app: AppHandle, webview: Webview) -> Result<settings::View, String> {
204 from_settings(&webview)?;
205 Ok(settings::view(&app))
206}
207
208/// The settings window changed something: everything but opening at sign-in.
209#[tauri::command]
210pub fn save_settings(app: AppHandle, webview: Webview, settings: Settings) -> Result<settings::View, String> {
211 from_settings(&webview)?;
212 settings::apply(&app, settings)?;
213 Ok(settings::view(&app))
214}
215
216/// Opening at sign-in, which the system keeps.
217#[tauri::command]
218pub fn set_open_at_login(app: AppHandle, webview: Webview, on: bool) -> Result<settings::View, String> {
219 from_settings(&webview)?;
220 settings::open_at_login(&app, on)?;
221 Ok(settings::view(&app))
222}
223
224/// The settings window's "Change…": the main window goes to the launcher's choice of server.
225#[tauri::command]
226pub fn change_server(app: AppHandle, webview: Webview) -> Result<(), String> {
227 from_settings(&webview)?;
228 if let Some(window) = app.get_webview_window(settings::SETTINGS) {
229 let _ = window.close();
230 }
231 crate::tray::launcher(&app, "change");
232 Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237 #[test]
238 fn the_bridge_has_nothing_left_to_fill_in() {
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.239 let script = super::script("main", true);
240 for placeholder in ["__VERSION__", "__PLATFORM__", "__OUTSIDE__", "__WINDOW__", "__FRAME__", "__HEIGHT__"] {
241 assert!(!script.contains(placeholder), "{placeholder} is left in the bridge");
242 }
243 assert!(script.contains(r#"var HEIGHT = 36;"#) && script.contains(r#"var FRAME = "custom";"#));
244 assert!(super::script("settings", false).contains(r#"var FRAME = "system";"#));
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.245 assert!(script.contains(r#"var OUTSIDE = ["/pricing""#));
246 }
247}