builtin-programs/terminal-ui.folk

# Manage terminal UI for Folk.

$::realStdout puts "Folk Computer (pid [pid])."
set host $([string match "*.local" $::thisNode] ? $::thisNode : "$::thisNode.local")
$::realStdout puts "Web interface at: http://$host:4273/"
$::realStdout puts ""
$::realStdout flush

# Don't run the watcher process if we're not literally at a terminal
# (if we're running on systemd, for instance.)
if {$::env(TERM) eq "dumb"} {
    return
}

set termRows 24
set termCols 80
catch { lassign [exec stty size] termRows termCols }
set maxLines [expr {$termRows - 4}]

# Visible length of a string (strips ANSI escape sequences).
fn visLen {s} {
    regsub -all {\x1b\[[0-9;]*[a-zA-Z]} $s {} s
    string length $s
}

set prevLineCount 0
set startMs [clock milliseconds]
while true {
    set elapsedMs [expr {[clock milliseconds] - $startMs}]
    set settled [expr {$elapsedMs > 60000}]
    after [expr {$settled ? 500 : 40}]

    set results [lsort -command {apply {{a b} {
        string compare [dict get $a program] [dict get $b program]
    }}} [Query! /program/ has program code /programCode/]]

    # Build groups: dict mapping dirname -> list of {status basename}
    set groups [dict create]
    foreach result $results {
        set program [dict get $result program]
        set programCode [dict get $result programCode]

        set runners [Query! when $programCode with environment [list [list this $program]]]
        if {[llength $runners] != 1} { continue }
        set runner [lindex $runners 0]
        set incompleteCount [__statementIncompleteChildMatchesCount [dict get $runner __ref]]

        set errors [Query! $program has error /err/ with info /info/]

        set dir [file dirname $program]
        set base [file rootname [file tail $program]]

        if {[llength $errors] > 0} {
            set status "\033\[31m!\033\[0m"
            set coloredBase "\033\[31m$base\033\[0m"
        } elseif {$incompleteCount == 0} {
            set status "\033\[32m✓\033\[0m"
            set coloredBase "\033\[32m$base\033\[0m"
        } else {
            if {$settled} {
                set status "\033\[33m·\033\[0m"
            } else {
                set spinIdx [expr {([clock milliseconds] / 80) % 4}]
                set status "\033\[33m[lindex {| / - \\} $spinIdx]\033\[0m"
            }
            set coloredBase "\033\[33m$base\033\[0m"
        }

        dict lappend groups $dir [list $status $coloredBase]
    }

    # Build lines, word-wrapping each group's entries to fit termCols.
    set lines {}
    dict for {dir members} $groups {
        set prefix "$dir/  "
        set indent [string repeat " " [visLen $prefix]]
        set line $prefix
        foreach member $members {
            lassign $member status base
            set word "${status} $base"
            if {[visLen $line] == [visLen $prefix]} {
                append line $word
            } elseif {[visLen $line] + 3 + [visLen $word] > $termCols} {
                lappend lines $line
                set line "$indent$word"
            } else {
                append line "  $word"
            }
        }
        lappend lines $line
    }
    if {[llength $lines] > $maxLines} {
        set lines [lrange $lines 0 $maxLines-1]
    }

    if {$prevLineCount > 0} {
        $::realStdout puts -nonewline "\033\[${prevLineCount}A\r"
    }
    $::realStdout puts -nonewline "\033\[?7l"
    foreach line $lines {
        $::realStdout puts -nonewline "$line\033\[K\n"
    }
    # Blank out leftover lines from a previously longer render.
    set extra [expr {$prevLineCount - [llength $lines]}]
    for {set i 0} {$i < $extra} {incr i} {
        $::realStdout puts -nonewline "\033\[K\n"
    }
    $::realStdout puts -nonewline "\033\[?7h"
    if {[llength $lines] > $prevLineCount} {
        set prevLineCount [llength $lines]
    }
}