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
214 linesCodeBlameRaw
1// The title bar's work: draw what the page under it reports, and send its
2// buttons to the app (src/bridge.rs: title_bar_current, title_bar_act,
3// window_control, window_menu). A click here hands the keyboard back to
4// the page, so typing goes on where it was.
5(function () {
6 var core = window.__TAURI__ && window.__TAURI__.core;
7 var events = window.__TAURI__ && window.__TAURI__.event;
8 function invoke(command, args) {
9 if (core && typeof core.invoke === "function") return core.invoke(command, args || {});
10 return Promise.reject(new Error("the title bar cannot reach the app"));
11 }
12 function quietly(promise) {
13 promise.catch(function () {});
14 }
15 function act(action) {
16 quietly(invoke("title_bar_act", { action: action }));
17 }
18 function control(action) {
19 return invoke("window_control", { action: action });
20 }
21
22 var ua = navigator.userAgent;
23 var MAC = /Macintosh|Mac OS X/.test(ua) && !/Windows/.test(ua);
24 if (MAC) document.documentElement.classList.add("mac");
25
26 function svg(body) {
27 return (
28 '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
29 body +
30 "</svg>"
31 );
32 }
33 // Drawn here, from lines and boxes, as each system draws its own.
34 var ICONS = {
35 menu: svg('<line x1="3" y1="4.5" x2="13" y2="4.5"/><line x1="3" y1="8" x2="13" y2="8"/><line x1="3" y1="11.5" x2="13" y2="11.5"/>'),
36 back: svg('<polyline points="9.5,3.5 5,8 9.5,12.5"/><line x1="5" y1="8" x2="13" y2="8"/>'),
37 forward: svg('<polyline points="6.5,3.5 11,8 6.5,12.5"/><line x1="3" y1="8" x2="11" y2="8"/>'),
38 search: svg('<circle cx="7" cy="7" r="4"/><line x1="10" y1="10" x2="13" y2="13"/>'),
39 settings: svg('<line x1="2.5" y1="4.5" x2="13.5" y2="4.5"/><line x1="2.5" y1="11.5" x2="13.5" y2="11.5"/><circle cx="10" cy="4.5" r="1.75" fill="var(--bg)"/><circle cx="6" cy="11.5" r="1.75" fill="var(--bg)"/>'),
40 minimize: svg('<line x1="3.5" y1="8" x2="12.5" y2="8"/>'),
41 maximize: svg('<rect x="3.5" y="3.5" width="9" height="9" rx="1"/>'),
42 restore: svg('<rect x="3.5" y="5.5" width="7" height="7" rx="1"/><path d="M5.5 5.5V4.5a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-1"/>'),
43 close: svg('<line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/>'),
44 };
45
46 function button(kind, label, icon, onClick) {
47 var b = document.createElement("button");
48 b.type = "button";
49 b.tabIndex = -1;
50 b.className = kind;
51 b.setAttribute("aria-label", label);
52 b.title = label;
53 if (icon) b.innerHTML = ICONS[icon];
54 b.addEventListener("click", function (event) {
55 event.stopPropagation();
56 onClick(b);
57 });
58 return b;
59 }
60
61 var bar = document.getElementById("bar");
62 var left = document.getElementById("left");
63 var middle = document.getElementById("middle");
64 var right = document.getElementById("right");
65
66 if (!MAC) {
67 left.append(
68 button("icon", "Menu", "menu", function (b) {
69 var box = b.getBoundingClientRect();
70 quietly(invoke("window_menu", { x: box.left, y: box.bottom }));
71 }),
72 );
73 }
74 var back = button("icon", "Back", "back", function () {
75 act("back");
76 });
77 var forward = button("icon", "Forward", "forward", function () {
78 act("forward");
79 });
80 left.append(back, forward);
81
82 // The runner, in a few words beside the settings, as Docker Desktop shows
83 // its engine: what it is doing, live, and a click opens its window
84 // (runner.html). The words are the app's (diagnosis.rs chip).
85 var chip = button("chip", "Runner", null, function () {
86 act("runner");
87 });
88 chip.hidden = true;
89 var chipDot = document.createElement("span");
90 chipDot.className = "chip-dot";
91 var chipText = document.createElement("span");
92 chipText.className = "chip-text";
93 chip.append(chipDot, chipText);
94 function drawChip(said) {
95 if (!said || !said.label) return;
96 chip.hidden = false;
97 chip.dataset.tone = said.tone;
98 chipText.textContent = said.label;
99 chip.setAttribute("aria-label", said.label + ": open the runner");
100 chip.title = "Open the runner";
101 }
102 // The app's settings, at the right of the bar, before the window's
103 // buttons: where they are found without the menu.
104 right.append(
105 chip,
106 button("icon", "Settings", "settings", function () {
107 act("settings");
108 }),
109 );
110
111 var maximize = null;
112 function drawMaximized(maximized) {
113 if (!maximize) return;
114 maximize.innerHTML = maximized ? ICONS.restore : ICONS.maximize;
115 maximize.setAttribute("aria-label", maximized ? "Restore" : "Maximize");
116 maximize.title = maximized ? "Restore" : "Maximize";
117 }
118 // The window's buttons, at the right as Windows and Linux put them; macOS has its own.
119 if (!MAC) {
120 maximize = button("caption", "Maximize", "maximize", function () {
121 control("maximize").then(drawMaximized, function () {});
122 });
123 right.append(
124 button("caption", "Minimize", "minimize", function () {
125 quietly(control("minimize"));
126 }),
127 maximize,
128 button("caption close", "Close", "close", function () {
129 quietly(control("close"));
130 }),
131 );
132 var resizing = 0;
133 window.addEventListener("resize", function () {
134 clearTimeout(resizing);
135 resizing = setTimeout(function () {
136 control("maximized").then(drawMaximized, function () {});
137 }, 120);
138 });
139 control("maximized").then(drawMaximized, function () {});
140 }
141
142 // Dragging the bar moves the window; a double click maximizes it, as a system's title bar does.
143 bar.addEventListener("mousedown", function (event) {
144 if (event.button !== 0 || event.target.closest("button")) return;
145 if (event.detail === 2) {
146 control("maximize").then(drawMaximized, function () {});
147 return;
148 }
149 quietly(control("drag"));
150 });
151 // The keyboard belongs to the page: a click here gives it back.
152 window.addEventListener("mouseup", function () {
153 act("focus");
154 });
155
156 var title = document.createElement("span");
157 title.className = "title";
158 var pill = button("search", "Search", "search", function () {
159 act("search");
160 });
161 var pillText = document.createElement("span");
162 pill.append(pillText);
163
164 function setColors(colors) {
165 var root = document.documentElement.style;
166 if (!colors) return;
167 var shade = colors.dark ? "237, 237, 239" : "15, 15, 17";
168 root.setProperty("--bg", colors.background);
169 root.setProperty("--fg", colors.foreground);
170 root.setProperty("--muted", colors.muted);
171 root.setProperty("--accent", colors.accent);
172 root.setProperty("--hover", "rgba(" + shade + ", " + (colors.dark ? 0.1 : 0.07) + ")");
173 root.setProperty("--pill", "rgba(" + shade + ", " + (colors.dark ? 0.09 : 0.05) + ")");
174 root.setProperty("--pill-hover", "rgba(" + shade + ", " + (colors.dark ? 0.14 : 0.09) + ")");
175 root.setProperty("--pill-edge", "rgba(" + shade + ", " + (colors.dark ? 0.08 : 0.1) + ")");
176 document.documentElement.style.colorScheme = colors.dark ? "dark" : "light";
177 }
178
179 function draw(page) {
180 if (!page) return;
181 middle.textContent = "";
182 if (page.search) {
183 pillText.textContent = page.search;
184 pill.setAttribute("aria-label", page.search);
185 pill.removeAttribute("title");
186 middle.append(pill);
187 } else {
188 title.textContent = page.title || "g1t";
189 middle.append(title);
190 }
191 back.disabled = page.canGoBack === false;
192 forward.disabled = page.canGoForward === false;
193 setColors(page.colors);
194 }
195
196 if (events && typeof events.listen === "function") {
197 events
198 .listen("g1t-title-bar", function (event) {
199 draw(event.payload);
200 })
201 .catch(function () {});
202 }
203 invoke("title_bar_current").then(draw, function () {});
204 if (events && typeof events.listen === "function") {
205 events
206 .listen("g1t-runner-status", function (event) {
207 drawChip(event.payload);
208 })
209 .catch(function () {});
210 }
211 invoke("runner_status").then(function (view) {
212 drawChip(view && view.chip);
213 }, function () {});
214})();