teek 0.1.1 → 0.1.3
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.
- checksums.yaml +4 -4
- data/README.md +46 -0
- data/Rakefile +162 -5
- data/ext/teek/extconf.rb +1 -1
- data/ext/teek/tcltkbridge.c +9 -0
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkeventsource.c +195 -0
- data/ext/teek/tkphoto.c +169 -5
- data/ext/teek/tkwin.c +84 -0
- data/lib/teek/background_ractor4x.rb +32 -4
- data/lib/teek/photo.rb +232 -0
- data/lib/teek/version.rb +1 -1
- data/lib/teek.rb +202 -5
- data/sample/gamepad_viewer/assets/controller.png +0 -0
- data/sample/gamepad_viewer/gamepad_viewer.rb +554 -0
- data/sample/optcarrot/thwaite.nes +0 -0
- data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
- data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
- data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
- data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
- data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
- data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
- data/sample/optcarrot/vendor/optcarrot.rb +14 -0
- data/sample/optcarrot.rb +354 -0
- data/sample/paint/assets/bucket.png +0 -0
- data/sample/paint/assets/cursor.png +0 -0
- data/sample/paint/assets/eraser.png +0 -0
- data/sample/paint/assets/pencil.png +0 -0
- data/sample/paint/assets/spray.png +0 -0
- data/sample/paint/layer.rb +255 -0
- data/sample/paint/layer_manager.rb +179 -0
- data/sample/paint/paint_demo.rb +837 -0
- data/sample/paint/sparse_pixel_buffer.rb +202 -0
- data/sample/sdl2_demo.rb +318 -0
- data/sample/yam/assets/click.wav +0 -0
- data/sample/yam/assets/explosion.wav +0 -0
- data/sample/yam/assets/flag.wav +0 -0
- data/sample/yam/assets/music.mp3 +0 -0
- data/sample/yam/assets/sweep.wav +0 -0
- data/sample/{minesweeper/minesweeper.rb → yam/yam.rb} +147 -12
- metadata +50 -14
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_0.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_1.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_2.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_3.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_4.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_5.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_6.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_7.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_8.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_F.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_M.png +0 -0
- /data/sample/{minesweeper → yam}/assets/MINESWEEPER_X.png +0 -0
data/lib/teek.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'tcltklib'
|
|
|
4
4
|
require_relative 'teek/version'
|
|
5
5
|
require_relative 'teek/ractor_support'
|
|
6
6
|
require_relative 'teek/widget'
|
|
7
|
+
require_relative 'teek/photo'
|
|
7
8
|
|
|
8
9
|
# Ruby interface to Tcl/Tk. Provides a thin wrapper around a Tcl interpreter
|
|
9
10
|
# with Ruby callbacks, event bindings, and background work support.
|
|
@@ -47,13 +48,15 @@ module Teek
|
|
|
47
48
|
|
|
48
49
|
class App
|
|
49
50
|
attr_reader :interp, :widgets, :debugger
|
|
51
|
+
attr_writer :_pending_exception # @api private
|
|
50
52
|
|
|
51
|
-
def initialize(track_widgets: true, debug: false, &block)
|
|
53
|
+
def initialize(title: nil, track_widgets: true, debug: false, &block)
|
|
52
54
|
@interp = Teek::Interp.new
|
|
53
55
|
@interp.tcl_eval('package require Tk')
|
|
54
56
|
hide
|
|
55
57
|
@widgets = {}
|
|
56
58
|
@widget_counters = Hash.new(0)
|
|
59
|
+
@_pending_exception = nil
|
|
57
60
|
debug ||= !!ENV['TEEK_DEBUG']
|
|
58
61
|
track_widgets = true if debug
|
|
59
62
|
setup_widget_tracking if track_widgets
|
|
@@ -61,6 +64,7 @@ module Teek
|
|
|
61
64
|
require_relative 'teek/debugger'
|
|
62
65
|
@debugger = Teek::Debugger.new(self)
|
|
63
66
|
end
|
|
67
|
+
set_window_title(title) if title
|
|
64
68
|
instance_eval(&block) if block
|
|
65
69
|
end
|
|
66
70
|
|
|
@@ -118,14 +122,24 @@ module Teek
|
|
|
118
122
|
|
|
119
123
|
# Schedule a one-shot timer. Calls the block after +ms+ milliseconds.
|
|
120
124
|
# @param ms [Integer] delay in milliseconds
|
|
125
|
+
# @param on_error [:raise, Proc, nil] error handling strategy:
|
|
126
|
+
# - +:raise+ (default) — exception propagates to Tcl background error handler.
|
|
127
|
+
# - +Proc+ — called with the exception; error is swallowed.
|
|
128
|
+
# - +nil+ — error is silently swallowed.
|
|
121
129
|
# @yield block to call when the timer fires
|
|
122
130
|
# @return [String] timer ID, pass to {#after_cancel} to cancel
|
|
123
131
|
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/after.htm#M5 after ms
|
|
124
|
-
def after(ms, &block)
|
|
132
|
+
def after(ms, on_error: :raise, &block)
|
|
125
133
|
cb_id = nil
|
|
126
134
|
cb_id = @interp.register_callback(proc { |*|
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
begin
|
|
136
|
+
block.call
|
|
137
|
+
rescue => e
|
|
138
|
+
raise if on_error == :raise
|
|
139
|
+
on_error.call(e) if on_error.is_a?(Proc)
|
|
140
|
+
ensure
|
|
141
|
+
@interp.unregister_callback(cb_id)
|
|
142
|
+
end
|
|
129
143
|
})
|
|
130
144
|
after_id = @interp.tcl_eval("after #{ms.to_i} {ruby_callback #{cb_id}}")
|
|
131
145
|
after_id.instance_variable_set(:@cb_id, cb_id)
|
|
@@ -147,6 +161,33 @@ module Teek
|
|
|
147
161
|
after_id
|
|
148
162
|
end
|
|
149
163
|
|
|
164
|
+
# Schedule a repeating timer. Calls the block every +ms+ milliseconds
|
|
165
|
+
# until cancelled. The block runs on the main thread in the event loop,
|
|
166
|
+
# so it must be fast (don't block the UI).
|
|
167
|
+
#
|
|
168
|
+
# @param ms [Integer] interval in milliseconds
|
|
169
|
+
# @param on_error [:raise, Proc, nil] error handling strategy:
|
|
170
|
+
# - +:raise+ (default) — cancels the timer and raises the exception
|
|
171
|
+
# from the next call to {#update}.
|
|
172
|
+
# - +Proc+ — called with the exception; timer keeps running.
|
|
173
|
+
# - +nil+ — cancels the timer silently; error stored in {RepeatingTimer#last_error}.
|
|
174
|
+
# @yield block to call on each tick
|
|
175
|
+
# @return [RepeatingTimer] cancel handle
|
|
176
|
+
#
|
|
177
|
+
# @example Basic polling loop
|
|
178
|
+
# timer = app.every(50) { update_display }
|
|
179
|
+
# timer.cancel # stop later
|
|
180
|
+
#
|
|
181
|
+
# @example With error handler (timer keeps running)
|
|
182
|
+
# timer = app.every(100, on_error: ->(e) { log(e) }) { risky_work }
|
|
183
|
+
#
|
|
184
|
+
# @example Silent cancel on error
|
|
185
|
+
# timer = app.every(50, on_error: nil) { maybe_fails }
|
|
186
|
+
# timer.last_error # => check later
|
|
187
|
+
def every(ms, on_error: :raise, &block)
|
|
188
|
+
RepeatingTimer.new(self, ms, on_error: on_error, &block)
|
|
189
|
+
end
|
|
190
|
+
|
|
150
191
|
# Cancel a pending {#after} or {#after_idle} timer.
|
|
151
192
|
# @param after_id [String] timer ID returned by {#after} or {#after_idle}
|
|
152
193
|
# @return [void]
|
|
@@ -313,7 +354,8 @@ module Teek
|
|
|
313
354
|
# @param widget [String] Tk widget path (e.g. ".frame1")
|
|
314
355
|
# @return [void]
|
|
315
356
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/destroy.htm destroy
|
|
316
|
-
def destroy(widget)
|
|
357
|
+
def destroy(widget = '.')
|
|
358
|
+
raise ArgumentError, 'widget path cannot be nil' if widget.nil?
|
|
317
359
|
tcl_eval("destroy #{widget}")
|
|
318
360
|
end
|
|
319
361
|
|
|
@@ -388,6 +430,10 @@ module Teek
|
|
|
388
430
|
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/update.htm update
|
|
389
431
|
def update
|
|
390
432
|
@interp.tcl_eval('update')
|
|
433
|
+
if (e = @_pending_exception)
|
|
434
|
+
@_pending_exception = nil
|
|
435
|
+
raise e
|
|
436
|
+
end
|
|
391
437
|
end
|
|
392
438
|
|
|
393
439
|
# Process only pending idle callbacks (e.g. geometry redraws), then return.
|
|
@@ -413,6 +459,44 @@ module Teek
|
|
|
413
459
|
@interp.tcl_eval("wm withdraw #{window}")
|
|
414
460
|
end
|
|
415
461
|
|
|
462
|
+
# Enable the Tk debug console. The console starts hidden and can be
|
|
463
|
+
# toggled with the given keyboard shortcut (default: F12).
|
|
464
|
+
#
|
|
465
|
+
# The Tk console is a built-in interactive Tcl shell — useful for
|
|
466
|
+
# inspecting variables, running Tcl commands, and debugging widget
|
|
467
|
+
# layouts at runtime. It is available on macOS and Windows only;
|
|
468
|
+
# on Linux this method is a no-op (Linux has the real terminal).
|
|
469
|
+
#
|
|
470
|
+
# @param keybinding [String] Tk event to toggle the console
|
|
471
|
+
# (default: "<F12>")
|
|
472
|
+
# @return [Boolean] true if the console was created, false if
|
|
473
|
+
# unavailable on this platform
|
|
474
|
+
# @example
|
|
475
|
+
# app = Teek::App.new
|
|
476
|
+
# app.add_debug_console # F12 toggles console
|
|
477
|
+
# app.add_debug_console("<F11>") # custom key
|
|
478
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/console.htm console
|
|
479
|
+
def add_debug_console(keybinding = '<F12>')
|
|
480
|
+
@interp.create_console
|
|
481
|
+
@_console_visible = false
|
|
482
|
+
|
|
483
|
+
toggle = proc do |*|
|
|
484
|
+
if @_console_visible
|
|
485
|
+
tcl_eval('console hide')
|
|
486
|
+
@_console_visible = false
|
|
487
|
+
else
|
|
488
|
+
tcl_eval('console show')
|
|
489
|
+
@_console_visible = true
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
command(:bind, '.', keybinding, toggle)
|
|
494
|
+
true
|
|
495
|
+
rescue TclError => e
|
|
496
|
+
warn "Teek: debug console not available on this platform (#{e.message})"
|
|
497
|
+
false
|
|
498
|
+
end
|
|
499
|
+
|
|
416
500
|
# Set a window's title.
|
|
417
501
|
# @param title [String] new title
|
|
418
502
|
# @param window [String] Tk window path
|
|
@@ -673,9 +757,122 @@ module Teek
|
|
|
673
757
|
"{ruby_callback #{id}}"
|
|
674
758
|
when Symbol
|
|
675
759
|
value.to_s
|
|
760
|
+
when Array
|
|
761
|
+
"{#{value.map { |v| tcl_value(v) }.join(' ')}}"
|
|
676
762
|
else
|
|
677
763
|
"{#{value}}"
|
|
678
764
|
end
|
|
679
765
|
end
|
|
680
766
|
end
|
|
767
|
+
|
|
768
|
+
# A cancellable repeating timer that fires on the main thread.
|
|
769
|
+
#
|
|
770
|
+
# Created via {App#every}. Reschedules itself after each tick using
|
|
771
|
+
# Tcl's +after+ command. The block runs in the event loop, so it
|
|
772
|
+
# must complete quickly to avoid blocking the UI.
|
|
773
|
+
#
|
|
774
|
+
# Tracks timing drift: if a tick fires significantly late (more than
|
|
775
|
+
# 2x the interval), a warning is printed to stderr. This helps catch
|
|
776
|
+
# blocks that are too slow for the requested interval.
|
|
777
|
+
#
|
|
778
|
+
# @see App#every
|
|
779
|
+
class RepeatingTimer
|
|
780
|
+
# @return [Integer] interval in milliseconds
|
|
781
|
+
attr_reader :interval
|
|
782
|
+
|
|
783
|
+
# @return [Exception, nil] the last error if the timer stopped due to an
|
|
784
|
+
# unhandled exception, nil otherwise
|
|
785
|
+
attr_reader :last_error
|
|
786
|
+
|
|
787
|
+
# @return [Integer] number of ticks that fired late (> 2x interval)
|
|
788
|
+
attr_reader :late_ticks
|
|
789
|
+
|
|
790
|
+
# @api private
|
|
791
|
+
def initialize(app, ms, on_error: nil, &block)
|
|
792
|
+
raise ArgumentError, "interval must be positive, got #{ms}" if ms <= 0
|
|
793
|
+
|
|
794
|
+
@app = app
|
|
795
|
+
@interval = ms
|
|
796
|
+
@block = block
|
|
797
|
+
@on_error = on_error
|
|
798
|
+
@cancelled = false
|
|
799
|
+
@after_id = nil
|
|
800
|
+
@last_error = nil
|
|
801
|
+
@late_ticks = 0
|
|
802
|
+
@next_expected = nil
|
|
803
|
+
schedule
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# Stop the timer. Safe to call multiple times.
|
|
807
|
+
# @return [void]
|
|
808
|
+
def cancel
|
|
809
|
+
return if @cancelled
|
|
810
|
+
@cancelled = true
|
|
811
|
+
@app.after_cancel(@after_id) if @after_id
|
|
812
|
+
@after_id = nil
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# @return [Boolean] true if the timer has been cancelled
|
|
816
|
+
def cancelled?
|
|
817
|
+
@cancelled
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# Change the interval. Takes effect on the next tick.
|
|
821
|
+
# @param ms [Integer] new interval in milliseconds
|
|
822
|
+
def interval=(ms)
|
|
823
|
+
raise ArgumentError, "interval must be positive, got #{ms}" if ms <= 0
|
|
824
|
+
@interval = ms
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
private
|
|
828
|
+
|
|
829
|
+
def schedule
|
|
830
|
+
return if @cancelled
|
|
831
|
+
@next_expected = now_ms + @interval
|
|
832
|
+
@after_id = @app.after(@interval) { tick }
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def tick
|
|
836
|
+
return if @cancelled
|
|
837
|
+
check_drift
|
|
838
|
+
@block.call
|
|
839
|
+
schedule
|
|
840
|
+
rescue => e
|
|
841
|
+
@last_error = e
|
|
842
|
+
case @on_error
|
|
843
|
+
when :raise
|
|
844
|
+
@cancelled = true
|
|
845
|
+
# Store on App so it raises from the next app.update call.
|
|
846
|
+
# Don't re-raise here — that would go through rb_protect → bgerror.
|
|
847
|
+
@app._pending_exception = e
|
|
848
|
+
when Proc
|
|
849
|
+
begin
|
|
850
|
+
@on_error.call(e)
|
|
851
|
+
rescue => handler_err
|
|
852
|
+
@last_error = handler_err
|
|
853
|
+
@cancelled = true
|
|
854
|
+
@app._pending_exception = handler_err
|
|
855
|
+
return
|
|
856
|
+
end
|
|
857
|
+
schedule
|
|
858
|
+
when nil
|
|
859
|
+
@cancelled = true
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
def check_drift
|
|
864
|
+
return unless @next_expected
|
|
865
|
+
actual = now_ms
|
|
866
|
+
drift = actual - @next_expected
|
|
867
|
+
if drift > @interval
|
|
868
|
+
@late_ticks += 1
|
|
869
|
+
warn "Teek::RepeatingTimer: tick #{@late_ticks} fired #{drift.round}ms late " \
|
|
870
|
+
"(interval=#{@interval}ms)"
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def now_ms
|
|
875
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
876
|
+
end
|
|
877
|
+
end
|
|
681
878
|
end
|
|
Binary file
|