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