Skip to content
305 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 function renderRunner(runner) {
97 if (!runner) return;
98 var diagnosis = runner.diagnosis;
99 byId("runner-dot").className = "dot " + diagnosis.tone;
100 byId("runner-title").textContent = diagnosis.title;
101 byId("runner-cause").textContent = diagnosis.cause || "";
102 byId("runner-fix").textContent = diagnosis.fix || "";
103 var actions = byId("runner-actions");
104 actions.textContent = "";
105 diagnosis.actions.forEach(function (action, at) {
106 var button = document.createElement("button");
107 button.type = "button";
108 button.textContent = action.label;
109 if (at === 0) button.className = "primary";
110 button.addEventListener("click", function () {
111 // Choosing is done here, in this window.
112 if (action.id === "choose_workspace") return byId("runnerWorkspace").focus();
113 if (action.id === "choose_backend") return byId("runnerBackend").focus();
114 invoke("runner_control", { action: action.id }).catch(fail);
115 });
116 actions.appendChild(button);
117 });
118 byId("runner").checked = !!runner.enabled;
119 var on = runner.enabled && !runner.paused;
120 byId("runner-toggle").textContent = on ? "Stop" : "Start";
121 byId("runner-toggle").dataset.action = on ? "stop" : "start";
122
123 var backend = byId("runnerBackend");
124 backend.value = runner.backend;
125 var about = BACKENDS[runner.backend] || BACKENDS.sandboxed;
126 byId("runner-backend-note").textContent = about.text;
127 byId("runner-backend-note").className = about.warn ? "warning-note" : "muted";
128
129 var workspaces = byId("runnerWorkspace");
130 workspaces.textContent = "";
131 var known = runner.workspaces.slice();
132 var listed = known.some(function (w) {
133 return w.slug === runner.workspace;
134 });
135 if (runner.workspace && !listed) known.unshift({ slug: runner.workspace, name: null });
136 known.forEach(function (w) {
137 workspaces.appendChild(option(w.slug, w.name ? w.name + " (" + w.slug + ")" : w.slug));
138 });
139 if (known.length === 0) workspaces.appendChild(option("", "Open g1t, signed in, to choose"));
140 workspaces.value = runner.workspace || "";
141 workspaces.disabled = known.length === 0;
142
143 byId("runner-machine").textContent = "This computer: " + runner.machine;
144 byId("runner-version").textContent = runner.version ? "g1t-runner " + runner.version : " ";
145 byId("runner-view").hidden = !runner.link;
146 byId("runner-log").textContent = runner.log.length ? runner.log.join("\n") : "Nothing yet.";
147 }
148
149 byId("runnerBackend").addEventListener("change", save);
150 byId("runnerWorkspace").addEventListener("change", save);
151 byId("runner-toggle").addEventListener("click", function (event) {
152 invoke("runner_control", { action: event.target.dataset.action || "start" }).catch(fail);
153 });
154 byId("runner-view").addEventListener("click", function () {
155 invoke("runner_control", { action: "view_runners" }).catch(fail);
156 });
157 byId("runner-open-log").addEventListener("click", function () {
158 invoke("runner_control", { action: "view_log" }).catch(fail);
159 });
160
161 function save() {
162 invoke("save_settings", { settings: settingsOf() }).then(render, function (error) {
163 fail(error);
164 if (current) render(current);
165 });
166 }
167
168 SWITCHES.forEach(function (key) {
169 byId(key).addEventListener("change", save);
170 });
171 byId("zoom").addEventListener("change", save);
172 byId("openAtLogin").addEventListener("change", function (event) {
173 invoke("set_open_at_login", { on: event.target.checked }).then(render, function (error) {
174 fail(error);
175 if (current) render(current);
176 });
177 });
178
179 // Updates: where this copy stands, said in the window, so a check from
180 // the menu or the tray is answered here and cannot be missed.
181 var checking = false;
182
183 function when(ms) {
184 var date = new Date(ms);
185 var today = new Date().toDateString() === date.toDateString();
186 var time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
187 return today ? "today at " + time : date.toLocaleDateString([], { month: "short", day: "numeric" }) + " at " + time;
188 }
189
190 function ready(version) {
191 byId("update-status").textContent = "Version " + version + " is downloaded and installs when g1t restarts.";
192 byId("restart").hidden = false;
193 }
194
195 function updateStatus(view) {
196 if (view.updateReady) return ready(view.updateReady);
197 byId("restart").hidden = true;
198 byId("update-status").textContent = view.checkedAt
199 ? "Up to date. Last checked " + when(view.checkedAt) + "."
200 : view.autoUpdate
201 ? "Not checked yet since g1t started; it checks on its own shortly."
202 : "Checking on its own is off.";
203 }
204
205 function check() {
206 if (checking) return;
207 checking = true;
208 var button = byId("check");
209 var status = byId("update-status");
210 button.disabled = true;
211 status.textContent = "Checking, and downloading any new version…";
212 invoke("check_for_updates")
213 .then(function (answer) {
214 if (answer.indexOf("ready:") === 0) ready(answer.slice(6));
215 else status.textContent = "Up to date. Last checked " + when(Date.now()) + ".";
216 })
217 .catch(function (error) {
218 var text = typeof error === "string" ? error : "";
219 status.textContent = /sending request|connect|dns|timed out/i.test(text) || !text
220 ? "Could not reach g1t.sh to check. Check your connection and try again."
221 : "Could not check: " + text;
222 })
223 .then(function () {
224 checking = false;
225 button.disabled = false;
226 });
227 }
228
229 byId("check").addEventListener("click", check);
230 byId("restart").addEventListener("click", function () {
231 byId("restart").disabled = true;
232 byId("update-status").textContent = "Installing…";
233 invoke("restart_to_update").catch(function (error) {
234 byId("restart").disabled = false;
235 fail(error);
236 });
237 });
238
239 // The menu's or tray's "Check for updates…" with this window open, and a
240 // download finishing in the background (src/settings.rs).
241 var events = window.__TAURI__ && window.__TAURI__.event;
242 if (events && typeof events.listen === "function") {
243 events.listen("g1t-check-updates", check).catch(function () {});
244 events
245 .listen("g1t-runner-status", function (event) {
246 if (current) current.runnerView = event.payload;
247 renderRunner(event.payload);
248 })
249 .catch(function () {});
250 events
251 .listen("g1t-update-ready", function (event) {
252 if (!checking && typeof event.payload === "string") ready(event.payload);
253 })
254 .catch(function () {});
255 }
256
257 byId("restart-app").addEventListener("click", function () {
258 byId("restart-app").disabled = true;
259 invoke("restart").catch(function (error) {
260 byId("restart-app").disabled = false;
261 fail(error);
262 });
263 });
264
265 byId("change-server").addEventListener("click", function () {
266 invoke("change_server").catch(fail);
267 });
268
269 // Links out, in the system's browser (src/bridge.js does the same on the site).
270 document.addEventListener("click", function (event) {
271 var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null;
272 if (!anchor) return;
273 event.preventDefault();
274 invoke("open_url", { url: anchor.href }).catch(function () {});
275 });
276
277 // g1t's own title bar, when the window has no system frame.
278 function frame(view) {
279 if (!view.customFrame) return;
280 if (view.platform === "macos") document.documentElement.classList.add("mac");
281 var bar = byId("titlebar");
282 bar.hidden = false;
283 document.body.classList.add("framed");
284 bar.addEventListener("mousedown", function (event) {
285 if (event.button !== 0 || event.target.closest("button")) return;
286 invoke("window_control", { action: "drag" }).catch(function () {});
287 });
288 byId("close-window").addEventListener("click", function () {
289 invoke("window_control", { action: "close" }).catch(function () {});
290 });
291 }
292
293 invoke("get_settings")
294 .then(function (view) {
295 frame(view);
296 platform(view.platform);
297 render(view);
298 // Opened by "Check for updates…" in a menu.
299 if (new URLSearchParams(location.search).has("check")) {
300 byId("updates-row").scrollIntoView({ block: "center" });
301 check();
302 }
303 })
304 .catch(fail);
305})();