Skip to content
311 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::{badge, config, frame, runner, updates};
17
18const SCRIPT: &str = include_str!("bridge.js");
19
20/// The bridge, run at the start of every page the site's web view loads:
21/// `custom` is whether g1t's title bar is above it (frame.rs).
22pub fn script(custom: bool) -> String {
23 SCRIPT
24 .replace("__FRAME__", if custom { "custom" } else { "system" })
25 .replace("__VERSION__", env!("CARGO_PKG_VERSION"))
26 .replace("__PLATFORM__", std::env::consts::OS)
27 .replace("\"__OUTSIDE__\"", &serde_json::to_string(crate::navigation::OUTSIDE).expect("a list of paths is JSON"))
28}
29
30/// What the site and the launcher learn about the app.
31#[derive(Serialize)]
32pub struct Info {
33 pub version: String,
34 pub platform: String,
35 pub server: String,
36 /// The version downloaded and waiting for a restart, if one is.
37 pub update_ready: Option<String>,
38}
39
40fn caller(webview: &Webview) -> Result<Url, String> {
41 webview.url().map_err(|error| error.to_string())
42}
43
44/// The page asking is one of the server's.
45fn from_site(app: &AppHandle, webview: &Webview) -> Result<(), String> {
46 let url = caller(webview)?;
47 if config::is_server(app, &url) { Ok(()) } else { Err("only g1t's own pages may ask that".into()) }
48}
49
50/// The page asking is the launcher.
51fn from_launcher(webview: &Webview) -> Result<(), String> {
52 let url = caller(webview)?;
53 if config::is_launcher(&url) { Ok(()) } else { Err("only the launcher may ask that".into()) }
54}
55
56/// The page asking is the settings window's, bundled with the app.
57fn from_settings(webview: &Webview) -> Result<(), String> {
58 from_launcher(webview)?;
59 if webview.label() == settings::SETTINGS { Ok(()) } else { Err("only the settings window may ask that".into()) }
60}
61
62fn from_either(app: &AppHandle, webview: &Webview) -> Result<(), String> {
63 from_site(app, webview).or_else(|_| from_launcher(webview))
64}
65
66/// The page asking is the one under the title bar, whichever it is: the
67/// site's, the launcher, or GitHub's while signing in.
68fn from_page(app: &AppHandle, webview: &Webview) -> Result<(), String> {
69 let url = caller(webview)?;
70 let shown = config::is_server(app, &url) || config::is_launcher(&url) || crate::navigation::signing_in(&url);
71 if webview.label() == frame::SITE && shown { Ok(()) } else { Err("only the page in the window may ask that".into()) }
72}
73
74/// The page asking is the title bar's, bundled with the app.
75fn from_bar(webview: &Webview) -> Result<(), String> {
76 from_launcher(webview)?;
77 if webview.label() == frame::BAR { Ok(()) } else { Err("only the title bar may ask that".into()) }
78}
79
80/// The page asking has window buttons: the title bar, or the settings window's own.
81fn from_frame(webview: &Webview) -> Result<(), String> {
82 from_bar(webview).or_else(|_| from_settings(webview))
83}
84
85#[tauri::command]
86pub fn desktop_info(app: AppHandle, webview: Webview) -> Result<Info, String> {
87 from_either(&app, &webview)?;
88 Ok(Info {
89 version: env!("CARGO_PKG_VERSION").into(),
90 platform: std::env::consts::OS.into(),
91 server: config::server(&app).to_string(),
92 update_ready: updates::ready(&app),
93 })
94}
95
96/// A notification from the system: the site asks for one when a message
97/// is for the person and the window is behind another.
98#[tauri::command]
99pub fn notify(app: AppHandle, webview: Webview, title: String, body: String) -> Result<(), String> {
100 from_site(&app, &webview)?;
101 let settings = config::settings(&app);
102 if let Some(window) = crate::frame::window(&app).filter(|window| settings.flash && !window.is_focused().unwrap_or(false)) {
103 let _ = window.request_user_attention(Some(UserAttentionType::Informational));
104 }
105 if !settings.notifications {
106 return Ok(());
107 }
108 app.notification().builder().title(title).body(body).show().map_err(|error| error.to_string())
109}
110
111/// What needs the person, on the icon and the tray; 0 clears it.
112#[tauri::command]
113pub fn set_badge(app: AppHandle, webview: Webview, count: i64) -> Result<(), String> {
114 from_site(&app, &webview)?;
115 badge::set(&app, count.max(0));
116 Ok(())
117}
118
119/// A link to somewhere else, opened in the browser the system uses.
120#[tauri::command]
121pub fn open_url(app: AppHandle, webview: Webview, url: String) -> Result<(), String> {
122 from_either(&app, &webview)?;
123 let parsed = Url::parse(&url).map_err(|error| error.to_string())?;
124 if !matches!(parsed.scheme(), "http" | "https" | "mailto") {
125 return Err(format!("{} links stay in the app", parsed.scheme()));
126 }
127 app.opener().open_url(parsed.as_str(), None::<&str>).map_err(|error| error.to_string())
128}
129
130/// Whether a server answers at all: the launcher asks before the window
131/// goes there, so a machine with no network sees a page of ours, not the
132/// web view's own error.
133#[tauri::command]
134pub async fn reachable(app: AppHandle, webview: Webview, url: Option<String>) -> Result<bool, String> {
135 from_launcher(&webview)?;
136 let target = match url {
137 Some(text) => config::parse(&text).ok_or("That is not a web address.")?,
138 None => config::server(&app),
139 };
140 let client = reqwest::Client::builder()
141 .timeout(std::time::Duration::from_secs(6))
142 .build()
143 .map_err(|error| error.to_string())?;
144 Ok(client.head(target).send().await.is_ok())
145}
146
147/// The launcher chose a server: it is kept, and the window goes there next.
148#[tauri::command]
149pub fn set_server(app: AppHandle, webview: Webview, url: String) -> Result<String, String> {
150 from_launcher(&webview)?;
151 config::set_server(&app, &url).map(|server| server.to_string())
152}
153
154/// Looks for an update now: `current`, `ready:<version>`, or an error.
155/// The settings window shows the answer itself; anyone else asking is also
156/// told with a notification.
157#[tauri::command]
158pub async fn check_for_updates(app: AppHandle, webview: Webview) -> Result<String, String> {
159 from_either(&app, &webview)?;
160 updates::check(&app, webview.label() != settings::SETTINGS).await
161}
162
163/// A title bar button: `minimize`, `maximize` (which toggles), `close`,
164/// `drag`, or `maximized` to ask. Answers whether the window is maximized.
165#[tauri::command]
166pub fn window_control(app: AppHandle, webview: Webview, action: String) -> Result<bool, String> {
167 from_frame(&webview)?;
168 crate::frame::control(&app, &webview.window(), &action)
169}
170
171/// The title bar's menu button: the app's menu, at that place in the window.
172#[tauri::command]
173pub fn window_menu(webview: Webview, x: f64, y: f64) -> Result<(), String> {
174 from_bar(&webview)?;
175 crate::frame::menu(&webview.window(), x, y)
176}
177
178/// The page under the title bar says what it is: its title, its search,
179/// whether there is a way back, its colours (frame.rs).
180#[tauri::command]
181pub fn title_bar_page(app: AppHandle, webview: Webview, page: frame::Page) -> Result<(), String> {
182 from_page(&app, &webview)?;
183 frame::page(&app, page);
184 Ok(())
185}
186
187/// What the title bar shows now: it asks as it loads.
188#[tauri::command]
189pub fn title_bar_current(app: AppHandle, webview: Webview) -> Result<frame::Page, String> {
190 from_bar(&webview)?;
191 Ok(frame::current(&app))
192}
193
194/// A title bar button that acts on the page: `back`, `forward`, `search`, `focus`.
195#[tauri::command]
196pub fn title_bar_act(app: AppHandle, webview: Webview, action: String) -> Result<(), String> {
197 from_bar(&webview)?;
198 frame::act(&app, &action)
199}
200
201/// Restarts g1t, installing a downloaded update on the way: the settings
202/// window offers it after a change that needs one.
203#[tauri::command]
204pub fn restart(app: AppHandle, webview: Webview) -> Result<(), String> {
205 from_settings(&webview)?;
206 if updates::ready(&app).is_some() {
207 updates::restart(&app);
208 }
209 runner::shutdown(&app);
210 app.restart()
211}
212
213/// The settings window's "Restart now": installs the downloaded update and starts it.
214#[tauri::command]
215pub fn restart_to_update(app: AppHandle, webview: Webview) -> Result<(), String> {
216 from_settings(&webview)?;
217 updates::restart(&app);
218 Ok(())
219}
220
221/// Opens the app's settings window: the site's account menu asks for it.
222#[tauri::command]
223pub fn open_settings(app: AppHandle, webview: Webview) -> Result<(), String> {
224 from_either(&app, &webview)?;
225 settings::open(&app);
226 Ok(())
227}
228
229/// What the settings window shows.
230#[tauri::command]
231pub fn get_settings(app: AppHandle, webview: Webview) -> Result<settings::View, String> {
232 from_settings(&webview)?;
233 Ok(settings::view(&app))
234}
235
236/// The settings window changed something: everything but opening at sign-in.
237#[tauri::command]
238pub fn save_settings(app: AppHandle, webview: Webview, settings: Settings) -> Result<settings::View, String> {
239 from_settings(&webview)?;
240 settings::apply(&app, settings)?;
241 Ok(settings::view(&app))
242}
243
244/// Opening at sign-in, which the system keeps.
245#[tauri::command]
246pub fn set_open_at_login(app: AppHandle, webview: Webview, on: bool) -> Result<settings::View, String> {
247 from_settings(&webview)?;
248 settings::open_at_login(&app, on)?;
249 Ok(settings::view(&app))
250}
251
252/// The settings window's "Change…": the main window goes to the launcher's choice of server.
253#[tauri::command]
254pub fn change_server(app: AppHandle, webview: Webview) -> Result<(), String> {
255 from_settings(&webview)?;
256 if let Some(window) = app.get_webview_window(settings::SETTINGS) {
257 let _ = window.close();
258 }
259 crate::tray::launcher(&app, "change");
260 Ok(())
261}
262
263/// The runner's state, diagnosis and recent log: for the site's card for
264/// this computer and the settings window.
265#[tauri::command]
266pub fn runner_status(app: AppHandle, webview: Webview) -> Result<runner::View, String> {
267 from_site(&app, &webview).or_else(|_| from_settings(&webview))?;
268 Ok(runner::view(&app))
269}
270
271/// The site, on every page: whether someone is signed in, the workspaces
272/// they own (the only ones they can add runners to), and the one they are
273/// in. Answers the workspace a registration token is wanted for, if one is.
274#[tauri::command]
275pub fn runner_offer(app: AppHandle, webview: Webview, signed_in: bool, workspaces: Vec<runner::Offered>, current: Option<String>) -> Result<Option<String>, String> {
276 from_site(&app, &webview)?;
277 runner::offer(&app, signed_in, workspaces, current);
278 Ok(runner::wanted(&app))
279}
280
281/// The site made a registration token for the workspace the app asked
282/// for, or says why not: `allowed` is false when the person may not add
283/// runners there. The token goes to the runner and nowhere else.
284#[tauri::command]
285pub fn runner_register(app: AppHandle, webview: Webview, workspace: String, token: Option<String>, allowed: bool, error: Option<String>) -> Result<(), String> {
286 from_site(&app, &webview)?;
287 runner::register(&app, workspace, token, allowed, error);
288 Ok(())
289}
290
291/// A button for the runner: Start, Stop, or a diagnosis's fix
292/// (runner/diagnosis.rs), from settings or the site's card.
293#[tauri::command]
294pub fn runner_control(app: AppHandle, webview: Webview, action: String) -> Result<(), String> {
295 from_site(&app, &webview).or_else(|_| from_settings(&webview))?;
296 runner::act(&app, &action)
297}
298
299#[cfg(test)]
300mod tests {
301 #[test]
302 fn the_bridge_has_nothing_left_to_fill_in() {
303 let script = super::script(true);
304 for placeholder in ["__VERSION__", "__PLATFORM__", "__OUTSIDE__", "__FRAME__"] {
305 assert!(!script.contains(placeholder), "{placeholder} is left in the bridge");
306 }
307 assert!(script.contains(r#"var FRAME = "custom";"#));
308 assert!(super::script(false).contains(r#"var FRAME = "system";"#));
309 assert!(script.contains(r#"var OUTSIDE = ["/pricing""#));
310 }
311}