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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +139 -0
- data/Rakefile +316 -0
- data/ext/teek/extconf.rb +79 -0
- data/ext/teek/stubs.h +33 -0
- data/ext/teek/tcl9compat.h +211 -0
- data/ext/teek/tcltkbridge.c +1597 -0
- data/ext/teek/tcltkbridge.h +42 -0
- data/ext/teek/tkfont.c +218 -0
- data/ext/teek/tkphoto.c +477 -0
- data/ext/teek/tkwin.c +144 -0
- data/lib/teek/background_none.rb +158 -0
- data/lib/teek/background_ractor4x.rb +410 -0
- data/lib/teek/background_thread.rb +272 -0
- data/lib/teek/debugger.rb +742 -0
- data/lib/teek/demo_support.rb +150 -0
- data/lib/teek/ractor_support.rb +246 -0
- data/lib/teek/version.rb +5 -0
- data/lib/teek.rb +540 -0
- data/sample/calculator.rb +260 -0
- data/sample/debug_demo.rb +45 -0
- data/sample/goldberg.rb +1803 -0
- data/sample/goldberg_helpers.rb +170 -0
- data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
- data/sample/minesweeper/minesweeper.rb +452 -0
- data/sample/threading_demo.rb +499 -0
- data/teek.gemspec +32 -0
- metadata +179 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# TeekDemo - Helper module for automated sample testing and recording
|
|
4
|
+
#
|
|
5
|
+
# Two independent modes:
|
|
6
|
+
# - Test mode (TK_READY_PORT): Quick verification that sample loads/runs
|
|
7
|
+
# - Record mode (TK_RECORD): Video capture with longer delays
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# require_relative '../lib/teek/demo_support'
|
|
11
|
+
#
|
|
12
|
+
# app = Teek::App.new
|
|
13
|
+
# TeekDemo.app = app
|
|
14
|
+
#
|
|
15
|
+
# if TeekDemo.active?
|
|
16
|
+
# TeekDemo.on_visible {
|
|
17
|
+
# do_something
|
|
18
|
+
# app.after(TeekDemo.delay(test: 200, record: 500)) {
|
|
19
|
+
# TeekDemo.finish
|
|
20
|
+
# }
|
|
21
|
+
# }
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
require 'socket'
|
|
25
|
+
|
|
26
|
+
# @api private
|
|
27
|
+
module TeekDemo
|
|
28
|
+
class << self
|
|
29
|
+
attr_accessor :app
|
|
30
|
+
|
|
31
|
+
def testing?
|
|
32
|
+
!!ENV['TK_READY_PORT']
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def recording?
|
|
36
|
+
!!ENV['TK_RECORD']
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def active?
|
|
40
|
+
testing? || recording?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get appropriate delay for current mode
|
|
44
|
+
def delay(test: 100, record: 1000)
|
|
45
|
+
recording? ? record : test
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Run block once when window becomes visible.
|
|
49
|
+
# @param window [String] Tcl path of the window to watch (default: ".")
|
|
50
|
+
def on_visible(window: '.', timeout: 60, &block)
|
|
51
|
+
return unless active?
|
|
52
|
+
raise ArgumentError, "block required" unless block
|
|
53
|
+
raise "TeekDemo.app not set" unless app
|
|
54
|
+
|
|
55
|
+
@demo_started = false
|
|
56
|
+
app.bind(window, 'Visibility') do
|
|
57
|
+
next if @demo_started
|
|
58
|
+
@demo_started = true
|
|
59
|
+
|
|
60
|
+
signal_recording_ready(window: window) if recording?
|
|
61
|
+
|
|
62
|
+
# Safety timeout
|
|
63
|
+
app.after(timeout * 1000) { finish }
|
|
64
|
+
|
|
65
|
+
app.after(50) { block.call }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Run block once when event loop is idle.
|
|
70
|
+
# Use when window is already created before binding can be set up.
|
|
71
|
+
def after_idle(timeout: 60, &block)
|
|
72
|
+
return unless active?
|
|
73
|
+
raise ArgumentError, "block required" unless block
|
|
74
|
+
raise "TeekDemo.app not set" unless app
|
|
75
|
+
|
|
76
|
+
@demo_started = false
|
|
77
|
+
app.after_idle {
|
|
78
|
+
next if @demo_started
|
|
79
|
+
@demo_started = true
|
|
80
|
+
|
|
81
|
+
signal_recording_ready if recording?
|
|
82
|
+
|
|
83
|
+
app.after(timeout * 1000) { finish }
|
|
84
|
+
|
|
85
|
+
block.call
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Signal recording harness that window is visible and ready to record.
|
|
90
|
+
# Polls until geometry is valid, then signals via TCP.
|
|
91
|
+
def signal_recording_ready(window: '.')
|
|
92
|
+
return unless (port = ENV['TK_STOP_PORT'])
|
|
93
|
+
return if @_recording_ready_sent
|
|
94
|
+
|
|
95
|
+
try_signal = proc do
|
|
96
|
+
app.tcl_eval('update idletasks')
|
|
97
|
+
width = app.tcl_eval("winfo width #{window}").to_i
|
|
98
|
+
height = app.tcl_eval("winfo height #{window}").to_i
|
|
99
|
+
|
|
100
|
+
if width >= 10 && height >= 10
|
|
101
|
+
@_recording_ready_sent = true
|
|
102
|
+
@_initial_geometry = [width, height]
|
|
103
|
+
|
|
104
|
+
begin
|
|
105
|
+
sock = TCPSocket.new('127.0.0.1', port.to_i)
|
|
106
|
+
sock.write("R:#{width}x#{height}")
|
|
107
|
+
sock.close
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
$stderr.puts "TeekDemo: signal error: #{e.message}"
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
app.after(10) { try_signal.call }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
try_signal.call
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Signal test harness that sample is ready (without exiting)
|
|
120
|
+
def signal_ready
|
|
121
|
+
$stdout.flush
|
|
122
|
+
if (port = ENV.delete('TK_READY_PORT'))
|
|
123
|
+
begin
|
|
124
|
+
TCPSocket.new('127.0.0.1', port.to_i).close
|
|
125
|
+
rescue StandardError
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Signal completion and exit cleanly.
|
|
131
|
+
# Handles TK_READY_PORT (test) and TK_STOP_PORT (record).
|
|
132
|
+
def finish
|
|
133
|
+
signal_ready
|
|
134
|
+
|
|
135
|
+
if (port = ENV['TK_STOP_PORT'])
|
|
136
|
+
Thread.new do
|
|
137
|
+
begin
|
|
138
|
+
sock = TCPSocket.new('127.0.0.1', port.to_i)
|
|
139
|
+
sock.read(1) # Block until harness sends byte or closes
|
|
140
|
+
sock.close
|
|
141
|
+
rescue StandardError
|
|
142
|
+
end
|
|
143
|
+
app.after(0) { app.destroy('.') }
|
|
144
|
+
end
|
|
145
|
+
else
|
|
146
|
+
app.after(0) { app.destroy('.') }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ractor and background work support for Teek applications.
|
|
4
|
+
#
|
|
5
|
+
# This module provides a unified API across Ruby versions:
|
|
6
|
+
# - Ruby 4.x: Uses Ractor::Port, Ractor.shareable_proc for true parallelism
|
|
7
|
+
# - Ruby 3.x: Ractor mode NOT supported (falls back to thread mode)
|
|
8
|
+
# - Thread fallback: Always available, works everywhere
|
|
9
|
+
#
|
|
10
|
+
# The implementation is selected automatically based on Ruby version.
|
|
11
|
+
|
|
12
|
+
require_relative 'background_thread'
|
|
13
|
+
|
|
14
|
+
if Ractor.respond_to?(:shareable_proc)
|
|
15
|
+
require_relative 'background_ractor4x'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module Teek
|
|
19
|
+
|
|
20
|
+
# Unified background work API. Delegates to a mode-specific implementation
|
|
21
|
+
# selected via the +mode:+ parameter.
|
|
22
|
+
#
|
|
23
|
+
# Modes:
|
|
24
|
+
# - +:thread+ — traditional threading; best for I/O-bound work where the GVL
|
|
25
|
+
# is released during blocking calls. Always available.
|
|
26
|
+
# - +:ractor+ — true parallel execution via Ractor (Ruby 4.x+ only); best
|
|
27
|
+
# for CPU-bound work.
|
|
28
|
+
#
|
|
29
|
+
# @example Basic usage
|
|
30
|
+
# task = Teek::BackgroundWork.new(app, data, mode: :thread) do |t, d|
|
|
31
|
+
# d.each { |item| t.yield(process(item)) }
|
|
32
|
+
# end
|
|
33
|
+
# task.on_progress { |r| update_ui(r) }
|
|
34
|
+
# .on_done { puts "Finished" }
|
|
35
|
+
#
|
|
36
|
+
# @example Pause / resume / stop
|
|
37
|
+
# task.pause
|
|
38
|
+
# task.resume
|
|
39
|
+
# task.stop
|
|
40
|
+
#
|
|
41
|
+
# @example Configuration
|
|
42
|
+
# Teek::BackgroundWork.poll_ms = 16
|
|
43
|
+
# Teek::BackgroundWork.drop_intermediate = true
|
|
44
|
+
# Teek::BackgroundWork.abort_on_error = false
|
|
45
|
+
class BackgroundWork
|
|
46
|
+
class << self
|
|
47
|
+
# @return [Integer] UI poll interval in milliseconds (default 16)
|
|
48
|
+
attr_accessor :poll_ms
|
|
49
|
+
# @return [Boolean] when true, only the latest progress value per poll
|
|
50
|
+
# cycle is delivered (default true)
|
|
51
|
+
attr_accessor :drop_intermediate
|
|
52
|
+
# @return [Boolean] when true, raise on ractor errors instead of warning
|
|
53
|
+
# (default false)
|
|
54
|
+
attr_accessor :abort_on_error
|
|
55
|
+
end
|
|
56
|
+
self.poll_ms = 16
|
|
57
|
+
self.drop_intermediate = true
|
|
58
|
+
self.abort_on_error = false
|
|
59
|
+
|
|
60
|
+
# @return [Boolean] whether Ractor mode is available (Ruby 4.x+)
|
|
61
|
+
RACTOR_SUPPORTED = Ractor.respond_to?(:shareable_proc)
|
|
62
|
+
|
|
63
|
+
# @api private
|
|
64
|
+
@background_modes = {}
|
|
65
|
+
|
|
66
|
+
# @api private
|
|
67
|
+
def self.register_background_mode(name, klass)
|
|
68
|
+
@background_modes[name.to_sym] = klass
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @api private
|
|
72
|
+
def self.background_modes
|
|
73
|
+
@background_modes
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @api private
|
|
77
|
+
def self.background_mode_class(name)
|
|
78
|
+
@background_modes[name.to_sym]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Register built-in modes
|
|
82
|
+
register_background_mode :thread, Teek::BackgroundThread::BackgroundWork
|
|
83
|
+
|
|
84
|
+
# Ractor mode only available on Ruby 4.x+
|
|
85
|
+
if RACTOR_SUPPORTED
|
|
86
|
+
register_background_mode :ractor, Teek::BackgroundRactor4x::BackgroundWork
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [String, nil] optional name for this task
|
|
90
|
+
attr_accessor :name
|
|
91
|
+
|
|
92
|
+
# @param app [Teek::App] the application instance
|
|
93
|
+
# @param data [Object] data passed to the worker block
|
|
94
|
+
# @param mode [Symbol] +:thread+ or +:ractor+
|
|
95
|
+
# @param worker [Class, nil] optional worker class (must respond to +#call(task, data)+)
|
|
96
|
+
# @yield [task, data] block executed in the background
|
|
97
|
+
# @yieldparam task [BackgroundThread::BackgroundWork::TaskContext, BackgroundRactor4x::BackgroundWork::TaskContext]
|
|
98
|
+
# @yieldparam data [Object]
|
|
99
|
+
# @raise [ArgumentError] if mode is unknown
|
|
100
|
+
def initialize(app, data, mode: :thread, worker: nil, &block)
|
|
101
|
+
impl_class = self.class.background_mode_class(mode)
|
|
102
|
+
unless impl_class
|
|
103
|
+
available = self.class.background_modes.keys.join(', ')
|
|
104
|
+
raise ArgumentError, "Unknown mode: #{mode}. Available: #{available}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
@impl = impl_class.new(app, data, worker: worker, &block)
|
|
108
|
+
@mode = mode
|
|
109
|
+
@name = nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [Symbol] the active mode (+:thread+ or +:ractor+)
|
|
113
|
+
def mode
|
|
114
|
+
@mode
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [Boolean]
|
|
118
|
+
def done?
|
|
119
|
+
@impl.done?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [Boolean]
|
|
123
|
+
def paused?
|
|
124
|
+
@impl.paused?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @yield [value] called on the main thread with each result
|
|
128
|
+
# @return [self]
|
|
129
|
+
def on_progress(&block)
|
|
130
|
+
@impl.on_progress(&block)
|
|
131
|
+
self
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @yield called on the main thread when the worker completes
|
|
135
|
+
# @return [self]
|
|
136
|
+
def on_done(&block)
|
|
137
|
+
@impl.on_done(&block)
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @yield [msg] called on the main thread with custom worker messages
|
|
142
|
+
# @return [self]
|
|
143
|
+
def on_message(&block)
|
|
144
|
+
@impl.on_message(&block)
|
|
145
|
+
self
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Send a message to the worker.
|
|
149
|
+
# @param msg [Object] any value (must be Ractor-shareable in +:ractor+ mode)
|
|
150
|
+
# @return [self]
|
|
151
|
+
def send_message(msg)
|
|
152
|
+
@impl.send_message(msg)
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Pause the worker.
|
|
157
|
+
# @return [self]
|
|
158
|
+
def pause
|
|
159
|
+
@impl.pause
|
|
160
|
+
self
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Resume a paused worker.
|
|
164
|
+
# @return [self]
|
|
165
|
+
def resume
|
|
166
|
+
@impl.resume
|
|
167
|
+
self
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Request the worker to stop.
|
|
171
|
+
# @return [self]
|
|
172
|
+
def stop
|
|
173
|
+
@impl.stop
|
|
174
|
+
self
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Force-close the worker and associated resources.
|
|
178
|
+
# @return [self]
|
|
179
|
+
def close
|
|
180
|
+
@impl.close if @impl.respond_to?(:close)
|
|
181
|
+
self
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Explicitly start the worker. Called automatically by {#on_progress}
|
|
185
|
+
# and {#on_done}.
|
|
186
|
+
# @return [self]
|
|
187
|
+
def start
|
|
188
|
+
@impl.start
|
|
189
|
+
self
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Simplified streaming API without pause/resume support.
|
|
194
|
+
# Uses Ractor on Ruby 4.x+, falls back to threads on 3.x.
|
|
195
|
+
#
|
|
196
|
+
# @example
|
|
197
|
+
# Teek::RactorStream.new(app, files) do |yielder, data|
|
|
198
|
+
# data.each { |f| yielder.yield(process(f)) }
|
|
199
|
+
# end.on_progress { |r| update_ui(r) }
|
|
200
|
+
# .on_done { puts "Done!" }
|
|
201
|
+
class RactorStream
|
|
202
|
+
def initialize(app, data, &block)
|
|
203
|
+
# Ruby 4.x: use Ractor with shareable_proc for true parallelism
|
|
204
|
+
# Ruby 3.x: use threads (Ractor mode not supported)
|
|
205
|
+
if BackgroundWork::RACTOR_SUPPORTED
|
|
206
|
+
shareable_block = Ractor.shareable_proc(&block)
|
|
207
|
+
wrapped_block = Ractor.shareable_proc do |task, d|
|
|
208
|
+
yielder = StreamYielder.new(task)
|
|
209
|
+
shareable_block.call(yielder, d)
|
|
210
|
+
end
|
|
211
|
+
@impl = Teek::BackgroundRactor4x::BackgroundWork.new(app, data, &wrapped_block)
|
|
212
|
+
else
|
|
213
|
+
wrapped_block = proc do |task, d|
|
|
214
|
+
yielder = StreamYielder.new(task)
|
|
215
|
+
block.call(yielder, d)
|
|
216
|
+
end
|
|
217
|
+
@impl = Teek::BackgroundThread::BackgroundWork.new(app, data, &wrapped_block)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def on_progress(&block)
|
|
222
|
+
@impl.on_progress(&block)
|
|
223
|
+
self
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def on_done(&block)
|
|
227
|
+
@impl.on_done(&block)
|
|
228
|
+
self
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def cancel
|
|
232
|
+
@impl.stop
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Adapter for old yielder API
|
|
236
|
+
class StreamYielder
|
|
237
|
+
def initialize(task)
|
|
238
|
+
@task = task
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def yield(value)
|
|
242
|
+
@task.yield(value)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|