Class: Teek::App

Inherits:
Object
  • Object
show all
Defined in:
lib/teek.rb,
lib/teek/dialogs.rb

Constant Summary collapse

BIND_SUBS =
Note:

Each substitution crosses from Tcl to Ruby once. Any #command calls inside the block are additional round-trips. This is negligible for click/key events but could matter for hot-path handlers like <Motion> that fire hundreds of times per second. For those, consider #tcl_eval with inline Tcl expressions to do all work in one evaluation.

Bind a Tk event on a widget, with optional substitutions forwarded as block arguments. Substitutions can be symbols (mapped via BIND_SUBS) or raw Tcl % codes passed through as-is.

Examples:

Mouse click with window coordinates

app.bind('.c', 'Button-1', :x, :y) { |x, y| puts "#{x},#{y}" }

Key press

app.bind('.', 'KeyPress', :keysym) { |k| puts k }

No substitutions

app.bind('.btn', 'Enter') { highlight }

Raw Tcl expression (for codes not in BIND_SUBS)

app.bind('.c', 'Button-1', '%T') { |type| ... }

Canvas coordinate conversion

app.bind(canvas, 'Button-1', :x, :y) do |x, y|
  cx = app.command(canvas, :canvasx, x).to_f
  cy = app.command(canvas, :canvasy, y).to_f
end

Yields:

  • (*values)

    called when the event fires, with substitution values

Returns:

  • (void)

See Also:

