teek 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +139 -0
  5. data/Rakefile +316 -0
  6. data/ext/teek/extconf.rb +79 -0
  7. data/ext/teek/stubs.h +33 -0
  8. data/ext/teek/tcl9compat.h +211 -0
  9. data/ext/teek/tcltkbridge.c +1597 -0
  10. data/ext/teek/tcltkbridge.h +42 -0
  11. data/ext/teek/tkfont.c +218 -0
  12. data/ext/teek/tkphoto.c +477 -0
  13. data/ext/teek/tkwin.c +144 -0
  14. data/lib/teek/background_none.rb +158 -0
  15. data/lib/teek/background_ractor4x.rb +410 -0
  16. data/lib/teek/background_thread.rb +272 -0
  17. data/lib/teek/debugger.rb +742 -0
  18. data/lib/teek/demo_support.rb +150 -0
  19. data/lib/teek/ractor_support.rb +246 -0
  20. data/lib/teek/version.rb +5 -0
  21. data/lib/teek.rb +540 -0
  22. data/sample/calculator.rb +260 -0
  23. data/sample/debug_demo.rb +45 -0
  24. data/sample/goldberg.rb +1803 -0
  25. data/sample/goldberg_helpers.rb +170 -0
  26. data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
  27. data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
  28. data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
  29. data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
  30. data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
  31. data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
  32. data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
  33. data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
  34. data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
  35. data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
  36. data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
  37. data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
  38. data/sample/minesweeper/minesweeper.rb +452 -0
  39. data/sample/threading_demo.rb +499 -0
  40. data/teek.gemspec +32 -0
  41. metadata +179 -0
