Skip to content
274 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};
Merge branch 'main' of github.com:flagon-io/g1t16use crate::{badge, config, frame, updates};
preparing for cloud work17
18const SCRIPT: &str = include_str!("bridge.js");
19
Merge branch 'main' of github.com:flagon-io/g1t20/// 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 {
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("__FRAME__", if custom { "custom" } else { "system" })
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.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"))
preparing for cloud work28}
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
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.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
preparing for cloud work62fn from_either(app: &AppHandle, webview: &Webview) -> Result<(), String> {
63 from_site(app, webview).or_else(|_| from_launcher(webview))
64}
65
Merge branch 'main' of github.com:flagon-io/g1t66/// 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> {
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.69 let url = caller(webview)?;
Merge branch 'main' of github.com:flagon-io/g1t70 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))
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.83}
84
preparing for cloud work85#[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)?;
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.101 let settings = config::settings(&app);
Merge branch 'main' of github.com:flagon-io/g1t102 if let Some(window) = crate::frame::window(&app).filter(|window| settings.flash && !window.is_focused().unwrap_or(false)) {
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.103 let _ = window.request_user_attention(Some(UserAttentionType::Informational));
104 }
105 if !settings.notifications {
106 return Ok(());
107 }
preparing for cloud work108 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.
Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version.155/// The settings window shows the answer itself; anyone else asking is also
156/// told with a notification.
preparing for cloud work157#[tauri::command]
158pub async fn check_for_updates(app: AppHandle, webview: Webview) -> Result<String, String> {
159 from_either(&app, &webview)?;
Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version.160 updates::check(&app, webview.label() != settings::SETTINGS).await
161}
162
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.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> {
Merge branch 'main' of github.com:flagon-io/g1t167 from_frame(&webview)?;
168 crate::frame::control(&app, &webview.window(), &action)
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.169}
170
171/// The title bar's menu button: the app's menu, at that place in the window.
172#[tauri::command]
Merge branch 'main' of github.com:flagon-io/g1t173pub 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(())
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.185}
186
Merge branch 'main' of github.com:flagon-io/g1t187/// 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
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.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 app.restart()
210}
211
Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version.212/// The settings window's "Restart now": installs the downloaded update and starts it.
213#[tauri::command]
214pub fn restart_to_update(app: AppHandle, webview: Webview) -> Result<(), String> {
215 from_settings(&webview)?;
216 updates::restart(&app);
217 Ok(())
preparing for cloud work218}
The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it.219
220/// Opens the app's settings window: the site's account menu asks for it.
221#[tauri::command]
222pub fn open_settings(app: AppHandle, webview: Webview) -> Result<(), String> {
223 from_either(&app, &webview)?;
224 settings::open(&app);
225 Ok(())
226}
227
228/// What the settings window shows.
229#[tauri::command]
230pub fn get_settings(app: AppHandle, webview: Webview) -> Result<settings::View, String> {
231 from_settings(&webview)?;
232 Ok(settings::view(&app))
233}
234
235/// The settings window changed something: everything but opening at sign-in.
236#[tauri::command]
237pub fn save_settings(app: AppHandle, webview: Webview, settings: Settings) -> Result<settings::View, String> {
238 from_settings(&webview)?;
239 settings::apply(&app, settings)?;
240 Ok(settings::view(&app))
241}
242
243/// Opening at sign-in, which the system keeps.
244#[tauri::command]
245pub fn set_open_at_login(app: AppHandle, webview: Webview, on: bool) -> Result<settings::View, String> {
246 from_settings(&webview)?;
247 settings::open_at_login(&app, on)?;
248 Ok(settings::view(&app))
249}
250
251/// The settings window's "Change…": the main window goes to the launcher's choice of server.
252#[tauri::command]
253pub fn change_server(app: AppHandle, webview: Webview) -> Result<(), String> {
254 from_settings(&webview)?;
255 if let Some(window) = app.get_webview_window(settings::SETTINGS) {
256 let _ = window.close();
257 }
258 crate::tray::launcher(&app, "change");
259 Ok(())
260}
261
262#[cfg(test)]
263mod tests {
264 #[test]
265 fn the_bridge_has_nothing_left_to_fill_in() {
Merge branch 'main' of github.com:flagon-io/g1t266 let script = super::script(true);
267 for placeholder in ["__VERSION__", "__PLATFORM__", "__OUTSIDE__", "__FRAME__"] {
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.268 assert!(!script.contains(placeholder), "{placeholder} is left in the bridge");
269 }
Merge branch 'main' of github.com:flagon-io/g1t270 assert!(script.contains(r#"var FRAME = "custom";"#));
271 assert!(super::script(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.272 assert!(script.contains(r#"var OUTSIDE = ["/pricing""#));
273 }
274}

This file's history is long; its oldest lines are credited to the oldest commit read.