From 63089c7f43ca157437a309bab4aa2951b42ccd4e Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Tue, 7 Jul 2026 18:34:05 +1000 Subject: [PATCH] script subcommand: match tclsh Tk main-loop behaviour for GUI scripts - 0.4.1 Root cause: Tk registers a main loop (Tcl_SetMainLoop); tclsh''s Tcl_Main services it after the script, so `tclsh gui.tcl` behaves like wish. The script subcommand sourced-then-exited, so a Tk script''s after-callbacks never ran (scriptlib/tktimer.tcl flashed and died; shell was the workaround). Confirmed: native tclsh tktimer 1 blocks ~1.35s; script did 0.38s. Fix: after the script body (success paths only), if a Tk main loop is registered (info exists ::tk_version && winfo exists .), tkwait window . services the event loop until the main window closes, then exit - the script-level equivalent of Tcl_Main. A script that exits from a callback (tktimer countdown) terminates directly. Console scripts have no main loop and exit at once; a script error exits at once (no hanging window). Verified both generations: tktimer via script now blocks the countdown (punk902z 1.49s, punksys 1.57s - Tk present in both) exit 0; tkhello_exit immediate; tk-script-that-errors exit 1 in 0.37s (no hang); tkhello (no self-close) blocks like wish; console script unaffected. Enables G-020 GUI automation via `script` instead of `shell` (noted in G-020 detail). Project version 0.4.1. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 4 +++ goals/G-020-screencap-input-module.md | 5 +++ punkproject.toml | 2 +- src/lib/AGENTS.md | 2 +- src/lib/app-punkscript/punkscript.tcl | 31 +++++++++++++++---- .../lib/app-punkscript/punkscript.tcl | 31 +++++++++++++++---- 6 files changed, 61 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c694b9e8..1b448606 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.4.1] - 2026-07-07 + +- `script` subcommand now matches `tclsh` for GUI scripts: if the script leaves a registered Tk main loop (Tk loaded with a live main window), the event loop is serviced until the last window closes, instead of sourcing-then-exiting immediately. So a Tk script with no explicit `exit` (or one that exits from an `after` callback, like `scriptlib/tktimer.tcl`) stays alive and runs — previously it flashed and died, and `shell` was needed as a workaround (relevant to G-020 GUI automation). Console scripts are unaffected; a script that errors exits at once (no hanging window). Verified on both generations. + ## [0.4.0] - 2026-07-07 - `script` subcommand supports `lib:` scriptlib scripts (with or without `.tcl` extension), matching the `shell` subcommand's scheme: `punkexe script lib:hello`, or bare `punkexe lib:hello` via reclassification. The prefix always wins — a literal path beginning `lib:` (pathological; illegal on Windows filesystems) is reachable via `./lib:...`. Not-found errors list the searched locations. Only `.tcl` runs via the script subcommand. diff --git a/goals/G-020-screencap-input-module.md b/goals/G-020-screencap-input-module.md index bb09293c..451b7c53 100644 --- a/goals/G-020-screencap-input-module.md +++ b/goals/G-020-screencap-input-module.md @@ -83,6 +83,11 @@ the module is a separate development, not a refactor of it. ## Notes +- Launch path (2026-07-07): the `script` subcommand (G-015) services a + registered Tk main loop after the script, matching tclsh - so a Tk app can + be launched via ` script app.tcl` and stays alive to be snapshotted + and driven, without needing the `shell` subcommand. This is the intended + entry point for GUI automation here. - Related: G-018 (kit composition; Tk as loadable package, no wish binaries), G-019 (a trimmed capture-capable executable is a plausible scan target), G-021 (agent-facing surface over this module), G-001 (backend plugin pattern). diff --git a/punkproject.toml b/punkproject.toml index 949c835c..890d81c5 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.4.0" +version = "0.4.1" diff --git a/src/lib/AGENTS.md b/src/lib/AGENTS.md index da4f65a2..e879c437 100644 --- a/src/lib/AGENTS.md +++ b/src/lib/AGENTS.md @@ -16,7 +16,7 @@ Source of truth for all editable `pkgIndex.tcl`-based library packages. These ar - Libraries here include: - `app-punk/` — Punk REPL app entry (`app-punk::repl`) - `app-punkshell/` — Punkshell app entry (`app-punkshell`); on eof of its input channel the `env(PUNK_PIPE_EOF)` policy (`exit`|`interactive`|unset=console heuristic) decides between terminating and reopening the console for an interactive repl — automated callers should set `exit` or end piped input with an explicit `exit` - - `app-punkscript/` — lean one-shot script runner behind the punk executable `script` subcommand (G-015): runs a file, a `lib:` scriptlib script (resolved via `punk::path::scriptlib_resolve`), or piped stdin in the default punk shell module/alias environment with honest exit codes; no shellfilter stacks or logging side effects, never falls into an interactive shell — the reliable path for automated/agent callers (prefer this over piping into `shell`); stdin form echoes the script's final result when non-empty + - `app-punkscript/` — lean one-shot script runner behind the punk executable `script` subcommand (G-015): runs a file, a `lib:` scriptlib script (resolved via `punk::path::scriptlib_resolve`), or piped stdin in the default punk shell module/alias environment with honest exit codes; no shellfilter stacks or logging side effects, never falls into an interactive shell — the reliable path for automated/agent callers (prefer this over piping into `shell`); stdin form echoes the script's final result when non-empty; GUI parity with `tclsh` — a script leaving a registered Tk main loop is serviced until its window closes (so `script app.tcl` keeps a Tk app alive), while console scripts and errored scripts exit immediately - `app-shellspy/` — ShellSpy app entry (`app-shellspy`) - `app_shell/` — Shell app helpers (`app_shell`) - `app_shellrun/` — Shell run helpers (`app_shellrun`) diff --git a/src/lib/app-punkscript/punkscript.tcl b/src/lib/app-punkscript/punkscript.tcl index ac7aea04..fe29bf4c 100644 --- a/src/lib/app-punkscript/punkscript.tcl +++ b/src/lib/app-punkscript/punkscript.tcl @@ -16,6 +16,12 @@ package provide app-punkscript 1.0 # output (and, on error, the error report to stderr) appears. # - exit codes: 0 on success, 1 on script error or usage error; a script's own # explicit exit code is honoured. +# - GUI parity with tclsh: if the script leaves a registered Tk main loop +# (loaded Tk with a live main window), the event loop is serviced until the +# last main window closes, exactly as tclsh's Tcl_Main does - so a Tk script +# with no explicit exit stays alive (needed for GUI/G-020 use) rather than +# flashing and dying. A pure console script has no main loop and exits at +# once; a script error exits at once (no hanging window). # #punk_main.tcl sets ::argv to the arguments after the 'script' subcommand #before requiring this package (a bare non-subcommand first argument is also @@ -32,6 +38,23 @@ apply {{} { expr {[dict exists $conf -inputmode] || [dict exists $conf -mode]} }}] + #Match tclsh: after the script body, if a Tk main loop is registered (Tk + #loaded with a live main window '.'), service the event loop until '.' is + #destroyed - the script-level equivalent of Tcl_Main running a registered + #main loop. Then exit with $code. A script that exits from a callback (e.g. + #tktimer at end of countdown) never returns from tkwait - its exit wins. + #Only the success paths call this; errors exit immediately above. + set finish [apply {{} { + return {{code} { + if {[info exists ::tk_version] && [winfo exists .]} { + catch {tkwait window .} + } + flush stdout + flush stderr + exit $code + }} + }}] + #default punk shell environment via shared definitions - no local alias lists package require punk package require punk::aliascore @@ -94,9 +117,7 @@ apply {{} { flush stderr exit 1 } - flush stdout - flush stderr - exit 0 + apply $finish 0 } else { #stdin form: read the whole of piped/redirected stdin as the script. #An interactive terminal with no scriptname is a usage error - never @@ -126,8 +147,6 @@ apply {{} { if {[string length $result]} { puts stdout $result } - flush stdout - flush stderr - exit 0 + apply $finish 0 } }} diff --git a/src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl b/src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl index ac7aea04..fe29bf4c 100644 --- a/src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl +++ b/src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl @@ -16,6 +16,12 @@ package provide app-punkscript 1.0 # output (and, on error, the error report to stderr) appears. # - exit codes: 0 on success, 1 on script error or usage error; a script's own # explicit exit code is honoured. +# - GUI parity with tclsh: if the script leaves a registered Tk main loop +# (loaded Tk with a live main window), the event loop is serviced until the +# last main window closes, exactly as tclsh's Tcl_Main does - so a Tk script +# with no explicit exit stays alive (needed for GUI/G-020 use) rather than +# flashing and dying. A pure console script has no main loop and exits at +# once; a script error exits at once (no hanging window). # #punk_main.tcl sets ::argv to the arguments after the 'script' subcommand #before requiring this package (a bare non-subcommand first argument is also @@ -32,6 +38,23 @@ apply {{} { expr {[dict exists $conf -inputmode] || [dict exists $conf -mode]} }}] + #Match tclsh: after the script body, if a Tk main loop is registered (Tk + #loaded with a live main window '.'), service the event loop until '.' is + #destroyed - the script-level equivalent of Tcl_Main running a registered + #main loop. Then exit with $code. A script that exits from a callback (e.g. + #tktimer at end of countdown) never returns from tkwait - its exit wins. + #Only the success paths call this; errors exit immediately above. + set finish [apply {{} { + return {{code} { + if {[info exists ::tk_version] && [winfo exists .]} { + catch {tkwait window .} + } + flush stdout + flush stderr + exit $code + }} + }}] + #default punk shell environment via shared definitions - no local alias lists package require punk package require punk::aliascore @@ -94,9 +117,7 @@ apply {{} { flush stderr exit 1 } - flush stdout - flush stderr - exit 0 + apply $finish 0 } else { #stdin form: read the whole of piped/redirected stdin as the script. #An interactive terminal with no scriptname is a usage error - never @@ -126,8 +147,6 @@ apply {{} { if {[string length $result]} { puts stdout $result } - flush stdout - flush stderr - exit 0 + apply $finish 0 } }}