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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teek
4
+ VERSION = "0.1.0"
5
+ end