teek 0.1.2 → 0.1.4

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -0
  3. data/Rakefile +121 -22
  4. data/ext/teek/extconf.rb +19 -1
  5. data/ext/teek/tcltkbridge.c +44 -2
  6. data/ext/teek/tcltkbridge.h +3 -0
  7. data/ext/teek/tkdrop.c +66 -0
  8. data/ext/teek/tkdrop.h +26 -0
  9. data/ext/teek/tkdrop_macos.m +141 -0
  10. data/ext/teek/tkdrop_win.c +232 -0
  11. data/ext/teek/tkdrop_x11.c +337 -0
  12. data/ext/teek/tkwin.c +42 -0
  13. data/lib/teek/platform.rb +29 -0
  14. data/lib/teek/version.rb +1 -1
  15. data/lib/teek.rb +248 -7
  16. data/teek.gemspec +3 -2
  17. metadata +7 -45
  18. data/sample/calculator.rb +0 -255
  19. data/sample/debug_demo.rb +0 -43
  20. data/sample/goldberg.rb +0 -1803
  21. data/sample/goldberg_helpers.rb +0 -170
  22. data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
  23. data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
  24. data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
  25. data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
  26. data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
  27. data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
  28. data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
  29. data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
  30. data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
  31. data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
  32. data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
  33. data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
  34. data/sample/minesweeper/minesweeper.rb +0 -452
  35. data/sample/optcarrot/vendor/optcarrot/apu.rb +0 -856
  36. data/sample/optcarrot/vendor/optcarrot/config.rb +0 -257
  37. data/sample/optcarrot/vendor/optcarrot/cpu.rb +0 -1162
  38. data/sample/optcarrot/vendor/optcarrot/driver.rb +0 -144
  39. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +0 -14
  40. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +0 -105
  41. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +0 -153
  42. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +0 -14
  43. data/sample/optcarrot/vendor/optcarrot/nes.rb +0 -105
  44. data/sample/optcarrot/vendor/optcarrot/opt.rb +0 -168
  45. data/sample/optcarrot/vendor/optcarrot/pad.rb +0 -92
  46. data/sample/optcarrot/vendor/optcarrot/palette.rb +0 -65
  47. data/sample/optcarrot/vendor/optcarrot/ppu.rb +0 -1468
  48. data/sample/optcarrot/vendor/optcarrot/rom.rb +0 -143
  49. data/sample/optcarrot/vendor/optcarrot.rb +0 -14
  50. data/sample/optcarrot.rb +0 -354
  51. data/sample/paint/assets/bucket.png +0 -0
  52. data/sample/paint/assets/cursor.png +0 -0
  53. data/sample/paint/assets/eraser.png +0 -0
  54. data/sample/paint/assets/pencil.png +0 -0
  55. data/sample/paint/assets/spray.png +0 -0
  56. data/sample/paint/layer.rb +0 -255
  57. data/sample/paint/layer_manager.rb +0 -179
  58. data/sample/paint/paint_demo.rb +0 -837
  59. data/sample/paint/sparse_pixel_buffer.rb +0 -202
  60. data/sample/sdl2_demo.rb +0 -318
  61. data/sample/threading_demo.rb +0 -494
