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.
- checksums.yaml +4 -4
- data/README.md +99 -15
- data/Rakefile +201 -2
- data/ext/teek/extconf.rb +1 -1
- data/ext/teek/tcltkbridge.c +3 -110
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkeventsource.c +195 -0
- data/ext/teek/tkphoto.c +169 -5
- data/ext/teek/tkwin.c +84 -0
- data/lib/teek/background_ractor4x.rb +35 -6
- data/lib/teek/debugger.rb +37 -32
- data/lib/teek/method_coverage_service.rb +265 -0
- data/lib/teek/photo.rb +232 -0
- data/lib/teek/ractor_support.rb +1 -1
- data/lib/teek/version.rb +1 -1
- data/lib/teek/widget.rb +104 -0
- data/lib/teek.rb +144 -1
- data/sample/calculator.rb +16 -21
- data/sample/debug_demo.rb +20 -22
- data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
- data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
- data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
- data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
- data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
- data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
- data/sample/optcarrot/vendor/optcarrot.rb +14 -0
- data/sample/optcarrot.rb +354 -0
- data/sample/paint/assets/bucket.png +0 -0
- data/sample/paint/assets/cursor.png +0 -0
- data/sample/paint/assets/eraser.png +0 -0
- data/sample/paint/assets/pencil.png +0 -0
- data/sample/paint/assets/spray.png +0 -0
- data/sample/paint/layer.rb +255 -0
- data/sample/paint/layer_manager.rb +179 -0
- data/sample/paint/paint_demo.rb +837 -0
- data/sample/paint/sparse_pixel_buffer.rb +202 -0
- data/sample/sdl2_demo.rb +318 -0
- data/sample/threading_demo.rb +127 -132
- metadata +31 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92cd230fed0f898dcf23f80a0a6058adcf308e14b4a8f7feb6985026be4692e0
|
|
4
|
+
data.tar.gz: 698f02315ab27973a05490552fbd6aec21447987a726119d583cbc98f197c486
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
15
|
+
app.set_window_title('Hello Teek')
|
|
16
16
|
|
|
17
|
-
# Create widgets with
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
data/ext/teek/tcltkbridge.c
CHANGED
|
@@ -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);
|
data/ext/teek/tcltkbridge.h
CHANGED
|
@@ -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 */
|