Browse Source

punk 0.2.0: help system restructured onto ::punk::helptopic topic registry

- each topic (topics|help, tcl, env|environment, console|term|terminal) is a
  handler proc returning {channel text} chunks, with its own punk::args
  definition (id ::punk::helptopic::<topic>) so 'i help <topic>' renders
  documented usage
- registering a topic (re)generates the ::punk::help / ::punk::help_chunks
  definitions, so 'i help' shows a documented topic table that always matches
  the registry (previously an autogenerated stub); the registry is the seam
  for future subshell-declared topics
- 'help topics' derived from the registry (all aliases listed, fits 80 cols)
- no-arg overview and command-fallthrough output byte-identical to before;
  80-column layout preserved; verified on tcl 9 and 8.6 kits via the script
  and shell subcommands
- 'help env' degrades to a one-line notice when punk::config is not
  initialised (e.g. script contexts) instead of an error stack + exit 1
- shared table_block helper destroys table objects after print (previously
  leaked several per help invocation)
- project 0.4.4 (CHANGELOG entry)

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
68e45c513d
  1. 4
      CHANGELOG.md
  2. 2
      punkproject.toml
  3. 390
      src/modules/punk-999999.0a1.0.tm
  4. 3
      src/modules/punk-buildversion.txt

4
CHANGELOG.md

@ -5,6 +5,10 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy.
## [0.4.4] - 2026-07-08
- help system restructured onto a topic registry (punk module 0.2.0, `::punk::helptopic`): each topic (`topics|help`, `tcl`, `env|environment`, `console|term|terminal`) is a handler proc with its own punk::args definition, and the `::punk::help`/`::punk::help_chunks` definitions are (re)generated from the registry — `i help` now renders a documented usage table with topic choices and summaries, and `i help <topic>` shows per-topic documented usage (previously an autogenerated stub). `help topics` is derived from the registry (lists all aliases, fits 80 columns). No-arg overview and command-fallthrough (`help <cmdname>`) output byte-identical to before; 80-column layout preserved; verified on both generations, script and shell subcommands. `help env` without an initialised punk::config (e.g. script contexts) degrades to a one-line notice instead of an error stack. The registry is the intended seam for future subshell-declared topics (punk::config-gated — not yet a goal).
## [0.4.3] - 2026-07-08
- vendored tcludp upgraded 1.0.12 -> 1.0.13 in the tcl9 kit vfs folders (punk9win.vfs, punk9win_for_tkruntime.vfs) and src/vendorlib_tcl9 — 1.0.12's Windows per-thread exit handler closed the process-global tcludp synchronization events, so the first udp-loaded worker thread to die froze every other udp-loaded thread's event loop (root cause of the G-036 wedge; fixed upstream in 1.0.13). Verified with the G-036 regression harness: run-2 syslog workers alive (baseline on 1.0.12: wedged 4/4). Note: punk8win.vfs still bundles udp 1.0.12 (8.6 appeared immune; pending decision). The manual vfs copy was required because `libs`/`vfscommonupdate`/`project` do not propagate vendorlib_tcl<N> platform libraries into kit vfs lib_tcl<N> trees — gap recorded as goal G-037.

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.4.3"
version = "0.4.4"

390
src/modules/punk-999999.0a1.0.tm

