module

Tryst

The Tcl-major-version auto-detection probe (TCL_VERSION=8/9 forces a choice; anything else auto-detects - see interp.cr's header comment for the full policy and why this needs to be a shell probe at all) used to be hand-copied verbatim at every {% if %}/{% unless %} site that needs it: twice in interp.cr, once in event_source.cr. Crystal macro locals don't persist across separate top-level {% %} blocks (confirmed directly - a {% x = ... %} in one block is invisible to the next), so sharing a computed result isn't an option. A plain Crystal constant doesn't have that limitation, though, and a macro backtick literal accepts #{...} interpolation of one directly - `#{X.id}` splices X's raw text into the backtick before it runs - so the SCRIPT TEXT lives here exactly once, and every call site becomes an identical one-liner: `#{Tryst::TCL_VERSION_PROBE.id}`.stringify.chomp == "9" (still re-executed at every site - that part of the duplication is a real Crystal limitation, not a style choice - just no longer re-typed, so a bug in the probe itself only needs fixing once).

Two variants, chosen once here rather than at every call site, for the same reason expand_lib_flags's Windows @[Link] ldflags need their own variant (see tcltk_link_windows.cr's header comment for the full explanation): a macro backtick literal evaluates through the same Process.run(shell: true) that never goes through an actual shell on Windows, so the POSIX case/if script below has to be handed to a real sh.exe explicitly there, with pkg-config resolved relative to the running crystal binary rather than trusted from PATH search order (a stale, unrelated MSYS/Cygwin install earlier in Machine PATH will otherwise silently win and misreport - confirmed directly).

Constants

TCL_MAJOR_VERSION = {{ (`#{Tryst::TCL_VERSION_PROBE.id}`).stringify.chomp == "9" ? 9 : 8 }}

Which Tcl/Tk major version this build was compiled to link against - see the @[Link] lines at the top of this file for why that has to be a compile-time choice rather than something one binary picks at runtime, and for what TCL_VERSION=8/TCL_VERSION=9/auto-detect actually do. Has to compute the exact same answer as every {% if %} above, so it runs the identical probe rather than just hardcoding a default here

  • see the @[Link] block's own comment for what it checks.

Interp#initialize cross-checks this against the version the runtime library actually reports and raises a friendly TclError on a mismatch

  • the lookups the @[Link] ldflags do to find the right library are heuristic (a system with both installed could resolve the "wrong" one, particularly on the 9.x arm - see the comment there), so this is the backstop that turns a silent ABI mismatch into an immediate, readable error instead of undefined behavior the first time a version-sensitive call is made.
TCL_VERSION_PROBE = "case \"$TCL_VERSION\" in\n 8) echo 8 ;;\n 9) echo 9 ;;\n *)\n if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists tcl9.0 tk9.0 2>/dev/null; then\n echo 9\n elif command -v pkg-config >/dev/null 2>&1 && pkg-config --exists tcl tk 2>/dev/null && case \"$(pkg-config --modversion tcl 2>/dev/null)\" in 9.*) true ;; *) false ;; esac; then\n echo 9\n elif command -v brew >/dev/null 2>&1 && (brew --prefix tcl-tk@9 >/dev/null 2>&1 || brew --prefix tcl-tk >/dev/null 2>&1); then\n echo 9\n else\n echo 8\n fi\n ;;\nesac"
WIDGET_COMMANDS = Set {"button", "label", "frame", "entry", "text", "canvas", "listbox", "scrollbar", "scale", "spinbox", "menu", "menubutton", "message", "panedwindow", "labelframe", "checkbutton", "radiobutton", "toplevel", "ttk::button", "ttk::label", "ttk::frame", "ttk::entry", "ttk::combobox", "ttk::checkbutton", "ttk::radiobutton", "ttk::scale", "ttk::scrollbar", "ttk::spinbox", "ttk::separator", "ttk::sizegrip", "ttk::progressbar", "ttk::notebook", "ttk::panedwindow", "ttk::labelframe", "ttk::menubutton", "ttk::treeview"}

