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.
18 files+1007−4840/18 viewed
| 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"> </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"> </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"> </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> |
| 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 | + | })(); |
| 115 | 115 | padding: 0.75rem 0; | |
| 116 | 116 | border-bottom: 1px solid var(--line); | |
| 117 | 117 | } | |
| 118 | + | /* A row hidden until it applies, such as Restart to change the title bar. */ | |
| 119 | + | .setting[hidden] { | |
| 120 | + | display: none; | |
| 121 | + | } | |
| 118 | 122 | section .setting:last-child { | |
| 119 | 123 | border-bottom: 0; | |
| 120 | 124 | } | |
| ⋯ | |||
| 632 | 636 | overflow-wrap: anywhere; | |
| 633 | 637 | user-select: text; | |
| 634 | 638 | } | |
| 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 | + | } | |
| 46 | 46 | ||
| 47 | 47 | <section aria-labelledby="runner-heading" id="runner-section"> | |
| 48 | 48 | <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"> </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). --> | |
| 84 | 50 | <div class="setting"> | |
| 85 | − | <span><strong id="runner-machine">This computer</strong><span class="muted" id="runner-version"> </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 | |
| 89 | 54 | > | |
| 55 | + | <span class="buttons"><button type="button" id="open-runner">Open runner</button></span> | |
| 90 | 56 | </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> | |
| 96 | 57 | </section> | |
| 97 | 58 | ||
| 98 | 59 | <section aria-labelledby="notifications-heading"> |
| 1 | 1 | // The settings window's work: show what the app has, and keep each change | |
| 2 | 2 | // 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. | |
| 4 | 5 | (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"]); | |
| 18 | 9 | var current = null; | |
| 19 | 10 | ||
| 20 | 11 | function byId(id) { | |
| ⋯ | |||
| 72 | 63 | renderRunner(view.runnerView); | |
| 73 | 64 | } | |
| 74 | 65 | ||
| 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) { | |
| 76 | 77 | var settings = {}; | |
| 78 | + | KEPT.forEach(function (key) { | |
| 79 | + | if (key in fresh) settings[key] = fresh[key]; | |
| 80 | + | }); | |
| 77 | 81 | SWITCHES.forEach(function (key) { | |
| 78 | 82 | settings[key] = byId(key).checked; | |
| 79 | 83 | }); | |
| 80 | 84 | 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; | |
| 85 | 85 | return settings; | |
| 86 | 86 | } | |
| 87 | 87 | ||
| 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 | − | } | |
| 97 | 88 | ||
| 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); | |
| 134 | 97 | }); | |
| 135 | − | return into; | |
| 136 | 98 | } | |
| 137 | 99 | ||
| 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); | |
| 456 | 102 | }); | |
| 457 | 103 | ||
| 458 | − | function save() { | |
| 459 | − | invoke("save_settings", { settings: settingsOf() }).then(render, function (error) { | |
| 460 | − | fail(error); | |
| 461 | − | if (current) render(current); | |
| 462 | − | }); | |
| 463 | − | } | |
| 464 | − | ||
| 465 | 104 | SWITCHES.forEach(function (key) { | |
| 466 | 105 | byId(key).addEventListener("change", save); | |
| 467 | 106 | }); | |
| ⋯ | |||
| 541 | 180 | events | |
| 542 | 181 | .listen("g1t-runner-status", function (event) { | |
| 543 | 182 | if (current) current.runnerView = event.payload; | |
| 544 | − | pending = {}; | |
| 545 | 183 | renderRunner(event.payload); | |
| 546 | 184 | }) | |
| 547 | 185 | .catch(function () {}); | |
| 40 | 40 | align-items: center; | |
| 41 | 41 | gap: 2px; | |
| 42 | 42 | flex: 1 1 0; | |
| 43 | − | min-width: 0; | |
| 43 | + | /* Never narrower than its buttons: the search shrinks instead. */ | |
| 44 | + | min-width: min-content; | |
| 44 | 45 | } | |
| 45 | 46 | .end { | |
| 46 | 47 | justify-content: flex-end; | |
| ⋯ | |||
| 112 | 113 | outline: 2px solid var(--accent); | |
| 113 | 114 | outline-offset: -2px; | |
| 114 | 115 | } | |
| 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 | + | } | |
| 79 | 79 | }); | |
| 80 | 80 | left.append(back, forward); | |
| 81 | 81 | ||
| 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. | |
| 84 | 104 | right.append( | |
| 105 | + | chip, | |
| 85 | 106 | button("icon", "Settings", "settings", function () { | |
| 86 | 107 | act("settings"); | |
| 87 | 108 | }), | |
| ⋯ | |||
| 180 | 201 | .catch(function () {}); | |
| 181 | 202 | } | |
| 182 | 203 | 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 () {}); | |
| 183 | 214 | })(); | |
| 1330 | 1330 | ||
| 1331 | 1331 | [[package]] | |
| 1332 | 1332 | name = "g1t-desktop" | |
| 1333 | − | version = "0.3.3" | |
| 1333 | + | version = "0.3.4" | |
| 1334 | 1334 | dependencies = [ | |
| 1335 | 1335 | "base64 0.22.1", | |
| 1336 | 1336 | "ed25519-dalek", |
| 1 | 1 | [package] | |
| 2 | 2 | name = "g1t-desktop" | |
| 3 | − | version = "0.3.3" | |
| 3 | + | version = "0.3.4" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | license = "MIT" | |
| 6 | 6 | repository = "https://g1t.sh/flagon-io/g1t" |
| 15 | 15 | "allow-window-menu", | |
| 16 | 16 | "allow-title-bar-page", | |
| 17 | 17 | "allow-title-bar-current", | |
| 18 | − | "allow-title-bar-act" | |
| 18 | + | "allow-title-bar-act", | |
| 19 | + | "allow-runner-status" | |
| 19 | 20 | ] | |
| 20 | 21 | } |
| 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 | + | } |
| 59 | 59 | if webview.label() == settings::SETTINGS { Ok(()) } else { Err("only the settings window may ask that".into()) } | |
| 60 | 60 | } | |
| 61 | 61 | ||
| 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 | + | ||
| 62 | 73 | fn from_either(app: &AppHandle, webview: &Webview) -> Result<(), String> { | |
| 63 | 74 | from_site(app, webview).or_else(|_| from_launcher(webview)) | |
| 64 | 75 | } | |
| ⋯ | |||
| 77 | 88 | if webview.label() == frame::BAR { Ok(()) } else { Err("only the title bar may ask that".into()) } | |
| 78 | 89 | } | |
| 79 | 90 | ||
| 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. | |
| 81 | 92 | 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)) | |
| 83 | 94 | } | |
| 84 | 95 | ||
| 85 | 96 | #[tauri::command] | |
| ⋯ | |||
| 229 | 240 | /// What the settings window shows. | |
| 230 | 241 | #[tauri::command] | |
| 231 | 242 | pub fn get_settings(app: AppHandle, webview: Webview) -> Result<settings::View, String> { | |
| 232 | − | from_settings(&webview)?; | |
| 243 | + | from_settings_or_runner(&webview)?; | |
| 233 | 244 | Ok(settings::view(&app)) | |
| 234 | 245 | } | |
| 235 | 246 | ||
| 236 | 247 | /// The settings window changed something: everything but opening at sign-in. | |
| 237 | 248 | #[tauri::command] | |
| 238 | 249 | pub fn save_settings(app: AppHandle, webview: Webview, settings: Settings) -> Result<settings::View, String> { | |
| 239 | − | from_settings(&webview)?; | |
| 250 | + | from_settings_or_runner(&webview)?; | |
| 240 | 251 | settings::apply(&app, settings)?; | |
| 241 | 252 | Ok(settings::view(&app)) | |
| 242 | 253 | } | |
| ⋯ | |||
| 264 | 275 | /// this computer and the settings window. | |
| 265 | 276 | #[tauri::command] | |
| 266 | 277 | 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))?; | |
| 268 | 279 | Ok(runner::view(&app)) | |
| 269 | 280 | } | |
| 270 | 281 | ||
| ⋯ | |||
| 293 | 304 | /// of job (`capability`: `linux`, `windows`), Start and Stop are its alone. | |
| 294 | 305 | #[tauri::command] | |
| 295 | 306 | 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))?; | |
| 297 | 308 | runner::act(&app, &action, capability.as_deref()) | |
| 298 | 309 | } | |
| 299 | 310 | ||
| 268 | 268 | crate::settings::open(app); | |
| 269 | 269 | return Ok(()); | |
| 270 | 270 | } | |
| 271 | + | // The runner on its own: the chip beside the settings button. | |
| 272 | + | "runner" => { | |
| 273 | + | crate::settings::open_runner(app); | |
| 274 | + | return Ok(()); | |
| 275 | + | } | |
| 271 | 276 | "focus" => {} | |
| 272 | 277 | other => return Err(format!("the bar cannot {other}")), | |
| 273 | 278 | } |
| 139 | 139 | pub fn handle(app: &AppHandle, id: &str) { | |
| 140 | 140 | match id { | |
| 141 | 141 | "open" => tray::show(app), | |
| 142 | − | "settings" | "runner-status" => settings::open(app), | |
| 142 | + | "settings" => settings::open(app), | |
| 143 | + | "runner-status" => settings::open_runner(app), | |
| 143 | 144 | "runner-toggle" => crate::runner::toggle(app), | |
| 144 | 145 | "server" => tray::launcher(app, "change"), | |
| 145 | 146 | // The settings window asks, and shows the answer where it cannot be missed. |
| 833 | 833 | line | |
| 834 | 834 | } | |
| 835 | 835 | ||
| 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 | + | ||
| 836 | 864 | /// What the site says of this computer while g1t is closed on it. | |
| 837 | 865 | pub fn closed(facts: &Facts) -> Diagnosis { | |
| 838 | 866 | Diagnosis { | |
| ⋯ | |||
| 1159 | 1187 | } | |
| 1160 | 1188 | ||
| 1161 | 1189 | #[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] | |
| 1162 | 1215 | fn a_diagnosis_without_the_sandbox_lists_no_capabilities() { | |
| 1163 | 1216 | let json = serde_json::to_value(diagnose(Code::Idle, &Facts { backend: "in Docker", ..facts() })).unwrap(); | |
| 1164 | 1217 | assert!(json.get("capabilities").is_none()); | |
| 45 | 45 | //! this computer is stopped between jobs and started on the new one. | |
| 46 | 46 | //! "Download updates by themselves" off, it looks only when asked. | |
| 47 | 47 | //! - **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 | |
| 50 | 51 | //! (log.rs), or for the sandbox from its status. The whole log is kept | |
| 51 | 52 | //! in `runner/runner.log`. | |
| 52 | 53 | //! | |
| ⋯ | |||
| 209 | 210 | pub platform: String, | |
| 210 | 211 | /// The last lines of the runner's log. | |
| 211 | 212 | pub log: Vec<String>, | |
| 213 | + | /// In a few words, for the title bar's chip. | |
| 214 | + | pub chip: diagnosis::Chip, | |
| 212 | 215 | } | |
| 213 | 216 | ||
| 214 | 217 | /// What the thread is told. | |
| ⋯ | |||
| 399 | 402 | let settings = config::settings(app); | |
| 400 | 403 | let facts = facts(app, &now); | |
| 401 | 404 | let server = config::server(app); | |
| 405 | + | let diagnosed = diagnosis::diagnose(now.code, &facts); | |
| 402 | 406 | View { | |
| 403 | − | diagnosis: diagnosis::diagnose(now.code, &facts), | |
| 407 | + | chip: diagnosis::chip(&diagnosed), | |
| 408 | + | diagnosis: diagnosed, | |
| 404 | 409 | closed: diagnosis::closed(&facts), | |
| 405 | 410 | running: matches!(now.code, Code::Idle | Code::Busy | Code::Offline) || facts.sandbox.as_ref().is_some_and(Status::running), | |
| 406 | 411 | version: now.version, | |
| ⋯ | |||
| 434 | 439 | let view = view(app); | |
| 435 | 440 | tray::runner_changed(app, &view); | |
| 436 | 441 | 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()); | |
| 437 | 444 | let _ = app.emit_to(EventTarget::webview(frame::SITE), STATUS_EVENT, view); | |
| 438 | 445 | } | |
| 439 | 446 | ||
| ⋯ | |||
| 552 | 559 | Some(workspace) => crate::links::go(app, &format!("/{workspace}/-/runners")), | |
| 553 | 560 | None => crate::links::go(app, "/"), | |
| 554 | 561 | }, | |
| 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), | |
| 556 | 564 | "view_log" => { | |
| 557 | 565 | let path = log_file(app); | |
| 558 | 566 | if !path.exists() { | |
| ⋯ | |||
| 2113 | 2121 | ||
| 2114 | 2122 | fn view(code: Code, job: Option<&str>) -> View { | |
| 2115 | 2123 | 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); | |
| 2116 | 2125 | View { | |
| 2117 | − | diagnosis: diagnosis::diagnose(code, &facts), | |
| 2126 | + | chip: diagnosis::chip(&diagnosed), | |
| 2127 | + | diagnosis: diagnosed, | |
| 2118 | 2128 | closed: diagnosis::closed(&facts), | |
| 2119 | 2129 | running: code == Code::Idle, | |
| 2120 | 2130 | version: None, | |
| 14 | 14 | ||
| 15 | 15 | /// The settings window's label. | |
| 16 | 16 | 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"; | |
| 17 | 21 | ||
| 18 | 22 | /// The argument the system starts g1t with at sign-in, so it can tell a | |
| 19 | 23 | /// start of its own from someone opening it. | |
| ⋯ | |||
| 217 | 221 | show(app, true); | |
| 218 | 222 | } | |
| 219 | 223 | ||
| 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 | + | ||
| 220 | 240 | 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) { | |
| 225 | 242 | if check { | |
| 226 | 243 | let _ = app.emit_to(SETTINGS, CHECK_EVENT, ()); | |
| 227 | 244 | } | |
| 228 | 245 | return; | |
| 229 | 246 | } | |
| 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) { | |
| 235 | 266 | 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)); | |
| 237 | 268 | if let Err(error) = spawned { | |
| 238 | − | eprintln!("g1t: the settings window could not open: {error}"); | |
| 269 | + | eprintln!("g1t: a window could not open: {error}"); | |
| 239 | 270 | } | |
| 240 | 271 | } | |
| 241 | 272 | ||
| 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) { | |
| 245 | 274 | 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); | |
| 248 | 277 | let built = crate::frame::dialog(builder, custom) | |
| 249 | − | .inner_size(620.0, 720.0) | |
| 278 | + | .inner_size(window.size.0, window.size.1) | |
| 250 | 279 | .min_inner_size(440.0, 420.0) | |
| 251 | 280 | .maximizable(false) | |
| 252 | 281 | .minimizable(false) | |
| ⋯ | |||
| 258 | 287 | Ok(window) => { | |
| 259 | 288 | let _ = window.set_focus(); | |
| 260 | 289 | } | |
| 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), | |
| 262 | 291 | } | |
| 263 | 292 | } | |
| 264 | 293 | ||
| 90 | 90 | | **☰** (Windows and Linux) | Opens the app's [menu](#the-menu). | | |
| 91 | 91 | | **← →** | Back and forward through the pages you have been on. | | |
| 92 | 92 | | **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). | | |
| 93 | 95 | | **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. | | |
| 94 | 96 | ||
| 95 | 97 | Drag the bar to move the window, and double-click it to maximize or | |
| ⋯ | |||
| 137 | 139 | | **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. | | |
| 138 | 140 | | **Zoom** | 100% | How large g1t is drawn, from 67% to 200%. The View menu changes it too. | | |
| 139 | 141 | | **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. | | |
| 144 | 142 | ||
| 145 | 143 | The settings window also shows the app's version and whether it is up to | |
| 146 | 144 | 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). | |
| 148 | 147 | ||
| 149 | 148 | ## The runner | |
| 150 | 149 | ||
| ⋯ | |||
| 158 | 157 | To stop it, press **Stop running jobs** in the tray; it starts again with | |
| 159 | 158 | **Start running jobs**, or the next time g1t opens. To keep it off, turn | |
| 160 | 159 | off **Run jobs on this computer while g1t is open** in | |
| 161 | − | [the settings](#settings). | |
| 160 | + | [the Runner window](#the-runner-window). | |
| 162 | 161 | ||
| 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** | |
| 165 | 163 | and **Start**. Stopping Windows jobs leaves Linux jobs running, and the | |
| 166 | 164 | other way round; a kind you stop stays stopped, across restarts, until you | |
| 167 | 165 | start it again. Both are on until you stop one. | |
| ⋯ | |||
| 175 | 173 | `-windows`: `build-box-linux` has `linux`, and `build-box-windows` has | |
| 176 | 174 | `windows`, on a Windows computer with Windows Sandbox. | |
| 177 | 175 | ||
| 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 | + | ||
| 178 | 193 | ### How jobs run | |
| 179 | 194 | ||
| 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 | |
| 181 | 196 | reach. The app never changes it by itself: when the one you chose cannot | |
| 182 | 197 | start, it says why and runs nothing. | |
| 183 | 198 | ||
| ⋯ | |||
| 236 | 251 | from its registration, signed in or not. | |
| 237 | 252 | ||
| 238 | 253 | 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 | |
| 240 | 255 | run, waits for a job that is running to end, then removes the old | |
| 241 | 256 | registration and makes a new one. The sandbox is registered with one | |
| 242 | 257 | workspace, so moving it to another removes it and makes it again, image | |
| ⋯ | |||
| 267 | 282 | ||
| 268 | 283 | Meanwhile each card says what is waiting: *0.3.2 ready, installs after | |
| 269 | 284 | 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 | |
| 271 | 286 | update that fails leaves the runner as it was, and the next hour's look | |
| 272 | 287 | tries again. Turning off **Download updates by themselves** stops the | |
| 273 | 288 | hourly look; **Check for runner updates** still looks, and installs. | |
| ⋯ | |||
| 276 | 291 | ||
| 277 | 292 | | Where | What it shows | | |
| 278 | 293 | | --- | --- | | |
| 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. | | |
| 281 | 297 | | 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. | | |
| 282 | 298 | ||
| 283 | 299 | ### When it isn't running | |
Comments
Sign in to comment.