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
288 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.

Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91//! The window's frame. g1t draws its own title bar, as Slack and Discord
2//! do, so the window is all g1t and looks the same on every system: on
3//! Windows and Linux the system's frame goes and the bar has the menu
4//! button, back and forward, search, and minimize, maximize and close; on
5//! macOS the bar sits under the window's own traffic lights.
6//!
7//! The main window holds two web views, one above the other: the bar, a
8//! page bundled with the app (`launcher/titlebar.html`), across the top,
9//! and the site under it. The site is never drawn under the bar, so no page
10//! of it, or of GitHub's while signing in, needs to know the bar is there.
11//! What the bar shows of the page (its title, its search, whether there is
12//! a way back, its colours) the page's bridge reports (`page`), and what
13//! the bar is asked to do comes back to the page through the app (`act`).
14//!
15//! Settings can give the window back its system frame, and the menu bar
16//! with it, from the next start; the site then has the window to itself.
17
18use std::sync::Mutex;
19
20use serde::{Deserialize, Serialize};
21use tauri::webview::WebviewBuilder;
22use tauri::window::{Color, WindowBuilder};
23use tauri::{AppHandle, Emitter, EventTarget, LogicalPosition, LogicalSize, Manager, Runtime, Webview, WebviewUrl, WebviewWindowBuilder, Window};
24
25use crate::{WINDOW, config, tray};
26
27/// The bar's height, in CSS pixels.
28pub const HEIGHT: f64 = 36.0;
29
30/// The site's web view, in the main window.
31pub const SITE: &str = "site";
32/// The bar's web view, across the top of the main window.
33pub const BAR: &str = "titlebar";
34
35/// What the bar listens for: what to show (`Page`).
36pub const PAGE_EVENT: &str = "g1t-title-bar";
37/// What the site listens for: the bar's search was clicked.
38pub const SEARCH_EVENT: &str = "g1t-search";
39
40/// g1t's rail, dark, which the window is painted while a page loads.
41const RAIL: Color = Color(5, 5, 7, 255);
42
43/// Whether g1t draws its own title bar, as the settings say now.
44pub fn custom(app: &AppHandle) -> bool {
45 !config::settings(app).system_title_bar
46}
47
48/// The title bar g1t started with, which its windows keep until it restarts.
49static STARTED_CUSTOM: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
50
51/// Whether this run draws g1t's own title bar: decided once, at the start.
52pub fn started(app: &AppHandle) -> bool {
53 *STARTED_CUSTOM.get_or_init(|| custom(app))
54}
55
56/// The main window, holding the bar and the site.
57pub fn window(app: &AppHandle) -> Option<Window> {
58 app.get_window(WINDOW)
59}
60
61/// The site's web view.
62pub fn site(app: &AppHandle) -> Option<Webview> {
63 app.get_webview(SITE)
64}
65
66/// The main window, before its web views: g1t's frame, or the system's.
67pub fn main_window<'a, R: Runtime, M: Manager<R>>(app: &'a M, custom: bool) -> tauri::Result<WindowBuilder<'a, R, M>> {
68 #[cfg(target_os = "macos")]
69 let builder = if custom {
70 // The traffic lights stay, over the bar and centred in it.
71 let config = tauri::utils::config::WindowConfig {
72 label: WINDOW.into(),
73 traffic_light_position: Some(tauri::utils::config::LogicalPosition { x: 14.0, y: (HEIGHT - 14.0) / 2.0 }),
74 ..Default::default()
75 };
76 WindowBuilder::from_config(app, &config)?.title_bar_style(tauri::TitleBarStyle::Overlay).hidden_title(true)
77 } else {
78 WindowBuilder::new(app, WINDOW)
79 };
80 #[cfg(not(target_os = "macos"))]
81 let builder = WindowBuilder::new(app, WINDOW).decorations(!custom);
82 Ok(builder.title("g1t").inner_size(1280.0, 840.0).min_inner_size(720.0, 480.0).visible(false))
83}
84
85/// The bar's and the site's web views, in the main window, as tall as the window leaves them.
86pub fn fill(window: &Window, custom: bool, site: WebviewBuilder<tauri::Wry>) -> tauri::Result<()> {
87 let size = logical_size(window);
88 let top = if custom { HEIGHT } else { 0.0 };
89 if custom {
90 window.add_child(
The title bar and the app's own windows never have the web inspector: with Tauri's devtools feature built in, every web view not told otherwise gets one, so the Developer tools setting governed only the site's page while the title bar and the settings and Runner windows answered F12 and Inspect regardless.91 // Never an inspector: the devtools feature gives every web view
92 // one unless it is told otherwise (src/devtools.rs).
93 WebviewBuilder::new(BAR, WebviewUrl::App("titlebar.html".into())).background_color(RAIL).on_navigation(config::is_launcher).devtools(false),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r994 LogicalPosition::new(0.0, 0.0),
95 LogicalSize::new(size.width, HEIGHT),
96 )?;
97 }
98 window.add_child(site.background_color(RAIL), LogicalPosition::new(0.0, top), LogicalSize::new(size.width, (size.height - top).max(0.0)))?;
99 #[cfg(target_os = "linux")]
100 if custom {
101 pack_bar(window);
102 }
103 Ok(())
104}
105
106/// On Linux the web views sit in a GTK box, one under the other, which
107/// shares the window between them equally whatever size they are given:
108/// the bar is told to keep its height, and the site takes the rest.
109#[cfg(target_os = "linux")]
110fn pack_bar(window: &Window) {
111 use gtk::prelude::*;
112 let Ok(vbox) = window.default_vbox() else { return };
113 // The bar is the first web view added; the menu bar, hidden, is not one.
114 if let Some(bar) = vbox.children().into_iter().find(|child| child.type_().name() == "WebKitWebView") {
115 vbox.set_child_packing(&bar, false, true, 0, gtk::PackType::Start);
116 bar.set_size_request(-1, HEIGHT as i32);
117 }
118}
119
120fn logical_size(window: &Window) -> LogicalSize<f64> {
121 let scale = window.scale_factor().unwrap_or(1.0);
122 window.inner_size().map(|size| size.to_logical(scale)).unwrap_or(LogicalSize::new(1280.0, 840.0))
123}
124
125/// The window changed size: the bar keeps its height, and the site takes the rest.
126pub fn layout(window: &Window) {
127 let app = window.app_handle();
128 let size = logical_size(window);
129 let bar = app.get_webview(BAR);
130 let top = if bar.is_some() { HEIGHT } else { 0.0 };
131 if let Some(bar) = bar {
132 let _ = bar.set_position(LogicalPosition::new(0.0, 0.0));
133 let _ = bar.set_size(LogicalSize::new(size.width, HEIGHT));
134 }
135 if let Some(site) = site(app) {
136 let _ = site.set_position(LogicalPosition::new(0.0, top));
137 let _ = site.set_size(LogicalSize::new(size.width, (size.height - top).max(0.0)));
138 }
139}
140
141/// The settings window: g1t's frame, or the system's. Its page draws its
142/// own bar (launcher/settings.html), being g1t's own.
143pub fn dialog<'a, R: Runtime, M: Manager<R>>(builder: WebviewWindowBuilder<'a, R, M>, custom: bool) -> WebviewWindowBuilder<'a, R, M> {
144 if !custom {
145 return builder;
146 }
147 #[cfg(target_os = "macos")]
148 {
149 builder
150 .title_bar_style(tauri::TitleBarStyle::Overlay)
151 .hidden_title(true)
152 .traffic_light_position(LogicalPosition::new(14.0, (HEIGHT - 14.0) / 2.0))
153 }
154 #[cfg(not(target_os = "macos"))]
155 {
156 builder.decorations(false)
157 }
158}
159
160/// The main window's menu bar: shown under the system's frame, and kept
161/// hidden under g1t's, whose menu button opens the same menu (`menu`) and
162/// whose shortcuts still work.
163pub fn menu_bar(window: &Window, custom: bool) {
164 let _ = if custom { window.hide_menu() } else { window.show_menu() };
165 // GTK shows everything in a window each time the window is shown, the
166 // hidden menu bar too, unless the menu bar is told to sit that out.
167 #[cfg(target_os = "linux")]
168 if let Ok(vbox) = window.default_vbox() {
169 use gtk::prelude::*;
170 for child in vbox.children().into_iter().filter(|child| child.is::<gtk::MenuBar>()) {
171 child.set_no_show_all(custom);
172 }
173 }
174}
175
176/// What a window button asks of the window it is in: `minimize`,
177/// `maximize` (which toggles), `close`, `drag`, or `maximized` to ask.
178/// Answers whether the window is maximized.
179pub fn control(app: &AppHandle, window: &Window, action: &str) -> Result<bool, String> {
180 let main = window.label() == WINDOW;
181 match action {
182 "minimize" => window.minimize().map_err(|error| error.to_string())?,
183 "maximize" => {
184 let maximized = window.is_maximized().unwrap_or(false);
185 let _ = if maximized { window.unmaximize() } else { window.maximize() };
186 }
187 "close" if main => tray::close(app),
188 "close" => window.close().map_err(|error| error.to_string())?,
189 "drag" => window.start_dragging().map_err(|error| error.to_string())?,
190 "maximized" => {}
191 other => return Err(format!("the window cannot {other}")),
192 }
193 Ok(window.is_maximized().unwrap_or(false))
194}
195
196/// The menu button: the window's menu, under the button.
197pub fn menu(window: &Window, x: f64, y: f64) -> Result<(), String> {
198 #[cfg(target_os = "macos")]
199 let menu = window.app_handle().menu();
200 #[cfg(not(target_os = "macos"))]
201 let menu = window.menu();
202 let menu = menu.ok_or("the window has no menu")?;
203 window.popup_menu_at(&menu, LogicalPosition::new(x, y)).map_err(|error| error.to_string())
204}
205
206/// The page's colours, as the bar takes them: CSS colours, resolved.
207#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
208pub struct Colors {
209 pub background: String,
210 pub foreground: String,
211 pub muted: String,
212 pub accent: String,
213 /// Whether the page is dark, for the bar's own hover shades.
214 pub dark: bool,
215}
216
217/// What the bar shows of the page in the window under it.
218#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub struct Page {
221 pub title: String,
222 /// The site's search, by its label ("Search Flagon"), when it has handed one over.
223 pub search: Option<String>,
224 /// Whether there is a page to go back or forward to; absent where the web view cannot tell.
225 pub can_go_back: Option<bool>,
226 pub can_go_forward: Option<bool>,
227 pub colors: Option<Colors>,
Merge branch 'main' into rm-agents228 /// The runner menu the site draws under the bar's dot is open.
229 #[serde(default)]
230 pub runner_menu: bool,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9231}
232
233/// The page as last reported, kept for a bar that loads after it.
234#[derive(Default)]
235pub struct State(Mutex<Page>);
236
237/// The site's page reported what it is: the bar is told.
238pub fn page(app: &AppHandle, mut page: Page) {
239 page.title.truncate(300);
240 if let Some(search) = page.search.as_mut() {
241 search.truncate(120);
242 }
243 let state = app.state::<State>();
244 let mut kept = state.0.lock().unwrap();
245 // A page that knows no colours of its own keeps the last ones.
246 if page.colors.is_none() {
247 page.colors = kept.colors.clone();
248 }
249 if *kept == page {
250 return;
251 }
252 *kept = page.clone();
253 drop(kept);
254 let _ = app.emit_to(EventTarget::webview(BAR), PAGE_EVENT, page);
255}
256
257/// What the bar shows now: it asks as it loads.
258pub fn current(app: &AppHandle) -> Page {
259 app.state::<State>().0.lock().unwrap().clone()
260}
261
262/// A button on the bar that acts on the page: `back`, `forward`, `search`,
Merge branch 'main' into rm-agents263/// `settings`, `runner` (the dot beside the settings, `at` where it is
264/// across the bar), or `focus`, which hands the keyboard back to the page
265/// after a click on the bar.
266pub fn act(app: &AppHandle, action: &str, at: Option<f64>) -> Result<(), String> {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9267 let site = site(app).ok_or("there is no page")?;
268 match action {
269 "back" => site.eval("history.back()").map_err(|error| error.to_string())?,
270 "forward" => site.eval("history.forward()").map_err(|error| error.to_string())?,
271 "search" => app.emit_to(EventTarget::webview(SITE), SEARCH_EVENT, ()).map_err(|error| error.to_string())?,
Merge branch 'main' into rm-agents272 // The app's settings, a click away rather than only in its menu:
273 // a dialog the site draws over itself.
The desktop app's settings are a button in its title bar. A Settings button sits at the right of the bar, before the window's own buttons (and at its end on macOS), so the runner and the rest are a click away instead of only in the File menu, the tray or the account menu.274 "settings" => {
275 crate::settings::open(app);
276 return Ok(());
277 }
Merge branch 'main' into rm-agents278 // The runner's menu, hanging under its dot beside the settings button.
The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.279 "runner" => {
Merge branch 'main' into rm-agents280 crate::settings::runner_menu(app, true, at);
The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.281 return Ok(());
282 }
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9283 "focus" => {}
284 other => return Err(format!("the bar cannot {other}")),
285 }
286 let _ = site.set_focus();
287 Ok(())
288}

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