teek 0.1.0 → 0.1.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +99 -15
  3. data/Rakefile +201 -2
  4. data/ext/teek/extconf.rb +1 -1
  5. data/ext/teek/tcltkbridge.c +3 -110
  6. data/ext/teek/tcltkbridge.h +3 -0
  7. data/ext/teek/tkeventsource.c +195 -0
  8. data/ext/teek/tkphoto.c +169 -5
  9. data/ext/teek/tkwin.c +84 -0
  10. data/lib/teek/background_ractor4x.rb +35 -6
  11. data/lib/teek/debugger.rb +37 -32
  12. data/lib/teek/method_coverage_service.rb +265 -0
  13. data/lib/teek/photo.rb +232 -0
  14. data/lib/teek/ractor_support.rb +1 -1
  15. data/lib/teek/version.rb +1 -1
  16. data/lib/teek/widget.rb +104 -0
  17. data/lib/teek.rb +144 -1
  18. data/sample/calculator.rb +16 -21
  19. data/sample/debug_demo.rb +20 -22
  20. data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
  21. data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
  22. data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
  23. data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
  24. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
  25. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
  26. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
  27. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
  28. data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
  29. data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
  30. data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
  31. data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
  32. data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
  33. data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
  34. data/sample/optcarrot/vendor/optcarrot.rb +14 -0
  35. data/sample/optcarrot.rb +354 -0
  36. data/sample/paint/assets/bucket.png +0 -0
  37. data/sample/paint/assets/cursor.png +0 -0
  38. data/sample/paint/assets/eraser.png +0 -0
  39. data/sample/paint/assets/pencil.png +0 -0
  40. data/sample/paint/assets/spray.png +0 -0
  41. data/sample/paint/layer.rb +255 -0
  42. data/sample/paint/layer_manager.rb +179 -0
  43. data/sample/paint/paint_demo.rb +837 -0
  44. data/sample/paint/sparse_pixel_buffer.rb +202 -0
  45. data/sample/sdl2_demo.rb +318 -0
  46. data/sample/threading_demo.rb +127 -132
  47. metadata +31 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a6639b7742fe7c1be6b94ddb2e7ea7e5f65f66d4525931f5a916a92ebe45a90
4
- data.tar.gz: 0ef9c6a423201a35123193b951577f6f1aa2e375cbabdd20d5d34a35bdd47335
3
+ metadata.gz: 92cd230fed0f898dcf23f80a0a6058adcf308e14b4a8f7feb6985026be4692e0
4
+ data.tar.gz: 698f02315ab27973a05490552fbd6aec21447987a726119d583cbc98f197c486
5
5
  SHA512:
6
- metadata.gz: 252f099b49231033ba5b59d7108c79f920247e357d80ac236b2ead0772b09e5af9aa023855b91597d73586e1c9d78a16724f8d57d7a1d12cddb082ce35e23de7
7
- data.tar.gz: bb50dfed0ede92e95c55a74a047a9382498605579c28c6a874cbd5837ceb5a4e13d94b1360446dca0af22ab556a35c9e9ea7919f10235fb4332f52c7f1529129
6
+ metadata.gz: b259a6ed4a721cfe5179d53eacffdd67f9e3213af17e81eb9c8e434a8857456bf55e2d2798ccddf0099bfe001294fae2cf187111f9aafa575e84e90a1528314e
7
+ data.tar.gz: 20c96ae1cb5f4340737bcd4db2539c01f47f19c7b837bd85fff18ada2d707c4719cabfe0dc14ab1bf986e9b15aee6d61d64883dba4f33f758c3896b9877014c1
data/README.md CHANGED
@@ -12,14 +12,13 @@ require 'teek'
12
12
  app = Teek::App.new
13
13
 
14
14
  app.show
15
- app.tcl_eval('wm title . "Hello Teek"')
15
+ app.set_window_title('Hello Teek')
16
16
 
17
- # Create widgets with tcl_eval
18
- app.tcl_eval('ttk::label .lbl -text "Hello, world!"')
19
- app.tcl_eval('pack .lbl -pady 10')
20
-
21
- # Or use the command helper — Ruby values are auto-quoted,
17
+ # Create widgets with the command helper — Ruby values are auto-quoted,
22
18
  # symbols pass through bare, and procs become callbacks
