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