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