19
+ app.command('ttk::label', '.lbl', text: 'Hello, world!')
20
+ app.command(:pack, '.lbl', pady: 10)
21
+
23
22
  app.command('ttk::button', '.btn', text: 'Click me', command: proc {
24
23
  app.command('.lbl', :configure, text: 'Clicked!')
25
24
  })
@@ -28,17 +27,57 @@ app.command(:pack, '.btn', pady: 10)
28
27
  app.mainloop
29
28
  ```
30
29
 
30
+ ## Widgets
31
+
32
+ `create_widget` returns a `Teek::Widget` — a thin wrapper that holds the widget path and provides convenience methods. Paths are auto-generated from the widget type.
33
+
34
+ ```ruby
35
+ btn = app.create_widget('ttk::button', text: 'Click me')
36
+ btn.pack(pady: 10)
37
+
38
+ btn.command(:configure, text: 'Updated') # widget subcommand
39
+ btn.destroy
40
+ ```
41
+
42
+ Nest widgets under a parent:
43
+
44
+ ```ruby
45
+ frame = app.create_widget('ttk::frame')
46
+ frame.pack(fill: :both, expand: 1)
47
+
48
+ label = app.create_widget('ttk::label', parent: frame, text: 'Hello')
49
+ label.pack(pady: 5)
50
+ ```
51
+
52
+ Widgets work anywhere a path string is expected (via `to_s`):
53
+
54
+ ```ruby
55
+ app.command(:pack, btn, pady: 10) # equivalent to btn.pack(pady: 10)
56
+ app.tcl_eval("#{btn} configure -text New") # string interpolation works
57
+ ```
58
+
59
+ The raw `app.command` approach still works for cases where you don't need a wrapper:
60
+
61
+ ```ruby
62
+ app.command('ttk::label', '.mylabel', text: 'Direct')
63
+ app.command(:pack, '.mylabel')
64
+ ```
65
+
31
66
  ## Callbacks
32
67
 
33
- Register Ruby procs as Tcl callbacks using `app.register_callback`:
68
+ Pass a `proc` to `command` and it becomes a Tcl callback automatically:
34
69
 
35
70
  ```ruby
36
71
  app = Teek::App.new
37
72
 
38
- cb = app.register_callback(proc { |*args|
39
- puts "clicked!"
40
- })
41
- app.tcl_eval("button .b -text Click -command {ruby_callback #{cb}}")
73
+ app.command(:button, '.b', text: 'Click', command: proc { puts "clicked!" })
74
+ ```
75
+
76
+ Use `bind` for event bindings with optional substitutions:
77
+
78
+ ```ruby
79
+ app.bind('.b', 'Enter') { puts "hovered" }
80
+ app.bind('.c', 'Button-1', :x, :y) { |x, y| puts "#{x},#{y}" }
42
81
  ```
43
82
 
44
83
  ### Stopping event propagation
@@ -46,11 +85,10 @@ app.tcl_eval("button .b -text Click -command {ruby_callback #{cb}}")
46
85
  In `bind` handlers, you can stop an event from propagating to subsequent binding tags by throwing `:teek_break`:
47
86
 
48
87
  ```ruby
