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.

261 linesCodeBlameRaw
1// The settings window's work: show what the app has, and keep each change
2// the moment it is made. Everything it asks is a command in
3// src/bridge.rs. The settings usually open over the site, which draws
4// them (apps/web components/desktop-settings.tsx); this window is for when
5// the window shows no page of the site's. The runner has a window of its
6// own (runner.html); here it is a line that opens it. A button that asks
7// the app for something is busy until it is done (busy.js).
8(function () {
9 var SWITCHES = ["startHidden", "closeToTray", "notifications", "flash", "badge", "systemTitleBar", "autoUpdate"];
10 // The settings the app keeps (src/settings.rs Settings); a save sends them all.
11 var KEPT = SWITCHES.concat(["zoom", "developerTools", "runner", "runnerBackend", "runnerWorkspace", "runnerCapabilities"]);
12 var current = null;
13
14 function byId(id) {
15 return document.getElementById(id);
16 }
17
18 function invoke(command, args) {
19 var core = window.__TAURI__ && window.__TAURI__.core;
20 if (core && typeof core.invoke === "function") return core.invoke(command, args || {});
21 return Promise.reject(new Error("This page belongs inside the g1t desktop app."));
22 }
23
24 function fail(error) {
25 var box = byId("error");
26 box.textContent = typeof error === "string" ? error : (error && error.message) || "That could not be changed.";
27 box.hidden = false;
28 }
29
30 function hostOf(server) {
31 try {
32 return new URL(server).host;
33 } catch (error) {
34 return server;
35 }
36 }
37
38 // What changes between Windows, macOS and Linux.
39 function platform(name) {
40 byId("flash-label").textContent = name === "macos" ? "Bounce the dock icon" : name === "windows" ? "Flash the taskbar icon" : "Ask for attention";
41 // macOS keeps its menu bar at the top of the screen, whichever title bar the window has.
42 if (name === "macos") byId("system-title-bar-note").textContent = "In place of g1t's own, which sits under the window's buttons.";
43 if (name === "macos") byId("zoom-note").textContent = "The View menu changes it too: ⌘+, ⌘− and ⌘0.";
44 else byId("zoom-note").textContent = "The View menu changes it too: Ctrl+=, Ctrl+− and Ctrl+0.";
45 }
46
47 function render(view) {
48 current = view;
49 byId("error").hidden = true;
50 byId("openAtLogin").checked = !!view.openAtLogin;
51 SWITCHES.forEach(function (key) {
52 byId(key).checked = !!view[key];
53 });
54 // Starting in the tray only means something when g1t starts at sign-in.
55 byId("startHidden").disabled = !view.openAtLogin;
56 byId("startHidden").closest(".setting").classList.toggle("off", !view.openAtLogin);
57 // The title bar changes when g1t restarts.
58 byId("restart-row").hidden = !view.titleBarChanged;
59 var zoom = byId("zoom");
60 zoom.value = String(view.zoom);
61 if (zoom.value === "") zoom.value = "1";
62 byId("version").textContent = "g1t " + view.version;
63 if (!checking) updateStatus(view);
64 byId("server").textContent = hostOf(view.server);
65 renderRunner(view.runnerView);
66 }
67
68 // The runner, in a line: what it is doing, and the window that has the rest.
69 function renderRunner(runner) {
70 if (!runner) return;
71 byId("runner-dot").className = "dot " + runner.chip.tone;
72 byId("runner-title").textContent = runner.chip.label;
73 byId("runner-summary").textContent = runner.diagnosis.title + (runner.version ? " · g1t-runner " + runner.version : "");
74 }
75
76 // Every setting, as the app has it now (the Runner window changes the
77 // runner's), with this window's own as they are on the page.
78 function settingsOf(fresh) {
79 var settings = {};
80 KEPT.forEach(function (key) {
81 if (key in fresh) settings[key] = fresh[key];
82 });
83 SWITCHES.forEach(function (key) {
84 settings[key] = byId(key).checked;
85 });
86 settings.zoom = Number(byId("zoom").value) || 1;
87 return settings;
88 }
89
90 function save() {
91 invoke("get_settings")
92 .then(function (fresh) {
93 return invoke("save_settings", { settings: settingsOf(fresh) });
94 })
95 .then(render, function (error) {
96 fail(error);
97 if (current) render(current);
98 });
99 }
100
101 byId("open-runner").addEventListener("click", function () {
102 window.g1tBusy.run(
103 byId("open-runner"),
104 "open-runner",
105 "",
106 false,
107 function () {
108 return invoke("runner_control", { action: "open_runner" });
109 },
110 fail,
111 );
112 });
113
114 SWITCHES.forEach(function (key) {
115 byId(key).addEventListener("change", save);
116 });
117 byId("zoom").addEventListener("change", save);
118 byId("openAtLogin").addEventListener("change", function (event) {
119 invoke("set_open_at_login", { on: event.target.checked }).then(render, function (error) {
120 fail(error);
121 if (current) render(current);
122 });
123 });
124
125 // Updates: where this copy stands, said in the window, so a check from
126 // the menu or the tray is answered here and cannot be missed.
127 var checking = false;
128
129 function when(ms) {
130 var date = new Date(ms);
131 var today = new Date().toDateString() === date.toDateString();
132 var time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
133 return today ? "today at " + time : date.toLocaleDateString([], { month: "short", day: "numeric" }) + " at " + time;
134 }
135
136 function ready(version) {
137 byId("update-status").textContent = "Version " + version + " is downloaded and installs when g1t restarts.";
138 byId("restart").hidden = false;
139 }
140
141 function updateStatus(view) {
142 if (view.updateReady) return ready(view.updateReady);
143 byId("restart").hidden = true;
144 byId("update-status").textContent = view.checkedAt
145 ? "Up to date. Last checked " + when(view.checkedAt) + "."
146 : view.autoUpdate
147 ? "Not checked yet since g1t started; it checks on its own shortly."
148 : "Checking on its own is off.";
149 }
150
151 function check() {
152 if (checking) return;
153 checking = true;
154 var button = byId("check");
155 var status = byId("update-status");
156 window.g1tBusy.start(button);
157 status.textContent = "Checking, and downloading any new version…";
158 invoke("check_for_updates")
159 .then(function (answer) {
160 if (answer.indexOf("ready:") === 0) ready(answer.slice(6));
161 else status.textContent = "Up to date. Last checked " + when(Date.now()) + ".";
162 })
163 .catch(function (error) {
164 var text = typeof error === "string" ? error : "";
165 status.textContent =
166 /sending request|connect|dns|timed out/i.test(text) || !text
167 ? "Could not reach g1t.sh to check. Check your connection and try again."
168 : "Could not check: " + text;
169 })
170 .then(function () {
171 checking = false;
172 window.g1tBusy.stop(button);
173 });
174 }
175
176 byId("check").addEventListener("click", check);
177 byId("restart").addEventListener("click", function () {
178 window.g1tBusy.start(byId("restart"));
179 byId("update-status").textContent = "Installing…";
180 invoke("restart_to_update").catch(function (error) {
181 window.g1tBusy.stop(byId("restart"));
182 fail(error);
183 });
184 });
185
186 // The menu's or tray's "Check for updates…" with this window open, and a
187 // download finishing in the background (src/settings.rs).
188 var events = window.__TAURI__ && window.__TAURI__.event;
189 if (events && typeof events.listen === "function") {
190 events.listen("g1t-check-updates", check).catch(function () {});
191 events
192 .listen("g1t-runner-status", function (event) {
193 if (current) current.runnerView = event.payload;
194 renderRunner(event.payload);
195 })
196 .catch(function () {});
197 events
198 .listen("g1t-update-ready", function (event) {
199 if (!checking && typeof event.payload === "string") ready(event.payload);
200 })
201 .catch(function () {});
202 }
203
204 byId("restart-app").addEventListener("click", function () {
205 window.g1tBusy.start(byId("restart-app"));
206 invoke("restart").catch(function (error) {
207 window.g1tBusy.stop(byId("restart-app"));
208 fail(error);
209 });
210 });
211
212 byId("change-server").addEventListener("click", function () {
213 window.g1tBusy.run(
214 byId("change-server"),
215 "change-server",
216 "",
217 false,
218 function () {
219 return invoke("change_server");
220 },
221 fail,
222 );
223 });
224
225 // Links out, in the system's browser (src/bridge.js does the same on the site).
226 document.addEventListener("click", function (event) {
227 var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null;
228 if (!anchor) return;
229 event.preventDefault();
230 invoke("open_url", { url: anchor.href }).catch(function () {});
231 });
232
233 // g1t's own title bar, when the window has no system frame.
234 function frame(view) {
235 if (!view.customFrame) return;
236 if (view.platform === "macos") document.documentElement.classList.add("mac");
237 var bar = byId("titlebar");
238 bar.hidden = false;
239 document.body.classList.add("framed");
240 bar.addEventListener("mousedown", function (event) {
241 if (event.button !== 0 || event.target.closest("button")) return;
242 invoke("window_control", { action: "drag" }).catch(function () {});
243 });
244 byId("close-window").addEventListener("click", function () {
245 invoke("window_control", { action: "close" }).catch(function () {});
246 });
247 }
248
249 invoke("get_settings")
250 .then(function (view) {
251 frame(view);
252 platform(view.platform);
253 render(view);
254 // Opened by "Check for updates…" in a menu.
255 if (new URLSearchParams(location.search).has("check")) {
256 byId("updates-row").scrollIntoView({ block: "center" });
257 check();
258 }
259 })
260 .catch(fail);
261})();