teek 0.1.1 → 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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -0
  3. data/Rakefile +161 -5
  4. data/ext/teek/extconf.rb +1 -1
  5. data/ext/teek/tcltkbridge.c +3 -0
  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 +32 -4
  11. data/lib/teek/photo.rb +232 -0
  12. data/lib/teek/version.rb +1 -1
  13. data/lib/teek.rb +3 -1
  14. data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
  15. data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
  16. data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
  17. data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
  18. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
  19. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
  20. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
  21. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
  22. data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
  23. data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
  24. data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
  25. data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
  26. data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
  27. data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
  28. data/sample/optcarrot/vendor/optcarrot.rb +14 -0
  29. data/sample/optcarrot.rb +354 -0
  30. data/sample/paint/assets/bucket.png +0 -0
  31. data/sample/paint/assets/cursor.png +0 -0
  32. data/sample/paint/assets/eraser.png +0 -0
  33. data/sample/paint/assets/pencil.png +0 -0
  34. data/sample/paint/assets/spray.png +0 -0
  35. data/sample/paint/layer.rb +255 -0
  36. data/sample/paint/layer_manager.rb +179 -0
  37. data/sample/paint/paint_demo.rb +837 -0
  38. data/sample/paint/sparse_pixel_buffer.rb +202 -0
  39. data/sample/sdl2_demo.rb +318 -0
  40. metadata +29 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbf5c504422362129bd97b61be21e564e0c4fc4eddced045f97320a14d77d999
4
- data.tar.gz: 2a2d74b99b040fe345f67ae83f52b7f759b24c5a2b86a644f3673d0539676023
3
+ metadata.gz: 92cd230fed0f898dcf23f80a0a6058adcf308e14b4a8f7feb6985026be4692e0
4
+ data.tar.gz: 698f02315ab27973a05490552fbd6aec21447987a726119d583cbc98f197c486
5
5
  SHA512:
6
- metadata.gz: 7a380c5190906c2537aaca29e7da5c6def0abb727db6ca1137ace00f2fd9f85876753b192cd1ff327108a62123b25c8db2db20dbe98c01ca82dbb8a2de11333d
7
- data.tar.gz: c6ed01ff47f5edf05407b3e60bd1108a94461988053e013becaadbed0fa9697990e92e28ceed55341299d7f42a11dcecdc21b8b803cef86ff50538efe6fcee1b
6
+ metadata.gz: b259a6ed4a721cfe5179d53eacffdd67f9e3213af17e81eb9c8e434a8857456bf55e2d2798ccddf0099bfe001294fae2cf187111f9aafa575e84e90a1528314e
7
+ data.tar.gz: 20c96ae1cb5f4340737bcd4db2539c01f47f19c7b837bd85fff18ada2d707c4719cabfe0dc14ab1bf986e9b15aee6d61d64883dba4f33f758c3896b9877014c1
data/README.md CHANGED
@@ -175,3 +175,49 @@ The debugger provides three tabs:
175
175
  - **Watches** — right-click or double-click a variable to watch it; tracks last 50 values with timestamps
176
176
 
177
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
@@ -107,6 +107,7 @@ task yard: 'docs:yard'
107
107
  # Clean up extconf cached config files
108
108
  CLEAN.include('ext/teek/config_list')
109
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')
110
111
 
111
112
  # Clean coverage artifacts before test runs to prevent accumulation
112
113
  CLEAN.include('coverage/.resultset.json', 'coverage/results')
@@ -119,6 +120,45 @@ if Gem::Specification.find_all_by_name('rake-compiler').any?
119
120
  ext.ext_dir = 'ext/teek'
120
121
  ext.lib_dir = 'lib'
121
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
122
162
  end
123
163
 
124
164
  desc "Clear stale coverage artifacts"
@@ -178,8 +218,78 @@ def detect_platform
178
218
  end
179
219
  end
180
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
+
181
234
  task :default => :compile
182
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
+
183
293
  # Docker tasks for local testing and CI
184
294
  namespace :docker do
185
295
  DOCKERFILE = 'Dockerfile.ci-test'
@@ -272,8 +382,32 @@ namespace :docker do
272
382
  Rake::Task['docker:test'].enhance { Rake::Task['docker:prune'].invoke }
273
383
 
274
384
  namespace :test do
275
- desc "Run tests with coverage and generate report"
276
- task coverage: 'docker:build' 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
277
411
  tcl_version = tcl_version_from_env
278
412
  ruby_version = ruby_version_from_env
279
413
  image_name = docker_image_name(tcl_version, ruby_version)
@@ -282,11 +416,17 @@ namespace :docker do
282
416
  FileUtils.rm_rf('coverage')
283
417
  FileUtils.mkdir_p('coverage/results')
284
418
 