@@ -1,494 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- # teek-record: title=Concurrency Demo - File Hasher
4
-
5
- # Concurrency Demo - File Hasher
6
- #
7
- # Compares concurrency modes:
8
- # - None: Direct execution, UI frozen. Shows what happens without background work.
9
- # - None+update: Synchronous but with forced UI updates so progress is visible.
10
- # - Thread: Background thread with GVL overhead. Enables Pause, UI stays responsive.
11
- # - Ractor: True parallelism (separate GVL). Best throughput. (Ruby 4.x+ only)
12
-
13
- require 'teek'
14
- require 'teek/background_none'
15
- require 'digest'
16
- require 'tmpdir'
17
-
18
- # Register :none mode for demo
19
- Teek::BackgroundWork.register_background_mode(:none, Teek::BackgroundNone::BackgroundWork)
20
-
21
- RACTOR_AVAILABLE = Teek::BackgroundWork::RACTOR_SUPPORTED
22
-
23
- class ThreadingDemo
24
- attr_reader :app
25
-
26
- ALGORITHMS = %w[SHA256 SHA512 SHA384 SHA1 MD5].freeze
27
-
28
- MODES = if RACTOR_AVAILABLE
29
- ['None', 'None+update', 'Thread', 'Ractor'].freeze
30
- else
31
- ['None', 'None+update', 'Thread'].freeze
32
- end
33
-
34
- def initialize
35
- @app = Teek::App.new
36
- @running = false
37
- @paused = false
38
- @stop_requested = false
39
- @background_task = nil
40
-
41
- build_ui
42
- collect_files
43
-
44
- @app.update
45
- w = @app.command(:winfo, 'width', '.')
46
- h = @app.command(:winfo, 'height', '.')
47
- @app.set_window_geometry("#{w}x#{h}+0+0")
48
- @app.set_window_resizable(true, true)
49
-
50
- close_proc = proc { |*|
51
- @background_task&.close
52
- @app.destroy('.')
53
- }
54
- @app.command(:wm, 'protocol', '.', 'WM_DELETE_WINDOW', close_proc)
55
- end
56
-
57
- def build_ui
58
- @app.show
59
- @app.set_window_title('Concurrency Demo - File Hasher')
60
- @app.command(:wm, 'minsize', '.', 600, 400)
61
-
62
- # Tcl variables for widget bindings
63
- @app.set_variable('::chunk_size', 3)
64
- @app.set_variable('::algorithm', 'SHA256')
65
- @app.set_variable('::mode', 'Thread')
66
- @app.set_variable('::allow_pause', 0)
67
- @app.set_variable('::progress', 0)
68
-
69
- ractor_note = RACTOR_AVAILABLE ? "Ractor: true parallel." : "(Ractor available on Ruby 4.x+)"
70
- @app.create_widget('ttk::label',
71
- text: "File hasher demo - compares concurrency modes.\n" \
72
- "None: UI frozen. None+update: progress visible, pause works. " \
73
- "Thread: responsive, GVL shared. #{ractor_note}",
74
- justify: :left).pack(fill: :x, padx: 10, pady: 10)
75
-
76
- build_controls
77
- build_statusbar
78
- build_log
79
- end
80
-
81
- def build_controls
82
- ctrl = @app.create_widget('ttk::frame')
83
- ctrl.pack(fill: :x, padx: 10, pady: 5)
84
-
85
- @start_btn = @app.create_widget('ttk::button', parent: ctrl,
86
- text: 'Start', command: proc { |*| start_hashing })
87
- @start_btn.pack(side: :left)
88
-
89
- @pause_btn = @app.create_widget('ttk::button', parent: ctrl,
90
- text: 'Pause', state: :disabled, command: proc { |*| toggle_pause })
91
- @pause_btn.pack(side: :left, padx: 5)
92
-
93
- @app.create_widget('ttk::button', parent: ctrl,
94
- text: 'Reset', command: proc { |*| reset }).pack(side: :left)
95
-
96
- @app.create_widget('ttk::label', parent: ctrl,
97
- text: 'Algorithm:').pack(side: :left, padx: 10)
98
-
99
- @algo_combo = @app.create_widget('ttk::combobox', parent: ctrl,
100
- textvariable: '::algorithm',
101
- values: Teek.make_list(*ALGORITHMS),
102
- width: 8,
103
- state: :readonly)
104
- @algo_combo.pack(side: :left)
105
-
106
- @app.create_widget('ttk::label', parent: ctrl,
107
- text: 'Batch:').pack(side: :left, padx: 10)
108
-
109
- @batch_val = @app.create_widget('ttk::label', parent: ctrl, text: '3', width: 3)
110
- @batch_val.pack(side: :left)
111
-
112
- @app.create_widget('ttk::scale', parent: ctrl,
113
- orient: :horizontal,
114
- from: 1,
115
- to: 100,
116
- length: 100,
117
- variable: '::chunk_size',
118
- command: proc { |v, *| @batch_val.command(:configure, text: v.to_f.round.to_s) })
119
- .pack(side: :left, padx: 5)
120
-
121
- @app.create_widget('ttk::label', parent: ctrl,
122
- text: 'Mode:').pack(side: :left, padx: 10)
123
-
124
- @mode_combo = @app.create_widget('ttk::combobox', parent: ctrl,
125
- textvariable: '::mode',
126
- values: Teek.make_list(*MODES),
127
- width: 10,
128
- state: :readonly)
129
- @mode_combo.pack(side: :left)
130
-
131
- @app.create_widget('ttk::checkbutton', parent: ctrl,
132
- text: 'Allow Pause',
133
- variable: '::allow_pause').pack(side: :left, padx: 10)
134
- end
135
-
136
- def build_statusbar
137
- status = @app.create_widget('ttk::frame')
138
- status.pack(side: :bottom, fill: :x, padx: 5, pady: 5)
139
-
140
- # Progress section (left)
141
- progress_frame = @app.create_widget('ttk::frame', parent: status,
142
- relief: :sunken, borderwidth: 2)
143
- progress_frame.pack(side: :left, fill: :x, expand: 1, padx: 2)
144
-
145
- @app.create_widget('ttk::progressbar', parent: progress_frame,
146
- orient: :horizontal,
147
- length: 200,
148
- mode: :determinate,
149
- variable: '::progress',
150
- maximum: 100).pack(side: :left, padx: 5, pady: 4)
151
-
152
- @status_label = @app.create_widget('ttk::label', parent: progress_frame,
153
- text: 'Ready', width: 20, anchor: :w)
154
- @status_label.pack(side: :left, padx: 10)
155
-
156
- @file_label = @app.create_widget('ttk::label', parent: progress_frame,
157
- text: '', width: 28, anchor: :w)
158
- @file_label.pack(side: :left, padx: 5)
159
-
160
- # Info section (right)
161
- info_frame = @app.create_widget('ttk::frame', parent: status,
162
- relief: :sunken, borderwidth: 2)
163
- info_frame.pack(side: :right, padx: 2)
164
-
165
- @files_label = @app.create_widget('ttk::label', parent: info_frame,
166
- text: '', width: 12, anchor: :e)
167
- @files_label.pack(side: :left, padx: 8, pady: 4)
168
-
169
- @app.create_widget('ttk::separator', parent: info_frame,
170
- orient: :vertical).pack(side: :left, fill: :y, pady: 4)
171
-
172
- @app.create_widget('ttk::label', parent: info_frame,
173
- text: "Ruby #{RUBY_VERSION}", anchor: :e).pack(side: :left, padx: 8, pady: 4)
174
- end
175
-
176
- def build_log
177
- log = @app.create_widget('ttk::labelframe', text: 'Output')
178
- log.pack(fill: :both, expand: 1, padx: 10, pady: 5)
179
-
180
- log_frame = @app.create_widget('ttk::frame', parent: log)
181
- log_frame.pack(fill: :both, expand: 1, padx: 5, pady: 5)
182
- @app.command(:pack, 'propagate', log_frame, 0)
183
-
184
- @log_text = @app.create_widget(:text, parent: log_frame,
185
- width: 80, height: 15, wrap: :none)
186
- @log_text.pack(side: :left, fill: :both, expand: 1)
187
-
188
- vsb = @app.create_widget('ttk::scrollbar', parent: log_frame,
189
- orient: :vertical, command: "#{@log_text} yview")
190
- @log_text.command(:configure, yscrollcommand: "#{vsb} set")
191
- vsb.pack(side: :right, fill: :y)
192
- end
193
-
194
- def collect_files
195
- base = File.exist?('/app') ? '/app' : Dir.pwd
196
- @files = Dir.glob("#{base}/**/*", File::FNM_DOTMATCH).select { |f| File.file?(f) }
197
- @files.reject! { |f| f.include?('/.git/') }
198
- @files.sort!
199
-
200
- max_files = ARGV.find { |a| a.start_with?('--max-files=') }&.split('=')&.last&.to_i
201
- max_files ||= ENV['DEMO_MAX_FILES']&.to_i
202
- max_files ||= 5 if ENV['TK_READY_PORT'] # test mode -- don't hash 200+ files
203
- @files = @files.first(max_files) if max_files && max_files > 0
204
-
205
- @files_label.command(:configure, text: "#{@files.size} files")
206
- end
207
-
208
- def current_mode
209
- @app.get_variable('::mode')
210
- end
211
-
212
- def set_combo_enabled(widget)
213
- # ttk state: must clear disabled AND set readonly in one call
214
- @app.tcl_eval("#{widget} state {!disabled readonly}")
215
- end
216
-
217
- def start_hashing
218
- @running = true
219
- @paused = false
220
- @stop_requested = false
221
-
222
- @start_btn.command(:state, 'disabled')
223
- @algo_combo.command(:state, 'disabled')
224
- @mode_combo.command(:state, 'disabled')
225
- @log_text.command(:delete, '1.0', 'end')
226
- @app.set_variable('::progress', 0)
227
- @status_label.command(:configure, text: 'Hashing...')
228
-
229
- if @app.get_variable('::allow_pause').to_i == 1
230
- @pause_btn.command(:state, '!disabled')
231
- else
232
- @pause_btn.command(:state, 'disabled')
233
- end
234
-
235
- @app.set_window_resizable(false, false) unless current_mode == 'Ractor'
236
-
237
- @metrics = {
238
- start_time: Process.clock_gettime(Process::CLOCK_MONOTONIC),
239
- ui_update_count: 0,
240
- ui_update_total_ms: 0.0,
241
- total: @files.size,
242
- files_done: 0,
243
- mode: current_mode
244
- }
245
-
246
- mode_sym = case current_mode
247
- when 'None', 'None+update' then :none
248
- else current_mode.downcase.to_sym
249
- end
250
- start_background_work(mode_sym)
251
- end
252
-
253
- def toggle_pause
254
- @paused = !@paused
255
- @pause_btn.command(:configure, text: @paused ? 'Resume' : 'Pause')
256
- @status_label.command(:configure, text: @paused ? 'Paused' : 'Hashing...')
257
- @app.set_window_resizable(@paused, @paused)
258
- if @paused
259
- set_combo_enabled(@mode_combo)
260
- else
261
- @mode_combo.command(:state, 'disabled')
262
- end
263
-
264
- if @background_task
265
- @paused ? @background_task.pause : @background_task.resume
266
- end
267
-
268
- write_metrics("PAUSED") if @paused && @metrics
269
- end
270
-
271
- def reset
272
- @stop_requested = true
273
- @paused = false
274
- @running = false
275
-
276
- @background_task&.stop
277
- @background_task = nil
278
-
279
- @start_btn.command(:state, '!disabled')
280
- @pause_btn.command(:state, 'disabled')
281
- @pause_btn.command(:configure, text: 'Pause')
282
- set_combo_enabled(@algo_combo)
283
- set_combo_enabled(@mode_combo)
284
- @app.set_window_resizable(true, true)
285
- @log_text.command(:delete, '1.0', 'end')
286
- @app.set_variable('::progress', 0)
287
- @status_label.command(:configure, text: 'Ready')
288
- @file_label.command(:configure, text: '')
289
-
290
- @app.set_variable('::mode', 'Thread')
291
- @app.set_variable('::algorithm', 'SHA256')
292
- @app.set_variable('::chunk_size', 3)
293
- @batch_val.command(:configure, text: '3')
294
- @app.set_variable('::allow_pause', 0)
295
- end
296
-
297
- def write_metrics(status = "DONE")
298
- return unless @metrics
299
- m = @metrics
300
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - m[:start_time]
301
- dir = File.writable?(__dir__) ? __dir__ : Dir.tmpdir
302
- File.open(File.join(dir, 'threading_demo_metrics.log'), 'a') do |f|
303
- f.puts "=" * 60
304
- f.puts "Status: #{status} at #{Time.now}"
305
- f.puts "Mode: #{m[:mode]}"
306
- f.puts "Algorithm: #{@app.get_variable('::algorithm')}"
307
- f.puts "Files processed: #{m[:files_done]}/#{m[:total]}"
308
- chunk = [@app.get_variable('::chunk_size').to_f.round, 1].max
309
- f.puts "Batch size: #{chunk}"
310
- f.puts "-" * 40
311
- f.puts "Elapsed: #{elapsed.round(3)}s"
312
- f.puts "UI updates: #{m[:ui_update_count]}"
313
- f.puts "UI update total: #{m[:ui_update_total_ms].round(1)}ms" if m[:ui_update_total_ms]
314
- f.puts "UI update avg: #{(m[:ui_update_total_ms] / m[:ui_update_count]).round(2)}ms" if m[:ui_update_count] > 0 && m[:ui_update_total_ms]
315
- f.puts "Files/sec: #{(m[:files_done] / elapsed).round(1)}" if elapsed > 0
316
- f.puts
317
- end
318
- end
319
-
320
- def finish_hashing
321
- write_metrics("DONE") unless @stop_requested
322
- return if @stop_requested
323
-
324
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @metrics[:start_time]
325
- files_per_sec = (@metrics[:files_done] / elapsed).round(1)
326
- @status_label.command(:configure,
327
- text: "Done #{elapsed.round(2)}s (#{files_per_sec}/s)")
328
- @file_label.command(:configure, text: '')
329
- @start_btn.command(:state, '!disabled')
330
- @pause_btn.command(:state, 'disabled')
331
- set_combo_enabled(@algo_combo)
332
- set_combo_enabled(@mode_combo)
333
- @app.set_window_resizable(true, true)
334
- @running = false
335
- end
336
-
337
- # ─────────────────────────────────────────────────────────────
338
- # All modes use unified Teek::BackgroundWork API
339
- # ─────────────────────────────────────────────────────────────
340
-
341
- def start_background_work(mode)
342
- ui_mode = current_mode
343
-
344
- files = @files.dup
345
- algo_name = @app.get_variable('::algorithm')
346
- chunk_size = [@app.get_variable('::chunk_size').to_f.round, 1].max
347
- base_dir = Dir.pwd
348
- allow_pause = @app.get_variable('::allow_pause').to_i == 1
349
-
350
- work_data = {
351
- files: files,
352
- algo_name: algo_name,
353
- chunk_size: chunk_size,
354
- base_dir: base_dir,
355
- allow_pause: allow_pause
356
- }
357
-
358
- if mode == :ractor
359
- work_data = Ractor.make_shareable({
360
- files: Ractor.make_shareable(files.freeze),
361
- algo_name: algo_name.freeze,
362
- chunk_size: chunk_size,
363
- base_dir: base_dir.freeze,
364
- allow_pause: allow_pause
365
- })
366
- end
367
-
368
- # Each progress value has unique log text — don't drop any
369
- Teek::BackgroundWork.drop_intermediate = false
370
-
371
- @background_task = Teek::BackgroundWork.new(@app, work_data, mode: mode) do |task, data|
372
- algo_class = Digest.const_get(data[:algo_name])
373
- total = data[:files].size
374
- pending = []
375
-
376
- data[:files].each_with_index do |path, index|
377
- if data[:allow_pause] && pending.empty?
378
- task.check_pause
379
- end
380
-
381
- begin
382
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
383
- hash = algo_class.file(path).hexdigest
384
- dt = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
385
- short_path = path.sub(%r{^/app/}, '').sub(data[:base_dir] + '/', '')
386
- pending << "#{short_path}: #{hash} #{dt < 0.01 ? format('%.5fs', dt) : format('%.2fs', dt)}\n"
387
- rescue StandardError => e
388
- short_path = path.sub(%r{^/app/}, '').sub(data[:base_dir] + '/', '')
389
- pending << "#{short_path}: ERROR - #{e.message}\n"
390
- end
391
-
392
- is_last = index == total - 1
393
- if pending.size >= data[:chunk_size] || is_last
394
- task.yield({
395
- index: index,
396
- total: total,
397
- updates: pending.join
398
- })
399
- pending = []
400
- end
401
- end
402
- end
403
-
404
- @background_task.on_progress do |msg|
405
- ui_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
406
-
407
- @log_text.command(:insert, 'end', msg[:updates])
408
- @log_text.command(:see, 'end')
409
- pct = ((msg[:index] + 1).to_f / msg[:total] * 100).round
410
- @app.set_variable('::progress', pct)
411
- @status_label.command(:configure,
412
- text: "Hashing... #{msg[:index] + 1}/#{msg[:total]}")
413
-
414
- @metrics[:ui_update_count] += 1
415
- @metrics[:ui_update_total_ms] += (Process.clock_gettime(Process::CLOCK_MONOTONIC) - ui_start) * 1000
416
- @metrics[:files_done] = msg[:index] + 1
417
-
418
- @app.update if ui_mode == 'None+update'
419
- end.on_done do
420
- @background_task = nil
421
- finish_hashing
422
- end
423
- end
424
-
425
- def run
426
- @app.mainloop
427
- end
428
- end
429
-
430
- demo = ThreadingDemo.new
431
-
432
- # Automated demo support (testing and recording)
433
- require_relative '../lib/teek/demo_support'
434
- TeekDemo.app = demo.app
435
-
436
- if TeekDemo.recording?
437
- demo.app.set_window_geometry('+0+0')
438
- demo.app.tcl_eval('. configure -cursor none')
439
- TeekDemo.signal_recording_ready
440
- end
441
-
442
- if TeekDemo.active?
443
- TeekDemo.after_idle {
444
- demo.app.after(100) {
445
- app = demo.app
446
-
447
- # Set batch size high for fast processing
448
- app.set_variable('::chunk_size', 100)
449
-
450
- # Test matrix: [mode, pause_enabled]
451
- tests = [['None', false], ['None+update', false], ['Thread', false]]
452
- tests << ['Ractor', false] if RACTOR_AVAILABLE
453
- # Quick mode for smoke tests
454
- tests = [['Thread', false]] if ARGV.include?('--quick') || TeekDemo.testing?
455
-
456
- test_index = 0
457
-
458
- run_next_test = proc do
459
- if test_index < tests.size
460
- mode, pause = tests[test_index]
461
-
462
- # Configure mode and pause
463
- app.set_variable('::mode', mode)
464
- app.set_variable('::allow_pause', pause ? 1 : 0)
465
-
466
- # Start hashing
467
- app.after(100) { demo.start_hashing }
468
-
469
- # Wait for completion
470
- check_done = proc do
471
- if demo.instance_variable_get(:@running)
472
- app.after(200, &check_done)
473
- else
474
- test_index += 1
475
- if test_index < tests.size
476
- app.after(200) {
477
- demo.reset
478
- app.after(200, &run_next_test)
479
- }
480
- else
481
- app.after(200) { TeekDemo.finish }
482
- end
483
- end
484
- end
485
- app.after(500, &check_done)
486
- end
487
- end
488
-
489
- run_next_test.call
490
- }
491
- }
492
- end
493
-
494
- demo.run