From c309ed47828aa37ba4da2eebb0c740fe9deaad27 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sat, 11 Jul 2026 23:19:30 +1000 Subject: [PATCH] punk::args 0.6.0: small restricted choice sets display as literal alternates in synopses An argument whose choice pool (-choices plus -choicegroups members, deduplicated) has 1-3 members and -choicerestricted true (the default) now renders those words unitalicised joined by | in synopses - e.g 'after cancel' shows literal cancel, a 3-choice option shows (left|centre|right) - matching the display style of literal()/literalprefix() type-alternates. Larger or unrestricted choice sets keep the italicised argname/ display, and an explicit -typesynopsis always takes precedence (textblock::frame -type unchanged). New shared helper punk::args::private::synopsis_choice_literals feeds both synopsis render paths (leaders/values via synopsis_form_arg_display, options inline in synopsis); applies only to single-element -type lists. Superseded commented-out single-choice sketches removed; define doc for -choices documents the rule. tests: synopsis.test 4 -> 13 - characterization coverage for literal/literalprefix/stringstartswith/stringendswith type-alternates, option alternate parenthesization, multi-element clause display (?type? members, argname tail-word hints), -typesynopsis value-element lists and option passthrough incl documenter ANSI, plus the new choice-literal rule (leader/option/value positions, choicegroups counting, >3 and unrestricted fallbacks, -typesynopsis precedence). rendering.test choicelabel fixtures padded to 4 choices so markercol keeps matching the choices cell rather than the now-literal synopsis line. Known residue (deliberate): option-path -typesynopsis ?-trim edge cases remain (todo comments at the render site) - resolve-time rejection deferred; any future normalization surgery belongs to punk::ansi::ansistring/opunk::Str. project 0.12.0 Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 4 + punkproject.toml | 2 +- src/modules/punk/args-999999.0a1.0.tm | 66 +++++-- src/modules/punk/args-buildversion.txt | 3 +- src/tests/modules/AGENTS.md | 2 +- .../punk/args/testsuites/args/rendering.test | 9 +- .../punk/args/testsuites/args/synopsis.test | 182 +++++++++++++++++- 7 files changed, 245 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3646f7fc..cabdf18d 100644 --- a/CHANGELOG.md +++ b/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.12.0] - 2026-07-11 + +- punk::args 0.6.0: command synopses (`s `, `i `, usage headers) now display small restricted choice sets as literal alternates — an argument whose choice pool (`-choices` plus `-choicegroups` members) has 1–3 members and `-choicerestricted` true renders those words unitalicised joined by `|` (e.g. `after cancel` shows literal `cancel`; a 3-choice option shows `(left|centre|right)`), matching the existing display style of `literal()`/`literalprefix()` type-alternates. Larger or unrestricted choice sets keep the italicised argname/type display and an explicit `-typesynopsis` always wins, so e.g. `textblock::frame -type (choice|)` is unchanged. Synopsis rendering of type-alternates, `-typesynopsis` passthrough (including documenter ANSI styling), and multi-element clause display gained characterization test coverage (`synopsis.test`). + ## [0.11.0] - 2026-07-11 - G-001 achieved: an interactive REPL can be launched against a non-detectable terminal-like device via an `::opunk::Console` subclass — `repl::init -console ` selects the console (channel pair, anchored instance name, or object value), `repl::start`'s input channel defaults to it, and the repl's output (prompts, results, and the code interp's stdout/stderr — diverted via shellfilter junction stacks and emitted per run) routes through the selected console's channels, with eof/size/capability answered by the subclass overrides (punk::repl 0.4.0; base `::opunk::Console` and `punk::console` untouched). Verified end-to-end by child-process driver tests: an `::opunk::SshConsole` socket session whose scripted remote terminal answers `CSI 6n` (size resolved over the socket) and a `::opunk::TkConsole` text widget wired as a live console by new `opunk::console::tk::console` (reflected output channel rendering into the widget + Return-binding input pipe, opunk::console::tk 0.2.0). Process-console behaviours (tcl_interactive prompt gating, stdin reopen on eof, raw-mode re-enable, the windows line re-decode experiment) now apply only when no foreign console is selected; default stdin/stdout repl behaviour is unchanged. diff --git a/punkproject.toml b/punkproject.toml index 470ecfe0..df52aa47 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.11.0" +version = "0.12.0" diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index fdc74cbc..2179d8c6 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -905,6 +905,13 @@ tcl::namespace::eval punk::args { -choices {} A list of allowable values for an argument. The -default value doesn't have to be in the list. + Synopsis display: a choice set of 1 to 3 members (counting + -choicegroups members) with -choicerestricted true displays + in the synopsis as unitalicised literal alternates + (e.g cancel, or left|centre|right) in the same style as + literal()/literalprefix() type-alternatives. Larger or + unrestricted choice sets display as the italicised argument + name or type, and a -typesynopsis always takes precedence. If a -type is specified - it doesn't apply to choice members. It will only be used for validation if the -choicerestricted option is set to false. If all choices are specified in values @@ -11598,6 +11605,34 @@ tcl::namespace::eval punk::args { } + proc private::synopsis_choice_literals {arginfo} { + #Small restricted choice sets display in synopses as unitalicised literal alternates, + #matching the display style of literal()/literalprefix() type-alternatives. + #Returns a list of list-protected choice words, or an empty list when the rule doesn't apply: + # - more than 3 choices (reader should refer to the full help for the choice list) + # - -choicerestricted 0 (other values are also acceptable - bare literals would be misleading) + #A defined -typesynopsis always takes precedence - callers consult it before this. + set allchoices [list] + if {[dict exists $arginfo -choices]} { + set allchoices [dict get $arginfo -choices] + } + foreach {groupname members} [Dict_getdef $arginfo -choicegroups {}] { + lappend allchoices {*}$members + } + set allchoices [punk::args::lib::lunique $allchoices] + if {[llength $allchoices] == 0 || [llength $allchoices] > 3} { + return [list] + } + if {![Dict_getdef $arginfo -choicerestricted 1]} { + return [list] + } + set literals [list] + foreach c $allchoices { + lappend literals [list $c] + } + return $literals + } + proc private::synopsis_form_arg_display {formdict argname} { #non-colour SGR such as bold/italic/strike - so we don't need to worry about NOCOLOR settings set I "\x1b\[3m" ;#[punk::ansi::a+ italic] @@ -11624,6 +11659,12 @@ tcl::namespace::eval punk::args { } else { set tp_displaylist [lrepeat [llength $typelist] ""] } + if {[llength $typelist] == 1} { + set choice_literals [private::synopsis_choice_literals $arginfo] + } else { + #choice semantics for multi-element clauses are per-clause - keep name/type display hints + set choice_literals [list] + } foreach typespec $typelist td $tp_displaylist elementname $name_tail { #elementname will commonly be empty after first iteration @@ -11663,7 +11704,9 @@ tcl::namespace::eval punk::args { #and if there are enough tail words in the argname to match the position in the type list #empty strings can be put in -typesynopsis positions to only override the type information for certain elements of the clause #- e.g for a type list of {string int} we could specify a typesynopsis of {"" "count"} to get display of "FILENAME count" for an argname of "file FILENAME FILECOUNT" - if {[llength $name_tail] >= [llength $typelist]} { + if {[llength $choice_literals]} { + lappend alternates {*}$choice_literals + } elseif {[llength $name_tail] >= [llength $typelist]} { #important to list protect $elementname e.g look at ::apply #The name may contain spaces e.g "{args body ?namespace?}" #This must not be split into multiple words - it is a single element name that happens to contain spaces. @@ -11696,13 +11739,6 @@ tcl::namespace::eval punk::args { set display "\[$clause\]..." } else { set display "\[$clause\]" - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "?[lindex [dict get $arginfo -choices] 0]?" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display "?$argname?" - #} else { - # set display "?$I$argname$NI?" - #} } } else { if {[dict get $arginfo -multiple]} { @@ -11710,13 +11746,6 @@ tcl::namespace::eval punk::args { set display "$clause \[$clause\]..." } else { set display $clause - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "[lindex [dict get $arginfo -choices] 0]" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display $argname - #} else { - # set display "$I$argname$NI" - #} } } return $display @@ -11885,6 +11914,7 @@ tcl::namespace::eval punk::args { } else { set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) + set choice_literals [private::synopsis_choice_literals $arginfo] set type_alternatives [private::split_type_expression $tp] foreach tp_alternative $type_alternatives { set match [lindex $tp_alternative 1] @@ -11902,7 +11932,11 @@ tcl::namespace::eval punk::args { lappend alternates [list *$match] } default { - lappend alternates $I<$tp_alternative>$NI + if {[llength $choice_literals]} { + lappend alternates {*}$choice_literals + } else { + lappend alternates $I<$tp_alternative>$NI + } } } } diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index 130fdcc9..274dfd65 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/src/modules/punk/args-buildversion.txt @@ -1,6 +1,7 @@ -0.5.0 +0.6.0 #First line must be a semantic version number #all other lines are ignored. +#0.6.0 - synopsis display: small restricted choice sets now render as literal alternates - an argument whose choice pool (-choices plus -choicegroups members, deduplicated) has 1-3 members and -choicerestricted true (the default) displays those words unitalicised joined by | in synopses (e.g 'after cancel' shows literal cancel; a 3-choice option shows (left|centre|right)), matching the display style of literal()/literalprefix() type-alternatives. Larger or unrestricted (-choicerestricted 0) choice sets keep the italicised argname/ display, and an explicit -typesynopsis always takes precedence. New private helper punk::args::private::synopsis_choice_literals shared by both synopsis render paths (leaders/values via synopsis_form_arg_display, options inline in synopsis); applies only to single-element -type lists (multi-element clause display unchanged). define doc for -choices documents the rule. Tests: synopsis.test - new characterization coverage for literal/literalprefix/stringstartswith/stringendswith type-alternates, option alternate parenthesization, multi-element clause display, -typesynopsis (value element lists, option passthrough incl documenter ANSI), plus the new choice-literal rule (small sets in leader/option/value positions, choicegroups counting, >3 and unrestricted fallbacks, -typesynopsis precedence) #0.5.0 - G-049 parse-status data model: new punk::args::parse_status - runs a parse attempt (withid/withdef) and returns a documented status structure instead of raising on validation failure (overall ok/status valid|invalid|incomplete/scheme/message/errorcode-minus-argspecs/failureclass/badarg/id/form/receivednames + per-argument argstatus with class/status ok|bad|unparsed/received/positions/hasvalue/value-in-effect incl -default fill). arg_error: new -parsestatus option - both renderers (table and string) now derive goodarg/badarg row marking and choice value-in-effect highlighting from the structure (built internally from -badarg/-parsedargs when not supplied), replacing the transient goodargs/badarg locals; scheme colours resolve per-render into a local array (scheme renders no longer mutate the shared arg_error_CLR array - the -nocolour leak) and the DOCUMENTED -scheme choice value 'nocolour' (and 'nocolor') now takes effect instead of falling through to leftover colours. parse: new -caller option overriding the %caller% frame-walk substitution in validation failure messages (parse_status defaults it to the definition's @cmd -name). get_dict: missingrequiredvalue/missingrequiredleader allocation failures now carry -badarg (type-failed words get badarg marking, not just choice violations). Tests: parsestatus.test (new), usagemarking.test nocolour/leak GAP pins flipped + -parsestatus render parity #0.4.2 - G-046: display-field deferral - resolve no longer tstr-expands display-only content during argument resolution: ${...} in -help (@cmd/@examples/argument records) and @formdisplay -header/-body is masked with inert tokens (spec key DISPLAY_DEFERRED) and expanded on demand at display time (arg_error/eg/resolved_def/@default-copyfrom hooks; separate argdefcache_display cache for non-dynamic defs; @dynamic display content re-expands per render preserving provider refresh). First parse of heavily documented commands drops accordingly (punk::ansi::mark_columns ~4.3s -> ~12ms; tclcore ::lseq resolve ~184ms -> ~2ms) and -help content that calls punk::args-parsing commands (including against its own id) no longer stalls or loops - plus a display-time reentrancy guard (raw ${...} source substituted on nested expansion of the same id). Record splitter factored to private::split_definition_records. Also: @dynamic second-round multiline substitutions into deferred fields now get the 'line' paramindents alignment (rendering_atdynamic_multiline_help_insertion GAP flipped); prefix/alias choice normalization writeback no longer list-quotes single-element-clause values ({\Deleted} shape bug, choicegroups_imap_prefix_shape GAP flipped); -return string renderer aligns cmd-help continuations under the Description label and its Example line shows the example instead of the doc url. Tests: deferredhelp.test (new), rendering.test/choicegroups.test updated per G-046 acceptance #0.4.1 - fixed ensemble_subcommands_definition building the definition against unloaded argdocs: subhelp/doctype-punkargs choiceinfo entries and synopsis choicelabels were omitted for documented subcommands whose registered namespace (::punk::args::register::NAMESPACES) had not yet been lazily loaded - e.g first 'i ansistring' in a fresh shell rendered the autogenerated ensemble help without subcommand-help markers until something else loaded ::punk::ansi::ansistring. The generator now runs update_definitions for the namespaces its id_exists checks could resolve in before testing them (tests: punk/args ensembledef.test, punk/ns cmdhelp.test cmdhelp_ensemble_lazy_registered_argdocs) diff --git a/src/tests/modules/AGENTS.md b/src/tests/modules/AGENTS.md index 53c2e0cf..104c6220 100644 --- a/src/tests/modules/AGENTS.md +++ b/src/tests/modules/AGENTS.md @@ -40,7 +40,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/` - `opunk/console/` — ::opunk::Console backend subclass tests (`testsuites/console/backends.test`, G-001): virtual dispatch of subclass overrides through base-class calls and punk::console::console_spec_resolve (both unchanged), TestConsole determinism + probe-free at_eof, SshConsole capability/eof + the flagship size-via-ANSI-query-over-socket case (a scripted remote terminal answers CSI 6n), TkConsole widget size/eof (gated behind env PUNK_TEST_TK=1 - Tk in the shared testinterp has side effects; also verifiable standalone under a tk-capable kit e.g `punk91 src