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,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teek
4
+ # Thread-based background work for Teek applications.
5
+ # Always available, works on all Ruby versions.
6
+ #
7
+ # Best for I/O-bound work (network, file downloads, database queries) where the
8
+ # worker releases the GVL during blocking calls, allowing real concurrency with
9
+ # the UI thread. For CPU-bound work, the GVL serializes execution and thread
10
+ # overhead makes this slower than synchronous mode. Use :ractor for CPU-bound
11
+ # parallelism (Ruby 4.x+).
12
+ module BackgroundThread
13
+
14
+ # High-level API for background work with messaging support.
15
+ #
16
+ # Example:
17
+ # task = Teek::BackgroundThread::BackgroundWork.new(app, data) do |t|
18
+ # data.each do |item|
19
+ # break if t.check_message == :stop
20
+ # t.yield(process(item))
21
+ # end
22
+ # end.on_progress { |r| update_ui(r) }
23
+ # .on_done { puts "Done!" }
24
+ #
25
+ # task.send_message(:pause)
26
+ # task.send_message(:resume)
27
+ # task.stop
28
+ #
29
+ # Poll interval when paused (slower to save CPU)
30
+ PAUSED_POLL_MS = 10000
31
+
32
+ class BackgroundWork
33
+ def initialize(app, data, worker: nil, &block)
34
+ # Thread mode supports both block and worker class for API consistency
35
+ @app = app
36
+ @data = data
37
+ @work_block = block || (worker && proc { |t, d| worker.new.call(t, d) })
38
+ @callbacks = { progress: nil, done: nil, message: nil }
39
+ @started = false
40
+ @done = false
41
+ @paused = false
42
+
43
+ # Communication channels
44
+ @output_queue = Thread::Queue.new # Worker -> Main
45
+ @message_queue = Thread::Queue.new # Main -> Worker
46
+ @worker_thread = nil
47
+ end
48
+
49
+ def on_progress(&block)
50
+ @callbacks[:progress] = block
51
+ maybe_start
52
+ self
53
+ end
54
+
55
+ def on_done(&block)
56
+ @callbacks[:done] = block
57
+ maybe_start
58
+ self
59
+ end
60
+
61
+ # Called when worker sends a non-result message back
62
+ def on_message(&block)
63
+ @callbacks[:message] = block
64
+ self
65
+ end
66
+
67
+ # Send a message to the worker (pause, resume, stop, or custom)
68
+ def send_message(msg)
69
+ @message_queue << msg
70
+ self
71
+ end
72
+
73
+ # Convenience methods
74
+ def pause
75
+ @paused = true
76
+ send_message(:pause)
77
+ self
78
+ end
79
+
80
+ def resume
81
+ @paused = false
82
+ send_message(:resume)
83
+ # Restart polling (was stopped when paused)
84
+ @app.after(0, &@poll_proc) if @poll_proc && !@done
85
+ self
86
+ end
87
+
88
+ def stop
89
+ send_message(:stop)
90
+ self
91
+ end
92
+
93
+ def close
94
+ @done = true
95
+ @worker_thread&.kill
96
+ self
97
+ end
98
+
99
+ def done?
100
+ @done
101
+ end
102
+
103
+ def paused?
104
+ @paused
105
+ end
106
+
107
+ def start
108
+ return self if @started
109
+ @started = true
110
+
111
+ @worker_thread = Thread.new do
112
+ Thread.current[:tk_in_background_work] = true
113
+ task = TaskContext.new(@output_queue, @message_queue)
114
+ begin
115
+ @work_block.call(task, @data)
116
+ @output_queue << [:done]
117
+ rescue StopIteration
118
+ @output_queue << [:done]
119
+ rescue => e
120
+ @output_queue << [:error, "#{e.class}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"]
121
+ @output_queue << [:done]
122
+ end
123
+ end
124
+
125
+ start_polling
126
+ self
127
+ end
128
+
129
+ private
130
+
131
+ def maybe_start
132
+ start unless @started
133
+ end
134
+
135
+ def start_polling
136
+ @dropped_count = 0
137
+ @choke_warned = false
138
+
139
+ @poll_proc = proc do
140
+ next if @done
141
+
142
+ drop_intermediate = Teek::BackgroundWork.drop_intermediate
143
+ # Drain queue. If drop_intermediate, only use LATEST progress value.
144
+ # This prevents UI choking when worker yields faster than UI polls.
145
+ last_progress = nil
146
+ results_this_poll = 0
147
+ until @output_queue.empty?
148
+ msg = @output_queue.pop(true)
149
+ type, value = msg
150
+ case type
151
+ when :done
152
+ @done = true
153
+ # Call progress with final value before done callback
154
+ @callbacks[:progress]&.call(last_progress) if last_progress
155
+ last_progress = nil # Prevent duplicate call after loop
156
+ warn_if_choked
157
+ @callbacks[:done]&.call
158
+ break
159
+ when :result
160
+ results_this_poll += 1
161
+ if drop_intermediate
162
+ last_progress = value # Keep only latest
163
+ else
164
+ @callbacks[:progress]&.call(value) # Call for every value
165
+ end
166
+ when :message
167
+ @callbacks[:message]&.call(value)
168
+ when :error
169
+ warn "[Thread] Background work error: #{value}"
170
+ end
171
+ end
172
+
173
+ # Track dropped messages (all but the last one we processed)
174
+ if drop_intermediate && results_this_poll > 1
175
+ dropped = results_this_poll - 1
176
+ @dropped_count += dropped
177
+ warn_choke_start(dropped) unless @choke_warned
178
+ end
179
+
180
+ # Call progress callback once with latest value (only if dropping)
181
+ @callbacks[:progress]&.call(last_progress) if drop_intermediate && last_progress && !@done
182
+
183
+ unless @done || @paused
184
+ @app.after(Teek::BackgroundWork.poll_ms, &@poll_proc)
185
+ end
186
+ end
187
+
188
+ @app.after(0, &@poll_proc)
189
+ end
190
+
191
+ def warn_choke_start(dropped)
192
+ @choke_warned = true
193
+ warn "[Teek::BackgroundWork] UI choking: worker yielding faster than UI can poll. " \
194
+ "#{dropped} progress values dropped this cycle. " \
195
+ "Consider yielding less frequently or increasing Tk.background_work_poll_ms."
196
+ end
197
+
198
+ def warn_if_choked
199
+ return unless @dropped_count > 0
200
+ warn "[Teek::BackgroundWork] Total #{@dropped_count} progress values dropped during task. " \
201
+ "Only latest values were shown to UI."
202
+ end
203
+
204
+ # Context passed to the work block
205
+ class TaskContext
206
+ def initialize(output_queue, message_queue)
207
+ @output_queue = output_queue
208
+ @message_queue = message_queue
209
+ @paused = false
210
+ end
211
+
212
+ # Yield a result to the main thread.
213
+ # Calls Thread.pass to give main thread a chance to process events.
214
+ def yield(value)
215
+ @output_queue << [:result, value]
216
+ Thread.pass
217
+ end
218
+
219
+ # Non-blocking check for messages from main thread.
220
+ # Returns the message or nil if none.
221
+ def check_message
222
+ return nil if @message_queue.empty?
223
+ msg = @message_queue.pop(true)
224
+ handle_control_message(msg)
225
+ msg
226
+ rescue ThreadError
227
+ nil
228
+ end
229
+
230
+ # Blocking wait for next message
231
+ def wait_message
232
+ msg = @message_queue.pop
233
+ handle_control_message(msg)
234
+ msg
235
+ end
236
+
237
+ # Send a message back to main thread (not a result)
238
+ def send_message(msg)
239
+ @output_queue << [:message, msg]
240
+ end
241
+
242
+ # Check pause state, blocking if paused
243
+ def check_pause
244
+ # First drain any pending messages (non-blocking)
245
+ until @message_queue.empty?
246
+ msg = @message_queue.pop(true)
247
+ handle_control_message(msg)
248
+ end
249
+
250
+ # Then block while paused
251
+ while @paused
252
+ msg = @message_queue.pop # Blocking wait
253
+ handle_control_message(msg)
254
+ end
255
+ end
256
+
257
+ private
258
+
259
+ def handle_control_message(msg)
260
+ case msg
261
+ when :pause
262
+ @paused = true
263
+ when :resume
264
+ @paused = false
265
+ when :stop
266
+ raise StopIteration
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end