def run-command [command: string, args: list] { let result = (run-external $command ...$args | complete) { ok: ($result.exit_code == 0) output: ($result.stdout | str trim) error: ($result.stderr | str trim) } } def first-line [text: string] { let line = ($text | lines | first) if $line == null { "" } else { $line } } def style [text: string, color: string] { $"(ansi $color)($text)(ansi reset)" } def screen [] { print -n $"(ansi cls)(ansi home)" } def key [] { let event = (input listen --types [key]) $event.code } def default-printer [] { let result = (run-command "lpstat" ["-d"]) if not $result.ok { return "" } try { $result.output | parse --regex '^system default destination: (?\S+)$' | get 0.name } catch { "" } } def printer-names [] { let result = (run-command "lpstat" ["-p"]) if not $result.ok { error make {msg: ($result.error | default "lpstat failed")} } $result.output | lines | where {|line| $line | str starts-with "printer "} | each {|line| try { $line | parse --regex '^printer (?\S+) (?.*)$' | get 0.name } catch { null } } | where {|name| $name != null} } def printer-summary [name: string] { let result = (run-command "lpstat" ["-p" $name]) let line = (first-line $result.output) try { $line | parse --regex '^printer \S+ (?.*)$' | get 0.summary } catch { if $result.error == "" { $line } else { $result.error } } } def printer-acceptance [name: string] { let result = (run-command "lpstat" ["-a" $name]) let line = (first-line $result.output) if $line == "" { $result.error } else { $line } } def printer-device [name: string] { let result = (run-command "lpstat" ["-v" $name]) try { $result.output | parse --regex '^device for \S+: (?.*)$' | get 0.device } catch { "" } } def printer-alerts [name: string] { let result = (run-command "lpstat" ["-l" "-p" $name]) $result.output | lines | each {|line| $line | str trim} | where {|line| $line | str starts-with "Alerts:"} | each {|line| $line | str replace "Alerts: " ""} | str join ", " } def printer-jobs [name: string] { let result = (run-command "lpstat" ["-o" $name]) if $result.output == "" { [] } else { $result.output | lines } } def ipp-value [xml: string, field: string] { let lines = ($xml | lines) let marker = $"($field)" let matches = ($lines | enumerate | where item == $marker) if ($matches | is-empty) { return "" } let raw = ($lines | get (($matches | get 0 | get index) + 1) | str trim) if ($raw | str contains "]+>(.*)]+>$' '$1' } } def printer-ipp [name: string] { let result = (run-command "ipptool" ["-q" "-X" "-T" "3" $"ipp://localhost/printers/($name)" "get-printer-attributes.test"]) if not $result.ok { return {} } { state: (ipp-value $result.output "printer-state") accepting: (ipp-value $result.output "printer-is-accepting-jobs") reasons: (ipp-value $result.output "printer-state-reasons") message: (ipp-value $result.output "printer-state-message") jobs: (ipp-value $result.output "queued-job-count") } } def printer-logs [name: string] { let files = (try { glob "/var/log/cups/error_log*" } catch { [] }) $files | each {|file| try { open --raw $file | lines | where {|line| $line | str contains $name} } catch { [] } } | flatten | last 12 } def printer-network [device: string] { if not (($device | str starts-with "ipp://") or ($device | str starts-with "ipps://")) { return "n/a" } let result = (run-command "ipptool" ["-q" "-t" "-T" "3" $device "get-printer-attributes.test"]) if $result.ok { "reachable" } else { "unreachable" } } def printer-record [name: string] { let device = (printer-device $name) let ipp = (printer-ipp $name) { name: $name summary: (printer-summary $name) acceptance: (printer-acceptance $name) device: $device alerts: (printer-alerts $name) jobs: (printer-jobs $name) ipp: $ipp network: (printer-network $device) logs: (printer-logs $name) } } def state-label [summary: string] { let lower = ($summary | str lowercase) if ($lower | str contains "disabled") or ($lower | str contains "stopped") or ($lower | str contains "error") { style "● DISABLED" "red_bold" } else if ($lower | str contains "printing") { style "● PRINTING" "yellow_bold" } else { style "● READY" "green_bold" } } def render-list [names: list, selected: int, notice: string] { screen print (style " PRINTERS " "cyan_bold") print "" let default = (default-printer) for $item in ($names | enumerate) { let marker = if $item.index == $selected { style ">" "cyan_bold" } else { " " } let suffix = if $item.item == $default { style " default" "d" } else { "" } print $" ($marker) ($item.item)($suffix)" } if $notice != "" { print "" print (style $notice "yellow") } print "" print (style " ↑/↓ select enter open q quit " "d") } def render-detail [record: record, notice: string] { screen print (style $" ($record.name) " "cyan_bold") print "" print $" (state-label $record.summary) ($record.summary)" print $" acceptance ($record.acceptance)" print $" device ($record.device)" print $" network ($record.network)" let ipp_reasons = ($record.ipp.reasons? | default "") let message = ($record.ipp.message? | default "") if $ipp_reasons != "" { print $" reasons ($ipp_reasons)" } if $message != "" { print $" message ($message)" } if $record.alerts != "" { print $" alerts ($record.alerts)" } print "" let jobs = ($record.jobs | length) let ipp_jobs = ($record.ipp.jobs? | default "") if $ipp_jobs != "" { print $" jobs ($jobs) queued / ($ipp_jobs) reported" } else { print $" jobs ($jobs) queued" } if $notice != "" { print "" print (style $notice "yellow") } print "" print (style " p print j jobs l logs e enable+accept d default " "d") print (style " r refresh ? help q quit " "d") } def render-jobs [record: record, selected: int, notice: string] { screen print (style $" ($record.name) / JOBS " "cyan_bold") print "" if ($record.jobs | is-empty) { print " no queued jobs" } else { for $item in ($record.jobs | enumerate) { let marker = if $item.index == $selected { style ">" "cyan_bold" } else { " " } print $" ($marker) ($item.item)" } } if $notice != "" { print "" print (style $notice "yellow") } print "" print (style " ↑/↓ select c cancel h hold u release b back " "d") } def render-logs [record: record] { screen print (style $" ($record.name) / CUPS ERRORS " "cyan_bold") print "" if ($record.logs | is-empty) { print " no matching entries in /var/log/cups/error_log*" } else { for $line in $record.logs { print $" ($line)" } } print "" print (style " b back r refresh q quit " "d") } def render-help [name: string] { screen print (style $" ($name) / HELP " "cyan_bold") print "" print " p print a file" print " j inspect and manage queued jobs" print " l show matching CUPS error log entries" print " e enable printer and accept new jobs" print " d set user default printer" print " r refresh status" print " q quit" print "" print (style " press any key " "d") } def confirm [prompt: string] { let answer = (input $"($prompt) [y/N] " | str lowercase | str trim) $answer == "y" or $answer == "yes" } def action-enable [name: string] { let enabled = (run-command "cupsenable" [$name]) if not $enabled.ok { return ($enabled.error | default "could not enable printer") } let accepted = (run-command "cupsaccept" [$name]) if not $accepted.ok { return ($accepted.error | default "enabled, but could not accept jobs") } "printer enabled; accepting jobs" } def action-default [name: string] { let result = (run-command "lpoptions" ["-d" $name]) if $result.ok { "user default updated" } else { $result.error | default "could not set default printer" } } def action-print [name: string] { let input_path = (input "file to print: " | str trim) if $input_path == "" { return "cancelled" } let file = ($input_path | path expand) if not ($file | path exists) { return $"file not found: ($file)" } let result = (run-command "lp" ["-d" $name $file]) if $result.ok { $result.output | default "print job submitted" } else { $result.error | default "print failed" } } def job-id [line: string] { $line | split row -r '\s+' | first } def action-job [job: string, action: string] { let id = (job-id $job) let result = if $action == "cancel" { run-command "cancel" [$id] } else if $action == "hold" { run-command "lp" ["-i" $id "-H" "hold"] } else { run-command "lp" ["-i" $id "-H" "resume"] } if $result.ok { $"job ($id): ($action)" } else { $result.error | default $"job action failed: ($action)" } } def printer-app [names: list, selected: int, initial_view: string] { mut view = $initial_view mut selected_printer = $selected mut selected_job = 0 mut notice = "" loop { if $view == "list" { render-list $names $selected_printer $notice let pressed = (key) match $pressed { "up" => { if $selected_printer > 0 { $selected_printer -= 1 } } "down" => { if $selected_printer < (($names | length) - 1) { $selected_printer += 1 } } "enter" => { $view = "detail"; $notice = "" } "q" | "esc" => { break } _ => {} } continue } let name = ($names | get $selected_printer) let record = (printer-record $name) if $view == "detail" { render-detail $record $notice let pressed = (key) match $pressed { "p" => { $notice = (action-print $name) } "j" => { $selected_job = 0; $view = "jobs"; $notice = ""} "l" => { $view = "logs"; $notice = "" } "e" => { $notice = (action-enable $name) } "d" => { $notice = (action-default $name) } "r" => { $notice = "refreshed" } "?" => { $view = "help"; $notice = "" } "q" | "esc" => { break } _ => {} } } else if $view == "jobs" { render-jobs $record $selected_job $notice let pressed = (key) match $pressed { "up" => { if $selected_job > 0 { $selected_job -= 1 } } "down" => { if not ($record.jobs | is-empty) and $selected_job < (($record.jobs | length) - 1) { $selected_job += 1 } } "c" => { if not ($record.jobs | is-empty) and (confirm "cancel selected job?") { $notice = (action-job ($record.jobs | get $selected_job) "cancel") } } "h" => { if not ($record.jobs | is-empty) and (confirm "hold selected job?") { $notice = (action-job ($record.jobs | get $selected_job) "hold") } } "u" => { if not ($record.jobs | is-empty) { $notice = (action-job ($record.jobs | get $selected_job) "release") } } "b" | "esc" => { $view = "detail"; $notice = "" } "q" => { break } _ => {} } } else if $view == "logs" { render-logs $record let pressed = (key) match $pressed { "b" | "esc" => { $view = "detail" } "r" => { $notice = "" } "q" => { break } _ => {} } } else { render-help $name let _ = (key) $view = "detail" } } } export def main [name?: string] { if (which lpstat | is-empty) { error make {msg: "lpstat is not installed"} } if (which lp | is-empty) { error make {msg: "lp is not installed"} } let names = (printer-names) if ($names | is-empty) { error make {msg: "CUPS has no configured printers"} } if $name != null and not ($name in $names) { error make {msg: $"unknown printer: ($name)"} } let default = (default-printer) let selected = if $name != null { $names | enumerate | where item == $name | get 0.index } else if $default != "" and ($default in $names) { $names | enumerate | where item == $default | get 0.index } else { 0 } let initial_view = if $name != null or (($names | length) == 1) { "detail" } else { "list" } printer-app $names $selected $initial_view }