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
256 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 app keeps between launches, and what it knows while running.
2//!
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r93//! The server, `https://g1t.sh` unless someone runs a g1t of their own,
4//! and the app's own settings (settings.rs) are kept in `desktop.json` in
5//! the app's config folder (`~/.config/sh.g1t.desktop` on Linux,
6//! `~/Library/Application Support/sh.g1t.desktop` on macOS,
7//! `%APPDATA%\sh.g1t.desktop` on Windows). The server's pages are the only
8//! remote pages allowed to talk to the app (`allow_site`).
preparing for cloud work9
10use std::fs;
11use std::path::PathBuf;
12use std::sync::Mutex;
13
14use serde::{Deserialize, Serialize};
15use tauri::ipc::CapabilityBuilder;
16use tauri::{AppHandle, Manager};
17use url::Url;
18
19use crate::WINDOW;
20
21pub const DEFAULT_SERVER: &str = "https://g1t.sh";
22
23/// What the running app knows: the server, where the window is, and what
24/// the updater has ready.
25#[derive(Default)]
26pub struct State {
27 /// The server, as an origin with a trailing slash: `https://g1t.sh/`.
28 pub server: Mutex<Option<Url>>,
29 /// The page the window last started loading.
30 pub at: Mutex<Option<Url>>,
31 /// Origins whose pages may call the app, each given its capability once.
32 pub allowed: Mutex<Vec<String>>,
33 /// An update downloaded and waiting to be installed, and its version.
34 pub update: Mutex<Option<updates::Ready>>,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r935 /// When the updater last heard back from g1t.sh, in milliseconds since 1970.
36 pub checked_at: Mutex<Option<u64>>,
preparing for cloud work37 /// What the badge shows.
38 pub waiting: Mutex<i64>,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r939 /// The app's own settings, as the settings window last left them.
40 pub settings: Mutex<Settings>,
Merge branch 'main' into rm-agents41 /// The site's page in the window draws the settings and the runner
42 /// menu itself: it said so once it could, and each page that starts
43 /// loading forgets it until it says so again (`site_ready`).
44 pub site_ready: Mutex<bool>,
preparing for cloud work45}
46
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r947use crate::settings::Settings;
preparing for cloud work48use crate::updates;
49
50#[derive(Default, Serialize, Deserialize)]
51struct File {
52 #[serde(default)]
53 server: Option<String>,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r954 #[serde(flatten)]
55 settings: Settings,
preparing for cloud work56}
57
58fn path(app: &AppHandle) -> Option<PathBuf> {
59 app.path().app_config_dir().ok().map(|dir| dir.join("desktop.json"))
60}
61
62/// A server as typed: http or https, with a host; only its origin is kept.
63pub fn parse(text: &str) -> Option<Url> {
64 let text = text.trim();
65 let text = if text.contains("://") { text.to_string() } else { format!("https://{text}") };
66 let mut url = Url::parse(&text).ok()?;
67 if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() {
68 return None;
69 }
70 url.set_path("/");
71 url.set_query(None);
72 url.set_fragment(None);
73 Some(url)
74}
75
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r976/// Reads the saved server, or takes g1t.sh, and the saved settings.
preparing for cloud work77pub fn load(app: &AppHandle) {
78 let file: File = path(app)
79 .and_then(|path| fs::read(path).ok())
80 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
81 .unwrap_or_default();
82 let server = file.server.as_deref().and_then(parse).unwrap_or_else(|| Url::parse(DEFAULT_SERVER).expect("the default server is a URL"));
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r983 let state = app.state::<State>();
84 *state.server.lock().unwrap() = Some(server);
85 *state.settings.lock().unwrap() = file.settings;
86}
87
88/// Writes the server and the settings as they are now.
89fn save(app: &AppHandle) -> Result<(), String> {
90 let state = app.state::<State>();
91 let file = File {
92 server: state.server.lock().unwrap().as_ref().map(Url::to_string),
93 settings: state.settings.lock().unwrap().clone(),
94 };
95 let path = path(app).ok_or("The app has no folder to keep its settings in.")?;
96 if let Some(dir) = path.parent() {
97 fs::create_dir_all(dir).map_err(|error| error.to_string())?;
98 }
99 fs::write(&path, serde_json::to_vec_pretty(&file).map_err(|error| error.to_string())?).map_err(|error| error.to_string())
100}
101
102pub fn settings(app: &AppHandle) -> Settings {
103 app.state::<State>().settings.lock().unwrap().clone()
104}
105
106/// Keeps new settings.
107pub fn set_settings(app: &AppHandle, settings: Settings) -> Result<(), String> {
108 *app.state::<State>().settings.lock().unwrap() = settings;
109 save(app)
preparing for cloud work110}
111
112pub fn server(app: &AppHandle) -> Url {
113 app.state::<State>()
114 .server
115 .lock()
116 .unwrap()
117 .clone()
118 .unwrap_or_else(|| Url::parse(DEFAULT_SERVER).expect("the default server is a URL"))
119}
120
121/// Saves a new server and lets its pages talk to the app.
122pub fn set_server(app: &AppHandle, text: &str) -> Result<Url, String> {
123 let server = parse(text).ok_or("That is not a web address. It looks like https://g1t.example.com.")?;
124 *app.state::<State>().server.lock().unwrap() = Some(server.clone());
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9125 save(app)?;
preparing for cloud work126 allow_site(app).map_err(|error| error.to_string())?;
127 Ok(server)
128}
129
130pub fn origin(url: &Url) -> String {
131 url.origin().ascii_serialization()
132}
133
134/// Whether a page is the server's.
135pub fn is_server(app: &AppHandle, url: &Url) -> bool {
136 matches!(url.scheme(), "http" | "https") && origin(url) == origin(&server(app))
137}
138
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9139/// Where the bundled pages are served under `tauri dev`: with no `devUrl`
140/// in tauri.conf.json, the Tauri CLI serves `launcher/` itself, from
141/// localhost, and the window starts there. Unset in a release build, which
142/// serves them from inside the app.
143static DEV_SERVER: std::sync::OnceLock<Option<Url>> = std::sync::OnceLock::new();
144
145/// Notes the dev server, once, as the app starts.
146pub fn note_dev_server(app: &AppHandle) {
147 let dev = if cfg!(debug_assertions) { app.config().build.dev_url.clone() } else { None };
148 let _ = DEV_SERVER.set(dev);
149}
150
151pub fn dev_server() -> Option<&'static Url> {
152 DEV_SERVER.get().and_then(Option::as_ref)
153}
154
preparing for cloud work155/// Whether a page is the launcher, bundled with the app.
156pub fn is_launcher(url: &Url) -> bool {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9157 url.scheme() == "tauri"
158 || (matches!(url.scheme(), "http" | "https") && url.host_str() == Some("tauri.localhost"))
159 || dev_server().is_some_and(|dev| origin(dev) == origin(url))
preparing for cloud work160}
161
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9162/// What the server's pages may call: the bridge's commands (bridge.rs).
163const SITE: &[&str] = &[
164 "core:event:allow-listen",
165 "core:event:allow-unlisten",
166 "allow-desktop-info",
167 "allow-notify",
168 "allow-set-badge",
169 "allow-open-url",
170 "allow-check-for-updates",
171 "allow-open-settings",
172 "allow-title-bar-page",
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.173 "allow-runner-status",
174 "allow-runner-offer",
175 "allow-runner-register",
176 "allow-runner-control",
Merge branch 'main' into rm-agents177 // The settings dialog and the runner menu, which the site draws over itself.
178 "allow-site-ready",
179 "allow-get-settings",
180 "allow-save-settings",
181 "allow-set-open-at-login",
182 "allow-restart",
183 "allow-restart-to-update",
184 "allow-change-server",
Merge branch 'worktree-agent-a6b141eb3a7e9cf01'185 // The web inspector, which answers only while Developer tools is on.
186 "allow-open-devtools",
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9187];
188
189/// What GitHub's pages may call while someone signs in: telling the title bar their title.
190const FRAME: &[&str] = &["allow-title-bar-page"];
191
preparing for cloud work192/// Lets the server's pages call the app: a capability for its origin,
193/// added once. Pages from anywhere else get no IPC at all, and bridge.rs
194/// checks the caller again on every command.
195pub fn allow_site(app: &AppHandle) -> tauri::Result<()> {
196 let origin = origin(&server(app));
197 let state = app.state::<State>();
198 let mut allowed = state.allowed.lock().unwrap();
A registration token never shows in the process list. The desktop app passed it to g1t-runner sandbox up as --token, which anyone on the computer can read while up runs; it goes in up's environment now, as G1T_RUNNER_TOKEN, which the launcher reads when --token is left out, as register-and-run already did.199 if allowed.contains(&origin) {
preparing for cloud work200 return Ok(());
201 }
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9202 let mut site = CapabilityBuilder::new(format!("site-{}", allowed.len())).window(WINDOW).remote(format!("{origin}/*"));
203 for permission in SITE {
204 site = site.permission(*permission);
205 }
206 app.add_capability(site)?;
207 // Signing in with GitHub: its pages tell the title bar what they are, and nothing else.
208 if allowed.is_empty() {
209 let mut sign_in = CapabilityBuilder::new("sign-in").window(WINDOW).remote("https://github.com/*".to_string());
210 for permission in FRAME {
211 sign_in = sign_in.permission(*permission);
212 }
213 app.add_capability(sign_in)?;
214 }
preparing for cloud work215 allowed.push(origin);
216 Ok(())
217}
218
Merge branch 'main' into rm-agents219/// The window started loading a page (`started`), or finished. A page
220/// that starts loading hasn't said it can draw the settings yet.
221pub fn at(app: &AppHandle, url: Url, started: bool) {
222 let state = app.state::<State>();
223 *state.at.lock().unwrap() = Some(url);
224 if started {
225 *state.site_ready.lock().unwrap() = false;
226 }
227}
228
229/// The site's page says whether it draws the settings and the runner menu.
230pub fn set_site_ready(app: &AppHandle, ready: bool) {
231 *app.state::<State>().site_ready.lock().unwrap() = ready;
232}
233
234pub fn site_ready(app: &AppHandle) -> bool {
235 *app.state::<State>().site_ready.lock().unwrap()
preparing for cloud work236}
237
238/// Whether the window is on one of the server's pages now.
239pub fn on_site(app: &AppHandle) -> bool {
240 app.state::<State>().at.lock().unwrap().as_ref().is_some_and(|url| is_server(app, url))
241}
242
243#[cfg(test)]
244mod tests {
245 use super::parse;
246
247 #[test]
248 fn a_server_is_its_origin() {
249 assert_eq!(parse("https://g1t.sh/acme/-/chat?x#y").unwrap().as_str(), "https://g1t.sh/");
250 assert_eq!(parse(" g1t.example.com ").unwrap().as_str(), "https://g1t.example.com/");
251 assert_eq!(parse("http://localhost:5173/login").unwrap().as_str(), "http://localhost:5173/");
252 assert!(parse("ftp://g1t.sh").is_none());
253 assert!(parse("not a url").is_none());
254 assert!(parse("").is_none());
255 }
256}

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