Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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