Skip to content
247 linesCodeBlameRaw
1//! 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;
10use tauri::{AppHandle, Manager, UserAttentionType, Webview};
11use tauri_plugin_notification::NotificationExt;
12use tauri_plugin_opener::OpenerExt;
13use url::Url;
14
15use crate::settings::{self, Settings};
16use crate::{WINDOW, badge, config, updates};
17
18const SCRIPT: &str = include_str!("bridge.js");
19
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 {
23 SCRIPT
24 .replace("__WINDOW__", window)
25 .replace("__FRAME__", if custom { "custom" } else { "system" })
26 .replace("\"__HEIGHT__\"", &crate::frame::HEIGHT.to_string())
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"))
30}
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
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
64fn from_either(app: &AppHandle, webview: &Webview) -> Result<(), String> {
65 from_site(app, webview).or_else(|_| from_launcher(webview))
66}
67
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
79#[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)?;
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 }
102 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.
149/// The settings window shows the answer itself; anyone else asking is also
150/// told with a notification.
151#[tauri::command]
152pub async fn check_for_updates(app: AppHandle, webview: Webview) -> Result<String, String> {
153 from_either(&app, &webview)?;
154 updates::check(&app, webview.label() != settings::SETTINGS).await
155}
156
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
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(())
191}
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() {
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";"#));
245 assert!(script.contains(r#"var OUTSIDE = ["/pricing""#));
246 }
247}