Skip to content
522 linesCodeBlameRaw
1// The Runner window's work: the runner on its own, live. What it is
2// doing, each kind of job and the job each is running, how jobs run and for
3// which workspace, its updates and its log, from the app's status
4// (src/runner/mod.rs View, in diagnosis.rs's words), and each change kept
5// the moment it is made. Everything it asks is a command in src/bridge.rs.
6(function () {
7 function byId(id) {
8 return document.getElementById(id);
9 }
10
11 function invoke(command, args) {
12 var core = window.__TAURI__ && window.__TAURI__.core;
13 if (core && typeof core.invoke === "function") return core.invoke(command, args || {});
14 return Promise.reject(new Error("This page belongs inside the g1t desktop app."));
15 }
16
17 function fail(error) {
18 var box = byId("error");
19 box.textContent = typeof error === "string" ? error : (error && error.message) || "That could not be changed.";
20 box.hidden = false;
21 }
22
23 // What each way of running jobs can reach, said plainly (src/runner/backend.rs).
24 var BACKENDS = {
25 sandboxed: { text: "The runner and every job run in a sandbox of their own. Nothing a job does reaches this computer.", warn: false },
26 docker: {
27 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.",
28 warn: true,
29 },
30 host: {
31 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.",
32 warn: true,
33 },
34 };
35 var current = null;
36
37 // ── The runner ──────────────────────────────────────────────────────
38 // Its diagnosis, from src/runner/diagnosis.rs, as the tray and the
39 // site's Runners page show it; its buttons are the fixes.
40 function option(value, text) {
41 var item = document.createElement("option");
42 item.value = value;
43 item.textContent = text;
44 return item;
45 }
46
47 var busy = window.g1tBusy;
48
49 // The whole runner's status, as far as a button waits on it: a press is
50 // done when any of this changes (busy.js).
51 function runnerStatus(runner) {
52 var d = runner.diagnosis;
53 var update = d.update || {};
54 return JSON.stringify([d.code, d.tone, d.title, d.cause, runner.chip.label, runner.enabled, runner.paused, update.checking, update.checked_at, update.ready]);
55 }
56
57 // One kind of job's, with the whole runner's.
58 function kindStatus(kind, runner) {
59 return JSON.stringify([runnerStatus(runner), kind.state, kind.enabled, kind.tone, kind.title, kind.cause]);
60 }
61
62 // A fix's button. Choosing is done here, in this window; a copy says it
63 // was copied. One that changes the runner (the catalogue says which:
64 // `waits`) stays busy until the status that follows; one the catalogue
65 // asks about first (`confirm`) asks.
66 function actionButton(action, primary, key, status) {
67 var button = document.createElement("button");
68 button.type = "button";
69 button.textContent = action.label;
70 if (primary) button.className = "primary";
71 key = key + ":" + action.id;
72 if (busy.pressed(key, status)) busy.start(button);
73 button.addEventListener("click", function () {
74 if (action.id === "choose_workspace") return byId("runnerWorkspace").focus();
75 if (action.id === "choose_backend") return byId("runnerBackend").focus();
76 if (action.confirm && !window.confirm(action.confirm)) return;
77 busy.run(button, key, status, !!action.waits, function () {
78 return invoke("runner_control", { action: action.id });
79 }, fail).then(function (done) {
80 if (!done || action.id.indexOf("copy_") !== 0) return;
81 button.textContent = "Copied";
82 setTimeout(function () {
83 button.textContent = action.label;
84 }, 2000);
85 });
86 });
87 return button;
88 }
89
90 function element(tag, className, text) {
91 var item = document.createElement(tag);
92 if (className) item.className = className;
93 if (text != null) item.textContent = text;
94 return item;
95 }
96
97 // The catalogue's sentences name commands in backticks: `wsl --install`.
98 // Said here as code, which can be selected and copied.
99 function prose(into, text) {
100 into.textContent = "";
101 String(text || "")
102 .split("`")
103 .forEach(function (part, at) {
104 if (!part) return;
105 into.appendChild(at % 2 ? element("code", null, part) : document.createTextNode(part));
106 });
107 return into;
108 }
109
110 // ── Each kind of job, a card of its own ───────────────────────────────
111 // Linux jobs and Windows jobs (macOS jobs once they are released): what
112 // each is doing, why one isn't running and its fix, and its own Start or
113 // Stop, which leaves the other as it is. The words are the app's
114 // (CapabilityLine in src/runner/diagnosis.rs); the card only lays them out.
115 var SVG = "http://www.w3.org/2000/svg";
116 // Plain line icons, 16px on a 24px grid: a terminal for Linux jobs, a
117 // window for Windows jobs, a laptop for macOS jobs.
118 var ICONS = {
119 linux: ["M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z", "M7 10l3 2.5L7 15", "M12.5 15h4"],
120 windows: ["M4 4h16a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z", "M3 9h18", "M6.5 6.5h.01", "M9 6.5h.01"],
121 macos: ["M5 6a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v9H5z", "M2.5 18.5h19", "M3.5 18.5L5 15h14l1.5 3.5"],
122 };
123 // How each runner keeps a job from this computer, said plainly.
124 var ISOLATION = { "nested-container": "A fresh container in the VM", vm: "A VM of its own", container: "A container of its own", host: "None" };
125 var BUSY = { busy: true, starting: true, downloading: true, updating: true };
126 var cards = {};
127
128 // A small arrow: the link goes out to g1t.
129 function outward() {
130 var svg = document.createElementNS(SVG, "svg");
131 svg.setAttribute("viewBox", "0 0 24 24");
132 svg.setAttribute("width", "11");
133 svg.setAttribute("height", "11");
134 svg.setAttribute("fill", "none");
135 svg.setAttribute("stroke", "currentColor");
136 svg.setAttribute("stroke-width", "2");
137 svg.setAttribute("stroke-linecap", "round");
138 svg.setAttribute("aria-hidden", "true");
139 var path = document.createElementNS(SVG, "path");
140 path.setAttribute("d", "M7 17L17 7M9 7h8v8");
141 svg.appendChild(path);
142 return svg;
143 }
144
145 function icon(id) {
146 var svg = document.createElementNS(SVG, "svg");
147 svg.setAttribute("viewBox", "0 0 24 24");
148 svg.setAttribute("width", "16");
149 svg.setAttribute("height", "16");
150 svg.setAttribute("fill", "none");
151 svg.setAttribute("stroke", "currentColor");
152 svg.setAttribute("stroke-width", "1.75");
153 svg.setAttribute("stroke-linecap", "round");
154 svg.setAttribute("stroke-linejoin", "round");
155 svg.setAttribute("aria-hidden", "true");
156 (ICONS[id] || ICONS.linux).forEach(function (d) {
157 var path = document.createElementNS(SVG, "path");
158 path.setAttribute("d", d);
159 svg.appendChild(path);
160 });
161 return svg;
162 }
163
164 // A card's parts, made once and filled in on every status.
165 function makeCard(kind) {
166 var card = element("article", "kind");
167 card.setAttribute("role", "listitem");
168 card.setAttribute("aria-labelledby", "kind-" + kind.id + "-title");
169 var head = element("header", "kind-head");
170 var badge = element("span", "kind-icon");
171 badge.appendChild(icon(kind.id));
172 var name = element("span", "kind-name");
173 var title = element("h3", null, kind.label);
174 title.id = "kind-" + kind.id + "-title";
175 name.appendChild(title);
176 var pill = element("span", "pill");
177 // Its state is said when it changes, as "Linux jobs: Waiting for jobs".
178 pill.setAttribute("role", "status");
179 pill.setAttribute("aria-live", "polite");
180 pill.setAttribute("aria-atomic", "true");
181 var said = element("span", "visually-hidden", kind.label + ": ");
182 pill.appendChild(said);
183 pill.appendChild(element("span", "pill-dot"));
184 pill.appendChild(element("span", "pill-text"));
185 name.appendChild(pill);
186 head.appendChild(badge);
187 head.appendChild(name);
188 card.appendChild(head);
189 card.appendChild(element("p", "kind-about"));
190 var body = element("div", "kind-body");
191 card.appendChild(body);
192 var details = element("dl", "kind-details");
193 card.appendChild(details);
194 var actions = element("div", "kind-actions");
195 card.appendChild(actions);
196 return { root: card, pill: pill, about: card.querySelector(".kind-about"), body: body, details: details, actions: actions, signature: "" };
197 }
198
199 // What it is running, with where to see it on g1t.
200 function jobRow(job) {
201 var row = element("div", "kind-job");
202 row.appendChild(element("span", "kind-job-label", "Running"));
203 var what = element("span", "kind-job-what");
204 if (job.link) {
205 var link = element("a", null, job.name);
206 link.href = job.link;
207 what.appendChild(link);
208 } else {
209 what.appendChild(element("strong", null, job.name));
210 }
211 if (job.repo) what.appendChild(element("span", "muted", " in " + job.repo));
212 row.appendChild(what);
213 return row;
214 }
215
216 function progressRow(kind) {
217 var row = element("div", "kind-progress");
218 var bar = element("div", "bar");
219 bar.setAttribute("role", "progressbar");
220 bar.setAttribute("aria-valuemin", "0");
221 bar.setAttribute("aria-valuemax", "100");
222 bar.setAttribute("aria-valuenow", String(kind.progress));
223 bar.setAttribute("aria-label", kind.label + ": the sandbox's image");
224 var fill = element("span");
225 fill.style.width = Math.max(2, Math.min(100, kind.progress)) + "%";
226 bar.appendChild(fill);
227 row.appendChild(bar);
228 row.appendChild(element("span", "muted small", kind.progress + "%"));
229 return row;
230 }
231
232 function detail(list, term, value, given) {
233 if (!value) return;
234 var row = element("div");
235 row.appendChild(element("dt", null, term));
236 var dd = element("dd", null, given ? null : value);
237 if (given) dd.appendChild(given);
238 row.appendChild(dd);
239 list.appendChild(row);
240 }
241
242 // Start or Stop for one kind of job, said for which: busy from the click
243 // until the status that follows says it started or stopped.
244 function toggleButton(kind, onFirst, runner) {
245 var on = kind.enabled;
246 var button = element("button", on ? "quiet" : onFirst ? "primary" : null);
247 button.type = "button";
248 button.textContent = on ? "Stop" : "Start";
249 button.setAttribute("aria-label", (on ? "Stop " : "Start ") + kind.label);
250 var key = "toggle:" + kind.id;
251 var status = kindStatus(kind, runner);
252 if (busy.pressed(key, status)) busy.start(button);
253 button.addEventListener("click", function () {
254 busy.run(button, key, status, true, function () {
255 return invoke("runner_control", { action: on ? "stop" : "start", capability: kind.id });
256 }, fail);
257 });
258 return button;
259 }
260
261 function fillCard(card, kind, runner) {
262 var root = card.root;
263 root.dataset.state = kind.state;
264 root.dataset.tone = kind.tone;
265 root.classList.toggle("off", !kind.enabled);
266 card.pill.dataset.tone = kind.tone;
267 card.pill.classList.toggle("live", !!BUSY[kind.state]);
268 // Said again only when it changes, so a screen reader hears each change once.
269 var text = card.pill.querySelector(".pill-text");
270 if (text.textContent !== kind.title) text.textContent = kind.title;
271 if (card.about.textContent !== kind.about) card.about.textContent = kind.about;
272
273 // What changes the buttons and the words is drawn again only when it changes,
274 // so a button keeps its focus while the status ticks.
275 var update = runner.diagnosis.update || null;
276 var signature = JSON.stringify([kind, runner.link, update && update.checked]);
277 if (signature === card.signature) return;
278 card.signature = signature;
279
280 var body = card.body;
281 body.textContent = "";
282 if (kind.job) body.appendChild(jobRow(kind.job));
283 if (kind.state === "downloading" && kind.progress != null) body.appendChild(progressRow(kind));
284 if (kind.headline) body.appendChild(element("p", "kind-headline", kind.headline));
285 if (kind.cause) body.appendChild(prose(element("p", "kind-cause"), kind.cause));
286 if (kind.fix) body.appendChild(prose(element("p", "kind-fix"), kind.fix));
287 body.hidden = !body.firstChild;
288
289 var details = card.details;
290 details.textContent = "";
291 // Its name on g1t opens the workspace's Runners page, where it is listed.
292 var named = null;
293 if (kind.runner && runner.link) {
294 named = element("button", "link", kind.runner);
295 named.type = "button";
296 named.setAttribute("aria-label", "View " + kind.runner + " on g1t");
297 named.appendChild(outward());
298 named.addEventListener("click", function () {
299 invoke("runner_control", { action: "view_runners" }).catch(fail);
300 });
301 }
302 detail(details, "On g1t", kind.runner, named);
303 detail(details, "Version", kind.version ? "g1t-runner " + kind.version : null);
304 // A newer version waiting for this kind of job, and when g1t last looked.
305 if (update && (kind.update || kind.version)) {
306 var updates = element("span", "kind-update");
307 if (kind.update) updates.appendChild(element("span", "kind-update-ready", kind.update));
308 updates.appendChild(element("span", "muted", update.checked));
309 detail(details, "Updates", true, updates);
310 }
311 detail(details, "Isolation", ISOLATION[kind.isolation] || kind.isolation);
312 details.hidden = !details.firstChild;
313
314 var actions = card.actions;
315 actions.textContent = "";
316 // Its first fix stands out when it is the person's to do; while g1t
317 // sees to it (waiting for the release's images), none does.
318 var urgent = kind.tone === "problem" || kind.tone === "warning";
319 kind.actions.forEach(function (action, at) {
320 actions.appendChild(actionButton(action, at === 0 && urgent && kind.state !== "waiting", kind.id, kindStatus(kind, runner)));
321 });
322 if (kind.can_toggle) {
323 var toggle = toggleButton(kind, kind.actions.length === 0, runner);
324 toggle.classList.add("end");
325 actions.appendChild(toggle);
326 }
327 actions.hidden = !actions.firstChild;
328 }
329
330 function renderKinds(runner) {
331 var list = byId("runner-kinds");
332 var kinds = (runner && runner.diagnosis.capabilities) || [];
333 list.hidden = kinds.length === 0;
334 var seen = {};
335 kinds.forEach(function (kind, at) {
336 seen[kind.id] = true;
337 var card = cards[kind.id] || (cards[kind.id] = makeCard(kind));
338 if (list.children[at] !== card.root) list.insertBefore(card.root, list.children[at] || null);
339 fillCard(card, kind, runner);
340 });
341 Object.keys(cards).forEach(function (id) {
342 if (seen[id]) return;
343 cards[id].root.remove();
344 delete cards[id];
345 });
346 list.classList.toggle("single", kinds.length === 1);
347 }
348
349 function renderRunner(runner) {
350 if (!runner) return;
351 var diagnosis = runner.diagnosis;
352 renderEngine(runner);
353 // The whole runner's line, when the cards don't already say it.
354 var cardsSayIt = diagnosis.in_capabilities && diagnosis.capabilities && diagnosis.capabilities.length > 0;
355 byId("runner-diagnosis").hidden = !!cardsSayIt;
356 byId("runner-dot").className = "dot " + diagnosis.tone;
357 byId("runner-title").textContent = diagnosis.title;
358 prose(byId("runner-cause"), diagnosis.cause);
359 prose(byId("runner-fix"), diagnosis.fix);
360 var actions = byId("runner-actions");
361 actions.textContent = "";
362 diagnosis.actions.forEach(function (action, at) {
363 actions.appendChild(actionButton(action, at === 0, "runner", runnerStatus(runner)));
364 });
365 renderKinds(runner);
366 byId("runner").checked = !!runner.enabled;
367
368 var backend = byId("runnerBackend");
369 backend.value = runner.backend;
370 var about = BACKENDS[runner.backend] || BACKENDS.sandboxed;
371 byId("runner-backend-note").textContent = about.text;
372 byId("runner-backend-note").className = about.warn ? "warning-note" : "muted";
373
374 var workspaces = byId("runnerWorkspace");
375 workspaces.textContent = "";
376 var known = runner.workspaces.slice();
377 var listed = known.some(function (w) {
378 return w.slug === runner.workspace;
379 });
380 if (runner.workspace && !listed) known.unshift({ slug: runner.workspace, name: null });
381 known.forEach(function (w) {
382 workspaces.appendChild(option(w.slug, w.name ? w.name + " (" + w.slug + ")" : w.slug));
383 });
384 if (known.length === 0) workspaces.appendChild(option("", "Open g1t, signed in, to choose"));
385 workspaces.value = runner.workspace || "";
386 workspaces.disabled = known.length === 0;
387
388 byId("runner-machine").textContent = "This computer: " + runner.machine;
389 // The runner's version, when g1t last looked for a newer one, and, with
390 // no cards to say it, what becomes of one waiting.
391 var update = diagnosis.update;
392 var about = [runner.version ? "g1t-runner " + runner.version : null, update ? update.checked : null, update && update.note].filter(Boolean);
393 byId("runner-version").textContent = about.length ? about.join(" · ") : " ";
394 var check = byId("runner-check");
395 check.hidden = !update;
396 if (update) {
397 check.textContent = update.check.label;
398 // Busy while the app looks, whoever asked, and from a click until it says it is looking.
399 if (update.checking || busy.pressed("check", runnerStatus(runner))) busy.start(check);
400 else busy.stop(check);
401 }
402 // A Start or Stop pressed is done once the status says so.
403 if (!busy.pressed("engine", runnerStatus(runner))) busy.stop(byId("engine-toggle"));
404 byId("runner-view").hidden = !runner.link;
405 // The log follows along, unless it was scrolled back to read.
406 var log = byId("runner-log");
407 var following = log.scrollTop + log.clientHeight >= log.scrollHeight - 8;
408 log.textContent = runner.log.length ? runner.log.join("\n") : "Nothing yet.";
409 if (following) log.scrollTop = log.scrollHeight;
410 }
411
412 byId("runnerBackend").addEventListener("change", save);
413 byId("runnerWorkspace").addEventListener("change", save);
414 // A button that asks the runner for something: busy until the status
415 // that follows when it `waits`, or until the app has it.
416 function control(id, key, action, waits) {
417 var button = byId(id);
418 button.addEventListener("click", function () {
419 var status = current && current.runnerView ? runnerStatus(current.runnerView) : "";
420 busy.run(button, key, status, waits, function () {
421 return invoke("runner_control", { action: typeof action === "function" ? action() : action });
422 }, fail);
423 });
424 }
425 control("runner-view", "view", "view_runners", false);
426 control("runner-open-log", "log", "view_log", false);
427 // Looks now; what it finds installs as each kind of job is idle, and the status that follows says so.
428 control("runner-check", "check", "check_updates", true);
429
430 // ── The whole runner, at the top ─────────────────────────────────────
431 // As the title bar's chip says it, with what it is doing in a line and
432 // its Start or Stop; the cards below say each kind of job.
433 function renderEngine(runner) {
434 byId("engine-dot").className = "dot " + runner.chip.tone;
435 var label = byId("engine-label");
436 if (label.textContent !== runner.chip.label) label.textContent = runner.chip.label;
437 var about = [runner.diagnosis.title, runner.version ? "g1t-runner " + runner.version : null, runner.workspace ? "for " + runner.workspace : null].filter(Boolean);
438 byId("engine-sub").textContent = about.join(" · ");
439 var toggle = byId("engine-toggle");
440 var on = runner.enabled && !runner.paused;
441 toggle.hidden = false;
442 toggle.textContent = on ? "Stop" : "Start";
443 toggle.className = on ? "quiet" : "primary";
444 toggle.dataset.action = on ? "stop" : "start";
445 }
446
447 control("engine-toggle", "engine", function () {
448 return byId("engine-toggle").dataset.action;
449 }, true);
450
451 // Every setting, as the app has it now (the settings window changes the
452 // rest), with the runner's own as they are on the page.
453 var KEPT = ["startHidden", "closeToTray", "notifications", "flash", "badge", "systemTitleBar", "autoUpdate", "zoom", "developerTools", "runner", "runnerBackend", "runnerWorkspace", "runnerCapabilities"];
454 function save() {
455 invoke("get_settings")
456 .then(function (fresh) {
457 var settings = {};
458 KEPT.forEach(function (key) {
459 if (key in fresh) settings[key] = fresh[key];
460 });
461 settings.runner = byId("runner").checked;
462 settings.runnerBackend = byId("runnerBackend").value || "sandboxed";
463 settings.runnerWorkspace = byId("runnerWorkspace").value || null;
464 return invoke("save_settings", { settings: settings });
465 })
466 .then(
467 function (view) {
468 current = view;
469 renderRunner(view.runnerView);
470 },
471 function (error) {
472 fail(error);
473 if (current) renderRunner(current.runnerView);
474 },
475 );
476 }
477 byId("runner").addEventListener("change", save);
478
479 var events = window.__TAURI__ && window.__TAURI__.event;
480 if (events && typeof events.listen === "function") {
481 events
482 .listen("g1t-runner-status", function (event) {
483 if (current) current.runnerView = event.payload;
484 renderRunner(event.payload);
485 })
486 .catch(function () {});
487 }
488
489 // Links out, in the system's browser (src/bridge.js does the same on the site).
490 document.addEventListener("click", function (event) {
491 var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null;
492 if (!anchor) return;
493 event.preventDefault();
494 invoke("open_url", { url: anchor.href }).catch(function () {});
495 });
496
497 // g1t's own title bar, when the window has no system frame.
498 function frame(view) {
499 if (!view.customFrame) return;
500 if (view.platform === "macos") document.documentElement.classList.add("mac");
501 var bar = byId("titlebar");
502 bar.hidden = false;
503 document.body.classList.add("framed");
504 bar.addEventListener("mousedown", function (event) {
505 if (event.button !== 0 || event.target.closest("button")) return;
506 invoke("window_control", { action: "drag" }).catch(function () {});
507 });
508 byId("close-window").addEventListener("click", function () {
509 invoke("window_control", { action: "close" }).catch(function () {});
510 });
511 }
512
513 invoke("get_settings")
514 .then(function (view) {
515 current = view;
516 frame(view);
517 renderRunner(view.runnerView);
518 var log = byId("runner-log");
519 log.scrollTop = log.scrollHeight;
520 })
521 .catch(fail);
522})();