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 work | 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 | ||
| 9 | use serde::Serialize; | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 10 | use tauri::{AppHandle, Manager, UserAttentionType, Webview}; |
| preparing for cloud work | 11 | use tauri_plugin_notification::NotificationExt; |
| 12 | use tauri_plugin_opener::OpenerExt; | |
| 13 | use url::Url; | |
| 14 | ||
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 15 | use crate::settings::{self, Settings}; |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 16 | use crate::{badge, config, frame, runner, updates}; |
| preparing for cloud work | 17 | |
| 18 | const SCRIPT: &str = include_str!("bridge.js"); | |
| 19 | ||
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 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). | |
| 22 | pub 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")) | |
| preparing for cloud work | 28 | } |
| 29 | ||
| 30 | /// What the site and the launcher learn about the app. | |
| 31 | #[derive(Serialize)] | |
| 32 | pub 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 | ||
| 40 | fn 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. | |
| 45 | fn 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. | |
| 51 | fn 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 | ||
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 56 | /// The page asking is the settings window's, bundled with the app. |
| 57 | fn 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 work | 62 | fn from_either(app: &AppHandle, webview: &Webview) -> Result<(), String> { |
| 63 | from_site(app, webview).or_else(|_| from_launcher(webview)) | |
| 64 | } | |
| 65 | ||
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 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. | |
| 68 | fn 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. | |
| 75 | fn 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. | |
| 81 | fn from_frame(webview: &Webview) -> Result<(), String> { | |
| 82 | from_bar(webview).or_else(|_| from_settings(webview)) | |
| 83 | } | |
| 84 | ||
| preparing for cloud work | 85 | #[tauri::command] |
| 86 | pub 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] | |
| 99 | pub fn notify(app: AppHandle, webview: Webview, title: String, body: String) -> Result<(), String> { | |
| 100 | from_site(&app, &webview)?; | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 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 | } | |
| preparing for cloud work | 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] | |
| 113 | pub 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] | |
| 121 | pub 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] | |
| 134 | pub 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] | |
| 149 | pub 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. | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 155 | /// The settings window shows the answer itself; anyone else asking is also |
| 156 | /// told with a notification. | |
| preparing for cloud work | 157 | #[tauri::command] |
| 158 | pub async fn check_for_updates(app: AppHandle, webview: Webview) -> Result<String, String> { | |
| 159 | from_either(&app, &webview)?; | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 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] | |
| 166 | pub 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] | |
| 173 | pub 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] | |
| 181 | pub 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] | |
| 189 | pub 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] | |
| 196 | pub 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] | |
| 204 | pub fn restart(app: AppHandle, webview: Webview) -> Result<(), String> { | |
| 205 | from_settings(&webview)?; | |
| 206 | if updates::ready(&app).is_some() { | |
| 207 | updates::restart(&app); | |
| 208 | } | |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 209 | runner::shutdown(&app); |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 210 | app.restart() |
| 211 | } | |
| 212 | ||
| 213 | /// The settings window's "Restart now": installs the downloaded update and starts it. | |
| 214 | #[tauri::command] | |
| 215 | pub 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] | |
| 223 | pub 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] | |
| 231 | pub 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] | |
| 238 | pub 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] | |
| 246 | pub 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] | |
| 254 | pub 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 | ||
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 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] | |
| 266 | pub 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] | |
| 275 | pub 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] | |
| 285 | pub 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] | |
| 294 | pub 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 | ||
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 299 | #[cfg(test)] |
| 300 | mod 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 | } | |
| preparing for cloud work | 311 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.