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

Commit

The desktop app is 0.3.4, and the runner has a window of its own. Beside the title bar's Settings button a chip says what the runner is doing, live, with a dot in its colour (Runner running, Running a job, Running 2 jobs, Runner starting, Runner updating, Runner stopped, Runner offline, Runner needs you), as Docker Desktop shows its engine; in a narrow window only the dot shows. It opens the Runner window, as the tray's runner row now does: at its top what the whole runner is doing, with what it is running, its version and workspace, and its own Stop or Start; then each kind of job's card as before; then its setup (running jobs here at all, how jobs run, the workspace, the computer with Check for runner updates and View on g1t); and its log, open and following along as it is written. Settings keeps the runner as one line, what it is doing with Open runner, and each window saves only its own settings over what the app has now, so neither undoes the other's. The chip's words are diagnosis.rs's, the Runner window is made off the main thread as Settings now is, and it has a capability of its own with only the commands it uses; the title bar may now ask the runner's status. Two things fixed on the way: a settings row hidden until it applies, Restart to change the title bar, showed all the time, since its row's display beat the hidden attribute; and in a narrow window the title bar's buttons ran over the search, which now shrinks instead. The desktop guide describes the chip, the Runner window and where each runner setting now is.

syntaqxcommitted Parentd2837eeBrowse files
18 files+1007−4840/18 viewed
+106−0
1+<!doctype html>
2+<!--
3+ The runner, in a window of its own, apart from the settings: opened from
4+ the chip beside the title bar's settings button, the tray, or the menu
5+ (src/settings.rs open_runner). What it is doing, each kind of job it
6+ runs and the job each is running, how jobs run and for which workspace,
7+ its updates and its log, live (runner.js). Its words are the app's
8+ (src/runner/diagnosis.rs).
9+-->
10+<html lang="en">
11+ <head>
12+ <meta charset="utf-8" />
13+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14+ <meta name="color-scheme" content="dark" />
15+ <title>g1t runner</title>
16+ <link rel="stylesheet" href="settings.css" />
17+ </head>
18+ <body>
19+ <!-- Its title bar, when g1t draws its own (src/frame.rs): drag it to move the window. -->
20+ <header class="titlebar" id="titlebar" hidden>
21+ <span>Runner</span>
22+ <button type="button" id="close-window" class="caption" aria-label="Close" title="Close" tabindex="-1">
23+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" aria-hidden="true">
24+ <line x1="4" y1="4" x2="12" y2="12" />
25+ <line x1="12" y1="4" x2="4" y2="12" />
26+ </svg>
27+ </button>
28+ </header>
29+ <div class="page">
30+ <main>
31+ <h1>Runner</h1>
32+ <p class="muted lede">Your workspace's <code>runs-on: self-hosted</code> jobs run on this computer while g1t is open, at no cost.</p>
33+
34+ <!-- What the whole runner is doing, as the title bar's chip says it, with its Start or Stop. -->
35+ <div class="engine" id="engine">
36+ <span class="dot" id="engine-dot" aria-hidden="true"></span>
37+ <span class="engine-text">
38+ <strong id="engine-label" role="status" aria-live="polite">Runner</strong>
39+ <span class="muted" id="engine-sub">&nbsp;</span>
40+ </span>
41+ <button type="button" id="engine-toggle" hidden>Stop</button>
42+ </div>
43+
44+ <section aria-labelledby="kinds-heading" id="runner-section">
45+ <h2 id="kinds-heading" class="visually-hidden">Jobs</h2>
46+ <!--
47+ What affects the whole runner (signed out, no workspace, turned off,
48+ the sandbox down): why, and how to fix it (src/runner/diagnosis.rs).
49+ Hidden when the cards below say it all.
50+ -->
51+ <div class="diagnosis" id="runner-diagnosis" role="status" aria-live="polite" hidden>
52+ <span class="dot" id="runner-dot" aria-hidden="true"></span>
53+ <span class="diagnosis-text">
54+ <strong id="runner-title">Runner</strong>
55+ <span class="muted" id="runner-cause"></span>
56+ <span id="runner-fix"></span>
57+ <span class="buttons" id="runner-actions"></span>
58+ </span>
59+ </div>
60+ <!-- Sandboxed: each kind of job it runs, a card of its own, started and stopped on its own. -->
61+ <div class="kinds" id="runner-kinds" role="list" aria-label="Kinds of job this computer runs" hidden></div>
62+ </section>
63+
64+ <section aria-labelledby="setup-heading">
65+ <h2 id="setup-heading">Setup</h2>
66+ <label class="setting">
67+ <span
68+ ><strong>Run jobs on this computer while g1t is open</strong
69+ ><span class="muted">Off, nothing runs here until you turn it on again.</span></span
70+ >
71+ <input type="checkbox" role="switch" id="runner" />
72+ </label>
73+ <label class="setting">
74+ <span><strong>How jobs run</strong><span class="muted" id="runner-backend-note">&nbsp;</span></span>
75+ <select id="runnerBackend">
76+ <option value="sandboxed">Sandboxed (recommended)</option>
77+ <option value="docker">In Docker</option>
78+ <option value="host">Directly on this computer</option>
79+ </select>
80+ </label>
81+ <label class="setting">
82+ <span><strong>Workspace</strong><span class="muted" id="runner-workspace-note">The workspace whose jobs it runs. Only a workspace's owners can add runners to it.</span></span>
83+ <select id="runnerWorkspace"></select>
84+ </label>
85+ <div class="setting">
86+ <span><strong id="runner-machine">This computer</strong><span class="muted" id="runner-version">&nbsp;</span></span>
87+ <span class="buttons"
88+ ><button type="button" id="runner-check" hidden>Check for runner updates</button
89+ ><button type="button" id="runner-view">View on g1t</button></span
90+ >
91+ </div>
92+ </section>
93+
94+ <section aria-labelledby="log-heading">
95+ <h2 id="log-heading">Log</h2>
96+ <pre class="live-log" id="runner-log" tabindex="0" aria-label="The runner's recent log">Nothing yet.</pre>
97+ <p class="buttons"><button type="button" id="runner-open-log">Open the whole log</button></p>
98+ </section>
99+
100+ <p class="error" id="error" role="alert" hidden></p>
101+ <p class="muted small foot"><a href="https://docs.g1t.sh/guides/desktop/#the-runner">About the runner</a></p>
102+ </main>
103+ </div>
104+ <script src="runner.js"></script>
105+ </body>
106+</html>
+511−0
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+ // A fix's button. Choosing is done here, in this window; a copy says it was copied.
48+ function actionButton(action, primary) {
49+ var button = document.createElement("button");
50+ button.type = "button";
51+ button.textContent = action.label;
52+ if (primary) button.className = "primary";
53+ button.addEventListener("click", function () {
54+ if (action.id === "choose_workspace") return byId("runnerWorkspace").focus();
55+ if (action.id === "choose_backend") return byId("runnerBackend").focus();
56+ invoke("runner_control", { action: action.id }).then(function () {
57+ if (action.id.indexOf("copy_") !== 0) return;
58+ button.textContent = "Copied";
59+ setTimeout(function () {
60+ button.textContent = action.label;
61+ }, 2000);
62+ }, fail);
63+ });
64+ return button;
65+ }
66+
67+ function element(tag, className, text) {
68+ var item = document.createElement(tag);
69+ if (className) item.className = className;
70+ if (text != null) item.textContent = text;
71+ return item;
72+ }
73+
74+ // The catalogue's sentences name commands in backticks: `wsl --install`.
75+ // Said here as code, which can be selected and copied.
76+ function prose(into, text) {
77+ into.textContent = "";
78+ String(text || "")
79+ .split("`")
80+ .forEach(function (part, at) {
81+ if (!part) return;
82+ into.appendChild(at % 2 ? element("code", null, part) : document.createTextNode(part));
83+ });
84+ return into;
85+ }
86+
87+ // ── Each kind of job, a card of its own ───────────────────────────────
88+ // Linux jobs and Windows jobs (macOS jobs once they are released): what
89+ // each is doing, why one isn't running and its fix, and its own Start or
90+ // Stop, which leaves the other as it is. The words are the app's
91+ // (CapabilityLine in src/runner/diagnosis.rs); the card only lays them out.
92+ var SVG = "http://www.w3.org/2000/svg";
93+ // Plain line icons, 16px on a 24px grid: a terminal for Linux jobs, a
94+ // window for Windows jobs, a laptop for macOS jobs.
95+ var ICONS = {
96+ 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"],
97+ 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"],
98+ 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"],
99+ };
100+ // How each runner keeps a job from this computer, said plainly.
101+ var ISOLATION = { "nested-container": "A fresh container in the VM", vm: "A VM of its own", container: "A container of its own", host: "None" };
102+ var BUSY = { busy: true, starting: true, downloading: true, updating: true };
103+ var cards = {};
104+ var pending = {};
105+
106+ // A small arrow: the link goes out to g1t.
107+ function outward() {
108+ var svg = document.createElementNS(SVG, "svg");
109+ svg.setAttribute("viewBox", "0 0 24 24");
110+ svg.setAttribute("width", "11");
111+ svg.setAttribute("height", "11");
112+ svg.setAttribute("fill", "none");
113+ svg.setAttribute("stroke", "currentColor");
114+ svg.setAttribute("stroke-width", "2");
115+ svg.setAttribute("stroke-linecap", "round");
116+ svg.setAttribute("aria-hidden", "true");
117+ var path = document.createElementNS(SVG, "path");
118+ path.setAttribute("d", "M7 17L17 7M9 7h8v8");
119+ svg.appendChild(path);
120+ return svg;
121+ }
122+
123+ function icon(id) {
124+ var svg = document.createElementNS(SVG, "svg");
125+ svg.setAttribute("viewBox", "0 0 24 24");
126+ svg.setAttribute("width", "16");
127+ svg.setAttribute("height", "16");
128+ svg.setAttribute("fill", "none");
129+ svg.setAttribute("stroke", "currentColor");
130+ svg.setAttribute("stroke-width", "1.75");
131+ svg.setAttribute("stroke-linecap", "round");
132+ svg.setAttribute("stroke-linejoin", "round");
133+ svg.setAttribute("aria-hidden", "true");
134+ (ICONS[id] || ICONS.linux).forEach(function (d) {
135+ var path = document.createElementNS(SVG, "path");
136+ path.setAttribute("d", d);
137+ svg.appendChild(path);
138+ });
139+ return svg;
140+ }
141+
142+ // A card's parts, made once and filled in on every status.
143+ function makeCard(kind) {
144+ var card = element("article", "kind");
145+ card.setAttribute("role", "listitem");
146+ card.setAttribute("aria-labelledby", "kind-" + kind.id + "-title");
147+ var head = element("header", "kind-head");
148+ var badge = element("span", "kind-icon");
149+ badge.appendChild(icon(kind.id));
150+ var name = element("span", "kind-name");
151+ var title = element("h3", null, kind.label);
152+ title.id = "kind-" + kind.id + "-title";
153+ name.appendChild(title);
154+ var pill = element("span", "pill");
155+ // Its state is said when it changes, as "Linux jobs: Waiting for jobs".
156+ pill.setAttribute("role", "status");
157+ pill.setAttribute("aria-live", "polite");
158+ pill.setAttribute("aria-atomic", "true");
159+ var said = element("span", "visually-hidden", kind.label + ": ");
160+ pill.appendChild(said);
161+ pill.appendChild(element("span", "pill-dot"));
162+ pill.appendChild(element("span", "pill-text"));
163+ name.appendChild(pill);
164+ head.appendChild(badge);
165+ head.appendChild(name);
166+ card.appendChild(head);
167+ card.appendChild(element("p", "kind-about"));
168+ var body = element("div", "kind-body");
169+ card.appendChild(body);
170+ var details = element("dl", "kind-details");
171+ card.appendChild(details);
172+ var actions = element("div", "kind-actions");
173+ card.appendChild(actions);
174+ return { root: card, pill: pill, about: card.querySelector(".kind-about"), body: body, details: details, actions: actions, signature: "" };
175+ }
176+
177+ // What it is running, with where to see it on g1t.
178+ function jobRow(job) {
179+ var row = element("div", "kind-job");
180+ row.appendChild(element("span", "kind-job-label", job.kind === "agent" ? "Agent work" : "Running"));
181+ var what = element("span", "kind-job-what");
182+ if (job.link) {
183+ var link = element("a", null, job.name);
184+ link.href = job.link;
185+ what.appendChild(link);
186+ } else {
187+ what.appendChild(element("strong", null, job.name));
188+ }
189+ if (job.repo) what.appendChild(element("span", "muted", " in " + job.repo));
190+ row.appendChild(what);
191+ return row;
192+ }
193+
194+ function progressRow(kind) {
195+ var row = element("div", "kind-progress");
196+ var bar = element("div", "bar");
197+ bar.setAttribute("role", "progressbar");
198+ bar.setAttribute("aria-valuemin", "0");
199+ bar.setAttribute("aria-valuemax", "100");
200+ bar.setAttribute("aria-valuenow", String(kind.progress));
201+ bar.setAttribute("aria-label", kind.label + ": the sandbox's image");
202+ var fill = element("span");
203+ fill.style.width = Math.max(2, Math.min(100, kind.progress)) + "%";
204+ bar.appendChild(fill);
205+ row.appendChild(bar);
206+ row.appendChild(element("span", "muted small", kind.progress + "%"));
207+ return row;
208+ }
209+
210+ function detail(list, term, value, given) {
211+ if (!value) return;
212+ var row = element("div");
213+ row.appendChild(element("dt", null, term));
214+ var dd = element("dd", null, given ? null : value);
215+ if (given) dd.appendChild(given);
216+ row.appendChild(dd);
217+ list.appendChild(row);
218+ }
219+
220+ // Start or Stop for one kind of job, said for which, and what it is doing meanwhile.
221+ function toggleButton(kind, onFirst) {
222+ var on = kind.enabled;
223+ var waiting = pending[kind.id];
224+ var button = element("button", on ? "quiet" : onFirst ? "primary" : null);
225+ button.type = "button";
226+ button.textContent = waiting ? (on ? "Stopping…" : "Starting…") : on ? "Stop" : "Start";
227+ button.setAttribute("aria-label", (on ? "Stop " : "Start ") + kind.label);
228+ button.disabled = !!waiting;
229+ button.addEventListener("click", function () {
230+ pending[kind.id] = true;
231+ renderKinds(lastKinds);
232+ invoke("runner_control", { action: on ? "stop" : "start", capability: kind.id })
233+ .catch(fail)
234+ .then(function () {
235+ // The status that follows says how it is; until then it says what was asked.
236+ setTimeout(function () {
237+ delete pending[kind.id];
238+ renderKinds(lastKinds);
239+ }, 1500);
240+ });
241+ });
242+ return button;
243+ }
244+
245+ function fillCard(card, kind, runner) {
246+ var root = card.root;
247+ root.dataset.state = kind.state;
248+ root.dataset.tone = kind.tone;
249+ root.classList.toggle("off", !kind.enabled);
250+ card.pill.dataset.tone = kind.tone;
251+ card.pill.classList.toggle("live", !!BUSY[kind.state]);
252+ // Said again only when it changes, so a screen reader hears each change once.
253+ var text = card.pill.querySelector(".pill-text");
254+ if (text.textContent !== kind.title) text.textContent = kind.title;
255+ if (card.about.textContent !== kind.about) card.about.textContent = kind.about;
256+
257+ // What changes the buttons and the words is drawn again only when it changes,
258+ // so a button keeps its focus while the status ticks.
259+ var update = runner.diagnosis.update || null;
260+ var signature = JSON.stringify([kind, !!pending[kind.id], runner.link, update && update.checked]);
261+ if (signature === card.signature) return;
262+ card.signature = signature;
263+
264+ var body = card.body;
265+ body.textContent = "";
266+ if (kind.job) body.appendChild(jobRow(kind.job));
267+ if (kind.state === "downloading" && kind.progress != null) body.appendChild(progressRow(kind));
268+ if (kind.headline) body.appendChild(element("p", "kind-headline", kind.headline));
269+ if (kind.cause) body.appendChild(prose(element("p", "kind-cause"), kind.cause));
270+ if (kind.fix) body.appendChild(prose(element("p", "kind-fix"), kind.fix));
271+ body.hidden = !body.firstChild;
272+
273+ var details = card.details;
274+ details.textContent = "";
275+ // Its name on g1t opens the workspace's Runners page, where it is listed.
276+ var named = null;
277+ if (kind.runner && runner.link) {
278+ named = element("button", "link", kind.runner);
279+ named.type = "button";
280+ named.setAttribute("aria-label", "View " + kind.runner + " on g1t");
281+ named.appendChild(outward());
282+ named.addEventListener("click", function () {
283+ invoke("runner_control", { action: "view_runners" }).catch(fail);
284+ });
285+ }
286+ detail(details, "On g1t", kind.runner, named);
287+ detail(details, "Version", kind.version ? "g1t-runner " + kind.version : null);
288+ // A newer version waiting for this kind of job, and when g1t last looked.
289+ if (update && (kind.update || kind.version)) {
290+ var updates = element("span", "kind-update");
291+ if (kind.update) updates.appendChild(element("span", "kind-update-ready", kind.update));
292+ updates.appendChild(element("span", "muted", update.checked));
293+ detail(details, "Updates", true, updates);
294+ }
295+ detail(details, "Isolation", ISOLATION[kind.isolation] || kind.isolation);
296+ details.hidden = !details.firstChild;
297+
298+ var actions = card.actions;
299+ actions.textContent = "";
300+ // Its first fix stands out when it is the person's to do; while g1t
301+ // sees to it (waiting for the release's images), none does.
302+ var urgent = kind.tone === "problem" || kind.tone === "warning";
303+ kind.actions.forEach(function (action, at) {
304+ actions.appendChild(actionButton(action, at === 0 && urgent && kind.state !== "waiting"));
305+ });
306+ if (kind.can_toggle) {
307+ var toggle = toggleButton(kind, kind.actions.length === 0);
308+ toggle.classList.add("end");
309+ actions.appendChild(toggle);
310+ }
311+ actions.hidden = !actions.firstChild;
312+ }
313+
314+ var lastKinds = null;
315+
316+ function renderKinds(runner) {
317+ lastKinds = runner;
318+ var list = byId("runner-kinds");
319+ var kinds = (runner && runner.diagnosis.capabilities) || [];
320+ list.hidden = kinds.length === 0;
321+ var seen = {};
322+ kinds.forEach(function (kind, at) {
323+ seen[kind.id] = true;
324+ var card = cards[kind.id] || (cards[kind.id] = makeCard(kind));
325+ if (list.children[at] !== card.root) list.insertBefore(card.root, list.children[at] || null);
326+ fillCard(card, kind, runner);
327+ });
328+ Object.keys(cards).forEach(function (id) {
329+ if (seen[id]) return;
330+ cards[id].root.remove();
331+ delete cards[id];
332+ });
333+ list.classList.toggle("single", kinds.length === 1);
334+ }
335+
336+ function renderRunner(runner) {
337+ if (!runner) return;
338+ var diagnosis = runner.diagnosis;
339+ renderEngine(runner);
340+ // The whole runner's line, when the cards don't already say it.
341+ var cardsSayIt = diagnosis.in_capabilities && diagnosis.capabilities && diagnosis.capabilities.length > 0;
342+ byId("runner-diagnosis").hidden = !!cardsSayIt;
343+ byId("runner-dot").className = "dot " + diagnosis.tone;
344+ byId("runner-title").textContent = diagnosis.title;
345+ prose(byId("runner-cause"), diagnosis.cause);
346+ prose(byId("runner-fix"), diagnosis.fix);
347+ var actions = byId("runner-actions");
348+ actions.textContent = "";
349+ diagnosis.actions.forEach(function (action, at) {
350+ actions.appendChild(actionButton(action, at === 0));
351+ });
352+ renderKinds(runner);
353+ byId("runner").checked = !!runner.enabled;
354+
355+ var backend = byId("runnerBackend");
356+ backend.value = runner.backend;
357+ var about = BACKENDS[runner.backend] || BACKENDS.sandboxed;
358+ byId("runner-backend-note").textContent = about.text;
359+ byId("runner-backend-note").className = about.warn ? "warning-note" : "muted";
360+
361+ var workspaces = byId("runnerWorkspace");
362+ workspaces.textContent = "";
363+ var known = runner.workspaces.slice();
364+ var listed = known.some(function (w) {
365+ return w.slug === runner.workspace;
366+ });
367+ if (runner.workspace && !listed) known.unshift({ slug: runner.workspace, name: null });
368+ known.forEach(function (w) {
369+ workspaces.appendChild(option(w.slug, w.name ? w.name + " (" + w.slug + ")" : w.slug));
370+ });
371+ if (known.length === 0) workspaces.appendChild(option("", "Open g1t, signed in, to choose"));
372+ workspaces.value = runner.workspace || "";
373+ workspaces.disabled = known.length === 0;
374+
375+ byId("runner-machine").textContent = "This computer: " + runner.machine;
376+ // The runner's version, when g1t last looked for a newer one, and, with
377+ // no cards to say it, what becomes of one waiting.
378+ var update = diagnosis.update;
379+ var about = [runner.version ? "g1t-runner " + runner.version : null, update ? update.checked : null, update && update.note].filter(Boolean);
380+ byId("runner-version").textContent = about.length ? about.join(" · ") : " ";
381+ var check = byId("runner-check");
382+ check.hidden = !update;
383+ if (update) {
384+ check.textContent = update.check.label;
385+ check.disabled = update.checking;
386+ }
387+ byId("runner-view").hidden = !runner.link;
388+ // The log follows along, unless it was scrolled back to read.
389+ var log = byId("runner-log");
390+ var following = log.scrollTop + log.clientHeight >= log.scrollHeight - 8;
391+ log.textContent = runner.log.length ? runner.log.join("\n") : "Nothing yet.";
392+ if (following) log.scrollTop = log.scrollHeight;
393+ }
394+
395+ byId("runnerBackend").addEventListener("change", save);
396+ byId("runnerWorkspace").addEventListener("change", save);
397+ byId("runner-view").addEventListener("click", function () {
398+ invoke("runner_control", { action: "view_runners" }).catch(fail);
399+ });
400+ byId("runner-open-log").addEventListener("click", function () {
401+ invoke("runner_control", { action: "view_log" }).catch(fail);
402+ });
403+ // Looks now; what it finds installs as each kind of job is idle, and the status that follows says so.
404+ byId("runner-check").addEventListener("click", function () {
405+ byId("runner-check").disabled = true;
406+ invoke("runner_control", { action: "check_updates" }).catch(function (error) {
407+ byId("runner-check").disabled = false;
408+ fail(error);
409+ });
410+ });
411+
412+ // ── The whole runner, at the top ─────────────────────────────────────
413+ // As the title bar's chip says it, with what it is doing in a line and
414+ // its Start or Stop; the cards below say each kind of job.
415+ function renderEngine(runner) {
416+ byId("engine-dot").className = "dot " + runner.chip.tone;
417+ var label = byId("engine-label");
418+ if (label.textContent !== runner.chip.label) label.textContent = runner.chip.label;
419+ var about = [runner.diagnosis.title, runner.version ? "g1t-runner " + runner.version : null, runner.workspace ? "for " + runner.workspace : null].filter(Boolean);
420+ byId("engine-sub").textContent = about.join(" · ");
421+ var toggle = byId("engine-toggle");
422+ var on = runner.enabled && !runner.paused;
423+ toggle.hidden = false;
424+ toggle.textContent = on ? "Stop" : "Start";
425+ toggle.className = on ? "quiet" : "primary";
426+ toggle.dataset.action = on ? "stop" : "start";
427+ }
428+
429+ byId("engine-toggle").addEventListener("click", function () {
430+ var toggle = byId("engine-toggle");
431+ toggle.disabled = true;
432+ invoke("runner_control", { action: toggle.dataset.action })
433+ .catch(fail)
434+ .then(function () {
435+ toggle.disabled = false;
436+ });
437+ });
438+
439+ // Every setting, as the app has it now (the settings window changes the
440+ // rest), with the runner's own as they are on the page.
441+ var KEPT = ["startHidden", "closeToTray", "notifications", "flash", "badge", "systemTitleBar", "autoUpdate", "zoom", "runner", "runnerBackend", "runnerWorkspace", "runnerCapabilities"];
442+ function save() {
443+ invoke("get_settings")
444+ .then(function (fresh) {
445+ var settings = {};
446+ KEPT.forEach(function (key) {
447+ if (key in fresh) settings[key] = fresh[key];
448+ });
449+ settings.runner = byId("runner").checked;
450+ settings.runnerBackend = byId("runnerBackend").value || "sandboxed";
451+ settings.runnerWorkspace = byId("runnerWorkspace").value || null;
452+ return invoke("save_settings", { settings: settings });
453+ })
454+ .then(
455+ function (view) {
456+ current = view;
457+ renderRunner(view.runnerView);
458+ },
459+ function (error) {
460+ fail(error);
461+ if (current) renderRunner(current.runnerView);
462+ },
463+ );
464+ }
465+ byId("runner").addEventListener("change", save);
466+
467+ var events = window.__TAURI__ && window.__TAURI__.event;
468+ if (events && typeof events.listen === "function") {
469+ events
470+ .listen("g1t-runner-status", function (event) {
471+ if (current) current.runnerView = event.payload;
472+ pending = {};
473+ renderRunner(event.payload);
474+ })
475+ .catch(function () {});
476+ }
477+
478+ // Links out, in the system's browser (src/bridge.js does the same on the site).
479+ document.addEventListener("click", function (event) {
480+ var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null;
481+ if (!anchor) return;
482+ event.preventDefault();
483+ invoke("open_url", { url: anchor.href }).catch(function () {});
484+ });
485+
486+ // g1t's own title bar, when the window has no system frame.
487+ function frame(view) {
488+ if (!view.customFrame) return;
489+ if (view.platform === "macos") document.documentElement.classList.add("mac");
490+ var bar = byId("titlebar");
491+ bar.hidden = false;
492+ document.body.classList.add("framed");
493+ bar.addEventListener("mousedown", function (event) {
494+ if (event.button !== 0 || event.target.closest("button")) return;
495+ invoke("window_control", { action: "drag" }).catch(function () {});
496+ });
497+ byId("close-window").addEventListener("click", function () {
498+ invoke("window_control", { action: "close" }).catch(function () {});
499+ });
500+ }
501+
502+ invoke("get_settings")
503+ .then(function (view) {
504+ current = view;
505+ frame(view);
506+ renderRunner(view.runnerView);
507+ var log = byId("runner-log");
508+ log.scrollTop = log.scrollHeight;
509+ })
510+ .catch(fail);
511+})();
+79−0
115115 padding: 0.75rem 0;
116116 border-bottom: 1px solid var(--line);
117117 }
118+/* A row hidden until it applies, such as Restart to change the title bar. */
119+.setting[hidden] {
120+ display: none;
121+}
118122 section .setting:last-child {
119123 border-bottom: 0;
120124 }
632636 overflow-wrap: anywhere;
633637 user-select: text;
634638 }
639+/* The Runner window's head: what the whole runner is doing, as the title
640+ bar's chip says it, with its Start or Stop, as Docker Desktop shows its
641+ engine. */
642+.engine {
643+ display: flex;
644+ align-items: center;
645+ gap: 0.75rem;
646+ margin: 0 0 1rem;
647+ padding: 0.875rem 1rem;
648+ border: 1px solid var(--line);
649+ border-radius: 10px;
650+ background: var(--surface);
651+}
652+.engine .dot {
653+ width: 10px;
654+ height: 10px;
655+ margin-top: 0;
656+}
657+.engine .dot.working {
658+ animation: breathe 1.6s ease-in-out infinite;
659+}
660+.engine-text {
661+ display: flex;
662+ flex-direction: column;
663+ gap: 0.125rem;
664+ flex: 1;
665+ min-width: 0;
666+}
667+.engine-text strong {
668+ font-size: 0.9375rem;
669+ font-weight: 500;
670+}
671+.engine-text .muted {
672+ font-size: 0.8125rem;
673+ overflow-wrap: anywhere;
674+}
675+@keyframes breathe {
676+ 50% {
677+ opacity: 0.35;
678+ }
679+}
680+@media (prefers-reduced-motion: reduce) {
681+ .engine .dot.working {
682+ animation: none;
683+ }
684+}
685+/* Its log, open and following along. */
686+pre.live-log {
687+ max-height: 16rem;
688+ overflow: auto;
689+ margin: 0.5rem 0;
690+ padding: 0.625rem 0.75rem;
691+ border: 1px solid var(--line);
692+ border-radius: 8px;
693+ background: #050507;
694+ color: var(--muted);
695+ font: 0.75rem/1.45 ui-monospace, SFMono-Regular, Consolas, monospace;
696+ white-space: pre-wrap;
697+ overflow-wrap: anywhere;
698+ user-select: text;
699+}
700+p.buttons {
701+ display: flex;
702+ gap: 0.5rem;
703+ margin: 0;
704+}
705+/* Settings' line for the runner: its dot beside what it is doing. */
706+.runner-line {
707+ display: inline-flex;
708+ align-items: center;
709+ gap: 0.5rem;
710+}
711+.runner-line .dot {
712+ margin-top: 0;
713+}
+5−44
4646
4747 <section aria-labelledby="runner-heading" id="runner-section">
4848 <h2 id="runner-heading">Runner</h2>
49− <label class="setting">
50− <span
51− ><strong>Run jobs on this computer while g1t is open</strong
52− ><span class="muted">Your workspace's <code>runs-on: self-hosted</code> jobs run here, at no cost, while g1t is open.</span></span
53− >
54− <input type="checkbox" role="switch" id="runner" />
55− </label>
56− <!--
57− What affects the whole runner (signed out, no workspace, turned off,
58− the sandbox down): why, and how to fix it (src/runner/diagnosis.rs).
59− Hidden when the cards below say it all.
60− -->
61− <div class="diagnosis" id="runner-diagnosis" role="status" aria-live="polite" hidden>
62− <span class="dot" id="runner-dot" aria-hidden="true"></span>
63− <span class="diagnosis-text">
64− <strong id="runner-title">Runner</strong>
65− <span class="muted" id="runner-cause"></span>
66− <span id="runner-fix"></span>
67− <span class="buttons" id="runner-actions"></span>
68− </span>
69− </div>
70− <!-- Sandboxed: each kind of job it runs, a card of its own, started and stopped on its own. -->
71− <div class="kinds" id="runner-kinds" role="list" aria-label="Kinds of job this computer runs" hidden></div>
72− <label class="setting">
73− <span><strong>How jobs run</strong><span class="muted" id="runner-backend-note">&nbsp;</span></span>
74− <select id="runnerBackend">
75− <option value="sandboxed">Sandboxed (recommended)</option>
76− <option value="docker">In Docker</option>
77− <option value="host">Directly on this computer</option>
78− </select>
79− </label>
80− <label class="setting">
81− <span><strong>Workspace</strong><span class="muted" id="runner-workspace-note">The workspace whose jobs it runs. Only a workspace's owners can add runners to it.</span></span>
82− <select id="runnerWorkspace"></select>
83− </label>
49+ <!-- What it is doing, in a line; the Runner window has the rest (runner.html). -->
8450 <div class="setting">
85− <span><strong id="runner-machine">This computer</strong><span class="muted" id="runner-version">&nbsp;</span></span>
86− <span class="buttons"
87− ><button type="button" id="runner-check" hidden>Check for runner updates</button
88− ><button type="button" id="runner-view">View on g1t</button></span
51+ <span
52+ ><strong class="runner-line"><span class="dot" id="runner-dot" aria-hidden="true"></span><span id="runner-title" role="status" aria-live="polite">Runner</span></strong
53+ ><span class="muted" id="runner-summary">Your workspace's <code>runs-on: self-hosted</code> jobs, on this computer while g1t is open.</span></span
8954 >
55+ <span class="buttons"><button type="button" id="open-runner">Open runner</button></span>
9056 </div>
91− <details class="log">
92− <summary>Recent log</summary>
93− <pre id="runner-log">Nothing yet.</pre>
94− <button type="button" id="runner-open-log">Open the whole log</button>
95− </details>
9657 </section>
9758
9859 <section aria-labelledby="notifications-heading">
+29−391
11 // The settings window's work: show what the app has, and keep each change
22 // the moment it is made. Everything it asks is a command in
3−// src/bridge.rs, answered only to this window.
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.
45 (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− };
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"]);
189 var current = null;
1910
2011 function byId(id) {
7263 renderRunner(view.runnerView);
7364 }
7465
75− function settingsOf() {
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) {
7677 var settings = {};
78+ KEPT.forEach(function (key) {
79+ if (key in fresh) settings[key] = fresh[key];
80+ });
7781 SWITCHES.forEach(function (key) {
7882 settings[key] = byId(key).checked;
7983 });
8084 settings.zoom = Number(byId("zoom").value) || 1;
81− settings.runnerBackend = byId("runnerBackend").value || "sandboxed";
82− settings.runnerWorkspace = byId("runnerWorkspace").value || null;
83− // Which kinds of job are on is changed by their cards' Start and Stop, and kept as it is here.
84− if (current && current.runnerCapabilities) settings.runnerCapabilities = current.runnerCapabilities;
8585 return settings;
8686 }
8787
88− // ── The runner ──────────────────────────────────────────────────────
89− // Its diagnosis, from src/runner/diagnosis.rs, as the tray and the
90− // site's Runners page show it; its buttons are the fixes.
91− function option(value, text) {
92− var item = document.createElement("option");
93− item.value = value;
94− item.textContent = text;
95− return item;
96− }
9788
98− // A fix's button. Choosing is done here, in this window; a copy says it was copied.
99− function actionButton(action, primary) {
100− var button = document.createElement("button");
101− button.type = "button";
102− button.textContent = action.label;
103− if (primary) button.className = "primary";
104− button.addEventListener("click", function () {
105− if (action.id === "choose_workspace") return byId("runnerWorkspace").focus();
106− if (action.id === "choose_backend") return byId("runnerBackend").focus();
107− invoke("runner_control", { action: action.id }).then(function () {
108− if (action.id.indexOf("copy_") !== 0) return;
109− button.textContent = "Copied";
110− setTimeout(function () {
111− button.textContent = action.label;
112− }, 2000);
113− }, fail);
114− });
115− return button;
116− }
117−
118− function element(tag, className, text) {
119− var item = document.createElement(tag);
120− if (className) item.className = className;
121− if (text != null) item.textContent = text;
122− return item;
123− }
124−
125− // The catalogue's sentences name commands in backticks: `wsl --install`.
126− // Said here as code, which can be selected and copied.
127− function prose(into, text) {
128− into.textContent = "";
129− String(text || "")
130− .split("`")
131− .forEach(function (part, at) {
132− if (!part) return;
133− into.appendChild(at % 2 ? element("code", null, part) : document.createTextNode(part));
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);
13497 });
135− return into;
13698 }
13799
138− // ── Each kind of job, a card of its own ───────────────────────────────
139− // Linux jobs and Windows jobs (macOS jobs once they are released): what
140− // each is doing, why one isn't running and its fix, and its own Start or
141− // Stop, which leaves the other as it is. The words are the app's
142− // (CapabilityLine in src/runner/diagnosis.rs); the card only lays them out.
143− var SVG = "http://www.w3.org/2000/svg";
144− // Plain line icons, 16px on a 24px grid: a terminal for Linux jobs, a
145− // window for Windows jobs, a laptop for macOS jobs.
146− var ICONS = {
147− 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"],
148− 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"],
149− 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"],
150− };
151− // How each runner keeps a job from this computer, said plainly.
152− var ISOLATION = { "nested-container": "A fresh container in the VM", vm: "A VM of its own", container: "A container of its own", host: "None" };
153− var BUSY = { busy: true, starting: true, downloading: true, updating: true };
154− var cards = {};
155− var pending = {};
156−
157− // A small arrow: the link goes out to g1t.
158− function outward() {
159− var svg = document.createElementNS(SVG, "svg");
160− svg.setAttribute("viewBox", "0 0 24 24");
161− svg.setAttribute("width", "11");
162− svg.setAttribute("height", "11");
163− svg.setAttribute("fill", "none");
164− svg.setAttribute("stroke", "currentColor");
165− svg.setAttribute("stroke-width", "2");
166− svg.setAttribute("stroke-linecap", "round");
167− svg.setAttribute("aria-hidden", "true");
168− var path = document.createElementNS(SVG, "path");
169− path.setAttribute("d", "M7 17L17 7M9 7h8v8");
170− svg.appendChild(path);
171− return svg;
172− }
173−
174− function icon(id) {
175− var svg = document.createElementNS(SVG, "svg");
176− svg.setAttribute("viewBox", "0 0 24 24");
177− svg.setAttribute("width", "16");
178− svg.setAttribute("height", "16");
179− svg.setAttribute("fill", "none");
180− svg.setAttribute("stroke", "currentColor");
181− svg.setAttribute("stroke-width", "1.75");
182− svg.setAttribute("stroke-linecap", "round");
183− svg.setAttribute("stroke-linejoin", "round");
184− svg.setAttribute("aria-hidden", "true");
185− (ICONS[id] || ICONS.linux).forEach(function (d) {
186− var path = document.createElementNS(SVG, "path");
187− path.setAttribute("d", d);
188− svg.appendChild(path);
189− });
190− return svg;
191− }
192−
193− // A card's parts, made once and filled in on every status.
194− function makeCard(kind) {
195− var card = element("article", "kind");
196− card.setAttribute("role", "listitem");
197− card.setAttribute("aria-labelledby", "kind-" + kind.id + "-title");
198− var head = element("header", "kind-head");
199− var badge = element("span", "kind-icon");
200− badge.appendChild(icon(kind.id));
201− var name = element("span", "kind-name");
202− var title = element("h3", null, kind.label);
203− title.id = "kind-" + kind.id + "-title";
204− name.appendChild(title);
205− var pill = element("span", "pill");
206− // Its state is said when it changes, as "Linux jobs: Waiting for jobs".
207− pill.setAttribute("role", "status");
208− pill.setAttribute("aria-live", "polite");
209− pill.setAttribute("aria-atomic", "true");
210− var said = element("span", "visually-hidden", kind.label + ": ");
211− pill.appendChild(said);
212− pill.appendChild(element("span", "pill-dot"));
213− pill.appendChild(element("span", "pill-text"));
214− name.appendChild(pill);
215− head.appendChild(badge);
216− head.appendChild(name);
217− card.appendChild(head);
218− card.appendChild(element("p", "kind-about"));
219− var body = element("div", "kind-body");
220− card.appendChild(body);
221− var details = element("dl", "kind-details");
222− card.appendChild(details);
223− var actions = element("div", "kind-actions");
224− card.appendChild(actions);
225− return { root: card, pill: pill, about: card.querySelector(".kind-about"), body: body, details: details, actions: actions, signature: "" };
226− }
227−
228− // What it is running, with where to see it on g1t.
229− function jobRow(job) {
230− var row = element("div", "kind-job");
231− row.appendChild(element("span", "kind-job-label", job.kind === "agent" ? "Agent work" : "Running"));
232− var what = element("span", "kind-job-what");
233− if (job.link) {
234− var link = element("a", null, job.name);
235− link.href = job.link;
236− what.appendChild(link);
237− } else {
238− what.appendChild(element("strong", null, job.name));
239− }
240− if (job.repo) what.appendChild(element("span", "muted", " in " + job.repo));
241− row.appendChild(what);
242− return row;
243− }
244−
245− function progressRow(kind) {
246− var row = element("div", "kind-progress");
247− var bar = element("div", "bar");
248− bar.setAttribute("role", "progressbar");
249− bar.setAttribute("aria-valuemin", "0");
250− bar.setAttribute("aria-valuemax", "100");
251− bar.setAttribute("aria-valuenow", String(kind.progress));
252− bar.setAttribute("aria-label", kind.label + ": the sandbox's image");
253− var fill = element("span");
254− fill.style.width = Math.max(2, Math.min(100, kind.progress)) + "%";
255− bar.appendChild(fill);
256− row.appendChild(bar);
257− row.appendChild(element("span", "muted small", kind.progress + "%"));
258− return row;
259− }
260−
261− function detail(list, term, value, given) {
262− if (!value) return;
263− var row = element("div");
264− row.appendChild(element("dt", null, term));
265− var dd = element("dd", null, given ? null : value);
266− if (given) dd.appendChild(given);
267− row.appendChild(dd);
268− list.appendChild(row);
269− }
270−
271− // Start or Stop for one kind of job, said for which, and what it is doing meanwhile.
272− function toggleButton(kind, onFirst) {
273− var on = kind.enabled;
274− var waiting = pending[kind.id];
275− var button = element("button", on ? "quiet" : onFirst ? "primary" : null);
276− button.type = "button";
277− button.textContent = waiting ? (on ? "Stopping…" : "Starting…") : on ? "Stop" : "Start";
278− button.setAttribute("aria-label", (on ? "Stop " : "Start ") + kind.label);
279− button.disabled = !!waiting;
280− button.addEventListener("click", function () {
281− pending[kind.id] = true;
282− renderKinds(lastKinds);
283− invoke("runner_control", { action: on ? "stop" : "start", capability: kind.id })
284− .catch(fail)
285− .then(function () {
286− // The status that follows says how it is; until then it says what was asked.
287− setTimeout(function () {
288− delete pending[kind.id];
289− renderKinds(lastKinds);
290− }, 1500);
291− });
292− });
293− return button;
294− }
295−
296− function fillCard(card, kind, runner) {
297− var root = card.root;
298− root.dataset.state = kind.state;
299− root.dataset.tone = kind.tone;
300− root.classList.toggle("off", !kind.enabled);
301− card.pill.dataset.tone = kind.tone;
302− card.pill.classList.toggle("live", !!BUSY[kind.state]);
303− // Said again only when it changes, so a screen reader hears each change once.
304− var text = card.pill.querySelector(".pill-text");
305− if (text.textContent !== kind.title) text.textContent = kind.title;
306− if (card.about.textContent !== kind.about) card.about.textContent = kind.about;
307−
308− // What changes the buttons and the words is drawn again only when it changes,
309− // so a button keeps its focus while the status ticks.
310− var update = runner.diagnosis.update || null;
311− var signature = JSON.stringify([kind, !!pending[kind.id], runner.link, update && update.checked]);
312− if (signature === card.signature) return;
313− card.signature = signature;
314−
315− var body = card.body;
316− body.textContent = "";
317− if (kind.job) body.appendChild(jobRow(kind.job));
318− if (kind.state === "downloading" && kind.progress != null) body.appendChild(progressRow(kind));
319− if (kind.headline) body.appendChild(element("p", "kind-headline", kind.headline));
320− if (kind.cause) body.appendChild(prose(element("p", "kind-cause"), kind.cause));
321− if (kind.fix) body.appendChild(prose(element("p", "kind-fix"), kind.fix));
322− body.hidden = !body.firstChild;
323−
324− var details = card.details;
325− details.textContent = "";
326− // Its name on g1t opens the workspace's Runners page, where it is listed.
327− var named = null;
328− if (kind.runner && runner.link) {
329− named = element("button", "link", kind.runner);
330− named.type = "button";
331− named.setAttribute("aria-label", "View " + kind.runner + " on g1t");
332− named.appendChild(outward());
333− named.addEventListener("click", function () {
334− invoke("runner_control", { action: "view_runners" }).catch(fail);
335− });
336− }
337− detail(details, "On g1t", kind.runner, named);
338− detail(details, "Version", kind.version ? "g1t-runner " + kind.version : null);
339− // A newer version waiting for this kind of job, and when g1t last looked.
340− if (update && (kind.update || kind.version)) {
341− var updates = element("span", "kind-update");
342− if (kind.update) updates.appendChild(element("span", "kind-update-ready", kind.update));
343− updates.appendChild(element("span", "muted", update.checked));
344− detail(details, "Updates", true, updates);
345− }
346− detail(details, "Isolation", ISOLATION[kind.isolation] || kind.isolation);
347− details.hidden = !details.firstChild;
348−
349− var actions = card.actions;
350− actions.textContent = "";
351− // Its first fix stands out when it is the person's to do; while g1t
352− // sees to it (waiting for the release's images), none does.
353− var urgent = kind.tone === "problem" || kind.tone === "warning";
354− kind.actions.forEach(function (action, at) {
355− actions.appendChild(actionButton(action, at === 0 && urgent && kind.state !== "waiting"));
356− });
357− if (kind.can_toggle) {
358− var toggle = toggleButton(kind, kind.actions.length === 0);
359− toggle.classList.add("end");
360− actions.appendChild(toggle);
361− }
362− actions.hidden = !actions.firstChild;
363− }
364−
365− var lastKinds = null;
366−
367− function renderKinds(runner) {
368− lastKinds = runner;
369− var list = byId("runner-kinds");
370− var kinds = (runner && runner.diagnosis.capabilities) || [];
371− list.hidden = kinds.length === 0;
372− var seen = {};
373− kinds.forEach(function (kind, at) {
374− seen[kind.id] = true;
375− var card = cards[kind.id] || (cards[kind.id] = makeCard(kind));
376− if (list.children[at] !== card.root) list.insertBefore(card.root, list.children[at] || null);
377− fillCard(card, kind, runner);
378− });
379− Object.keys(cards).forEach(function (id) {
380− if (seen[id]) return;
381− cards[id].root.remove();
382− delete cards[id];
383− });
384− list.classList.toggle("single", kinds.length === 1);
385− }
386−
387− function renderRunner(runner) {
388− if (!runner) return;
389− var diagnosis = runner.diagnosis;
390− // The whole runner's line, when the cards don't already say it.
391− var cardsSayIt = diagnosis.in_capabilities && diagnosis.capabilities && diagnosis.capabilities.length > 0;
392− byId("runner-diagnosis").hidden = !!cardsSayIt;
393− byId("runner-dot").className = "dot " + diagnosis.tone;
394− byId("runner-title").textContent = diagnosis.title;
395− prose(byId("runner-cause"), diagnosis.cause);
396− prose(byId("runner-fix"), diagnosis.fix);
397− var actions = byId("runner-actions");
398− actions.textContent = "";
399− diagnosis.actions.forEach(function (action, at) {
400− actions.appendChild(actionButton(action, at === 0));
401− });
402− renderKinds(runner);
403− byId("runner").checked = !!runner.enabled;
404−
405− var backend = byId("runnerBackend");
406− backend.value = runner.backend;
407− var about = BACKENDS[runner.backend] || BACKENDS.sandboxed;
408− byId("runner-backend-note").textContent = about.text;
409− byId("runner-backend-note").className = about.warn ? "warning-note" : "muted";
410−
411− var workspaces = byId("runnerWorkspace");
412− workspaces.textContent = "";
413− var known = runner.workspaces.slice();
414− var listed = known.some(function (w) {
415− return w.slug === runner.workspace;
416− });
417− if (runner.workspace && !listed) known.unshift({ slug: runner.workspace, name: null });
418− known.forEach(function (w) {
419− workspaces.appendChild(option(w.slug, w.name ? w.name + " (" + w.slug + ")" : w.slug));
420− });
421− if (known.length === 0) workspaces.appendChild(option("", "Open g1t, signed in, to choose"));
422− workspaces.value = runner.workspace || "";
423− workspaces.disabled = known.length === 0;
424−
425− byId("runner-machine").textContent = "This computer: " + runner.machine;
426− // The runner's version, when g1t last looked for a newer one, and, with
427− // no cards to say it, what becomes of one waiting.
428− var update = diagnosis.update;
429− var about = [runner.version ? "g1t-runner " + runner.version : null, update ? update.checked : null, update && update.note].filter(Boolean);
430− byId("runner-version").textContent = about.length ? about.join(" · ") : " ";
431− var check = byId("runner-check");
432− check.hidden = !update;
433− if (update) {
434− check.textContent = update.check.label;
435− check.disabled = update.checking;
436− }
437− byId("runner-view").hidden = !runner.link;
438− byId("runner-log").textContent = runner.log.length ? runner.log.join("\n") : "Nothing yet.";
439− }
440−
441− byId("runnerBackend").addEventListener("change", save);
442− byId("runnerWorkspace").addEventListener("change", save);
443− byId("runner-view").addEventListener("click", function () {
444− invoke("runner_control", { action: "view_runners" }).catch(fail);
445− });
446− byId("runner-open-log").addEventListener("click", function () {
447− invoke("runner_control", { action: "view_log" }).catch(fail);
448− });
449− // Looks now; what it finds installs as each kind of job is idle, and the status that follows says so.
450− byId("runner-check").addEventListener("click", function () {
451− byId("runner-check").disabled = true;
452− invoke("runner_control", { action: "check_updates" }).catch(function (error) {
453− byId("runner-check").disabled = false;
454− fail(error);
455− });
100+ byId("open-runner").addEventListener("click", function () {
101+ invoke("runner_control", { action: "open_runner" }).catch(fail);
456102 });
457103
458− function save() {
459− invoke("save_settings", { settings: settingsOf() }).then(render, function (error) {
460− fail(error);
461− if (current) render(current);
462− });
463− }
464−
465104 SWITCHES.forEach(function (key) {
466105 byId(key).addEventListener("change", save);
467106 });
541180 events
542181 .listen("g1t-runner-status", function (event) {
543182 if (current) current.runnerView = event.payload;
544− pending = {};
545183 renderRunner(event.payload);
546184 })
547185 .catch(function () {});
+57−1
4040 align-items: center;
4141 gap: 2px;
4242 flex: 1 1 0;
43− min-width: 0;
43+ /* Never narrower than its buttons: the search shrinks instead. */
44+ min-width: min-content;
4445 }
4546 .end {
4647 justify-content: flex-end;
112113 outline: 2px solid var(--accent);
113114 outline-offset: -2px;
114115 }
116+/* The runner's chip: a dot in its tone and what it is doing. */
117+.chip {
118+ gap: 6px;
119+ height: 24px;
120+ margin-right: 4px;
121+ padding: 0 9px;
122+ border-radius: 6px;
123+ white-space: nowrap;
124+}
125+.chip[hidden] {
126+ display: none;
127+}
128+.chip:hover {
129+ background: var(--hover);
130+ color: var(--fg);
131+}
132+.chip-dot {
133+ width: 7px;
134+ height: 7px;
135+ border-radius: 50%;
136+ background: var(--muted);
137+}
138+.chip[data-tone="ok"] .chip-dot {
139+ background: #4ade80;
140+}
141+.chip[data-tone="working"] .chip-dot {
142+ background: #60a5fa;
143+ animation: breathe 1.6s ease-in-out infinite;
144+}
145+.chip[data-tone="warning"] .chip-dot {
146+ background: #fbbf24;
147+}
148+.chip[data-tone="problem"] .chip-dot {
149+ background: #f87171;
150+}
151+.chip[data-tone="off"] .chip-dot {
152+ background: var(--muted);
153+ opacity: 0.6;
154+}
155+@keyframes breathe {
156+ 50% {
157+ opacity: 0.35;
158+ }
159+}
160+@media (prefers-reduced-motion: reduce) {
161+ .chip[data-tone="working"] .chip-dot {
162+ animation: none;
163+ }
164+}
165+/* A narrow window keeps the dot alone. */
166+@media (max-width: 760px) {
167+ .chip-text {
168+ display: none;
169+ }
170+}
+33−2
7979 });
8080 left.append(back, forward);
8181
82− // The app's settings (the runner among them), at the right of the bar,
83− // before the window's buttons: where they are found without the menu.
82+ // The runner, in a few words beside the settings, as Docker Desktop shows
83+ // its engine: what it is doing, live, and a click opens its window
84+ // (runner.html). The words are the app's (diagnosis.rs chip).
85+ var chip = button("chip", "Runner", null, function () {
86+ act("runner");
87+ });
88+ chip.hidden = true;
89+ var chipDot = document.createElement("span");
90+ chipDot.className = "chip-dot";
91+ var chipText = document.createElement("span");
92+ chipText.className = "chip-text";
93+ chip.append(chipDot, chipText);
94+ function drawChip(said) {
95+ if (!said || !said.label) return;
96+ chip.hidden = false;
97+ chip.dataset.tone = said.tone;
98+ chipText.textContent = said.label;
99+ chip.setAttribute("aria-label", said.label + ": open the runner");
100+ chip.title = "Open the runner";
101+ }
102+ // The app's settings, at the right of the bar, before the window's
103+ // buttons: where they are found without the menu.
84104 right.append(
105+ chip,
85106 button("icon", "Settings", "settings", function () {
86107 act("settings");
87108 }),
180201 .catch(function () {});
181202 }
182203 invoke("title_bar_current").then(draw, function () {});
204+ if (events && typeof events.listen === "function") {
205+ events
206+ .listen("g1t-runner-status", function (event) {
207+ drawChip(event.payload);
208+ })
209+ .catch(function () {});
210+ }
211+ invoke("runner_status").then(function (view) {
212+ drawChip(view && view.chip);
213+ }, function () {});
183214 })();
+1−1
13301330
13311331 [[package]]
13321332 name = "g1t-desktop"
1333−version = "0.3.3"
1333+version = "0.3.4"
13341334 dependencies = [
13351335 "base64 0.22.1",
13361336 "ed25519-dalek",
+1−1
11 [package]
22 name = "g1t-desktop"
3−version = "0.3.3"
3+version = "0.3.4"
44 edition = "2024"
55 license = "MIT"
66 repository = "https://g1t.sh/flagon-io/g1t"
+2−1
1515 "allow-window-menu",
1616 "allow-title-bar-page",
1717 "allow-title-bar-current",
18− "allow-title-bar-act"
18+ "allow-title-bar-act",
19+ "allow-runner-status"
1920 ]
2021 }
+15−0
1+{
2+ "$schema": "../gen/schemas/desktop-schema.json",
3+ "identifier": "runner",
4+ "description": "The Runner window, a page bundled with the app (launcher/runner.html): the runner on its own, apart from the settings. Its commands answer only the windows they name (src/bridge.rs).",
5+ "windows": ["runner"],
6+ "permissions": [
7+ "core:default",
8+ "allow-get-settings",
9+ "allow-save-settings",
10+ "allow-open-url",
11+ "allow-window-control",
12+ "allow-runner-status",
13+ "allow-runner-control"
14+ ]
15+}
+17−6
5959 if webview.label() == settings::SETTINGS { Ok(()) } else { Err("only the settings window may ask that".into()) }
6060 }
6161
62+/// The Runner window, a page bundled with the app (launcher/runner.html).
63+fn from_runner(webview: &Webview) -> Result<(), String> {
64+ from_launcher(webview)?;
65+ if webview.label() == settings::RUNNER { Ok(()) } else { Err("only the Runner window may ask that".into()) }
66+}
67+
68+/// The settings window, or the Runner window, which keeps the runner's settings.
69+fn from_settings_or_runner(webview: &Webview) -> Result<(), String> {
70+ from_settings(webview).or_else(|_| from_runner(webview))
71+}
72+
6273 fn from_either(app: &AppHandle, webview: &Webview) -> Result<(), String> {
6374 from_site(app, webview).or_else(|_| from_launcher(webview))
6475 }
7788 if webview.label() == frame::BAR { Ok(()) } else { Err("only the title bar may ask that".into()) }
7889 }
7990
80−/// The page asking has window buttons: the title bar, or the settings window's own.
91+/// The page asking has window buttons: the title bar, or the settings or Runner window's own.
8192 fn from_frame(webview: &Webview) -> Result<(), String> {
82− from_bar(webview).or_else(|_| from_settings(webview))
93+ from_bar(webview).or_else(|_| from_settings_or_runner(webview))
8394 }
8495
8596 #[tauri::command]
229240 /// What the settings window shows.
230241 #[tauri::command]
231242 pub fn get_settings(app: AppHandle, webview: Webview) -> Result<settings::View, String> {
232− from_settings(&webview)?;
243+ from_settings_or_runner(&webview)?;
233244 Ok(settings::view(&app))
234245 }
235246
236247 /// The settings window changed something: everything but opening at sign-in.
237248 #[tauri::command]
238249 pub fn save_settings(app: AppHandle, webview: Webview, settings: Settings) -> Result<settings::View, String> {
239− from_settings(&webview)?;
250+ from_settings_or_runner(&webview)?;
240251 settings::apply(&app, settings)?;
241252 Ok(settings::view(&app))
242253 }
264275 /// this computer and the settings window.
265276 #[tauri::command]
266277 pub fn runner_status(app: AppHandle, webview: Webview) -> Result<runner::View, String> {
267− from_site(&app, &webview).or_else(|_| from_settings(&webview))?;
278+ from_site(&app, &webview).or_else(|_| from_settings_or_runner(&webview)).or_else(|_| from_bar(&webview))?;
268279 Ok(runner::view(&app))
269280 }
270281
293304 /// of job (`capability`: `linux`, `windows`), Start and Stop are its alone.
294305 #[tauri::command]
295306 pub fn runner_control(app: AppHandle, webview: Webview, action: String, capability: Option<String>) -> Result<(), String> {
296− from_site(&app, &webview).or_else(|_| from_settings(&webview))?;
307+ from_site(&app, &webview).or_else(|_| from_settings_or_runner(&webview))?;
297308 runner::act(&app, &action, capability.as_deref())
298309 }
299310
+5−0
268268 crate::settings::open(app);
269269 return Ok(());
270270 }
271+ // The runner on its own: the chip beside the settings button.
272+ "runner" => {
273+ crate::settings::open_runner(app);
274+ return Ok(());
275+ }
271276 "focus" => {}
272277 other => return Err(format!("the bar cannot {other}")),
273278 }
+2−1
139139 pub fn handle(app: &AppHandle, id: &str) {
140140 match id {
141141 "open" => tray::show(app),
142− "settings" | "runner-status" => settings::open(app),
142+ "settings" => settings::open(app),
143+ "runner-status" => settings::open_runner(app),
143144 "runner-toggle" => crate::runner::toggle(app),
144145 "server" => tray::launcher(app, "change"),
145146 // The settings window asks, and shows the answer where it cannot be missed.
+53−0
833833 line
834834 }
835835
836+/// The runner in a few words, for the chip beside the title bar's settings
837+/// button, which opens the Runner window: `Runner running`, `Running 2 jobs`.
838+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
839+pub struct Chip {
840+ pub tone: Tone,
841+ pub label: String,
842+}
843+
844+/// The chip, from the whole runner's diagnosis and its kinds of job: what
845+/// it is doing, and whether anything there wants the person.
846+pub fn chip(diagnosis: &Diagnosis) -> Chip {
847+ let jobs = diagnosis.capabilities.iter().filter(|kind| kind.job.is_some()).count().max(usize::from(diagnosis.code == Code::Busy));
848+ let wanting = diagnosis.capabilities.iter().any(|kind| kind.enabled && matches!(kind.tone, Tone::Problem | Tone::Warning));
849+ let (tone, label) = match diagnosis.code {
850+ _ if jobs > 1 => (Tone::Working, format!("Running {jobs} jobs")),
851+ _ if jobs == 1 => (Tone::Working, "Running a job".to_owned()),
852+ Code::Idle => (if wanting { Tone::Warning } else { Tone::Ok }, "Runner running".to_owned()),
853+ Code::Starting | Code::Registering => (Tone::Working, "Runner starting".to_owned()),
854+ Code::Updating => (Tone::Working, "Runner updating".to_owned()),
855+ Code::UpdateFailed | Code::UpdateRefused => (Tone::Warning, "Runner running".to_owned()),
856+ Code::Off => (Tone::Off, "Runner off".to_owned()),
857+ Code::Stopped | Code::KindsStopped => (Tone::Off, "Runner stopped".to_owned()),
858+ Code::Offline => (Tone::Warning, "Runner offline".to_owned()),
859+ _ => (diagnosis.tone, "Runner needs you".to_owned()),
860+ };
861+ Chip { tone, label }
862+}
863+
836864 /// What the site says of this computer while g1t is closed on it.
837865 pub fn closed(facts: &Facts) -> Diagnosis {
838866 Diagnosis {
11591187 }
11601188
11611189 #[test]
1190+ fn the_title_bars_chip_says_the_runner_in_a_few_words() {
1191+ let said = |code: Code, facts: &Facts| {
1192+ let chip = chip(&diagnose(code, facts));
1193+ (chip.tone, chip.label)
1194+ };
1195+ assert_eq!(said(Code::Idle, &facts()), (Tone::Ok, "Runner running".into()));
1196+ assert_eq!(said(Code::Busy, &Facts { job: Some("build (acme/web)".into()), ..facts() }), (Tone::Working, "Running a job".into()));
1197+ assert_eq!(said(Code::Starting, &facts()), (Tone::Working, "Runner starting".into()));
1198+ assert_eq!(said(Code::Stopped, &facts()), (Tone::Off, "Runner stopped".into()));
1199+ assert_eq!(said(Code::Off, &facts()), (Tone::Off, "Runner off".into()));
1200+ assert_eq!(said(Code::NoWsl, &facts()), (Tone::Problem, "Runner needs you".into()));
1201+ assert_eq!(said(Code::UpdateFailed, &Facts { detail: Some("x".into()), ..facts() }).1, "Runner running");
1202+ // Both kinds of job busy at once.
1203+ let both = status(
1204+ r#"{"schema":1,"state":"running","capabilities":[
1205+ {"id":"linux","available":true,"state":"running","turned_off":false,"job":{"kind":"workflow","id":"a","name":"build","repo":"acme/web"}},
1206+ {"id":"windows","available":true,"state":"running","turned_off":false,"job":{"kind":"workflow","id":"b","name":"installer","repo":"acme/app"}}]}"#,
1207+ );
1208+ assert_eq!(said(Code::Busy, &Facts { sandbox: Some(both), live: true, ..facts() }), (Tone::Working, "Running 2 jobs".into()));
1209+ // Linux jobs run while Windows jobs can't start: running, and wanting a look.
1210+ let one_failing = status(NO_WSL_PRO);
1211+ assert_eq!(said(Code::Idle, &Facts { sandbox: Some(one_failing), live: true, ..facts() }), (Tone::Warning, "Runner running".into()));
1212+ }
1213+
1214+ #[test]
11621215 fn a_diagnosis_without_the_sandbox_lists_no_capabilities() {
11631216 let json = serde_json::to_value(diagnose(Code::Idle, &Facts { backend: "in Docker", ..facts() })).unwrap();
11641217 assert!(json.get("capabilities").is_none());
+15−5
4545 //! this computer is stopped between jobs and started on the new one.
4646 //! "Download updates by themselves" off, it looks only when asked.
4747 //! - **Seen.** What it is doing, and when it is not running why and how to
48−//! fix it (diagnosis.rs), is in the tray, in settings, and on the site's
49−//! Runners page (`g1t-runner-status`), read from what its log says
48+//! fix it (diagnosis.rs), is in the title bar's chip, the Runner window
49+//! (launcher/runner.html), the tray, a line in settings, and on the
50+//! site's Runners page (`g1t-runner-status`), read from what its log says
5051 //! (log.rs), or for the sandbox from its status. The whole log is kept
5152 //! in `runner/runner.log`.
5253 //!
209210 pub platform: String,
210211 /// The last lines of the runner's log.
211212 pub log: Vec<String>,
213+ /// In a few words, for the title bar's chip.
214+ pub chip: diagnosis::Chip,
212215 }
213216
214217 /// What the thread is told.
399402 let settings = config::settings(app);
400403 let facts = facts(app, &now);
401404 let server = config::server(app);
405+ let diagnosed = diagnosis::diagnose(now.code, &facts);
402406 View {
403− diagnosis: diagnosis::diagnose(now.code, &facts),
407+ chip: diagnosis::chip(&diagnosed),
408+ diagnosis: diagnosed,
404409 closed: diagnosis::closed(&facts),
405410 running: matches!(now.code, Code::Idle | Code::Busy | Code::Offline) || facts.sandbox.as_ref().is_some_and(Status::running),
406411 version: now.version,
434439 let view = view(app);
435440 tray::runner_changed(app, &view);
436441 let _ = app.emit_to(settings::SETTINGS, STATUS_EVENT, view.clone());
442+ let _ = app.emit_to(settings::RUNNER, STATUS_EVENT, view.clone());
443+ let _ = app.emit_to(EventTarget::webview(frame::BAR), STATUS_EVENT, view.chip.clone());
437444 let _ = app.emit_to(EventTarget::webview(frame::SITE), STATUS_EVENT, view);
438445 }
439446
552559 Some(workspace) => crate::links::go(app, &format!("/{workspace}/-/runners")),
553560 None => crate::links::go(app, "/"),
554561 },
555− "choose_workspace" | "choose_backend" => settings::open(app),
562+ // They are chosen in the Runner window.
563+ "choose_workspace" | "choose_backend" | "open_runner" => settings::open_runner(app),
556564 "view_log" => {
557565 let path = log_file(app);
558566 if !path.exists() {
21132121
21142122 fn view(code: Code, job: Option<&str>) -> View {
21152123 let facts = Facts { workspace: Some("acme".into()), backend: "sandboxed", machine: "box".into(), job: job.map(str::to_owned), ..Facts::default() };
2124+ let diagnosed = diagnosis::diagnose(code, &facts);
21162125 View {
2117− diagnosis: diagnosis::diagnose(code, &facts),
2126+ chip: diagnosis::chip(&diagnosed),
2127+ diagnosis: diagnosed,
21182128 closed: diagnosis::closed(&facts),
21192129 running: code == Code::Idle,
21202130 version: None,
+47−18
1414
1515 /// The settings window's label.
1616 pub const SETTINGS: &str = "settings";
17+/// The Runner window's label: the runner on its own, apart from the
18+/// settings, opened from the title bar's chip, the tray or the menu
19+/// (launcher/runner.html).
20+pub const RUNNER: &str = "runner";
1721
1822 /// The argument the system starts g1t with at sign-in, so it can tell a
1923 /// start of its own from someone opening it.
217221 show(app, true);
218222 }
219223
224+/// Opens the Runner window, or brings it forward.
225+pub fn open_runner(app: &AppHandle) {
226+ if !forward(app, RUNNER) {
227+ made_aside(app, Window { label: RUNNER, page: "runner.html", title: "g1t runner", size: (720.0, 760.0) });
228+ }
229+}
230+
231+/// Brings a window that is open forward: false when it isn't.
232+fn forward(app: &AppHandle, label: &str) -> bool {
233+ let Some(window) = app.get_webview_window(label) else { return false };
234+ let _ = window.unminimize();
235+ let _ = window.show();
236+ let _ = window.set_focus();
237+ true
238+}
239+
220240 fn show(app: &AppHandle, check: bool) {
221− if let Some(window) = app.get_webview_window(SETTINGS) {
222− let _ = window.unminimize();
223− let _ = window.show();
224− let _ = window.set_focus();
241+ if forward(app, SETTINGS) {
225242 if check {
226243 let _ = app.emit_to(SETTINGS, CHECK_EVENT, ());
227244 }
228245 return;
229246 }
230− // Made on a thread of its own, whoever asks. Settings opens from
231− // synchronous commands too (the title bar's button, the site's account
232− // menu), which run on the main thread, and on Windows a window made
233− // there deadlocks: it appears, white, and its page never loads. From
234− // another thread, Tauri hands the making to the event loop and waits.
247+ // A window still loading has no listener yet: the address asks instead.
248+ let page = if check { "settings.html?check" } else { "settings.html" };
249+ made_aside(app, Window { label: SETTINGS, page, title: "g1t settings", size: (620.0, 720.0) });
250+}
251+
252+/// A window of the app's own pages.
253+struct Window {
254+ label: &'static str,
255+ page: &'static str,
256+ title: &'static str,
257+ size: (f64, f64),
258+}
259+
260+/// Makes a window on a thread of its own, whoever asks. The app's windows
261+/// open from synchronous commands too (the title bar's buttons, the site's
262+/// account menu), which run on the main thread, and on Windows a window
263+/// made there deadlocks: it appears, white, and its page never loads. From
264+/// another thread, Tauri hands the making to the event loop and waits.
265+fn made_aside(app: &AppHandle, window: Window) {
235266 let app = app.clone();
236− let spawned = std::thread::Builder::new().name("g1t-settings-window".into()).spawn(move || build(&app, check));
267+ let spawned = std::thread::Builder::new().name(format!("g1t-{}-window", window.label)).spawn(move || build(&app, &window));
237268 if let Err(error) = spawned {
238− eprintln!("g1t: the settings window could not open: {error}");
269+ eprintln!("g1t: a window could not open: {error}");
239270 }
240271 }
241272
242−fn build(app: &AppHandle, check: bool) {
243− // A window still loading has no listener yet: the address asks instead.
244− let page = if check { "settings.html?check" } else { "settings.html" };
273+fn build(app: &AppHandle, window: &Window) {
245274 let custom = crate::frame::started(app);
246− let builder = WebviewWindowBuilder::new(app, SETTINGS, WebviewUrl::App(page.into()))
247− .title("g1t settings");
275+ let builder = WebviewWindowBuilder::new(app, window.label, WebviewUrl::App(window.page.into()))
276+ .title(window.title);
248277 let built = crate::frame::dialog(builder, custom)
249− .inner_size(620.0, 720.0)
278+ .inner_size(window.size.0, window.size.1)
250279 .min_inner_size(440.0, 420.0)
251280 .maximizable(false)
252281 .minimizable(false)
258287 Ok(window) => {
259288 let _ = window.set_focus();
260289 }
261− Err(error) => eprintln!("g1t: the settings window could not open: {error}"),
290+ Err(error) => eprintln!("g1t: the {} window could not open: {error}", window.label),
262291 }
263292 }
264293
+29−13
9090 | **☰** (Windows and Linux) | Opens the app's [menu](#the-menu). |
9191 | **← →** | Back and forward through the pages you have been on. |
9292 | **Search** | In the middle: searches your workspace, as **Ctrl+K** or **⌘K** does. Before you are signed in, it shows the page's title. |
93+| **The runner** | At the right, beside **Settings**: what [the runner](#the-runner) is doing in a few words, with a dot in its colour (*Runner running*, *Running a job*, *Running 2 jobs*, *Runner starting*, *Runner stopped*, *Runner needs you*), live. It opens [the Runner window](#the-runner-window). In a narrow window only its dot shows. |
94+| **Settings** | Opens [the settings](#settings). |
9395 | **Minimize, maximize, close** (Windows and Linux) | At the right. Close keeps g1t in the tray unless [the settings](#settings) say it quits. On macOS the window's own buttons sit at the left of the bar. |
9496
9597 Drag the bar to move the window, and double-click it to maximize or
137139 | **Use the system's title bar** | Off | The system's title bar in place of [g1t's own](#the-title-bar), with the menu bar under it on Windows and Linux. Takes effect when g1t restarts. |
138140 | **Zoom** | 100% | How large g1t is drawn, from 67% to 200%. The View menu changes it too. |
139141 | **Download updates by themselves** | On | Off, the app never checks on its own; **Check for updates** still does. The runner's updates follow it too: off, only **Check for runner updates** looks for a newer runner. |
140−| **Run jobs on this computer while g1t is open** | On | Starts [the runner](#the-runner) a few seconds after the window. Off, nothing runs. |
141−| **How jobs run** | Sandboxed | Sandboxed, in Docker, or directly on this computer. See [how jobs run](#how-jobs-run). |
142−| **Linux jobs**, **Windows jobs** (sandboxed) | Both on | Each kind of job's card has its own **Stop** and **Start**, which leave the other kind as it is. See [the runner](#the-runner). |
143−| **Workspace** | The one you were in when it first registered | The workspace whose jobs the runner runs. Lists the workspaces you own. |
144142
145143 The settings window also shows the app's version and whether it is up to
146144 date, with **Check for updates** (see [Updates](#updates)), and the server it opens, with **Change…** to [use another
147−g1t](#your-own-g1t).
145+g1t](#your-own-g1t). The runner is a line there, what it is doing with
146+**Open runner**: its own settings are in [the Runner window](#the-runner-window).
148147
149148 ## The runner
150149
158157 To stop it, press **Stop running jobs** in the tray; it starts again with
159158 **Start running jobs**, or the next time g1t opens. To keep it off, turn
160159 off **Run jobs on this computer while g1t is open** in
161−[the settings](#settings).
160+[the Runner window](#the-runner-window).
162161
163−Sandboxed, each kind of job is a card of its own in the settings' Runner
164−section, **Linux jobs** and **Windows jobs**, each with its own **Stop**
162+Sandboxed, each kind of job is a card of its own in the Runner window, **Linux jobs** and **Windows jobs**, each with its own **Stop**
165163 and **Start**. Stopping Windows jobs leaves Linux jobs running, and the
166164 other way round; a kind you stop stays stopped, across restarts, until you
167165 start it again. Both are on until you stop one.
175173 `-windows`: `build-box-linux` has `linux`, and `build-box-windows` has
176174 `windows`, on a Windows computer with Windows Sandbox.
177175
176+### The Runner window
177+
178+The runner has a window of its own, apart from the settings, as Docker
179+Desktop has for its engine: open it from the chip beside **Settings** in
180+the title bar, from the runner's row in the tray, or with **Open runner**
181+in the settings. It shows what the runner is doing as it happens, and keeps
182+its settings:
183+
184+| Setting | Default | What it does |
185+| --- | --- | --- |
186+| **Run jobs on this computer while g1t is open** | On | Starts the runner a few seconds after the window. Off, nothing runs. |
187+| **How jobs run** | Sandboxed | Sandboxed, in Docker, or directly on this computer. See [how jobs run](#how-jobs-run). |
188+| **Linux jobs**, **Windows jobs** (sandboxed) | Both on | Each kind of job's card has its own **Stop** and **Start**, which leave the other kind as it is. |
189+| **Workspace** | The one you were in when it first registered | The workspace whose jobs the runner runs. Lists the workspaces you own. |
190+
191+[Seeing it](#seeing-it) says what else it shows.
192+
178193 ### How jobs run
179194
180−**How jobs run** in the settings decides what a job on your computer can
195+**How jobs run** in the Runner window decides what a job on your computer can
181196 reach. The app never changes it by itself: when the one you chose cannot
182197 start, it says why and runs nothing.
183198
236251 from its registration, signed in or not.
237252
238253 The workspace is the one you were in when it first registered; choose
239−another under **Workspace** in the settings. Changing it, or how jobs
254+another under **Workspace** in the Runner window. Changing it, or how jobs
240255 run, waits for a job that is running to end, then removes the old
241256 registration and makes a new one. The sandbox is registered with one
242257 workspace, so moving it to another removes it and makes it again, image
267282
268283 Meanwhile each card says what is waiting: *0.3.2 ready, installs after
269284 this job*. **Check for runner updates**, beside your computer's name in
270−the settings, looks at once, and installs what it finds the same way. An
285+the Runner window, looks at once, and installs what it finds the same way. An
271286 update that fails leaves the runner as it was, and the next hour's look
272287 tries again. Turning off **Download updates by themselves** stops the
273288 hourly look; **Check for runner updates** still looks, and installs.
276291
277292 | Where | What it shows |
278293 | --- | --- |
279−| The tray | A row with what the runner is doing, *Runner: Waiting for jobs*, which opens the settings; **Stop running jobs** or **Start running jobs** under it. The icon's hover text says it too. |
280−| The settings window | Sandboxed, a card for each kind of job this computer can have, side by side: its state (*Running a job*, *Waiting for jobs*, *Starting*, *Downloading image* with how far, *Updating*, *Waiting for image*, *Stopped*, *Not available*, *Problem*), what it is, the job it is running with a link to it, and when it isn't running why and the fix, with the fix's buttons and its own **Stop** or **Start**; below, its runner's name on g1t, version, updates (when the app last looked, *Checked 12 min ago*, and a newer one waiting for it, *0.3.2 ready, installs after this job*) and isolation. Above the cards, only what is about the whole runner: signed out, no workspace, turned off, stopped, the sandbox down. Then how jobs run; the workspace; your computer's name, the runner's version and when the app last looked for a newer one, with **Check for runner updates** and **View on g1t**; and the runner's recent log, with **Open the whole log**. In Docker or directly on this computer, one line says what it is doing, or why it isn't running and the fix. |
294+| The title bar | Its chip, beside **Settings**: *Runner running*, *Running a job*, with a dot in its colour, which opens the Runner window. |
295+| The tray | A row with what the runner is doing, *Runner: Waiting for jobs*, which opens the Runner window; **Stop running jobs** or **Start running jobs** under it. The icon's hover text says it too. |
296+| [The Runner window](#the-runner-window) | At the top, what the whole runner is doing, as the chip says it, with what it is running, its version and workspace, and its own **Stop** or **Start**. Below, sandboxed, a card for each kind of job this computer can have, side by side: its state (*Running a job*, *Waiting for jobs*, *Starting*, *Downloading image* with how far, *Updating*, *Waiting for image*, *Stopped*, *Not available*, *Problem*), what it is, the job it is running with a link to it, and when it isn't running why and the fix, with the fix's buttons and its own **Stop** or **Start**; below, its runner's name on g1t, version, updates (when the app last looked, *Checked 12 min ago*, and a newer one waiting for it, *0.3.2 ready, installs after this job*) and isolation. Above the cards, only what is about the whole runner: signed out, no workspace, turned off, stopped, the sandbox down. Then its setup: **Run jobs on this computer while g1t is open**, how jobs run, the workspace, and your computer's name, the runner's version and when the app last looked for a newer one, with **Check for runner updates** and **View on g1t**. Last, its log, following along as it is written, with **Open the whole log**. In Docker or directly on this computer, one line says what it is doing, or why it isn't running and the fix. |
281297 | The workspace's [Runners page](/guides/self-hosted-runners/#the-runners-page) | **Desktops**: every computer with the app in the workspace, with what each is doing, or why it isn't running and the fix, in the same words; a sandboxed one's kinds of job as cards of their own, as in the settings, with a link to the run each is running, its version, when the app last looked for updates and a newer one waiting. Inside the app, this computer comes first, live, with its buttons, each kind's **Stop** or **Start** and **Check for runner updates** among them. A computer whose g1t is closed says *g1t is closed on* its name. |
282298
283299 ### When it isn't running

Comments

Sign in to comment.