49
- cb = app.register_callback(proc { |*|
50
- puts "handled - stop here"
88
+ app.bind('.entry', 'KeyPress', :keysym) { |key|
89
+ puts "handled #{key} - stop here"
51
90
  throw :teek_break
52
- })
53
- app.tcl_eval("bind .entry <Key-Return> {ruby_callback #{cb}}")
91
+ }
54
92
  ```
55
93
 
56
94
  This is equivalent to Tcl's `break` command in a bind script.
@@ -137,3 +175,49 @@ The debugger provides three tabs:
137
175
  - **Watches** — right-click or double-click a variable to watch it; tracks last 50 values with timestamps
138
176
 
139
177
  The debugger runs in the same interpreter as your app (as a [Toplevel](https://www.tcl-lang.org/man/tcl8.6/TkCmd/toplevel.htm) window) and filters its own widgets from `app.widgets`.
178
+
179
+ ## Background Work
180
+
181
+ Tk applications need to keep the UI responsive while doing CPU-intensive work. The `Teek.background_work` API runs work in a background Ractor with automatic UI integration.
182
+
183
+ **This API is designed for Ruby 4.x.** Ractors on Ruby 3.x lack shareable procs, making them impractical for our use case. A `:thread` mode exists but is rarely beneficial — Ruby threads share the GVL, so thread-based background work often performs *worse* than running inline unless the work involves non-blocking I/O.
184
+
185
+ ```ruby
186
+ app = Teek::App.new
187
+ app.show
188
+ app.set_variable('::progress', 0)
189
+
190
+ log = app.create_widget(:text, width: 60, height: 10)
191
+ log.pack(fill: :both, expand: 1, padx: 10, pady: 5)
192
+
193
+ app.create_widget('ttk::progressbar', variable: '::progress', maximum: 100)
194
+ .pack(fill: :x, padx: 10, pady: 5)
195
+
196
+ files = Dir.glob('**/*').select { |f| File.file?(f) }
197
+
198
+ task = Teek::BackgroundWork.new(app, files) do |t, data|
199
+ # Background work goes here - this block cannot access Tk
200
+ data.each_with_index do |file, i|
201
+ t.check_pause
202
+ hash = Digest::SHA256.file(file).hexdigest
203
+ t.yield({ file: file, hash: hash, pct: (i + 1) * 100 / data.size })
204
+ end
205
+ end.on_progress do |msg|
206
+ # This block can access Tk
207
+ log.command(:insert, :end, "#{msg[:file]}: #{msg[:hash]}\n")
208
+ log.command(:see, :end)
209
+ app.set_variable('::progress', msg[:pct])
210
+ end.on_done do
211
+ # This block can also access Tk
212
+ log.command(:insert, :end, "Done!\n")
213
+ end
214
+
215
+ # Control the task
216
+ task.pause # Pause work (resumes at next t.check_pause)
217
+ task.resume # Resume paused work
218
+ task.stop # Stop completely
219
+ ```
220
+
221
+ The work block runs in a background Ractor and cannot access Tk directly. Use `t.yield()` to send results to `on_progress`, which runs on the main thread where Tk is available. Callbacks (`on_progress`, `on_done`) can be chained in any order.
222
+
223
+ See [`sample/threading_demo.rb`](sample/threading_demo.rb) for a complete file hasher example.
data/Rakefile CHANGED
@@ -26,8 +26,18 @@ namespace :docs do
26
26
  end
27
27
  end
28
28
 
29
+ desc "Generate per-method coverage JSON from SimpleCov data"
30
+ task :method_coverage do
31
+ if Dir.exist?('coverage/results')
32
+ require_relative 'lib/teek/method_coverage_service'
33
+ Teek::MethodCoverageService.new(coverage_dir: 'coverage').call
34
+ else
35
+ puts "No coverage data found (run tests with COVERAGE=1 first)"
36
+ end
37
+ end
38
+
29
39
  desc "Generate API docs (YARD JSON -> HTML)"
30
- task yard: :yard_json do
40
+ task yard: [:yard_json, :method_coverage] do
31
41
  Bundler.with_unbundled_env do
32
42
  sh 'BUNDLE_GEMFILE=docs_site/Gemfile bundle exec ruby docs_site/build_api_docs.rb'
33
43
  end
@@ -97,6 +107,7 @@ task yard: 'docs:yard'
97
107
  # Clean up extconf cached config files
98
108
  CLEAN.include('ext/teek/config_list')
99
109
  CLOBBER.include('tmp', 'lib/*.bundle', 'lib/*.so', 'ext/**/*.o', 'ext/**/*.bundle', 'ext/**/*.bundle.dSYM')
110
+ CLOBBER.include('teek-sdl2/lib/*.bundle', 'teek-sdl2/lib/*.so', 'teek-sdl2/ext/**/*.o', 'teek-sdl2/ext/**/*.bundle')
100
111
 
101
112
  # Clean coverage artifacts before test runs to prevent accumulation
102
113
  CLEAN.include('coverage/.resultset.json', 'coverage/results')
@@ -109,6 +120,45 @@ if Gem::Specification.find_all_by_name('rake-compiler').any?
109
120
  ext.ext_dir = 'ext/teek'
110
121
  ext.lib_dir = 'lib'
111
122
  end
123
+
124
+ Rake::ExtensionTask.new do |ext|
125
+ ext.name = 'teek_sdl2'
126
+ ext.ext_dir = 'teek-sdl2/ext/teek_sdl2'
127
+ ext.lib_dir = 'teek-sdl2/lib'
128
+ end
129
+ end
130
+
131
+ namespace :screenshots do
132
+ desc "Bless current unverified screenshots as the new baselines"
133
+ task :bless do
134
+ require_relative 'test/screenshot_helper'
135
+ src = ScreenshotHelper.unverified_dir
136
+ dst = ScreenshotHelper.blessed_dir
137
+
138
+ pngs = Dir.glob(File.join(src, '*.png'))
139
+ if pngs.empty?
140
+ puts "No unverified screenshots in #{src}"
141
+ next
142
+ end
143
+
144
+ FileUtils.mkdir_p(dst)
145
+ pngs.each do |f|
146
+ FileUtils.cp(f, dst)
147
+ puts " Blessed: #{File.basename(f)}"
148
+ end
149
+ puts "#{pngs.size} screenshot(s) blessed to #{dst}"
150
+ end
151
+
152
+ desc "Remove unverified screenshots and diffs"
153
+ task :clean do
154
+ require_relative 'test/screenshot_helper'
155
+ [ScreenshotHelper.unverified_dir, ScreenshotHelper.diffs_dir].each do |dir|
156
+ if Dir.exist?(dir)
157
+ FileUtils.rm_rf(dir)
158
+ puts " Removed: #{dir}"
159
+ end
160
+ end
161
+ end
112
162
  end
113
163
 
114
164
  desc "Clear stale coverage artifacts"
@@ -168,8 +218,78 @@ def detect_platform
168
218
  end
169
219
  end
170
220
 
221
+ namespace :sdl2 do
222
+ desc "Compile teek-sdl2 C extension"
223
+ task compile: 'compile:teek_sdl2'
224
+
225
+ Rake::TestTask.new(:test) do |t|
226
+ t.libs << 'teek-sdl2/test' << 'teek-sdl2/lib'
227
+ t.test_files = FileList['teek-sdl2/test/**/test_*.rb'] - FileList['teek-sdl2/test/test_helper.rb']
228
+ t.ruby_opts << '-r test_helper'
229
+ t.verbose = true
230
+ end
231
+ task test: 'compile:teek_sdl2'
232
+ end
233
+
171
234
  task :default => :compile
172
235
 
236
+ namespace :release do
237
+ desc "Build gems, install to temp dir, run smoke test"
238
+ task :smoke do
239
+ require 'tmpdir'
240
+ require 'fileutils'
241
+
242
+ Dir.mktmpdir('teek-smoke') do |tmpdir|
243
+ gem_home = File.join(tmpdir, 'gems')
244
+
245
+ # Build both gems
246
+ puts "Building gems..."
247
+ sh "gem build teek.gemspec -o #{tmpdir}/teek.gem 2>&1"
248
+ Dir.chdir('teek-sdl2') { sh "gem build teek-sdl2.gemspec -o #{tmpdir}/teek-sdl2.gem 2>&1" }
249
+
250
+ # Install into isolated GEM_HOME
251
+ puts "\nInstalling gems..."
252
+ sh "GEM_HOME=#{gem_home} gem install #{tmpdir}/teek.gem --no-document 2>&1"
253
+ sh "GEM_HOME=#{gem_home} gem install #{tmpdir}/teek-sdl2.gem --no-document 2>&1"
254
+
255
+ # Run smoke test using only the installed gems (no -I, no bundle)
256
+ puts "\nRunning SDL2 smoke test..."
257
+ smoke = <<~'RUBY'
258
+ require "teek"
259
+ require "teek/sdl2"
260
+
261
+ app = Teek::App.new
262
+ app.set_window_title("Release Smoke Test")
263
+ app.set_window_geometry("320x240")
264
+ app.show
265
+ app.update
266
+
267
+ vp = Teek::SDL2::Viewport.new(app, width: 300, height: 200)
268
+ vp.pack
269
+
270
+ vp.render do |r|
271
+ r.clear(30, 30, 30)
272
+ r.fill(20, 20, 120, 80, r: 200, g: 50, b: 50)
273
+ r.outline(160, 20, 120, 80, r: 50, g: 200, b: 50)
274
+ r.line(20, 130, 280, 180, r: 50, g: 50, b: 200)
275
+ end
276
+
277
+ w, h = vp.renderer.output_size
278
+ pixels = vp.renderer.read_pixels
279
+ raise "read_pixels size mismatch" unless pixels.bytesize == w * h * 4
280
+
281
+ app.after(500) { vp.destroy; app.destroy }
282
+ app.mainloop
283
+ puts "Release smoke test passed (teek #{Teek::VERSION}, teek-sdl2 #{Teek::SDL2::VERSION})"
284
+ RUBY
285
+
286
+ smoke_file = File.join(tmpdir, 'smoke.rb')
287
+ File.write(smoke_file, smoke)
288
+ sh "GEM_HOME=#{gem_home} GEM_PATH=#{gem_home} ruby #{smoke_file}"
289
+ end
290
+ end
291
+ end
292
+
173
293
  # Docker tasks for local testing and CI
174
294
  namespace :docker do
175
295
  DOCKERFILE = 'Dockerfile.ci-test'
@@ -261,10 +381,89 @@ namespace :docker do
261
381
 
262
382
  Rake::Task['docker:test'].enhance { Rake::Task['docker:prune'].invoke }
263
383
 
384
+ namespace :test do
385
+ desc "Run teek-sdl2 tests in Docker"
386
+ task sdl2: :build do
387
+ tcl_version = tcl_version_from_env
388
+ ruby_version = ruby_version_from_env
389
+ image_name = docker_image_name(tcl_version, ruby_version)
390
+
391
+ require 'fileutils'
392
+ FileUtils.mkdir_p('coverage')
393
+
394
+ puts "Running teek-sdl2 tests in Docker (Ruby #{ruby_version}, Tcl #{tcl_version})..."
395
+ cmd = "docker run --rm --init"
396
+ cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
397
+ cmd += " -v #{Dir.pwd}/screenshots:/app/screenshots"
398
+ cmd += " -e TCL_VERSION=#{tcl_version}"
399
+ if ENV['COVERAGE'] == '1'
400
+ cmd += " -e COVERAGE=1"
401
+ cmd += " -e COVERAGE_NAME=#{ENV['COVERAGE_NAME'] || 'sdl2'}"
402
+ end
403
+ cmd += " #{image_name}"
404
+ cmd += " xvfb-run -a bundle exec rake sdl2:test"
405
+
406
+ sh cmd
407
+ end
408
+
409
+ desc "Run all tests (teek + teek-sdl2) with coverage and generate report"
410
+ task all: 'docker:build' do
411
+ tcl_version = tcl_version_from_env
412
+ ruby_version = ruby_version_from_env
413
+ image_name = docker_image_name(tcl_version, ruby_version)
414
+
415
+ require 'fileutils'
416
+ FileUtils.rm_rf('coverage')
417
+ FileUtils.mkdir_p('coverage/results')
418
+
419
+ # Run both test suites with coverage enabled and distinct COVERAGE_NAMEs
420
+ ENV['COVERAGE'] = '1'
421
+
422
+ ENV['COVERAGE_NAME'] = 'main'
423
+ Rake::Task['docker:test'].invoke
424
+
425
+ ENV['COVERAGE_NAME'] = 'sdl2'
426
+ Rake::Task['docker:test:sdl2'].reenable
427
+ Rake::Task['docker:build'].reenable
428
+ Rake::Task['docker:test:sdl2'].invoke
429
+
430
+ # Collate inside Docker (paths match /app/lib/...)
431
+ puts "Collating coverage results..."
432
+ cmd = "docker run --rm --init"
433
+ cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
434
+ cmd += " #{image_name}"
435
+ cmd += " bundle exec rake coverage:collate"
436
+
437
+ sh cmd
438
+
439
+ # Generate per-method coverage (runs locally, just needs Prism)
440
+ puts "Generating per-method coverage..."
441
+ Rake::Task['docs:method_coverage'].invoke
442
+
443
+ puts "Coverage report: coverage/index.html"
444
+ end
445
+ end
446
+
447
+ namespace :screenshots do
448
+ desc "Bless linux screenshots inside Docker (copies unverified/ to blessed/)"
449
+ task bless: :build do
450
+ ruby_version = ruby_version_from_env
451
+ tcl_version = tcl_version_from_env
452
+ image_name = docker_image_name(tcl_version, ruby_version)
453
+
454
+ cmd = "docker run --rm --init"
455
+ cmd += " -v #{Dir.pwd}/screenshots:/app/screenshots"
456
+ cmd += " #{image_name}"
457
+ cmd += " bundle exec rake screenshots:bless"
458
+
459
+ sh cmd
460
+ end
461
+ end
462
+
264
463
  # Scan sample files for # teek-record magic comment
265
464
  # Format: # teek-record: title=My Demo, codec=vp9
266
465
  def find_recordable_samples
267
- Dir['sample/**/*.rb'].filter_map do |path|
466
+ Dir['sample/**/*.rb', 'teek-sdl2/sample/**/*.rb'].filter_map do |path|
268
467
  first_lines = File.read(path, 500)
269
468
  match = first_lines.match(/^#\s*teek-record(?::\s*(.+))?$/)
270
469
  next unless match
data/ext/teek/extconf.rb CHANGED
@@ -74,6 +74,6 @@ end
74
74
  find_tcltk
75
75
 
76
76
  # Source files for the extension
77
- $srcs = ['tcltkbridge.c', 'tkphoto.c', 'tkfont.c', 'tkwin.c']
77
+ $srcs = ['tcltkbridge.c', 'tkphoto.c', 'tkfont.c', 'tkwin.c', 'tkeventsource.c']
78
78
 
79
79
  create_makefile('tcltklib')
@@ -130,9 +130,6 @@ static void interp_deleted_callback(ClientData, Tcl_Interp *);
130
130
  /* 16ms ≈ 60fps - balances UI responsiveness with scheduler contention */
131
131
  #define DEFAULT_TIMER_INTERVAL_MS 16
132
132
 
133
- /* Global timer interval for TclTkLib.mainloop (mutable) */
134
- static int g_thread_timer_ms = DEFAULT_TIMER_INTERVAL_MS;
135
-
136
133
  /* struct tcltk_interp is defined in tcltkbridge.h */
137
134
 
138
135
  /* ---------------------------------------------------------
@@ -1066,109 +1063,6 @@ interp_mainloop(VALUE self)
1066
1063
  * yields between events).
1067
1064
  * --------------------------------------------------------- */
1068
1065
 
1069
- /* Global timer handler - re-registers itself using global interval */
1070
- static void
1071
- global_keepalive_timer_proc(ClientData clientData)
1072
- {
1073
- if (g_thread_timer_ms > 0) {
1074
- Tcl_CreateTimerHandler(g_thread_timer_ms, global_keepalive_timer_proc, NULL);
1075
- }
1076
- }
1077
-
1078
- static VALUE
1079
- lib_mainloop(int argc, VALUE *argv, VALUE self)
1080
- {
1081
- int check_root = 1; /* default: exit when no windows remain */
1082
- int event_flags = TCL_ALL_EVENTS;
1083
-
1084
- /* Optional check_root argument:
1085
- * true (default): exit when Tk_GetNumMainWindows() == 0
1086
- * false: keep running even with no windows (for timers, traces, etc.)
1087
- */
1088
- if (argc > 0 && argv[0] != Qnil) {
1089
- check_root = RTEST(argv[0]);
1090
- }
1091
-
1092
- for (;;) {
1093
- /* Exit if check_root enabled and no windows remain */
1094
- if (check_root && Tk_GetNumMainWindows() <= 0) {
1095
- break;
1096
- }
1097
-
1098
- if (rb_thread_alone()) {
1099
- /* No other threads - simple blocking wait */
1100
- Tcl_DoOneEvent(event_flags);
1101
- } else {
1102
- /* Other threads exist - use polling with brief sleep.
1103
- *
1104
- * We tried rb_thread_call_without_gvl() with Tcl_ThreadAlert to
1105
- * efficiently release GVL during blocking waits, but it proved
1106
- * unstable - crashes in Digest and other C extensions, UI freezes,
1107
- * and unreliable notifier wakeup across platforms.
1108
- *
1109
- * This polling approach is simple and stable:
1110
- * - Process any pending events without blocking
1111
- * - If no events, brief sleep to avoid spinning (uses ~1-3% CPU idle)
1112
- * - rb_thread_schedule() lets background threads run during sleep
1113
- */
1114
- int had_event = Tcl_DoOneEvent(event_flags | TCL_DONT_WAIT);
1115
- if (!had_event) {
1116
- rb_thread_schedule();
1117
- #ifdef _WIN32
1118
- Sleep(5); /* 5ms */
1119
- #else
1120
- struct timespec ts = {0, 5000000}; /* 5ms */
1121
- nanosleep(&ts, NULL);
1122
- #endif
1123
- }
1124
- }
1125
-
1126
- /* Check for Ruby interrupts (Ctrl-C, etc) */
1127
- rb_thread_check_ints();
1128
- }
1129
-
1130
- return Qnil;
1131
- }
1132
-
1133
- static VALUE
1134
- lib_get_thread_timer_ms(VALUE self)
1135
- {
1136
- return INT2NUM(g_thread_timer_ms);
1137
- }
1138
-
1139
- static VALUE
1140
- lib_set_thread_timer_ms(VALUE self, VALUE val)
1141
- {
1142
- int ms = NUM2INT(val);
1143
- if (ms < 0) {
1144
- rb_raise(rb_eArgError, "thread_timer_ms must be >= 0 (got %d)", ms);
1145
- }
1146
- g_thread_timer_ms = ms;
1147
- return val;
1148
- }
1149
-
1150
- /* ---------------------------------------------------------
1151
- * TclTkLib.do_one_event(flags = ALL_EVENTS) - Process single event
1152
- *
1153
- * Global function - Tcl_DoOneEvent doesn't require an interpreter.
1154
- * Returns true if event was processed, false if nothing to do.
1155
- * --------------------------------------------------------- */
1156
-
1157
- static VALUE
1158
- lib_do_one_event(int argc, VALUE *argv, VALUE self)
1159
- {
1160
- int flags = TCL_ALL_EVENTS;
1161
- int result;
1162
-
1163
- if (argc > 0) {
1164
- flags = NUM2INT(argv[0]);
1165
- }
1166
-
1167
- result = Tcl_DoOneEvent(flags);
1168
-
1169
- return result ? Qtrue : Qfalse;
1170
- }
1171
-
1172
1066
  /* ---------------------------------------------------------
1173
1067
  * Interp#thread_timer_ms / #thread_timer_ms= - Get/set timer interval
1174
1068
  * --------------------------------------------------------- */
@@ -1539,10 +1433,6 @@ Init_tcltklib(void)
1539
1433
  rb_define_module_function(mTeek, "split_list", teek_split_list, 1);
1540
1434
  rb_define_module_function(mTeek, "tcl_to_bool", teek_tcl_to_bool, 1);
1541
1435
 
1542
- /* Global thread timer - doesn't require an interpreter */
1543
- rb_define_module_function(mTeek, "thread_timer_ms", lib_get_thread_timer_ms, 0);
1544
- rb_define_module_function(mTeek, "thread_timer_ms=", lib_set_thread_timer_ms, 1);
1545
-
1546
1436
  /* Callback depth detection for unsafe operation warnings */
1547
1437
  rb_define_module_function(mTeek, "in_callback?", lib_in_callback_p, 0);
1548
1438
 
@@ -1591,6 +1481,9 @@ Init_tcltklib(void)
1591
1481
  /* Tk window query functions (tkwin.c) */
1592
1482
  Init_tkwin(cInterp);
1593
1483
 
1484
+ /* External event source integration (tkeventsource.c) */
1485
+ Init_tkeventsource(mTeek);
1486
+
1594
1487
  /* Class methods for instance tracking */
1595
1488
  rb_define_singleton_method(cInterp, "instance_count", tcltkip_instance_count, 0);
1596
1489
  rb_define_singleton_method(cInterp, "instances", tcltkip_instances, 0);
@@ -39,4 +39,7 @@ void Init_tkfont(VALUE cInterp);
39
39
  /* Tk window query functions - defined in tkwin.c */
40
40
  void Init_tkwin(VALUE cInterp);
41
41
 
42
+ /* External event source integration - defined in tkeventsource.c */
43
+ void Init_tkeventsource(VALUE mTeek);
44
+
42
45
  #endif /* TCLTKBRIDGE_H */