Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

nushell/modules/printer.nu

Raw
def run-command [command: string, args: list<string>] {
    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: (?<name>\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 (?<name>\S+) (?<summary>.*)$'
            | 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+ (?<summary>.*)$'
        | 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+: (?<device>.*)$'
        | 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 = $"<key>($field)</key>"
    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 "<true") { "true" } else if ($raw | str contains "<false") { "false" } else {
        $raw | str replace --regex '^<[^>]+>(.*)</[^>]+>$' '$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<string>, 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<string>, 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
}