{
  x: '%x', y: '%y',                   # window coordinates
  root_x: '%X', root_y: '%Y',         # screen coordinates
  widget: '%W',                        # widget path
  keysym: '%K', keycode: '%k',         # key events
  char: '%A',                          # character (key events)
  width: '%w', height: '%h',           # Configure events
  button: '%b',                        # mouse button number
  mouse_wheel: '%D',                   # mousewheel delta
  type: '%T',                          # event type
  data: '%d',                          # virtual event data (Tk 8.6+)
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(title: nil, track_widgets: true, debug: false, &block) ⇒ App

Returns a new instance of App.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/teek.rb', line 96

def initialize(title: nil, track_widgets: true, debug: false, &block)
  @interp = Teek::Interp.new
  @interp.tcl_eval('package require Tk')
  @winfo = Teek::Winfo.new(self)
  @wm = Teek::Wm.new(self)
  hide
  @widgets = {}
  @widget_counters = Hash.new(0)
  @callback_registry = Teek::CallbackRegistry.new(self)
  @widget_types_by_path = {}
  @_pending_exception = nil
  debug ||= !!ENV['TEEK_DEBUG']
  track_widgets = true if debug
  @track_widgets = track_widgets
  setup_widget_tracking if track_widgets
  setup_destroy_cleanup
  if debug
    require_relative 'teek/debugger'
    @debugger = Teek::Debugger.new(self)
  end
  set_window_title(title) if title
  instance_eval(&block) if block
end

Instance Attribute Details

#_pending_exception=(value) ⇒ Object (writeonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



94
95
96
# File 'lib/teek.rb', line 94

def _pending_exception=(value)
  @_pending_exception = value
end

#callback_registryObject (readonly)

Returns the value of attribute callback_registry.



93
94
95
# File 'lib/teek.rb', line 93

def callback_registry
  @callback_registry
end

#debuggerObject (readonly)

Returns the value of attribute debugger.



93
94
95
# File 'lib/teek.rb', line 93

def debugger
  @debugger
end

#interpObject (readonly)

Returns the value of attribute interp.



93
94
95
# File 'lib/teek.rb', line 93

def interp
  @interp
end

#widgetsObject (readonly)

Returns the value of attribute widgets.



93
94
95
# File 'lib/teek.rb', line 93

def widgets
  @widgets
end

#winfoObject (readonly)

Returns the value of attribute winfo.



93
94
95
# File 'lib/teek.rb', line 93

def winfo
  @winfo
end

#wmObject (readonly)

Returns the value of attribute wm.



93
94
95
# File 'lib/teek.rb', line 93

def wm
  @wm
end

Instance Method Details

#add_debug_console(keybinding = '<F12>') ⇒ Boolean

Enable the Tk debug console. The console starts hidden and can be toggled with the given keyboard shortcut (default: F12).

The Tk console is a built-in interactive Tcl shell — useful for inspecting variables, running Tcl commands, and debugging widget layouts at runtime. It is available on macOS and Windows only; on Linux this method is a no-op (Linux has the real terminal).

Examples:

app = Teek::App.new
app.add_debug_console            # F12 toggles console
app.add_debug_console("<F11>")   # custom key

Parameters:

  • keybinding (String) (defaults to: '<F12>')

    Tk event to toggle the console (default: "")

Returns:

  • (Boolean)

    true if the console was created, false if unavailable on this platform

See Also:



715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
# File 'lib/teek.rb', line 715

def add_debug_console(keybinding = '<F12>')
  @interp.create_console
  @_console_visible = false

  toggle = proc do |*|
    if @_console_visible
      tcl_eval('console hide')
      @_console_visible = false
    else
      tcl_eval('console show')
      @_console_visible = true
    end
  end

  command(:bind, '.', keybinding, toggle)
  true
rescue TclError => e
  warn "Teek: debug console not available on this platform (#{e.message})"
  false
end

#add_package_path(path) ⇒ void

This method returns an undefined value.

Add a directory to Tcl's package search path.

Parameters:

  • path (String)

    directory containing Tcl packages



518
519
520
# File 'lib/teek.rb', line 518

def add_package_path(path)
  tcl_eval("lappend ::auto_path {#{path}}")
end

#after(ms, on_error: :raise) { ... } ⇒ String

Schedule a one-shot timer. Calls the block after ms milliseconds.

Parameters:

  • ms (Integer)

    delay in milliseconds

  • on_error (:raise, Proc, nil) (defaults to: :raise)

    error handling strategy:

    • :raise (default) — exception propagates to Tcl background error handler.
    • Proc — called with the exception; error is swallowed.
    • nil — error is silently swallowed.

Yields:

  • block to call when the timer fires

Returns:

See Also:



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/teek.rb', line 220

def after(ms, on_error: :raise, &block)
  cb_id = nil
  cb_id = @interp.register_callback(proc { |*|
    begin
      block.call
    rescue => e
      raise if on_error == :raise
      on_error.call(e) if on_error.is_a?(Proc)
    ensure
      @interp.unregister_callback(cb_id)
    end
  })
  after_id = @interp.tcl_eval("after #{ms.to_i} {ruby_callback #{cb_id}}")
  after_id.instance_variable_set(:@cb_id, cb_id)
  after_id
end

#after_cancel(after_id) ⇒ void

This method returns an undefined value.

Cancel a pending #after or #after_idle timer.

Parameters:

See Also:



283
284
285
286
287
288
289
290
# File 'lib/teek.rb', line 283

def after_cancel(after_id)
  @interp.tcl_eval("after cancel #{after_id}")
  if (cb_id = after_id.instance_variable_get(:@cb_id))
    @interp.unregister_callback(cb_id)
    after_id.instance_variable_set(:@cb_id, nil)
  end
  after_id
end

#after_idle { ... } ⇒ String

Schedule a block to run once when the event loop is idle.

Yields:

  • block to call when the event loop is idle

Returns:

See Also:



241
242
243
244
245
246
247
248
249
250
# File 'lib/teek.rb', line 241

def after_idle(&block)
  cb_id = nil
  cb_id = @interp.register_callback(proc { |*|
    block.call
    @interp.unregister_callback(cb_id)
  })
  after_id = @interp.tcl_eval("after idle {ruby_callback #{cb_id}}")
  after_id.instance_variable_set(:@cb_id, cb_id)
  after_id
end

#appearanceString?

Get the macOS window appearance. No-op (returns nil) on non-macOS.

Examples:

app.appearance          # => "aqua", "darkaqua", or "auto"
app.appearance = :light # force light mode
app.appearance = :dark  # force dark mode
app.appearance = :auto  # follow system setting

Returns:

  • (String, nil)

    "aqua", "darkaqua", "auto", or nil on non-macOS

See Also:



910
911
912
913
914
915
916
917
# File 'lib/teek.rb', line 910

def appearance
  return nil unless aqua?
  if tk_major >= 9
    @interp.tcl_eval('wm attributes . -appearance').delete('"')
  else
    @interp.tcl_eval('tk::unsupported::MacWindowStyle appearance .')
  end
end

#appearance=(mode) ⇒ void

This method returns an undefined value.

Set the macOS window appearance. No-op on non-macOS.

Parameters:

  • mode (Symbol, String)

    :light, :dark, :auto, or a raw Tk value



922
923
924
925
926
927
928
929
930
931
932
933
934
935
# File 'lib/teek.rb', line 922

def appearance=(mode)
  return unless aqua?
  value = case mode.to_sym
          when :light then 'aqua'
          when :dark  then 'darkaqua'
          when :auto  then 'auto'
          else mode.to_s
          end
  if tk_major >= 9
    @interp.tcl_eval("wm attributes . -appearance #{value}")
  else
    @interp.tcl_eval("tk::unsupported::MacWindowStyle appearance . #{value}")
  end
end

#bind(widget, event, *subs, &block) ⇒ Object



839
840
841
842
843
844
845
846
# File 'lib/teek.rb', line 839

def bind(widget, event, *subs, &block)
  event_str = event.start_with?('<') ? event : "<#{event}>"
  cb = register_callback(proc { |*args| block.call(*args) })
  @callback_registry.reconcile([:bind, widget]) { |before| before.merge(event_str => cb) }
  tcl_subs = subs.map { |s| s.is_a?(Symbol) ? BIND_SUBS.fetch(s) : s.to_s }
  sub_str = tcl_subs.empty? ? '' : ' ' + tcl_subs.join(' ')
  @interp.tcl_eval("bind #{widget} #{event_str} {ruby_callback #{cb}#{sub_str}}")
end

#bool_to_tcl(val) ⇒ String

Convert a Ruby boolean to a Tcl boolean string ("1" or "0").

Parameters:

  • val (Boolean)

Returns:

  • (String)

    "1" or "0"



316
317
318
# File 'lib/teek.rb', line 316

def bool_to_tcl(val)
  Teek.bool_to_tcl(val)
end

#busy(window: '.') { ... } ⇒ Object

Show a busy cursor on a window while executing a block. The cursor is restored even if the block raises.

Parameters:

  • window (String) (defaults to: '.')

    Tk window path

Yields:

  • the work to perform while busy

Returns:

  • the block's return value

See Also:



639
640
641
642
643
644
645
# File 'lib/teek.rb', line 639

def busy(window: '.')
  tcl_eval("tk busy hold #{window}")
  tcl_eval('update idletasks')
  yield
ensure
  tcl_eval("tk busy forget #{window}")
end

#choose_color(initial: nil, title: nil, parent: nil) ⇒ String?

Show the native color picker dialog.

Parameters:

  • initial (String, nil) (defaults to: nil)

    initial color (e.g. "#ff0000")

  • title (String, nil) (defaults to: nil)

    dialog window title

  • parent (String, nil) (defaults to: nil)

    parent window (defaults to the root window)

Returns:

  • (String, nil)

    the chosen color as "#rrggbb", or nil if cancelled

See Also:



99
100
101
102
103
104
105
106
107
# File 'lib/teek/dialogs.rb', line 99

def choose_color(initial: nil, title: nil, parent: nil)
  args = ['tk_chooseColor']
  args.push('-initialcolor', initial) if initial
  args.push('-title', title) if title
  args.push('-parent', parent) if parent

  result = tcl_invoke(*args)
  result.empty? ? nil : result
end

#choose_open_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, multiple: false, parent: nil) ⇒ String, ...

Show the native "choose file to open" dialog.

Parameters:

  • filetypes (Array<Array>, nil) (defaults to: nil)

    e.g. [["PNG Images", ".png"], ["All Files", "*"]] - the second element of each pair can also be an array of extensions (+["Images", [".png", ".jpg"]]+)

  • initialdir (String, nil) (defaults to: nil)

    directory the dialog starts in

  • initialfile (String, nil) (defaults to: nil)

    filename pre-filled in the dialog

  • title (String, nil) (defaults to: nil)

    dialog window title

  • multiple (Boolean) (defaults to: false)

    allow selecting more than one file

  • parent (String, nil) (defaults to: nil)

    parent window (defaults to the root window)

Returns:

  • (String, Array<String>, nil)

    the chosen path (an array of paths if multiple:), or nil if the dialog was cancelled

See Also:



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/teek/dialogs.rb', line 19

def choose_open_file(filetypes: nil, initialdir: nil, initialfile: nil,
                      title: nil, multiple: false, parent: nil)
  args = ['tk_getOpenFile']
  args.push('-filetypes', build_filetypes(filetypes)) if filetypes
  args.push('-initialdir', initialdir) if initialdir
  args.push('-initialfile', initialfile) if initialfile
  args.push('-title', title) if title
  args.push('-parent', parent) if parent
  args.push('-multiple', bool_to_tcl(true)) if multiple

  result = tcl_invoke(*args)
  return nil if result.empty?

  multiple ? split_list(result) : result
end

#choose_save_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, defaultextension: nil, confirmoverwrite: true, parent: nil) ⇒ String?

Show the native "choose file to save" dialog.

Parameters:

  • filetypes (Array<Array>, nil) (defaults to: nil)
  • initialdir (String, nil) (defaults to: nil)

    directory the dialog starts in

  • initialfile (String, nil) (defaults to: nil)

    filename pre-filled in the dialog

  • title (String, nil) (defaults to: nil)

    dialog window title

  • defaultextension (String, nil) (defaults to: nil)

    extension appended if the typed filename doesn't already have one

  • confirmoverwrite (Boolean) (defaults to: true)

    ask before overwriting an existing file (Tk's own default is true; pass false to skip the confirmation)

  • parent (String, nil) (defaults to: nil)

    parent window (defaults to the root window)

Returns:

  • (String, nil)

    the chosen path, or nil if cancelled

See Also:



49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/teek/dialogs.rb', line 49

def choose_save_file(filetypes: nil, initialdir: nil, initialfile: nil,
                      title: nil, defaultextension: nil, confirmoverwrite: true, parent: nil)
  args = ['tk_getSaveFile']
  args.push('-filetypes', build_filetypes(filetypes)) if filetypes
  args.push('-initialdir', initialdir) if initialdir
  args.push('-initialfile', initialfile) if initialfile
  args.push('-title', title) if title
  args.push('-defaultextension', defaultextension) if defaultextension
  args.push('-confirmoverwrite', bool_to_tcl(false)) unless confirmoverwrite
  args.push('-parent', parent) if parent

  result = tcl_invoke(*args)
  result.empty? ? nil : result
end

#command(cmd, *args, **kwargs) ⇒ String

Build and evaluate a Tcl command from Ruby values. Positional args are converted: Symbols pass bare, Procs become callbacks, everything else is brace-quoted. Keyword args become -key value option pairs.

Any Proc-valued arg or kwarg is tracked and released on overwrite, explicit removal, or the owning widget's destruction - there is no unsafe way to attach a callback through this method, regardless of whether the call happens to match a registered per-widget-type interceptor (see CommandInterceptors) or falls through to the generic default. Widget type is inferred automatically from calls shaped like widget creation (a WIDGET_COMMANDS name as cmd, the new path as the first positional arg) - not tied to track_widgets.

Examples:

app.command(:pack, '.btn', side: :left, padx: 10)
# evaluates: pack .btn -side left -padx {10}

Parameters:

  • cmd (Symbol, String)

    the Tcl command name

  • args

    positional arguments

  • kwargs

    keyword arguments mapped to -key value pairs

Returns:

  • (String)

    the Tcl result

Raises:



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/teek.rb', line 341

def command(cmd, *args, **kwargs)
  record_widget_type(cmd, args)

  type = @widget_types_by_path[cmd.to_s]
  entries = type ? CommandInterceptors.for_type(type) : []
  matches = entries.map { |entry| [entry.label, entry.block.call(self, cmd, args, kwargs)] }
                    .reject { |_label, result| result.nil? }

  case matches.size
  when 0 then raw_command(cmd, *args, **track_widget_option_callbacks(cmd, args, kwargs))
  when 1 then matches.first.last
  else
    labels = matches.map(&:first).join(', ')
    raise AmbiguousCommandError,
      "#{matches.size} command interceptors (#{labels}) matched #{cmd.inspect} #{args.inspect} " \
      "for widget type #{type.inspect}"
  end
end

#create_widget(type, path = nil, parent: nil, idempotent: false, **kwargs) ⇒ Widget

Create a Tk widget and return a Widget wrapper.

Auto-generates a unique path if none is given. The path is derived from the widget type and a monotonic counter.

Examples:

Auto-named

btn = app.create_widget('ttk::button', text: 'Click')
# btn.path => ".ttkbtn1"

Explicit path

frm = app.create_widget('ttk::frame', '.myframe')

Nested under a parent

frm = app.create_widget('ttk::frame')
btn = app.create_widget('ttk::button', parent: frm, text: 'Click')
# btn.path => ".ttkfrm1.ttkbtn1"

Parameters:

  • type (String, Symbol)

    Tk widget command (e.g. 'ttk::button', :canvas)

  • path (String, nil) (defaults to: nil)

    explicit Tk path, or nil for auto-naming

  • parent (Widget, String, nil) (defaults to: nil)

    parent widget for path nesting

  • idempotent (Boolean) (defaults to: false)

    skip the creation command if a widget already exists at path - for widgets meant to be fetched by a stable, caller-chosen path and reused across many calls (see #menu) rather than freshly created each time

  • kwargs

    keyword arguments passed to the Tk widget command

Returns:

  • (Widget)

    the created widget



488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/teek.rb', line 488

def create_widget(type, path = nil, parent: nil, idempotent: false, **kwargs)
  type_s = type.to_s
  path ||= next_widget_path(type_s, parent)
  if idempotent && @winfo.exists?(path)
    # Still record the type even though creation itself is skipped - a
    # path that already existed (e.g. built with a raw tcl_eval) is
    # otherwise never seen by #command, so no interceptor could ever
    # engage for it.
    record_widget_type(type_s, [path])
  else
    command(type_s, path, **kwargs)
  end
  Widget.new(self, path)
end

#dark?Boolean

Returns true if the window is currently displayed in dark mode. Always returns false on non-macOS.

Returns:

  • (Boolean)


940
941
942
943
# File 'lib/teek.rb', line 940

def dark?
  return false unless aqua?
  @interp.tcl_eval('tk::unsupported::MacWindowStyle isdark .').delete('"') == '1'
end

#destroy(widget = '.') ⇒ void

This method returns an undefined value.

Destroy a widget and all its children.

Parameters:

  • widget (String) (defaults to: '.')

    Tk widget path (e.g. ".frame1")

Raises:

  • (ArgumentError)

See Also:



591
592
593
594
# File 'lib/teek.rb', line 591

def destroy(widget = '.')
  raise ArgumentError, 'widget path cannot be nil' if widget.nil?
  tcl_eval("destroy #{widget}")
end

#ensure_tcl_helper(name) ⇒ void

This method returns an undefined value.

Evaluate script once per App instance under name, skipping it on later calls. Meant for widget-behavior modules that need to define a Tcl-side helper proc (e.g. a scan routine) without re-sending and re-parsing that definition on every call.

Parameters:

  • name (Symbol)

    unique name for this helper

Yield Returns:

  • (String)

    the Tcl script to evaluate the first time



204
205
206
207
208
209
# File 'lib/teek.rb', line 204

def ensure_tcl_helper(name)
  @installed_tcl_helpers ||= {}
  return if @installed_tcl_helpers[name]
  @interp.tcl_eval(yield)
  @installed_tcl_helpers[name] = true
end

#every(ms, on_error: :raise) { ... } ⇒ RepeatingTimer

Schedule a repeating timer. Calls the block every ms milliseconds until cancelled. The block runs on the main thread in the event loop, so it must be fast (don't block the UI).

Examples:

Basic polling loop

timer = app.every(50) { update_display }
timer.cancel  # stop later

With error handler (timer keeps running)

timer = app.every(100, on_error: ->(e) { log(e) }) { risky_work }

Silent cancel on error

timer = app.every(50, on_error: nil) { maybe_fails }
timer.last_error  # => check later

Parameters:

  • ms (Integer)

    interval in milliseconds

  • on_error (:raise, Proc, nil) (defaults to: :raise)

    error handling strategy:

    • :raise (default) — cancels the timer and raises the exception from the next call to #update.
    • Proc — called with the exception; timer keeps running.
    • nil — cancels the timer silently; error stored in RepeatingTimer#last_error.

Yields:

  • block to call on each tick

Returns:



275
276
277
# File 'lib/teek.rb', line 275

def every(ms, on_error: :raise, &block)
  RepeatingTimer.new(self, ms, on_error: on_error, &block)
end

#font_metrics(font) ⇒ Hash{Symbol => Integer}

Get font metrics (ascent, descent, linespace) for a given font. Uses Tk's C font API directly.

Parameters:

  • font (String)

    font description (e.g. "Helvetica 12", "TkDefaultFont")

Returns:

  • (Hash{Symbol => Integer})

    :ascent, :descent, :linespace

Raises:

See Also:



613
614
615
# File 'lib/teek.rb', line 613

def font_metrics(font)
  @interp.font_metrics(font)
end

#get_variable(name) ⇒ String

Get a Tcl variable's value.

Parameters:

  • name (String)

    variable name (array-element and namespaced forms work)

Returns:

  • (String)

    the value

Raises:

See Also:



581
582
583
584
585
# File 'lib/teek.rb', line 581

def get_variable(name)
  value = @interp.tcl_get_var(name.to_s)
  return value unless value.nil?
  raise Teek::TclError, "can't read \"#{name}\": no such variable"
end

#hide(window = '.') ⇒ void

This method returns an undefined value.

Hide a window without destroying it. Defaults to the root window (".").

Parameters:

  • window (String) (defaults to: '.')

    Tk window path

See Also:



694
695
696
# File 'lib/teek.rb', line 694

def hide(window = '.')
  @wm.withdraw(window: window)
end

#mainloopvoid

This method returns an undefined value.

Enter the Tk event loop. Blocks until the application exits.

See Also:



650
651
652
653
654
655
656
657
658
659
660
# File 'lib/teek.rb', line 650

def mainloop
  if defined?(IRB) || defined?(Pry) || $0 == 'irb' || $0 == 'pry'
    warn "Teek: mainloop blocks the current thread and will make your REPL unresponsive.\n" \
         "  Instead, use app.update in a loop or call app.update manually between commands:\n" \
         "    app.show\n" \
         "    app.update          # process pending events\n" \
         "    # ... interact with your app ...\n" \
         "    app.update          # process again after changes"
  end
  @interp.mainloop
end

#make_list(*args) ⇒ String

Build a properly-escaped Tcl list from Ruby strings.

Parameters:

  • args (Array<String>)

    elements to join

Returns:

  • (String)

    a Tcl-formatted list



302
303
304
# File 'lib/teek.rb', line 302

def make_list(*args)
  Teek.make_list(*args)
end

#measure_chars(font, text, max_pixels, **opts) ⇒ Hash{Symbol => Integer}

Measure how many bytes of text fit within a pixel width limit. Useful for text truncation, ellipsis, and line wrapping.

Parameters:

  • font (String)

    font description (e.g. "Helvetica 12")

  • text (String)

    text to measure

  • max_pixels (Integer)

    maximum pixel width (-1 for unlimited)

  • opts (Hash)

    options

Options Hash (**opts):

  • :partial_ok (Boolean)

    allow partial character at boundary

  • :whole_words (Boolean)

    break only at word boundaries

  • :at_least_one (Boolean)

    always return at least one character

Returns:

  • (Hash{Symbol => Integer})

    :bytes and :width

Raises:

See Also:



629
630
631
# File 'lib/teek.rb', line 629

def measure_chars(font, text, max_pixels, **opts)
  @interp.measure_chars(font, text, max_pixels, opts)
end

Wrap a Tk menu at the given path, creating it (tearoff disabled) if it doesn't exist yet. Safe to call repeatedly with the same path - it's a flyweight, not a handle you need to hold onto: call this again any time you're about to rebuild the menu (e.g. on every right-click).

Parameters:

  • path (String)

    Tk menu path (e.g. ".card.ctx")

  • kwargs

    extra options for the underlying menu command, used only the first time this path is created

Returns:



511
512
513
# File 'lib/teek.rb', line 511

def menu(path, **kwargs)
  create_widget(:menu, path, idempotent: true, tearoff: 0, **kwargs)
end

#message_box(message:, title: nil, detail: nil, icon: :info, type: :ok, default: nil, parent: nil) ⇒ Symbol

Show a message box with one or more buttons.

Parameters:

  • message (String)

    the main message text

  • title (String, nil) (defaults to: nil)

    dialog window title

  • detail (String, nil) (defaults to: nil)

    additional explanatory text, shown smaller below message

  • icon (:error, :info, :question, :warning) (defaults to: :info)

    icon to display

  • type (:ok, :okcancel, :abortretryignore, :yesno, :yesnocancel, :retrycancel) (defaults to: :ok)

    which button(s) to show

  • default (Symbol, nil) (defaults to: nil)

    which button is focused by default (e.g. :cancel); defaults to Tk's own choice if omitted

  • parent (String, nil) (defaults to: nil)

    parent window (defaults to the root window)

Returns:

  • (Symbol)

    the pressed button - :ok, :cancel, :yes, :no, :abort, :retry, or :ignore

See Also:



79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/teek/dialogs.rb', line 79

def message_box(message:, title: nil, detail: nil, icon: :info, type: :ok,
                 default: nil, parent: nil)
  args = ['tk_messageBox', '-message', message.to_s]
  args.push('-title', title) if title
  args.push('-detail', detail) if detail
  args.push('-icon', icon.to_s) if icon
  args.push('-type', type.to_s) if type
  args.push('-default', default.to_s) if default
  args.push('-parent', parent) if parent

  tcl_invoke(*args).to_sym
end

#on_close(window: '.') { ... } ⇒ void

This method returns an undefined value.

Register a handler for the window manager's close button (WM_DELETE_WINDOW - the titlebar close box, Cmd-W, Alt-F4, etc., depending on platform).

Tk's own default behavior (destroy the window) only applies when nothing else has claimed this protocol - setting a handler here replaces it, so the block is entirely responsible for deciding whether the window actually closes. Call #destroy yourself if you want it to; do nothing (or show a confirmation first) if you don't.

Examples:

Confirm before quitting

app.on_close { app.destroy('.') if app.message_box(message: 'Quit?', type: :yesno) == :yes }

A toplevel that just hides instead of closing

app.on_close(window: settings_window) { app.tcl_eval("wm withdraw #{settings_window}") }

Parameters:

  • window (String) (defaults to: '.')

    Tk window path (default: the root window)

Yields:

  • called when the window's close button is pressed

See Also:



880
881
882
883
884
# File 'lib/teek.rb', line 880

def on_close(window: '.', &block)
  cb = register_callback(block, relay_break_continue: false)
  @callback_registry.reconcile([:wm_protocol, window]) { |before| before.merge('WM_DELETE_WINDOW' => cb) }
  @interp.tcl_eval("wm protocol #{window} WM_DELETE_WINDOW {ruby_callback #{cb}}")
end

#package_namesArray<String>

List all packages known to this interpreter. Scans auto_path for package indexes before querying.

Returns:

  • (Array<String>)

See Also:



539
540
541
542
# File 'lib/teek.rb', line 539

def package_names
  scan_packages
  split_list(tcl_eval('package names'))
end

#package_present?(name) ⇒ Boolean

Check if a package is already loaded in this interpreter.

Parameters:

  • name (String)

    package name

Returns:

  • (Boolean)

See Also:



548
549
550
551
552
553
# File 'lib/teek.rb', line 548

def package_present?(name)
  tcl_eval("package present #{name}")
  true
rescue Teek::TclError
  false
end

#package_versions(name) ⇒ Array<String>

List available versions of a package. Scans auto_path for package indexes before querying.

Parameters:

  • name (String)

    package name

Returns:

  • (Array<String>)

See Also:



560
561
562
563
# File 'lib/teek.rb', line 560

def package_versions(name)
  scan_packages
  split_list(tcl_eval("package versions #{name}"))
end

This method returns an undefined value.

Pop up a menu at the given screen coordinates.

Parameters:

  • menu (Widget, String)

    the menu to pop up

  • x (Integer)

    screen x coordinate

  • y (Integer)

    screen y coordinate

  • entry (Integer, String, nil) (defaults to: nil)

    index or label of the entry to show as active

See Also:



118
119
120
121
122
123
# File 'lib/teek/dialogs.rb', line 118

def popup_menu(menu, x:, y:, entry: nil)
  args = ['tk_popup', menu.to_s, x.to_s, y.to_s]
  args << entry.to_s if entry
  tcl_invoke(*args)
  nil
end

#raw_command(cmd, *args, **kwargs) ⇒ String

The dumb Tcl builder underneath #command - no interceptor lookup, no per-widget-type awareness. Used internally by interceptors (to actually perform their Tcl work without re-entering dispatch) and by #command's own generic fallback. Any Proc here still gets registered as a real, working callback (positional: bind-shaped, relay_break_continue: true; kwarg, via #tcl_arg_value: option-shaped, relay_break_continue: false) - it just isn't tracked for release. Prefer #command; call this directly only from within an interceptor.

Built as a plain argv array passed to Interp#tcl_invoke (Tcl_EvalObjv) rather than a joined string handed to tcl_eval, so no value needs escaping - unbalanced braces, $, [, newlines, whatever, all pass through verbatim. There is nothing here for a value to "break out" of.

Returns:

  • (String)

    the Tcl result



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/teek.rb', line 376

def raw_command(cmd, *args, **kwargs)
  argv = [cmd.to_s]
  i = 0
  while i < args.length
    arg = args[i]
    if arg.is_a?(Proc)
      id = register_callback(arg)
      subs = []
      while i + 1 < args.length && args[i + 1].is_a?(String) && args[i + 1].start_with?('%')
        subs << args[i + 1]
        i += 1
      end
      argv << if subs.empty?
                "ruby_callback #{id}"
              else
                "ruby_callback #{id} #{subs.join(' ')}"
              end
    else
      argv << tcl_arg_value(arg)
    end
    i += 1
  end
  kwargs.each do |key, value|
    argv << "-#{key}"
    argv << tcl_arg_value(value)
  end
  @interp.tcl_invoke(*argv)
end

#record_widget_type(cmd, args) ⇒ void

This method returns an undefined value.

Records that args[0] is a widget of type cmd, if this call looks like widget creation (+cmd+ is a known WIDGET_COMMANDS entry). Not tied to track_widgets - this is what lets #command look up a registered interceptor for a bare path string on any later call, regardless of how the widget was created.



453
454
455
456
457
458
459
# File 'lib/teek.rb', line 453

def record_widget_type(cmd, args)
  return unless WIDGET_COMMANDS.include?(cmd.to_s)
  path = args[0]
  return unless path.is_a?(String)

  @widget_types_by_path[path] = cmd.to_s
end

#register_callback(callable, relay_break_continue: true) ⇒ Integer

Register a Ruby callable as a Tcl callback. The callable can use throw for Tcl control flow:

throw :teek_break    - stop event propagation (like Tcl "break")
throw :teek_continue - Tcl TCL_CONTINUE
throw :teek_return   - Tcl TCL_RETURN

+:teek_break+/+:teek_continue+ only mean something when Tcl actually dispatches the result through a context that knows how to handle TCL_BREAK/TCL_CONTINUE - Tk's bind mechanism does; a plain script invocation (a menu entry's or widget's -command) does not, and returning either code there is a Tcl error ("invoked break/continue outside of a loop"), not a no-op. relay_break_continue: false is for exactly those non-bind callers: throw is still caught (so it can't crash as an uncaught throw), but is treated as the callback simply finishing, instead of being relayed to Tcl. :teek_return is always relayed either way - TCL_RETURN is safe in any context.

Parameters:

  • callable (#call)

    a Proc or lambda to invoke from Tcl

  • relay_break_continue (Boolean) (defaults to: true)

    whether a caught :teek_break/ :teek_continue is relayed to Tcl as TCL_BREAK/TCL_CONTINUE (true, for bind-dispatched callbacks) or silently absorbed (false, for callbacks invoked as a plain script - menu/widget -command options)

Returns:

  • (Integer)

    callback ID, usable as ruby_callback <id> in Tcl

See Also:



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/teek.rb', line 169

def register_callback(callable, relay_break_continue: true)
  wrapped = proc { |*args|
    caught = nil
    catch(:teek_break) do
      catch(:teek_continue) do
        catch(:teek_return) do
          callable.call(*args)
          caught = :_none
        end
        caught ||= :return
      end
      caught ||= :continue
    end
    caught ||= :break
    next nil if caught == :_none
    next caught if caught == :return
    relay_break_continue ? caught : nil
  }
  @interp.register_callback(wrapped)
end

#register_drop_target(widget) ⇒ void

This method returns an undefined value.

Register a widget as a file drop target. After registration, dropping files onto the widget generates a single <<DropFile>> virtual event with all file paths as a Tcl list in the event data. Use #split_list to convert to a Ruby array.

Examples:

app.register_drop_target('.')
app.bind('.', '<<DropFile>>', :data) do |data|
  paths = app.split_list(data)
  puts "Dropped #{paths.length} file(s): #{paths.inspect}"
end

Parameters:

  • widget (String)

    Tk widget path (e.g., ".", ".frame")



898
899
900
# File 'lib/teek.rb', line 898

def register_drop_target(widget)
  @interp.register_drop_target(widget.to_s)
end

#require_package(name, version = nil) ⇒ String

Load a Tcl package into this interpreter.

Parameters:

  • name (String)

    package name (e.g. "BWidget")

  • version (String, nil) (defaults to: nil)

    minimum version constraint

Returns:

  • (String)

    the version that was loaded

Raises:

See Also:



528
529
530
531
532
533
# File 'lib/teek.rb', line 528

def require_package(name, version = nil)
  cmd = version ? "package require #{name} #{version}" : "package require #{name}"
  tcl_eval(cmd)
rescue Teek::TclError => e
  raise Teek::TclError, "Package '#{name}' not found. Ensure it is installed and on Tcl's auto_path. (#{e.message})"
end

#set_variable(name, value) ⇒ String

Set a Tcl variable. Useful for widget textvariable and variable options. Goes through Tcl_SetVar directly (no re-parsing), so the value never needs escaping - braces, backslashes, $, [, whatever, all safe.

Parameters:

  • name (String)

    variable name (array-element and namespaced forms work)

  • value (String)

    value to set

Returns:

  • (String)

    the value

See Also:



572
573
574
# File 'lib/teek.rb', line 572

def set_variable(name, value)
  @interp.tcl_set_var(name.to_s, value.to_s)
end

#set_window_geometry(geometry, window: '.') ⇒ String

Set a window's geometry (e.g. "400x300", "400x300+100+50").

Parameters:

  • geometry (String)

    geometry string

  • window (String) (defaults to: '.')

    Tk window path

Returns:

  • (String)

    the geometry

See Also:



761
762
763
# File 'lib/teek.rb', line 761

def set_window_geometry(geometry, window: '.')
  @wm.set_geometry(geometry, window: window)
end

#set_window_resizable(width, height, window: '.') ⇒ void

This method returns an undefined value.

Set whether a window is resizable.

Parameters:

  • width (Boolean)

    allow horizontal resize

  • height (Boolean)

    allow vertical resize

  • window (String) (defaults to: '.')

    Tk window path

See Also:



781
782
783
# File 'lib/teek.rb', line 781

def set_window_resizable(width, height, window: '.')
  @wm.set_resizable(width, height, window: window)
end

#set_window_title(title, window: '.') ⇒ String

Set a window's title.

Parameters:

  • title (String)

    new title

  • window (String) (defaults to: '.')

    Tk window path

Returns:

  • (String)

    the title

See Also:



742
743
744
# File 'lib/teek.rb', line 742

def set_window_title(title, window: '.')
  @wm.set_title(title, window: window)
end

#show(window = '.') ⇒ void

This method returns an undefined value.

Show a window. Defaults to the root window (".").

Parameters:

  • window (String) (defaults to: '.')

    Tk window path

See Also:



685
686
687
# File 'lib/teek.rb', line 685

def show(window = '.')
  @wm.deiconify(window: window)
end

#split_list(str) ⇒ Array<String>

Split a Tcl list string into a Ruby array of strings.

Parameters:

  • str (String)

    a Tcl-formatted list

Returns:

  • (Array<String>)


295
296
297
# File 'lib/teek.rb', line 295

def split_list(str)
  Teek.split_list(str)
end

#tcl_eval(script) ⇒ String

Note:

Any callback embedded in script (e.g. a hand-built ruby_callback <id>) is on you to register and release - none of #command's tracking applies here. Creating a widget this way (instead of via #command/#create_widget/#menu) also means its type is never recorded, so a registered CommandInterceptors entry won't engage for it even on later, ordinary #command calls - create widgets through those methods and reach for tcl_eval for everything else.

Evaluate a raw Tcl script string and return the result. Prefer #command for building commands from Ruby values; use this when you need Tcl-level features like variable substitution or inline expressions that #command can't express.

Parameters:

  • script (String)

    Tcl code to evaluate

Returns:

  • (String)

    the Tcl result



134
135
136
# File 'lib/teek.rb', line 134

def tcl_eval(script)
  @interp.tcl_eval(script)
end

#tcl_invoke(*args) ⇒ String

Invoke a Tcl command with pre-split arguments (no Tcl parsing). Safer than #tcl_eval when arguments may contain special characters.

Parameters:

  • args (Array<String>)

    command name followed by arguments

Returns:

  • (String)

    the Tcl result



142
143
144
# File 'lib/teek.rb', line 142

def tcl_invoke(*args)
  @interp.tcl_invoke(*args)
end

#tcl_to_bool(str) ⇒ Boolean

Convert a Tcl boolean string ("0", "1", "yes", "no", etc.) to Ruby boolean.

Parameters:

  • str (String)

    a Tcl boolean value

Returns:

  • (Boolean)


309
310
311
# File 'lib/teek.rb', line 309

def tcl_to_bool(str)
  Teek.tcl_to_bool(str)
end

#text_width(font, text) ⇒ Integer

Measure the pixel width of a text string in a given font. Uses Tk's C font API directly — faster than the Tcl font measure command.

Parameters:

  • font (String)

    font description (e.g. "Helvetica 12", "TkDefaultFont")

  • text (String)

    text to measure

Returns:

  • (Integer)

    pixel width

Raises:

See Also:



603
604
605
# File 'lib/teek.rb', line 603

def text_width(font, text)
  @interp.text_width(font, text)
end

#track_widget_option_callbacks(cmd, args, kwargs) ⇒ Hash

#command's fallback for any call that no registered interceptor claimed: registers any Proc-valued kwarg (e.g. command:, validatecommand:) as a callback tracked under cmd, releasing it if reconfigured or when the widget is destroyed. A widget's own options are never silently renumbered or invalidated out from under us the way menu entries are, so this uses a cheap in-memory CallbackRegistry#reconcile rather than a live-scan one.

Tracked under the widget's own path, by [*context, key], where context is args normalized to strings - except a bare configure (or widget creation - the container is the new widget's path, not the cmd used to create it) normalizes to an empty context, since all three address the same underlying option namespace and must replace each other. Any other subcommand (e.g. a treeview's heading col) keeps its own args as part of the key, so two different targets sharing an option name (two columns both using command:) don't collide.

Returns:

  • (Hash)

    kwargs with any Proc values swapped for the Tcl script #raw_command embeds



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/teek.rb', line 424

def track_widget_option_callbacks(cmd, args, kwargs)
  proc_kwargs = kwargs.select { |_, value| value.is_a?(Proc) }
  return kwargs if proc_kwargs.empty?

  if WIDGET_COMMANDS.include?(cmd.to_s) && args[0].is_a?(String)
    widget_path = args[0]
    context = []
  else
    widget_path = cmd.to_s
    context = (args.empty? || args[0].to_s == 'configure') ? [] : args.map(&:to_s)
  end

  ids = {}
  replacements = {}
  proc_kwargs.each do |key, value|
    id = register_callback(value, relay_break_continue: false)
    ids[context + [key.to_s]] = id
    replacements[key] = "ruby_callback #{id}"
  end
  @callback_registry.reconcile([:widget_option, widget_path]) { |before| before.merge(ids) }
  kwargs.merge(replacements)
end

#unbind(widget, event) ⇒ void

This method returns an undefined value.

Remove an event binding previously set with #bind.

Parameters:

  • widget (String)

    Tk widget path or class tag

  • event (String)

    Tk event name, with or without angle brackets

See Also:



854
855
856
857
858
# File 'lib/teek.rb', line 854

def unbind(widget, event)
  event_str = event.start_with?('<') ? event : "<#{event}>"
  @callback_registry.reconcile([:bind, widget]) { |before| before.reject { |k, _| k == event_str } }
  @interp.tcl_eval("bind #{widget} #{event_str} {}")
end

#unregister_callback(id) ⇒ void

This method returns an undefined value.

Remove a previously registered callback by its ID.

Parameters:



193
194
195
# File 'lib/teek.rb', line 193

def unregister_callback(id)
  @interp.unregister_callback(id)
end

#updatevoid

This method returns an undefined value.

Process all pending events and idle callbacks, then return.

See Also:



665
666
667
668
669
670
671
# File 'lib/teek.rb', line 665

def update
  @interp.tcl_eval('update')
  if (e = @_pending_exception)
    @_pending_exception = nil
    raise e
  end
end

#update_idletasksvoid

This method returns an undefined value.

Process only pending idle callbacks (e.g. geometry redraws), then return.

See Also:



676
677
678
# File 'lib/teek.rb', line 676

def update_idletasks
  @interp.tcl_eval('update idletasks')
end

#window_geometry(window: '.') ⇒ String

Get a window's current geometry.

Parameters:

  • window (String) (defaults to: '.')

    Tk window path

Returns:

  • (String)

    geometry string (e.g. "400x300+0+0")

See Also:



770
771
772
# File 'lib/teek.rb', line 770

def window_geometry(window: '.')
  @wm.geometry(window: window)
end

#window_resizable(window: '.') ⇒ Array(Boolean, Boolean)

Get whether a window is resizable.

Parameters:

  • window (String) (defaults to: '.')

    Tk window path

Returns:

  • (Array(Boolean, Boolean))

    [width_resizable, height_resizable]

See Also:



790
791
792
# File 'lib/teek.rb', line 790

def window_resizable(window: '.')
  @wm.resizable(window: window)
end

#window_title(window: '.') ⇒ String

Get a window's current title.

Parameters:

  • window (String) (defaults to: '.')

    Tk window path

Returns:

  • (String)

    current title

See Also:



751
752
753
# File 'lib/teek.rb', line 751

def window_title(window: '.')
  @wm.title(window: window)
end