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]
}
}