Tk widget-creation commands #command/#create_widget recognize - used to auto-record a widget's type at its path (#record_widget_type) and to decide where a Proc-valued kwarg's ownership is scoped (#track_widget_option_callbacks). Mirrors ruby-tryst's Tryst::WIDGET_COMMANDS (lib/tryst.rb). A Set, not a list: every #command call asks whether the command it was handed is one of these, twice.

Class methods

bool_to_tcl(val) : String

Converts a Ruby-ish truthy/falsy value to a Tcl boolean string ("1" or "0") - only nil/false are falsy, same as Crystal's own if/ternary truthiness. Pure Crystal, no Tcl interpreter involved. Mirrors ruby-tryst's Tryst.bool_to_tcl (lib/tryst.rb).

Source
decode_modified_utf8_nul(ptr : Pointer(LibC::Char), len : LibTcl::TclSize) : String

Tcl's internal string representation never contains a raw NUL byte - an embedded NUL is instead encoded as the two bytes 0xC0 0x80, Tcl's "modified UTF-8" (the same trick Java uses internally). 0xC0/0xC1 can never start a real UTF-8 sequence (both would only ever produce an overlong encoding, which UTF-8 forbids), so any 0xC0 byte here is unambiguously this escape, never misread genuine UTF-8 content. Shared by Interp#obj_to_string and tryst_crystal_callback_dispatch's id/arg extraction (interp.cr, below) - the two boundaries where Tcl hands string bytes back to Crystal.

Source
enter_callback

@api private - called by Interp#dispatch_callback only.

Source
exit_callback

@api private - called by Interp#dispatch_callback only.

Source
in_callback?
Source
in_callback_on_this_fiber?

Whether THIS fiber is the one running the open callback - the question App#off_thread actually needs answered, and a different one from .in_callback?: a fiber spawned from inside a callback is scheduled while that callback is still open (the moment the callback's own fiber suspends - see Interp#spin_until), so .in_callback? reads true on it too, but its C stack is inside no Tcl call at all and it must not act as if it were.

Source
make_list(args : Enumerable(String)) : String

Same as the splat overload above, for a runtime-sized collection (e.g. already have an Array(String) in hand, rather than individual arguments) - a splat can't be applied to a runtime Array directly (verified directly: "argument to splat must be a tuple"), the same reason Interp#tcl_invoke has this same pair of overloads.

Source
make_list

A typed splat (*args : String) rejects a zero-argument call outright (verified directly) - unlike an untyped splat, which just yields an empty Enumerable - so the empty case needs its own overload instead of an args.empty? guard inside the main one below.

Source
make_list(*args : String) : String

Builds a properly Tcl-quoted list string from the given elements (only where quoting is actually needed - Tcl's own list-formatting rules). Mirrors ruby-tryst's Tryst.make_list (ext/tryst/tcltkbridge.c).

Source
platform

The one Platform instance for this process, built on first use.

Source
resolve_widget_target(target) : String

:root as a widget/window target, resolved to Tk's own "." spelling - the one raw-path lore App's otherwise-untyped widget/window/path parameters still forced on a caller. Anything else (a Widget, a raw path String) passes through via #to_s exactly as it always has.

Source
split_list(str : String | Nil) : Array(String)

Parses a Tcl list string into an array of strings - does not recursively parse nested lists. nil/empty input returns an empty array. Mirrors ruby-tryst's Tryst.split_list (ext/tryst/tcltkbridge.c).

Source
tcl_to_bool(str : String) : Bool

Converts a Tcl boolean string ("1"/"0", "true"/"false", "yes"/"no", "on"/"off", any numeric value, case-insensitive) to a Bool. Mirrors ruby-tryst's Tryst.tcl_to_bool (ext/tryst/tcltkbridge.c).

Source

Nested types