Skip to content
349 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.
4(function () {
5 var SWITCHES = ["startHidden", "closeToTray", "notifications", "flash", "badge", "systemTitleBar", "autoUpdate", "runner"];
6 // What each way of running jobs can reach, said plainly (src/runner/backend.rs).
7 var BACKENDS = {
8 sandboxed: { text: "The runner and every job run in a sandbox of their own. Nothing a job does reaches this computer.", warn: false },
9 docker: {
10 text: "Each job runs in a container of its own, but the runner controls this computer's Docker, which can reach your files. Not a sandbox.",
11 warn: true,
12 },
13 host: {
14 text: "Jobs run directly on this computer, as you: they can read, change and delete anything your account can. For jobs that need this system itself, such as building a Windows installer.",
15 warn: true,
16 },
17 };
18 var current = null;
19
20 function byId(id) {
21 return document.getElementById(id);
22 }
23
24 function invoke(command, args) {
25 var core = window.__TAURI__ && window.__TAURI__.core;
26 if (core && typeof core.invoke === "function") return core.invoke(command, args || {});
27 return Promise.reject(new Error("This page belongs inside the g1t desktop app."));
28 }
29
30 function fail(error) {
31 var box = byId("error");
32 box.textContent = typeof error === "string" ? error : (error && error.message) || "That could not be changed.";
33 box.hidden = false;
34 }
35
36 function hostOf(server) {
37 try {
38 return new URL(server).host;
39 } catch (error) {
40 return server;
41 }
42 }
43
44 // What changes between Windows, macOS and Linux.
45 function platform(name) {
46 byId("flash-label").textContent =
47 name === "macos" ? "Bounce the dock icon" : name === "windows" ? "Flash the taskbar icon" : "Ask for attention";
48 // macOS keeps its menu bar at the top of the screen, whichever title bar the window has.
49 if (name === "macos") byId("system-title-bar-note").textContent = "In place of g1t's own, which sits under the window's buttons.";
50 if (name === "macos") byId("zoom-note").textContent = "The View menu changes it too: ⌘+, ⌘− and ⌘0.";
51 else byId("zoom-note").textContent = "The View menu changes it too: Ctrl+=, Ctrl+− and Ctrl+0.";
52 }
53
54 function render(view) {
55 current = view;
56 byId("error").hidden = true;
57 byId("openAtLogin").checked = !!view.openAtLogin;
58 SWITCHES.forEach(function (key) {
59 byId(key).checked = !!view[key];
60 });
61 // Starting in the tray only means something when g1t starts at sign-in.
62 byId("startHidden").disabled = !view.openAtLogin;
63 byId("startHidden").closest(".setting").classList.toggle("off", !view.openAtLogin);
64 // The title bar changes when g1t restarts.
65 byId("restart-row").hidden = !view.titleBarChanged;
66 var zoom = byId("zoom");
67 zoom.value = String(view.zoom);
68 if (zoom.value === "") zoom.value = "1";
69 byId("version").textContent = "g1t " + view.version;
70 if (!checking) updateStatus(view);
71 byId("server").textContent = hostOf(view.server);
72 renderRunner(view.runnerView);
73 }
74
75 function settingsOf() {
76 var settings = {};
77 SWITCHES.forEach(function (key) {
78 settings[key] = byId(key).checked;
79 });
80 settings.zoom = Number(byId("zoom").value) || 1;
81 settings.runnerBackend = byId("runnerBackend").value || "sandboxed";
82 settings.runnerWorkspace = byId("runnerWorkspace").value || null;
83 return settings;
84 }
85
86 // ── The runner ──────────────────────────────────────────────────────
87 // Its diagnosis, from src/runner/diagnosis.rs, as the tray and the
88 // site's Runners page show it; its buttons are the fixes.
89 function option(value, text) {
90 var item = document.createElement("option");
91 item.value = value;
92 item.textContent = text;
93 return item;
94 }
95
96 // A fix's button. Choosing is done here, in this window; a copy says it was copied.
97 function actionButton(action, primary) {
98 var button = document.createElement("button");
99 button.type = "button";
100 button.textContent = action.label;
101 if (primary) button.className = "primary";
102 button.addEventListener("click", function () {
103 if (action.id === "choose_workspace") return byId("runnerWorkspace").focus();
104 if (action.id === "choose_backend") return byId("runnerBackend").focus();
105 invoke("runner_control", { action: action.id }).then(function () {
106 if (action.id.indexOf("copy_") !== 0) return;
107 button.textContent = "Copied";
108 setTimeout(function () {
109 button.textContent = action.label;
110 }, 2000);
111 }, fail);
112 });
113 return button;
114 }
115
116 function span(className, text) {
117 var item = document.createElement("span");
118 if (className) item.className = className;
119 item.textContent = text;
120 return item;
121 }
122
123 // Each kind of job the sandbox runs, when there is more than one: Linux
124 // jobs can run while Windows jobs cannot.
125 function renderCapabilities(capabilities) {
126 var list = byId("runner-capabilities");
127 list.textContent = "";
128 list.hidden = !capabilities || capabilities.length < 2;
129 if (list.hidden) return;
130 capabilities.forEach(function (capability) {
131 var item = document.createElement("li");
132 item.appendChild(span("dot " + capability.tone, ""));
133 var text = span("diagnosis-text", "");
134 var title = document.createElement("strong");
135 title.textContent = capability.label + ": " + capability.title;
136 text.appendChild(title);
137 if (capability.cause) text.appendChild(span("muted", capability.cause));
138 if (capability.fix) text.appendChild(span("", capability.fix));
139 var buttons = span("buttons", "");
140 capability.actions.forEach(function (action) {
141 buttons.appendChild(actionButton(action, false));
142 });
143 text.appendChild(buttons);
144 item.appendChild(text);
145 list.appendChild(item);
146 });
147 }
148
149 function renderRunner(runner) {
150 if (!runner) return;
151 var diagnosis = runner.diagnosis;
152 byId("runner-dot").className = "dot " + diagnosis.tone;
153 byId("runner-title").textContent = diagnosis.title;
154 byId("runner-cause").textContent = diagnosis.cause || "";
155 byId("runner-fix").textContent = diagnosis.fix || "";
156 var actions = byId("runner-actions");
157 actions.textContent = "";
158 diagnosis.actions.forEach(function (action, at) {
159 actions.appendChild(actionButton(action, at === 0));
160 });
161 renderCapabilities(diagnosis.capabilities);
162 byId("runner").checked = !!runner.enabled;
163 var on = runner.enabled && !runner.paused;
164 byId("runner-toggle").textContent = on ? "Stop" : "Start";
165 byId("runner-toggle").dataset.action = on ? "stop" : "start";
166
167 var backend = byId("runnerBackend");
168 backend.value = runner.backend;
169 var about = BACKENDS[runner.backend] || BACKENDS.sandboxed;
170 byId("runner-backend-note").textContent = about.text;
171 byId("runner-backend-note").className = about.warn ? "warning-note" : "muted";
172
173 var workspaces = byId("runnerWorkspace");
174 workspaces.textContent = "";
175 var known = runner.workspaces.slice();
176 var listed = known.some(function (w) {
177 return w.slug === runner.workspace;
178 });
179 if (runner.workspace && !listed) known.unshift({ slug: runner.workspace, name: null });
180 known.forEach(function (w) {
181 workspaces.appendChild(option(w.slug, w.name ? w.name + " (" + w.slug + ")" : w.slug));
182 });
183 if (known.length === 0) workspaces.appendChild(option("", "Open g1t, signed in, to choose"));
184 workspaces.value = runner.workspace || "";
185 workspaces.disabled = known.length === 0;
186
187 byId("runner-machine").textContent = "This computer: " + runner.machine;
188 byId("runner-version").textContent = runner.version ? "g1t-runner " + runner.version : " ";
189 byId("runner-view").hidden = !runner.link;
190 byId("runner-log").textContent = runner.log.length ? runner.log.join("\n") : "Nothing yet.";
191 }
192
193 byId("runnerBackend").addEventListener("change", save);
194 byId("runnerWorkspace").addEventListener("change", save);
195 byId("runner-toggle").addEventListener("click", function (event) {
196 invoke("runner_control", { action: event.target.dataset.action || "start" }).catch(fail);
197 });
198 byId("runner-view").addEventListener("click", function () {
199 invoke("runner_control", { action: "view_runners" }).catch(fail);
200 });
201 byId("runner-open-log").addEventListener("click", function () {
202 invoke("runner_control", { action: "view_log" }).catch(fail);
203 });
204
205 function save() {
206 invoke("save_settings", { settings: settingsOf() }).then(render, function (error) {
207 fail(error);
208 if (current) render(current);
209 });
210 }
211
212 SWITCHES.forEach(function (key) {
213 byId(key).addEventListener("change", save);
214 });
215 byId("zoom").addEventListener("change", save);
216 byId("openAtLogin").addEventListener("change", function (event) {
217 invoke("set_open_at_login", { on: event.target.checked }).then(render, function (error) {
218 fail(error);
219 if (current) render(current);
220 });
221 });
222
223 // Updates: where this copy stands, said in the window, so a check from
224 // the menu or the tray is answered here and cannot be missed.
225 var checking = false;
226
227 function when(ms) {
228 var date = new Date(ms);
229 var today = new Date().toDateString() === date.toDateString();
230 var time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
231 return today ? "today at " + time : date.toLocaleDateString([], { month: "short", day: "numeric" }) + " at " + time;
232 }
233
234 function ready(version) {
235 byId("update-status").textContent = "Version " + version + " is downloaded and installs when g1t restarts.";
236 byId("restart").hidden = false;
237 }
238
239 function updateStatus(view) {
240 if (view.updateReady) return ready(view.updateReady);
241 byId("restart").hidden = true;
242 byId("update-status").textContent = view.checkedAt
243 ? "Up to date. Last checked " + when(view.checkedAt) + "."
244 : view.autoUpdate
245 ? "Not checked yet since g1t started; it checks on its own shortly."
246 : "Checking on its own is off.";
247 }
248
249 function check() {
250 if (checking) return;
251 checking = true;
252 var button = byId("check");
253 var status = byId("update-status");
254 button.disabled = true;
255 status.textContent = "Checking, and downloading any new version…";
256 invoke("check_for_updates")
257 .then(function (answer) {
258 if (answer.indexOf("ready:") === 0) ready(answer.slice(6));
259 else status.textContent = "Up to date. Last checked " + when(Date.now()) + ".";
260 })
261 .catch(function (error) {
262 var text = typeof error === "string" ? error : "";
263 status.textContent = /sending request|connect|dns|timed out/i.test(text) || !text
264 ? "Could not reach g1t.sh to check. Check your connection and try again."
265 : "Could not check: " + text;
266 })
267 .then(function () {
268 checking = false;
269 button.disabled = false;
270 });
271 }
272
273 byId("check").addEventListener("click", check);
274 byId("restart").addEventListener("click", function () {
275 byId("restart").disabled = true;
276 byId("update-status").textContent = "Installing…";
277 invoke("restart_to_update").catch(function (error) {
278 byId("restart").disabled = false;
279 fail(error);
280 });
281 });
282
283 // The menu's or tray's "Check for updates…" with this window open, and a
284 // download finishing in the background (src/settings.rs).
285 var events = window.__TAURI__ && window.__TAURI__.event;
286 if (events && typeof events.listen === "function") {
287 events.listen("g1t-check-updates", check).catch(function () {});
288 events
289 .listen("g1t-runner-status", function (event) {
290 if (current) current.runnerView = event.payload;
291 renderRunner(event.payload);
292 })
293 .catch(function () {});
294 events
295 .listen("g1t-update-ready", function (event) {
296 if (!checking && typeof event.payload === "string") ready(event.payload);
297 })
298 .catch(function () {});
299 }
300
301 byId("restart-app").addEventListener("click", function () {
302 byId("restart-app").disabled = true;
303 invoke("restart").catch(function (error) {
304 byId("restart-app").disabled = false;
305 fail(error);
306 });
307 });
308
309 byId("change-server").addEventListener("click", function () {
310 invoke("change_server").catch(fail);
311 });
312
313 // Links out, in the system's browser (src/bridge.js does the same on the site).
314 document.addEventListener("click", function (event) {
315 var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null;
316 if (!anchor) return;
317 event.preventDefault();
318 invoke("open_url", { url: anchor.href }).catch(function () {});
319 });
320
321 // g1t's own title bar, when the window has no system frame.
322 function frame(view) {
323 if (!view.customFrame) return;
324 if (view.platform === "macos") document.documentElement.classList.add("mac");
325 var bar = byId("titlebar");
326 bar.hidden = false;
327 document.body.classList.add("framed");
328 bar.addEventListener("mousedown", function (event) {
329 if (event.button !== 0 || event.target.closest("button")) return;
330 invoke("window_control", { action: "drag" }).catch(function () {});
331 });
332 byId("close-window").addEventListener("click", function () {
333 invoke("window_control", { action: "close" }).catch(function () {});
334 });
335 }
336
337 invoke("get_settings")
338 .then(function (view) {
339 frame(view);
340 platform(view.platform);
341 render(view);
342 // Opened by "Check for updates…" in a menu.
343 if (new URLSearchParams(location.search).has("check")) {
344 byId("updates-row").scrollIntoView({ block: "center" });
345 check();
346 }
347 })
348 .catch(fail);
349})();