285
- # Run tests with coverage enabled
419
+ # Run both test suites with coverage enabled and distinct COVERAGE_NAMEs
286
420
  ENV['COVERAGE'] = '1'
287
- ENV['COVERAGE_NAME'] ||= 'main'
421
+
422
+ ENV['COVERAGE_NAME'] = 'main'
288
423
  Rake::Task['docker:test'].invoke
289
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
+
290
430
  # Collate inside Docker (paths match /app/lib/...)
291
431
  puts "Collating coverage results..."
292
432
  cmd = "docker run --rm --init"
@@ -304,10 +444,26 @@ namespace :docker do
304
444
  end
305
445
  end
306
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
+
307
463
  # Scan sample files for # teek-record magic comment
308
464
  # Format: # teek-record: title=My Demo, codec=vp9
309
465
  def find_recordable_samples
310
- Dir['sample/**/*.rb'].filter_map do |path|
466
+ Dir['sample/**/*.rb', 'teek-sdl2/sample/**/*.rb'].filter_map do |path|
311
467
  first_lines = File.read(path, 500)
312
468
  match = first_lines.match(/^#\s*teek-record(?::\s*(.+))?$/)
313
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')
@@ -1481,6 +1481,9 @@ Init_tcltklib(void)
1481
1481
  /* Tk window query functions (tkwin.c) */
1482
1482
  Init_tkwin(cInterp);
1483
1483
 
1484
+ /* External event source integration (tkeventsource.c) */
1485
+ Init_tkeventsource(mTeek);
1486
+
1484
1487
  /* Class methods for instance tracking */
1485
1488
  rb_define_singleton_method(cInterp, "instance_count", tcltkip_instance_count, 0);
1486
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 */
@@ -0,0 +1,195 @@
1
+ /*
2
+ * tkeventsource.c - External event source integration via Tcl_CreateEventSource
3
+ *
4
+ * Allows other C extensions (e.g. teek-sdl2) to register poll callbacks
5
+ * that run inside Tcl's event loop with zero Ruby overhead in the hot path.
6
+ *
7
+ * The consumer passes a C function pointer via a Ruby method call at
8
+ * registration time. The Tcl event source setup/check procs call that
9
+ * pointer directly — no rb_funcall, no method dispatch.
10
+ */
11
+
12
+ #include "tcltkbridge.h"
13
+ #include <stdint.h>
14
+
15
+ static VALUE cEventSource;
16
+
17
+ /* ---------------------------------------------------------
18
+ * Event source struct — wrapped as Ruby TypedData
19
+ * --------------------------------------------------------- */
20
+
21
+ typedef void (*event_source_check_fn)(void *client_data);
22
+
23
+ struct event_source {
24
+ event_source_check_fn check_fn; /* C function pointer from consumer */
25
+ void *client_data; /* Opaque data from consumer */
26
+ Tcl_Time max_block; /* Max block time for setup proc */
27
+ int registered; /* Whether Tcl event source is active */
28
+ };
29
+
30
+ /* Forward declarations */
31
+ static void es_setup_proc(ClientData cd, int flags);
32
+ static void es_check_proc(ClientData cd, int flags);
33
+
34
+ /* ---------------------------------------------------------
35
+ * TypedData functions
36
+ * --------------------------------------------------------- */
37
+
38
+ static void
39
+ event_source_free(void *ptr)
40
+ {
41
+ struct event_source *es = ptr;
42
+ if (es->registered) {
43
+ Tcl_DeleteEventSource(es_setup_proc, es_check_proc, (ClientData)es);
44
+ es->registered = 0;
45
+ }
46
+ xfree(es);
47
+ }
48
+
49
+ static size_t
50
+ event_source_memsize(const void *ptr)
51
+ {
52
+ return sizeof(struct event_source);
53
+ }
54
+
55
+ static const rb_data_type_t event_source_type = {
56
+ .wrap_struct_name = "Teek::EventSource",
57
+ .function = {
58
+ .dmark = NULL, /* No Ruby VALUEs to mark */
59
+ .dfree = event_source_free,
60
+ .dsize = event_source_memsize,
61
+ },
62
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
63
+ };
64
+
65
+ /* ---------------------------------------------------------
66
+ * Tcl event source callbacks (hot path — pure C)
67
+ * --------------------------------------------------------- */
68
+
69
+ /*
70
+ * Setup proc: called before Tcl_WaitForEvent.
71
+ * Caps the block time so our check proc runs frequently.
72
+ */
73
+ static void
74
+ es_setup_proc(ClientData cd, int flags)
75
+ {
76
+ struct event_source *es = (struct event_source *)cd;
77
+
78
+ if (!(flags & TCL_FILE_EVENTS) && !(flags & TCL_ALL_EVENTS))
79
+ return;
80
+
81
+ Tcl_SetMaxBlockTime(&es->max_block);
82
+ }
83
+
84
+ /*
85
+ * Check proc: called after Tcl_WaitForEvent returns.
86
+ * Calls the consumer's C function pointer directly.
87
+ * No rb_funcall, no Ruby method dispatch — just a function pointer call.
88
+ */
89
+ static void
90
+ es_check_proc(ClientData cd, int flags)
91
+ {
92
+ struct event_source *es = (struct event_source *)cd;
93
+
94
+ if (!(flags & TCL_FILE_EVENTS) && !(flags & TCL_ALL_EVENTS))
95
+ return;
96
+
97
+ es->check_fn(es->client_data);
98
+ }
99
+
100
+ /* ---------------------------------------------------------
101
+ * Ruby methods
102
+ * --------------------------------------------------------- */
103
+
104
+ /*
105
+ * Teek._register_event_source(check_fn_ptr, client_data_ptr, interval_ms) -> EventSource
106
+ *
107
+ * Registers a C function as a Tcl event source. The function will be called
108
+ * on every event loop iteration with no Ruby overhead.
109
+ *
110
+ * check_fn_ptr: Integer — address of a C function with signature void(*)(void*)
111
+ * client_data_ptr: Integer — address passed to check_fn (0 for NULL)
112
+ * interval_ms: Integer — max block time in ms (e.g. 16 for ~60fps)
113
+ *
114
+ * Returns an opaque EventSource object. Hold a reference to keep it alive.
115
+ * Call #unregister or let GC collect it to remove the event source.
116
+ */
117
+ static VALUE
118
+ teek_register_event_source(VALUE self, VALUE fn_ptr, VALUE data_ptr, VALUE interval)
119
+ {
120
+ struct event_source *es;
121
+ VALUE obj;
122
+ int ms;
123
+
124
+ /* Validate */
125
+ event_source_check_fn fn = (event_source_check_fn)(uintptr_t)NUM2ULL(fn_ptr);
126
+ if (!fn) {
127
+ rb_raise(rb_eArgError, "check_fn_ptr must not be NULL");
128
+ }
129
+
130
+ ms = NUM2INT(interval);
131
+ if (ms < 1) ms = 1;
132
+
133
+ /* Allocate and populate */
134
+ obj = TypedData_Make_Struct(cEventSource, struct event_source, &event_source_type, es);
135
+ es->check_fn = fn;
136
+ es->client_data = (void *)(uintptr_t)NUM2ULL(data_ptr);
137
+ es->max_block.sec = ms / 1000;
138
+ es->max_block.usec = (ms % 1000) * 1000;
139
+ es->registered = 0;
140
+
141
+ /* Register with Tcl */
142
+ Tcl_CreateEventSource(es_setup_proc, es_check_proc, (ClientData)es);
143
+ es->registered = 1;
144
+
145
+ return obj;
146
+ }
147
+
148
+ /*
149
+ * EventSource#unregister -> nil
150
+ *
151
+ * Explicitly removes the event source from Tcl's notifier.
152
+ * Safe to call multiple times.
153
+ */
154
+ static VALUE
155
+ event_source_unregister(VALUE self)
156
+ {
157
+ struct event_source *es;
158
+ TypedData_Get_Struct(self, struct event_source, &event_source_type, es);
159
+
160
+ if (es->registered) {
161
+ Tcl_DeleteEventSource(es_setup_proc, es_check_proc, (ClientData)es);
162
+ es->registered = 0;
163
+ }
164
+ return Qnil;
165
+ }
166
+
167
+ /*
168
+ * EventSource#registered? -> true/false
169
+ */
170
+ static VALUE
171
+ event_source_registered_p(VALUE self)
172
+ {
173
+ struct event_source *es;
174
+ TypedData_Get_Struct(self, struct event_source, &event_source_type, es);
175
+ return es->registered ? Qtrue : Qfalse;
176
+ }
177
+
178
+ /* ---------------------------------------------------------
179
+ * Init — called from Init_tcltklib
180
+ * --------------------------------------------------------- */
181
+
182
+ static VALUE cEventSource;
183
+
184
+ void
185
+ Init_tkeventsource(VALUE mTeek)
186
+ {
187
+ cEventSource = rb_define_class_under(mTeek, "EventSource", rb_cObject);
188
+ rb_undef_alloc_func(cEventSource); /* No Ruby-side new */
189
+
190
+ rb_define_method(cEventSource, "unregister", event_source_unregister, 0);
191
+ rb_define_method(cEventSource, "registered?", event_source_registered_p, 0);
192
+
193
+ rb_define_module_function(mTeek, "_register_event_source",
194
+ teek_register_event_source, 3);
195
+ }