Browse Source

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
master
Julian Noble 1 week ago
parent
commit
63089c7f43
  1. 4
      CHANGELOG.md
  2. 5
      goals/G-020-screencap-input-module.md
  3. 2
      punkproject.toml
  4. 2
      src/lib/AGENTS.md
  5. 31
      src/lib/app-punkscript/punkscript.tcl
  6. 31
      src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl

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.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:<name>` 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.

5
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 `<punkexe> 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).

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.4.0"
version = "0.4.1"

2
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:<name>` 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:<name>` 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`)

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

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

Loading…
Cancel
Save