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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user