Own the network: details, VPN, enterprise Wi-Fi, and a firewall that can also allow

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 16:31:52 -04:00
parent aba2d16ffa
commit b30bf40407
29 changed files with 4452 additions and 241 deletions
+105
View File
@@ -74,6 +74,12 @@ Singleton {
return count;
}
// printer name -> the helper's get-options shape. Cached because a pair of
// dropdowns asks for the same printer on every repaint, and each answer is
// a round trip to CUPS.
property var options: ({})
property var pendingOptions: []
function refresh(): void {
if (query.running)
return;
@@ -81,6 +87,48 @@ Singleton {
query.running = true;
}
// Paper size and two-sided, as the printer reports them. Returns the cached
// answer, or null while the first one is on its way -- and asks for it, so
// a dropdown that binds to this fills itself in.
function optionsFor(name: string): var {
if (name === "")
return null;
if (root.options[name] !== undefined)
return root.options[name];
root.requestOptions(name);
return null;
}
function requestOptions(name: string): void {
if (name === "" || root.pendingOptions.indexOf(name) >= 0)
return;
root.pendingOptions = root.pendingOptions.concat([name]);
root.drainOptions();
}
function refreshOptions(name: string): void { root.requestOptions(name); }
function drainOptions(): void {
if (optionsQuery.running || root.pendingOptions.length === 0)
return;
optionsQuery.subject = root.pendingOptions[0];
optionsQuery.command = [root.helperPath, "get-options", optionsQuery.subject];
optionsQuery.running = true;
}
// Reassigned rather than mutated: QML does not notice a property change
// made inside a var object.
function absorbOptions(parsed: var): void {
const name = String(parsed?.printer ?? "");
if (name === "")
return;
const next = Object.assign({}, root.options);
next[name] = parsed;
root.options = next;
if (String(parsed.error ?? "") !== "")
root.lastError = String(parsed.error);
}
function search(): void {
if (root.searching)
return;
@@ -96,6 +144,11 @@ Singleton {
root.jobs = Array.isArray(parsed.jobs) ? parsed.jobs : [];
root.service = parsed.service ?? ({});
root.lastError = String(parsed.error ?? "");
// set-option answers with the snapshot AND the printer it touched,
// re-read -- so the dropdown that made the change updates from the
// reply rather than from a second round trip.
if (parsed.options)
root.absorbOptions(parsed.options);
} catch (error) {
root.lastError = "Could not read the printing service's answer.";
console.warn("Printers: could not parse helper output:", error);
@@ -119,6 +172,23 @@ Singleton {
function cancel(jobId: int): void { root.run(["cancel", String(jobId)]); }
function testPage(name: string): void { root.run(["test-page", name]); }
// Holding keeps the job in the queue; cancelling throws it away. The
// difference matters because "stop this print" and "reprint fifty pages"
// are not meant to be the same button.
function hold(jobId: int): void { root.run(["hold", String(jobId)]); }
function release(jobId: int): void { root.run(["release", String(jobId)]); }
// One printer default, from the helper's closed vocabulary: media is
// Letter, A4 or Legal; sides is one-sided or one of the two two-sided
// bindings. Anything else the helper refuses -- there is no passthrough to
// lpadmin here.
function setOption(name: string, key: string, value: string): void {
root.run(["set-option", name, key, value]);
}
// Whether a job is held, from the state the helper reports.
function isHeld(job: var): bool { return String(job?.state ?? "") === "held"; }
// A queue name CUPS will accept, derived from what the printer calls itself.
function suggestedName(label: string): string {
const cleaned = String(label).replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "");
@@ -143,6 +213,41 @@ Singleton {
}
}
// Kept out of `busy`: reading a printer's defaults changes nothing, so it
// must not disable the controls that do.
Process {
id: optionsQuery
property string subject: ""
stdout: StdioCollector {
onStreamFinished: {
try {
root.absorbOptions(JSON.parse(this.text));
} catch (error) {
root.lastError = "Could not read that printer's settings.";
console.warn("Printers: could not parse get-options output:", error);
}
}
}
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
onExited: {
root.pendingOptions = root.pendingOptions.filter(
name => name !== optionsQuery.subject);
optionsDrain.restart();
}
}
// One tick later, because `running` has not gone false inside onExited and
// the queue would stall on its own guard.
Timer {
id: optionsDrain
interval: 0
onTriggered: root.drainOptions()
}
Process {
id: discovery
stdout: StdioCollector {