data/lib/teek.rb ADDED
@@ -0,0 +1,540 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tcltklib'
4
+ require_relative 'teek/version'
5
+ require_relative 'teek/ractor_support'
6
+
7
+ # Ruby interface to Tcl/Tk. Provides a thin wrapper around a Tcl interpreter
8
+ # with Ruby callbacks, event bindings, and background work support.
9
+ #
10
+ # The main entry point is {Teek::App}, which initializes Tcl/Tk and provides
11
+ # methods for evaluating Tcl code, creating widgets, and running the event loop.
12
+ #
13
+ # @example Basic usage
14
+ # app = Teek::App.new
15
+ # app.command('ttk::button', '.btn', text: 'Click', command: proc { puts "hi" })
16
+ # app.command(:pack, '.btn')
17
+ # app.show
18
+ # app.mainloop
19
+ #
20
+ # @example Background work (keeps UI responsive)
21
+ # app.background_work(urls, mode: :thread) do |task, data|
22
+ # data.each { |url| task.yield(fetch(url)) }
23
+ # end.on_progress { |result| update_ui(result) }
24
+ # .on_done { puts "Finished" }
25
+ #
26
+ # @see Teek::App
27
+ # @see Teek::BackgroundWork
28
+ module Teek
29
+
30
+ def self.bool_to_tcl(val)
31
+ val ? "1" : "0"
32
+ end
33
+
34
+ WIDGET_COMMANDS = %w[
35
+ button label frame entry text canvas listbox
36
+ scrollbar scale spinbox menu menubutton message
37
+ panedwindow labelframe checkbutton radiobutton
38
+ toplevel
39
+ ttk::button ttk::label ttk::frame ttk::entry
40
+ ttk::combobox ttk::checkbutton ttk::radiobutton
41
+ ttk::scale ttk::scrollbar ttk::spinbox ttk::separator
42
+ ttk::sizegrip ttk::progressbar ttk::notebook
43
+ ttk::panedwindow ttk::labelframe ttk::menubutton
44
+ ttk::treeview
45
+ ].freeze
46
+
47
+ class App
48
+ attr_reader :interp, :widgets, :debugger
49
+
50
+ def initialize(track_widgets: true, debug: false, &block)
51
+ @interp = Teek::Interp.new
52
+ @interp.tcl_eval('package require Tk')
53
+ hide
54
+ @widgets = {}
55
+ debug ||= !!ENV['TEEK_DEBUG']
56
+ track_widgets = true if debug
57
+ setup_widget_tracking if track_widgets
58
+ if debug
59
+ require_relative 'teek/debugger'
60
+ @debugger = Teek::Debugger.new(self)
61
+ end
62
+ instance_eval(&block) if block
63
+ end
64
+
65
+ # Evaluate a raw Tcl script string and return the result.
66
+ # Prefer {#command} for building commands from Ruby values; use this
67
+ # when you need Tcl-level features like variable substitution or
68
+ # inline expressions that {#command} can't express.
69
+ # @param script [String] Tcl code to evaluate
70
+ # @return [String] the Tcl result
71
+ def tcl_eval(script)
72
+ @interp.tcl_eval(script)
73
+ end
74
+
75
+ # Invoke a Tcl command with pre-split arguments (no Tcl parsing).
76
+ # Safer than {#tcl_eval} when arguments may contain special characters.
77
+ # @param args [Array<String>] command name followed by arguments
78
+ # @return [String] the Tcl result
79
+ def tcl_invoke(*args)
80
+ @interp.tcl_invoke(*args)
81
+ end
82
+
83
+ # Register a Ruby callable as a Tcl callback.
84
+ # The callable can use +throw+ for Tcl control flow:
85
+ # throw :teek_break - stop event propagation (like Tcl "break")
86
+ # throw :teek_continue - Tcl TCL_CONTINUE
87
+ # throw :teek_return - Tcl TCL_RETURN
88
+ # @param callable [#call] a Proc or lambda to invoke from Tcl
89
+ # @return [Integer] callback ID, usable as +ruby_callback <id>+ in Tcl
90
+ # @see #unregister_callback
91
+ def register_callback(callable)
92
+ wrapped = proc { |*args|
93
+ caught = nil
94
+ catch(:teek_break) do
95
+ catch(:teek_continue) do
96
+ catch(:teek_return) do
97
+ callable.call(*args)
98
+ caught = :_none
99
+ end
100
+ caught ||= :return
101
+ end
102
+ caught ||= :continue
103
+ end
104
+ caught ||= :break
105
+ caught == :_none ? nil : caught
106
+ }
107
+ @interp.register_callback(wrapped)
108
+ end
109
+
110
+ # Remove a previously registered callback by its ID.
111
+ # @param id [Integer] callback ID returned by {#register_callback}
112
+ # @return [void]
113
+ def unregister_callback(id)
114
+ @interp.unregister_callback(id)
115
+ end
116
+
117
+ # Schedule a one-shot timer. Calls the block after +ms+ milliseconds.
118
+ # @param ms [Integer] delay in milliseconds
119
+ # @yield block to call when the timer fires
120
+ # @return [String] timer ID, pass to {#after_cancel} to cancel
121
+ def after(ms, &block)
122
+ cb_id = nil
123
+ cb_id = @interp.register_callback(proc { |*|
124
+ block.call
125
+ @interp.unregister_callback(cb_id)
126
+ })
127
+ after_id = @interp.tcl_eval("after #{ms.to_i} {ruby_callback #{cb_id}}")
128
+ after_id.instance_variable_set(:@cb_id, cb_id)
129
+ after_id
130
+ end
131
+
132
+ # Schedule a block to run once when the event loop is idle.
133
+ # @yield block to call when the event loop is idle
134
+ # @return [String] timer ID, pass to {#after_cancel} to cancel
135
+ def after_idle(&block)
136
+ cb_id = nil
137
+ cb_id = @interp.register_callback(proc { |*|
138
+ block.call
139
+ @interp.unregister_callback(cb_id)
140
+ })
141
+ after_id = @interp.tcl_eval("after idle {ruby_callback #{cb_id}}")
142
+ after_id.instance_variable_set(:@cb_id, cb_id)
143
+ after_id
144
+ end
145
+
146
+ # Cancel a pending {#after} or {#after_idle} timer.
147
+ # @param after_id [String] timer ID returned by {#after} or {#after_idle}
148
+ # @return [void]
149
+ def after_cancel(after_id)
150
+ @interp.tcl_eval("after cancel #{after_id}")
151
+ if (cb_id = after_id.instance_variable_get(:@cb_id))
152
+ @interp.unregister_callback(cb_id)
153
+ after_id.instance_variable_set(:@cb_id, nil)
154
+ end
155
+ after_id
156
+ end
157
+
158
+ # Split a Tcl list string into a Ruby array of strings.
159
+ # @param str [String] a Tcl-formatted list
160
+ # @return [Array<String>]
161
+ def split_list(str)
162
+ Teek.split_list(str)
163
+ end
164
+
165
+ # Build a properly-escaped Tcl list from Ruby strings.
166
+ # @param args [Array<String>] elements to join
167
+ # @return [String] a Tcl-formatted list
168
+ def make_list(*args)
169
+ Teek.make_list(*args)
170
+ end
171
+
172
+ # Convert a Tcl boolean string ("0", "1", "yes", "no", etc.) to Ruby boolean.
173
+ # @param str [String] a Tcl boolean value
174
+ # @return [Boolean]
175
+ def tcl_to_bool(str)
176
+ Teek.tcl_to_bool(str)
177
+ end
178
+
179
+ # Convert a Ruby boolean to a Tcl boolean string ("1" or "0").
180
+ # @param val [Boolean]
181
+ # @return [String] "1" or "0"
182
+ def bool_to_tcl(val)
183
+ Teek.bool_to_tcl(val)
184
+ end
185
+
186
+ # Build and evaluate a Tcl command from Ruby values.
187
+ # Positional args are converted: Symbols pass bare, Procs become
188
+ # callbacks, everything else is brace-quoted. Keyword args become
189
+ # +-key value+ option pairs.
190
+ # @example
191
+ # app.command(:pack, '.btn', side: :left, padx: 10)
192
+ # # evaluates: pack .btn -side left -padx {10}
193
+ # @param cmd [Symbol, String] the Tcl command name
194
+ # @param args positional arguments
195
+ # @param kwargs keyword arguments mapped to +-key value+ pairs
196
+ # @return [String] the Tcl result
197
+ def command(cmd, *args, **kwargs)
198
+ parts = [cmd.to_s]
199
+ args.each do |arg|
200
+ parts << tcl_value(arg)
201
+ end
202
+ kwargs.each do |key, value|
203
+ parts << "-#{key}"
204
+ parts << tcl_value(value)
205
+ end
206
+ @interp.tcl_eval(parts.join(' '))
207
+ end
208
+
209
+ # Add a directory to Tcl's package search path.
210
+ # @param path [String] directory containing Tcl packages
211
+ # @return [void]
212
+ def add_package_path(path)
213
+ tcl_eval("lappend ::auto_path {#{path}}")
214
+ end
215
+
216
+ # Load a Tcl package into this interpreter.
217
+ # @param name [String] package name (e.g. "BWidget")
218
+ # @param version [String, nil] minimum version constraint
219
+ # @return [String] the version that was loaded
220
+ # @raise [Teek::TclError] if the package is not found
221
+ def require_package(name, version = nil)
222
+ cmd = version ? "package require #{name} #{version}" : "package require #{name}"
223
+ tcl_eval(cmd)
224
+ rescue Teek::TclError => e
225
+ raise Teek::TclError, "Package '#{name}' not found. Ensure it is installed and on Tcl's auto_path. (#{e.message})"
226
+ end
227
+
228
+ # List all packages known to this interpreter.
229
+ # Scans +auto_path+ for package indexes before querying.
230
+ # @return [Array<String>]
231
+ def package_names
232
+ scan_packages
233
+ split_list(tcl_eval('package names'))
234
+ end
235
+
236
+ # Check if a package is already loaded in this interpreter.
237
+ # @param name [String] package name
238
+ # @return [Boolean]
239
+ def package_present?(name)
240
+ tcl_eval("package present #{name}")
241
+ true
242
+ rescue Teek::TclError
243
+ false
244
+ end
245
+
246
+ # List available versions of a package.
247
+ # Scans +auto_path+ for package indexes before querying.
248
+ # @param name [String] package name
249
+ # @return [Array<String>]
250
+ def package_versions(name)
251
+ scan_packages
252
+ split_list(tcl_eval("package versions #{name}"))
253
+ end
254
+
255
+ # Set a Tcl variable. Useful for widget +textvariable+ and +variable+ options.
256
+ # @param name [String] variable name
257
+ # @param value [String] value to set
258
+ # @return [String] the value
259
+ def set_variable(name, value)
260
+ tcl_eval("set #{name} {#{value}}")
261
+ end
262
+
263
+ # Get a Tcl variable's value.
264
+ # @param name [String] variable name
265
+ # @return [String] the value
266
+ # @raise [Teek::TclError] if the variable doesn't exist
267
+ def get_variable(name)
268
+ tcl_eval("set #{name}")
269
+ end
270
+
271
+ # Destroy a widget and all its children.
272
+ # @param widget [String] Tk widget path (e.g. ".frame1")
273
+ # @return [void]
274
+ def destroy(widget)
275
+ tcl_eval("destroy #{widget}")
276
+ end
277
+
278
+ # Show a busy cursor on a window while executing a block.
279
+ # The cursor is restored even if the block raises.
280
+ # @param window [String] Tk window path
281
+ # @yield the work to perform while busy
282
+ # @return the block's return value
283
+ def busy(window: '.')
284
+ tcl_eval("tk busy hold #{window}")
285
+ tcl_eval('update idletasks')
286
+ yield
287
+ ensure
288
+ tcl_eval("tk busy forget #{window}")
289
+ end
290
+
291
+ # Enter the Tk event loop. Blocks until the application exits.
292
+ # @return [void]
293
+ def mainloop
294
+ if defined?(IRB) || defined?(Pry) || $0 == 'irb' || $0 == 'pry'
295
+ warn "Teek: mainloop blocks the current thread and will make your REPL unresponsive.\n" \
296
+ " Instead, use app.update in a loop or call app.update manually between commands:\n" \
297
+ " app.show\n" \
298
+ " app.update # process pending events\n" \
299
+ " # ... interact with your app ...\n" \
300
+ " app.update # process again after changes"
301
+ end
302
+ @interp.mainloop
303
+ end
304
+
305
+ # Process all pending events and idle callbacks, then return.
306
+ # @return [void]
307
+ def update
308
+ @interp.tcl_eval('update')
309
+ end
310
+
311
+ # Process only pending idle callbacks (e.g. geometry redraws), then return.
312
+ # @return [void]
313
+ def update_idletasks
314
+ @interp.tcl_eval('update idletasks')
315
+ end
316
+
317
+ # Show a window. Defaults to the root window (".").
318
+ # @param window [String] Tk window path
319
+ # @return [void]
320
+ def show(window = '.')
321
+ @interp.tcl_eval("wm deiconify #{window}")
322
+ end
323
+
324
+ # Hide a window without destroying it. Defaults to the root window (".").
325
+ # @param window [String] Tk window path
326
+ # @return [void]
327
+ def hide(window = '.')
328
+ @interp.tcl_eval("wm withdraw #{window}")
329
+ end
330
+
331
+ # Set a window's title.
332
+ # @param title [String] new title
333
+ # @param window [String] Tk window path
334
+ # @return [String] the title
335
+ def set_window_title(title, window: '.')
336
+ tcl_eval("wm title #{window} {#{title}}")
337
+ end
338
+
339
+ # Get a window's current title.
340
+ # @param window [String] Tk window path
341
+ # @return [String] current title
342
+ def window_title(window: '.')
343
+ tcl_eval("wm title #{window}")
344
+ end
345
+
346
+ # Set a window's geometry (e.g. "400x300", "400x300+100+50").
347
+ # @param geometry [String] geometry string
348
+ # @param window [String] Tk window path
349
+ # @return [String] the geometry
350
+ def set_window_geometry(geometry, window: '.')
351
+ tcl_eval("wm geometry #{window} #{geometry}")
352
+ end
353
+
354
+ # Get a window's current geometry.
355
+ # @param window [String] Tk window path
356
+ # @return [String] geometry string (e.g. "400x300+0+0")
357
+ def window_geometry(window: '.')
358
+ tcl_eval("wm geometry #{window}")
359
+ end
360
+
361
+ # Set whether a window is resizable.
362
+ # @param width [Boolean] allow horizontal resize
363
+ # @param height [Boolean] allow vertical resize
364
+ # @param window [String] Tk window path
365
+ # @return [void]
366
+ def set_window_resizable(width, height, window: '.')
367
+ tcl_eval("wm resizable #{window} #{width ? 1 : 0} #{height ? 1 : 0}")
368
+ end
369
+
370
+ # Get whether a window is resizable.
371
+ # @param window [String] Tk window path
372
+ # @return [Array(Boolean, Boolean)] [width_resizable, height_resizable]
373
+ def window_resizable(window: '.')
374
+ parts = tcl_eval("wm resizable #{window}").split
375
+ [parts[0] == '1', parts[1] == '1']
376
+ end
377
+
378
+ # Bind a Tk event on a widget, with optional substitutions forwarded
379
+ # as block arguments. Substitutions can be symbols (mapped via
380
+ # {BIND_SUBS}) or raw Tcl +%+ codes passed through as-is.
381
+ #
382
+ # @example Mouse click with window coordinates
383
+ # app.bind('.c', 'Button-1', :x, :y) { |x, y| puts "#{x},#{y}" }
384
+ # @example Key press
385
+ # app.bind('.', 'KeyPress', :keysym) { |k| puts k }
386
+ # @example No substitutions
387
+ # app.bind('.btn', 'Enter') { highlight }
388
+ # @example Raw Tcl expression (for codes not in BIND_SUBS)
389
+ # app.bind('.c', 'Button-1', '%T') { |type| ... }
390
+ # @example Canvas coordinate conversion
391
+ # app.bind(canvas, 'Button-1', :x, :y) do |x, y|
392
+ # cx = app.command(canvas, :canvasx, x).to_f
393
+ # cy = app.command(canvas, :canvasy, y).to_f
394
+ # end
395
+ #
396
+ # @note Each substitution crosses from Tcl to Ruby once. Any {#command}
397
+ # calls inside the block are additional round-trips. This is negligible
398
+ # for click/key events but could matter for hot-path handlers like
399
+ # +<Motion>+ that fire hundreds of times per second. For those, consider
400
+ # {#tcl_eval} with inline Tcl expressions to do all work in one evaluation.
401
+ #
402
+ # @param widget [String] Tk widget path or class tag (e.g. ".btn", "Entry")
403
+ # @param event [String] Tk event name, with or without angle brackets
404
+ # @param subs [Array<Symbol, String>] substitution codes (see {BIND_SUBS})
405
+ # @yield [*values] called when the event fires, with substitution values
406
+ # @return [void]
407
+ # @see #unbind
408
+ #
409
+ BIND_SUBS = {
410
+ x: '%x', y: '%y', # window coordinates
411
+ root_x: '%X', root_y: '%Y', # screen coordinates
412
+ widget: '%W', # widget path
413
+ keysym: '%K', keycode: '%k', # key events
414
+ char: '%A', # character (key events)
415
+ width: '%w', height: '%h', # Configure events
416
+ button: '%b', # mouse button number
417
+ mouse_wheel: '%D', # mousewheel delta
418
+ type: '%T', # event type
419
+ }.freeze
420
+
421
+ def bind(widget, event, *subs, &block)
422
+ event_str = event.start_with?('<') ? event : "<#{event}>"
423
+ cb = register_callback(proc { |*args| block.call(*args) })
424
+ tcl_subs = subs.map { |s| s.is_a?(Symbol) ? BIND_SUBS.fetch(s) : s.to_s }
425
+ sub_str = tcl_subs.empty? ? '' : ' ' + tcl_subs.join(' ')
426
+ @interp.tcl_eval("bind #{widget} #{event_str} {ruby_callback #{cb}#{sub_str}}")
427
+ end
428
+
429
+ # Remove an event binding previously set with {#bind}.
430
+ # @param widget [String] Tk widget path or class tag
431
+ # @param event [String] Tk event name, with or without angle brackets
432
+ # @return [void]
433
+ # @see #bind
434
+ def unbind(widget, event)
435
+ event_str = event.start_with?('<') ? event : "<#{event}>"
436
+ @interp.tcl_eval("bind #{widget} #{event_str} {}")
437
+ end
438
+
439
+ # Get the macOS window appearance. No-op (returns +nil+) on non-macOS.
440
+ # @example
441
+ # app.appearance # => "aqua", "darkaqua", or "auto"
442
+ # app.appearance = :light # force light mode
443
+ # app.appearance = :dark # force dark mode
444
+ # app.appearance = :auto # follow system setting
445
+ # @return [String, nil] "aqua", "darkaqua", "auto", or nil on non-macOS
446
+ # @see #dark?
447
+ def appearance
448
+ return nil unless aqua?
449
+ if tk_major >= 9
450
+ @interp.tcl_eval('wm attributes . -appearance').delete('"')
451
+ else
452
+ @interp.tcl_eval('tk::unsupported::MacWindowStyle appearance .')
453
+ end
454
+ end
455
+
456
+ # Set the macOS window appearance. No-op on non-macOS.
457
+ # @param mode [Symbol, String] +:light+, +:dark+, +:auto+, or a raw Tk value
458
+ # @return [void]
459
+ def appearance=(mode)
460
+ return unless aqua?
461
+ value = case mode.to_sym
462
+ when :light then 'aqua'
463
+ when :dark then 'darkaqua'
464
+ when :auto then 'auto'
465
+ else mode.to_s
466
+ end
467
+ if tk_major >= 9
468
+ @interp.tcl_eval("wm attributes . -appearance #{value}")
469
+ else
470
+ @interp.tcl_eval("tk::unsupported::MacWindowStyle appearance . #{value}")
471
+ end
472
+ end
473
+
474
+ # Returns true if the window is currently displayed in dark mode.
475
+ # Always returns false on non-macOS.
476
+ # @return [Boolean]
477
+ def dark?
478
+ return false unless aqua?
479
+ @interp.tcl_eval('tk::unsupported::MacWindowStyle isdark .').delete('"') == '1'
480
+ end
481
+
482
+ private
483
+
484
+ # Force Tcl to scan auto_path for pkgIndex.tcl files so that
485
+ # package_names and package_versions reflect all discoverable packages.
486
+ def scan_packages
487
+ tcl_eval('catch {package require __teek_scan__}')
488
+ end
489
+
490
+ def aqua?
491
+ @aqua ||= @interp.tcl_eval('tk windowingsystem') == 'aqua'
492
+ end
493
+
494
+ def tk_major
495
+ @tk_major ||= @interp.tcl_eval('info patchlevel').split('.').first.to_i
496
+ end
497
+
498
+ def setup_widget_tracking
499
+ @create_cb_id = @interp.register_callback(proc { |path, cls|
500
+ next if path.start_with?('.teek_debug')
501
+ @widgets[path] = { class: cls, parent: File.dirname(path).gsub(/\A$/, '.') }
502
+ @debugger&.on_widget_created(path, cls)
503
+ })
504
+ @destroy_cb_id = @interp.register_callback(proc { |path|
505
+ next if path.start_with?('.teek_debug')
506
+ @widgets.delete(path)
507
+ @debugger&.on_widget_destroyed(path)
508
+ })
509
+
510
+ # Tcl proc called on widget creation (trace leave)
511
+ @interp.tcl_eval("proc ::teek_track_create {cmd_string code result op} {
512
+ set path [lindex $cmd_string 1]
513
+ if {$code == 0 && [winfo exists $path]} {
514
+ set cls [winfo class $path]
515
+ ruby_callback #{@create_cb_id} $path $cls
516
+ }
517
+ }")
518
+
519
+ # Tcl proc called on widget destruction (bind)
520
+ @interp.tcl_eval("bind all <Destroy> {ruby_callback #{@destroy_cb_id} %W}")
521
+
522
+ # Add trace on each widget command
523
+ Teek::WIDGET_COMMANDS.each do |cmd|
524
+ @interp.tcl_eval("catch {trace add execution #{cmd} leave ::teek_track_create}")
525
+ end
526
+ end
527
+
528
+ def tcl_value(value)
529
+ case value
530
+ when Proc
531
+ id = @interp.register_callback(value)
532
+ "{ruby_callback #{id}}"
533
+ when Symbol
534
+ value.to_s
535
+ else
536
+ "{#{value}}"
537
+ end
538
+ end
539
+ end
540
+ end