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.

228 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(
40 '<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)"/>',
41 ),
42 minimize: svg('<line x1="3.5" y1="8" x2="12.5" y2="8"/>'),
43 maximize: svg('<rect x="3.5" y="3.5" width="9" height="9" rx="1"/>'),
44 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"/>'),
45 close: svg('<line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/>'),
46 };
47
48 function button(kind, label, icon, onClick) {
49 var b = document.createElement("button");
50 b.type = "button";
51 b.tabIndex = -1;
52 b.className = kind;
53 b.setAttribute("aria-label", label);
54 b.title = label;
55 if (icon) b.innerHTML = ICONS[icon];
56 b.addEventListener("click", function (event) {
57 event.stopPropagation();
58 onClick(b);
59 });
60 return b;
61 }
62
63 var bar = document.getElementById("bar");
64 var left = document.getElementById("left");
65 var middle = document.getElementById("middle");
66 var right = document.getElementById("right");
67
68 if (!MAC) {
69 left.append(
70 button("icon", "Menu", "menu", function (b) {
71 var box = b.getBoundingClientRect();
72 quietly(invoke("window_menu", { x: box.left, y: box.bottom }));
73 }),
74 );
75 }
76 var back = button("icon", "Back", "back", function () {
77 act("back");
78 });
79 var forward = button("icon", "Forward", "forward", function () {
80 act("forward");
81 });
82 left.append(back, forward);
83
84 // The runner, as a dot right beside the settings: its colour says what
85 // it is doing, live, and a wide window says it in words too. A click
86 // opens its menu, which the site draws under the dot (or, with no page
87 // of the site's to draw it, the Runner window). The words are the app's
88 // (diagnosis.rs chip).
89 var chip = button("chip", "Runner", null, function (b) {
90 var box = b.getBoundingClientRect();
91 var width = document.documentElement.clientWidth || window.innerWidth || 1;
92 // Where the dot's right edge is, as a share of the bar, so the menu
93 // lines up under it whatever the site's zoom.
94 quietly(invoke("title_bar_act", { action: "runner", at: box.right / width }));
95 });
96 chip.hidden = true;
97 chip.setAttribute("aria-haspopup", "dialog");
98 chip.setAttribute("aria-expanded", "false");
99 var chipDot = document.createElement("span");
100 chipDot.className = "chip-dot";
101 var chipText = document.createElement("span");
102 chipText.className = "chip-text";
103 chip.append(chipDot, chipText);
104 function drawChip(said) {
105 if (!said || !said.label) return;
106 chip.hidden = false;
107 chip.dataset.tone = said.tone;
108 chipText.textContent = said.label;
109 chip.setAttribute("aria-label", /runner/i.test(said.label) ? said.label : "Runner: " + said.label);
110 chip.title = said.label;
111 }
112 // The app's settings, at the right of the bar, before the window's
113 // buttons: where they are found without the menu.
114 right.append(
115 chip,
116 button("icon", "Settings", "settings", function () {
117 act("settings");
118 }),
119 );
120
121 var maximize = null;
122 function drawMaximized(maximized) {
123 if (!maximize) return;
124 maximize.innerHTML = maximized ? ICONS.restore : ICONS.maximize;
125 maximize.setAttribute("aria-label", maximized ? "Restore" : "Maximize");
126 maximize.title = maximized ? "Restore" : "Maximize";
127 }
128 // The window's buttons, at the right as Windows and Linux put them; macOS has its own.
129 if (!MAC) {
130 maximize = button("caption", "Maximize", "maximize", function () {
131 control("maximize").then(drawMaximized, function () {});
132 });
133 right.append(
134 button("caption", "Minimize", "minimize", function () {
135 quietly(control("minimize"));
136 }),
137 maximize,
138 button("caption close", "Close", "close", function () {
139 quietly(control("close"));
140 }),
141 );
142 var resizing = 0;
143 window.addEventListener("resize", function () {
144 clearTimeout(resizing);
145 resizing = setTimeout(function () {
146 control("maximized").then(drawMaximized, function () {});
147 }, 120);
148 });
149 control("maximized").then(drawMaximized, function () {});
150 }
151
152 // Dragging the bar moves the window; a double click maximizes it, as a system's title bar does.
153 bar.addEventListener("mousedown", function (event) {
154 if (event.button !== 0 || event.target.closest("button")) return;
155 if (event.detail === 2) {
156 control("maximize").then(drawMaximized, function () {});
157 return;
158 }
159 quietly(control("drag"));
160 });
161 // The keyboard belongs to the page: a click here gives it back.
162 window.addEventListener("mouseup", function () {
163 act("focus");
164 });
165
166 var title = document.createElement("span");
167 title.className = "title";
168 var pill = button("search", "Search", "search", function () {
169 act("search");
170 });
171 var pillText = document.createElement("span");
172 pill.append(pillText);
173
174 function setColors(colors) {
175 var root = document.documentElement.style;
176 if (!colors) return;
177 var shade = colors.dark ? "237, 237, 239" : "15, 15, 17";
178 root.setProperty("--bg", colors.background);
179 root.setProperty("--fg", colors.foreground);
180 root.setProperty("--muted", colors.muted);
181 root.setProperty("--accent", colors.accent);
182 root.setProperty("--hover", "rgba(" + shade + ", " + (colors.dark ? 0.1 : 0.07) + ")");
183 root.setProperty("--pill", "rgba(" + shade + ", " + (colors.dark ? 0.09 : 0.05) + ")");
184 root.setProperty("--pill-hover", "rgba(" + shade + ", " + (colors.dark ? 0.14 : 0.09) + ")");
185 root.setProperty("--pill-edge", "rgba(" + shade + ", " + (colors.dark ? 0.08 : 0.1) + ")");
186 document.documentElement.style.colorScheme = colors.dark ? "dark" : "light";
187 }
188
189 function draw(page) {
190 if (!page) return;
191 middle.textContent = "";
192 if (page.search) {
193 pillText.textContent = page.search;
194 pill.setAttribute("aria-label", page.search);
195 pill.removeAttribute("title");
196 middle.append(pill);
197 } else {
198 title.textContent = page.title || "g1t";
199 middle.append(title);
200 }
201 chip.setAttribute("aria-expanded", page.runnerMenu ? "true" : "false");
202 back.disabled = page.canGoBack === false;
203 forward.disabled = page.canGoForward === false;
204 setColors(page.colors);
205 }
206
207 if (events && typeof events.listen === "function") {
208 events
209 .listen("g1t-title-bar", function (event) {
210 draw(event.payload);
211 })
212 .catch(function () {});
213 }
214 invoke("title_bar_current").then(draw, function () {});
215 if (events && typeof events.listen === "function") {
216 events
217 .listen("g1t-runner-status", function (event) {
218 drawChip(event.payload);
219 })
220 .catch(function () {});
221 }
222 invoke("runner_status").then(
223 function (view) {
224 drawChip(view && view.chip);
225 },
226 function () {},
227 );
228})();