@ -8411,99 +8411,104 @@ namespace eval punk {
}
puts -nonewline stdout \n
}
#return list of {chan chunk} elements
namespace eval argdoc {
punk::args::define {
@id -id ::punk::help_chunks
@cmd -name "punk::help_chunks"\
-summary\
""\
-help\
""
@opts
-- -type none
@values -min 0 -max -1
arg -type any -optional 1 -multiple 1
}
}
proc help_chunks {args} {
set argd [punk::args::parse $args withid ::punk::help_chunks]
lassign [dict values $argd] leaders opts values received
if {[dict exists $values arg]} {
set topicparts [dict get $values arg]
} else {
set topicparts [list ""]
#punk::help topic registry
#Each registered topic has:
# - a handler proc ::punk::helptopic::<topic> taking a context dict plus any extra words,
# returning the topic's help content as a list of {channel text} chunks
# - a punk::args definition with id ::punk::helptopic::<topic> so that 'i help <topic>' shows documented usage
#Registering a topic (re)generates the ::punk::help and ::punk::help_chunks punk::args definitions,
#so the documented topic choices always match the registry.
#The 'overview' handler is not a registered topic - it is what a plain 'help' displays.
#Unregistered topic words fall through to basic command-info lookup (see ::punk::help_chunks).
namespace eval helptopic {
variable topics [dict create]
tcl::namespace::import ::punk::ansi::a ::punk::ansi::a+
proc register {topic aliases summary} {
variable topics
if {![llength [info commands [namespace current]::$topic]]} {
error "punk::helptopic::register - no handler proc '[namespace current]::$topic' exists for topic '$topic'"
}
dict set topics $topic [dict create aliases $aliases summary $summary]
define_docs
return
}
#set topic [lindex $args end]
#set argopts [lrange $args 0 end-1]
set chunks [list]
set linesep [string repeat - 76]
set warningblock ""
set I [punk::ansi::a+ italic]
set NI [punk::ansi::a+ noitalic]
set sizedict [punk::console::get_size]
set cols [dict get $sizedict columns]
set rows [dict get $sizedict rows]
#todo - provide a mechanism to configure the default frametype everywhere and describe it in this help.
set frametype ascii ;#conservative default.
#if the test char width fails - it's likely we're on a very old terminal that doesn't support unicode at all.
if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
if {$cols <= 80} {
# Be conservative with frame types on narrow terminals for help.
# an 80x30 terminal is more likely to be an older style terminal and may not have unicode support.
# unicode on a non-unicode terminal is a bad experience - with the frame chars showing as garbage (e.g 3 chars per grapheme).
set frametype ascii
} else {
if {$testcharwidth == 1} {
set frametype light ;#unicode box-drawing chars.
#return the canonical topic name for a topic word or alias - empty string if not a registered topic
proc resolve {word} {
variable topics
dict for {topic tinfo} $topics {
if {$word eq $topic || $word in [dict get $tinfo aliases]} {
return $topic
}
}
return ""
}
# -------------------------------------------------------
set logoblock ""
if {[catch {
package require patternpunk
#lappend chunks [list stderr [>punk . rhs]]
append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]]
}]} {
append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""]
}
set title "[a+ brightgreen] Help System: "
set cmdinfo [list]
lappend cmdinfo [list help "?${I}topic${NI}?" "This help.\nTo see available subitems type:\nhelp topics\n\nFor an unrecognised ${I}topic${NI}\nhelp will look for basic\ninfo for it as a command.\n"]
set t [textblock::class::table new -minwidth 51 -show_seps 0]
#standard command-info table as used by the help overview blocks
proc table_block {frametype title rows {minwidth 80}} {
set t [textblock::class::table new -minwidth $minwidth -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
foreach row $rows {
$t add_row $row
}
set width_0 [$t column_datawidth 0]
$t configure_column 0 -minwidth [expr {$width_0 + 2}]
set width_1 [$t column_datawidth 1]
$t configure_column 1 -minwidth [expr {$width_1 + 1}]
$t configure_column 0 -minwidth [expr {[$t column_datawidth 0] + 2}]
$t configure_column 1 -minwidth [expr {[$t column_datawidth 1] + 1}]
$t configure -title $title
set text [$t print]
$t destroy
return $text
}
#(re)generate the punk::args definitions for ::punk::help and ::punk::help_chunks from the registry
#so that 'i help' shows a documented topic table matching the registered topics.
proc define_docs {} {
variable topics
set choices [list]
set choiceinfo [dict create]
set choicelabels [dict create]
dict for {topic tinfo} $topics {
foreach name [list $topic {*}[dict get $tinfo aliases]] {
lappend choices $name
dict set choiceinfo $name [list [list doctype punkargs] [list subhelp [namespace current]::$topic]]
dict set choicelabels $name [dict get $tinfo summary]
}
}
set basehelp {Help system for the punk shell.
With no arguments - an overview of some key shell commands is displayed.
When the first argument is a recognised topic - help for that topic is displayed.
('help topics' lists the available topics)
Anything else is treated as a command name - basic command info (type and
synopsis) is shown for a resolvable command, or the resolved path for an
external executable.}
set topichelp {Help topic, or command words for basic command info. (topics must match exactly - no prefix matching)}
set specs [list]
lappend specs ::punk::help help "Punk shell help system." ""
lappend specs ::punk::help_chunks punk::help_chunks "Punk shell help system - content as {channel text} chunks." "\n\nhelp_chunks returns the help content as a list of {channel text} chunks rather than emitting it."
foreach {id name summary extra} $specs {
set def ""
append def "@id -id $id" \n
append def "@cmd -name $name -summary \"$summary\" -help \"$basehelp$extra\"" \n
append def "@leaders -min 0 -max -1" \n
append def "topic -optional 1 -multiple 1 -type string -choiceprefix 0 -choicerestricted 0 -choices {$choices} -choiceinfo {$choiceinfo} -choicelabels {$choicelabels} -help \"$topichelp\"" \n
append def "@values -min 0 -max 0"
if {[punk::args::id_exists $id]} {
#quiet undefine - redefinition on registry change is expected, not noteworthy
punk::args::undefine $id 1
}
punk::args::define $def
}
}
#The no-topic overview of key shell commands (what a plain 'help' displays)
proc overview {context args} {
set frametype [dict get $context frametype]
set I [dict get $context I]
set NI [dict get $context NI]
set chunks [list]
set introblock [textblock::join -- $logoblock $text]
lappend chunks [list stdout $introblock\n]
# -------------------------------------------------------
switch -- [lindex $topicparts 0] {
"" {
# -------------------------------------------------------
set title "[a+ brightgreen] Filesystem navigation: "
set cmdinfo [list]
lappend cmdinfo [list ./ "?${I}glob${NI}?" "view/change dir, list dirs."]
@ -8511,69 +8516,23 @@ namespace eval punk {
lappend cmdinfo [list ../ "?${I}path${NI}" "go up one dir, then to path if given"]
lappend cmdinfo [list newdir "${I}subdir${NI}..." "make new dir or dirs and show status"]
lappend cmdinfo [list fcat "${I}file ?file?...${NI}" "cat file(s)"]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
set width_0 [$t column_datawidth 0]
$t configure_column 0 -minwidth [expr {$width_0 + 2}]
set width_1 [$t column_datawidth 1]
$t configure_column 1 -minwidth [expr {$width_1 + 1}]
$t configure -title $title
set text ""
append text [$t print]
lappend chunks [list stdout $text\n]
# -------------------------------------------------------
lappend chunks [list stdout [table_block $frametype $title $cmdinfo]\n]
# -------------------------------------------------------
set title "[a+ brightgreen] Namespace navigation: "
set cmdinfo [list]
lappend cmdinfo [list n/ "?${I}ns${NI}|${I}glob${NI}?" "view/change namespace\n (accepts ns path globs e.g **::*get* to match\n commands at any level )"]
lappend cmdinfo [list n// "?${I}ns${NI}|${I}glob${NI}?" "view/change namespace (with command listing)"]
lappend cmdinfo [list "nn/" "" "go up one namespace"]
lappend cmdinfo [list "newns" "${I}ns${NI}" "make child namespace and switch to it"]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
set width_0 [$t column_datawidth 0]
$t configure_column 0 -minwidth [expr {$width_0 + 2}]
set width_1 [$t column_datawidth 1]
$t configure_column 1 -minwidth [expr {$width_1 + 1}]
$t configure -title $title
set text ""
append text [$t print]
lappend chunks [list stdout $text\n]
# -------------------------------------------------------
lappend chunks [list stdout [table_block $frametype $title $cmdinfo]\n]
# -------------------------------------------------------
set title "[a+ brightgreen] Command help: "
set cmdinfo [list]
lappend cmdinfo [list i "${I}cmd${NI} ?${I}subcommand${NI}...?" "Show usage for a command or ensemble subcommand"]
lappend cmdinfo [list s "${I}cmd${NI} ?${I}subcommand${NI}...?" "Show synopsis for a command or ensemble subcommand"]
lappend cmdinfo [list eg "${I}cmd${NI} ?${I}subcommand${NI}...?" "Show example from manpage"]
lappend cmdinfo [list corp "${I}proc${NI}" "View proc body and arguments with basic highlighting"]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
set width_0 [$t column_datawidth 0]
$t configure_column 0 -minwidth [expr {$width_0 + 2}]
set width_1 [$t column_datawidth 1]
$t configure_column 1 -minwidth [expr {$width_1 + 1}]
$t configure -title $title
set text ""
append text [$t print]
lappend chunks [list stdout $text\n]
# -------------------------------------------------------
lappend chunks [list stdout [table_block $frametype $title $cmdinfo]\n]
set title "[a+ brightgreen] Miscellaneous: "
#todo - load from source code annotation?
@ -8582,25 +8541,41 @@ namespace eval punk {
lappend cmdinfo [list a? "?${I}subcommand${NI}...?" "view ANSI colours\n e.g a? web\n or individual code samples/diagnostics\n e.g a? red bold\n e.g a? underline web-orange Term-purple3"]
lappend cmdinfo [list a+ "?${I}colourcode${NI}...?" "Return ANSI codes\n e.g puts \"\[a+ purple\]purple\[a+ Green\]purple on green\[a\]\"\n [a+ purple]purple[a+ Green]purple on green[a] "]
lappend cmdinfo [list a "?${I}colourcode${NI}...?" "Return ANSI codes (with leading reset)\n e.g puts \"\[a+ purple\]purple\[a Green\]normal on green\[a\]\"\n [a+ purple]purple[a Green]normal on green[a] "]
lappend chunks [list stdout [table_block $frametype $title $cmdinfo]]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
return $chunks
}
set width_0 [$t column_datawidth 0]
$t configure_column 0 -minwidth [expr {$width_0 + 2}]
set width_1 [$t column_datawidth 1]
$t configure_column 1 -minwidth [expr {$width_1 + 1}]
$t configure -title $title
set text ""
append text [$t print]
lappend chunks [list stdout $text]
# -------------------------------------------------------
punk::args::define {
@id -id ::punk::helptopic::topics
@cmd -name "help topics" -summary "List help topics." -help "Show the table of topics known to the help system, including registered aliases for each topic."
@values -min 0 -max 0
}
proc topics {context args} {
variable topics
set frametype [dict get $context frametype]
set t [textblock::class::table new -show_seps 0]
$t configure -frametype $frametype
$t add_column -headers [list "Topic"]
$t add_column
dict for {topic tinfo} $topics {
$t add_row [list [join [list $topic {*}[dict get $tinfo aliases]] |] [dict get $tinfo summary]]
}
$t add_row [list "*" "Look up as a command or external executable"]
$t configure_column 0 -minwidth [expr {[$t column_datawidth 0] + 4}]
set text \n[$t print]
$t destroy
return [list [list stdout $text]]
}
punk::args::define {
@id -id ::punk::helptopic::tcl
@cmd -name "help tcl" -summary "Tcl version warnings." -help "Show the running Tcl patchlevel and build-info, with warnings for known Tcl bugs affecting this interpreter (as detected by the punk::lib::check::has_tclbug_* checks)."
@values -min 0 -max 0
}
tcl {
proc tcl {context args} {
set chunks [list]
set warningblock ""
set text "Tcl Patchlevel: [info patchlevel]"
catch {
append text \n "Tcl build-info: [::tcl::build-info]"
@ -8641,8 +8616,26 @@ namespace eval punk {
append warningblock [a]
}
lappend chunks [list stdout $text]
lappend chunks [list stderr $warningblock]
return $chunks
}
punk::args::define {
@id -id ::punk::helptopic::env
@cmd -name "help env" -summary "punkshell environment vars." -help "Show punk-related and other relevant environment variables with their current values."
@values -min 0 -max 0
}
env - environment {
proc env {context args} {
if {[catch {package require punk::config} errM]} {
return [list [list stderr "help env: punk::config package not available - no environment variable info ($errM)\n"]]
}
if {![info exists ::punk::config::punk_env_vars_config] || ![info exists ::punk::config::other_env_vars_config]} {
#punk::config::init has side effects (config dir/file creation) - don't trigger it just for help
return [list [list stderr "help env: punk::config not initialised - no environment variable info\n"]]
}
set frametype [dict get $context frametype]
set linesep [string repeat - 76]
set chunks [list]
set text ""
#todo - move to punk::config?
upvar ::punk::config::punk_env_vars_config punkenv_config
@ -8725,8 +8718,17 @@ namespace eval punk {
}
lappend chunks [list stdout $text]
return $chunks
}
punk::args::define {
@id -id ::punk::helptopic::console
@cmd -name "help console" -summary "Console behaviour tests and warnings." -help "Run some console/terminal behaviour tests (control string hiding, grapheme cluster support, cursor position query timing) and report warnings. Requires a responsive interactive console."
@values -min 0 -max 0
}
console - term - terminal {
proc console {context args} {
set chunks [list]
set warningblock ""
set term_env_vars {TERM TERM_PROGRAM TERM_PROGRAM_VERSION COLORTERM}
set term_dict [dict create]
foreach e $term_env_vars {
@ -8889,31 +8891,85 @@ namespace eval punk {
puts stdout [punk::ansi::move_back 200] ;#hack for some horizontal position bugs where the above tests can leave the cursor in the wrong place for the next output.
#200 is arbitrary large number to move back enough to get to start of line.
}
lappend chunks [list stderr $warningblock]
return $chunks
}
topics - help {
set text ""
set topics [dict create {*}{
"topics|help" "List help topics"
"tcl" "Tcl version warnings"
"env|environment" "punkshell environment vars"
"console|terminal" "Some console behaviour tests and warnings"
"*" "Try to find help on the topic as a command or external executable"
}]
set t [textblock::class::table new -show_seps 0]
$t configure -frametype $frametype
$t add_column -headers [list "Topic"]
$t add_column
foreach {k v} $topics {
$t add_row [list $k $v]
register topics {help} "List help topics"
register tcl {} "Tcl version warnings"
register env {environment} "punkshell environment vars"
register console {term terminal} "Some console behaviour tests and warnings"
}
set widest0 [$t column_datawidth 0]
$t configure_column 0 -minwidth [expr {$widest0 + 4}]
append text \n [$t print]
lappend chunks [list stdout $text]
#return list of {chan chunk} elements
proc help_chunks {args} {
set argd [punk::args::parse $args withid ::punk::help]
lassign [dict values $argd] leaders opts values received
if {[dict exists $leaders topic]} {
set topicparts [dict get $leaders topic]
} else {
set topicparts [list ""]
}
default {
set chunks [list]
set I [punk::ansi::a+ italic]
set NI [punk::ansi::a+ noitalic]
set sizedict [punk::console::get_size]
set cols [dict get $sizedict columns]
set rows [dict get $sizedict rows]
#todo - provide a mechanism to configure the default frametype everywhere and describe it in this help.
set frametype ascii ;#conservative default.
#if the test char width fails - it's likely we're on a very old terminal that doesn't support unicode at all.
if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
if {$cols <= 80} {
# Be conservative with frame types on narrow terminals for help.
# an 80x30 terminal is more likely to be an older style terminal and may not have unicode support.
# unicode on a non-unicode terminal is a bad experience - with the frame chars showing as garbage (e.g 3 chars per grapheme).
set frametype ascii
} else {
if {$testcharwidth == 1} {
set frametype light ;#unicode box-drawing chars.
}
}
}
set context [dict create cols $cols rows $rows frametype $frametype I $I NI $NI]
# -------------------------------------------------------
set logoblock ""
if {[catch {
package require patternpunk
#lappend chunks [list stderr [>punk . rhs]]
append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]]
}]} {
append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""]
}
set title "[a+ brightgreen] Help System: "
set cmdinfo [list]
lappend cmdinfo [list help "?${I}topic${NI}?" "This help.\nTo see available subitems type:\nhelp topics\n\nFor an unrecognised ${I}topic${NI}\nhelp will look for basic\ninfo for it as a command.\n"]
set text [helptopic::table_block $frametype $title $cmdinfo 51]
set introblock [textblock::join -- $logoblock $text]
lappend chunks [list stdout $introblock\n]
# -------------------------------------------------------
set word0 [lindex $topicparts 0]
set topic [helptopic::resolve $word0]
if {$word0 eq ""} {
#no topic supplied - overview of key shell commands
lappend chunks {*}[helptopic::overview $context {*}[lrange $topicparts 1 end]]
} elseif {$topic ne ""} {
lappend chunks {*}[helptopic::$topic $context {*}[lrange $topicparts 1 end]]
} else {
#not a registered topic - fall through to basic command info lookup
set text ""
set cinfo [uplevel 1 [list ::punk::ns::cmdwhich [lindex $topicparts 0]]]
set wtype [dict get $cinfo whichtype]
@ -8946,10 +9002,6 @@ namespace eval punk {
}
lappend chunks [list stdout $text]
}
}
lappend chunks [list stderr $warningblock]
return $chunks
}
proc mode {{raw_or_line query}} {

3
src/modules/punk-buildversion.txt

@ -1,3 +1,4 @@
0.1.1
0.2.0
#First line must be a semantic version number
#all other lines are ignored.
#0.2.0 - help system restructured onto a topic registry (::punk::helptopic: register/resolve, per-topic handler procs each with a punk::args definition); ::punk::help and ::punk::help_chunks punk::args definitions (re)generated from the registry so 'i help' / 'i help <topic>' render documented usage; 'help topics' derived from the registry; command-fallthrough and no-arg overview output unchanged; 'help env' degrades cleanly when punk::config is not initialised; help table objects destroyed after printing (leak fix)

Loading…
Cancel
Save