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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +139 -0
- data/Rakefile +316 -0
- data/ext/teek/extconf.rb +79 -0
- data/ext/teek/stubs.h +33 -0
- data/ext/teek/tcl9compat.h +211 -0
- data/ext/teek/tcltkbridge.c +1597 -0
- data/ext/teek/tcltkbridge.h +42 -0
- data/ext/teek/tkfont.c +218 -0
- data/ext/teek/tkphoto.c +477 -0
- data/ext/teek/tkwin.c +144 -0
- data/lib/teek/background_none.rb +158 -0
- data/lib/teek/background_ractor4x.rb +410 -0
- data/lib/teek/background_thread.rb +272 -0
- data/lib/teek/debugger.rb +742 -0
- data/lib/teek/demo_support.rb +150 -0
- data/lib/teek/ractor_support.rb +246 -0
- data/lib/teek/version.rb +5 -0
- data/lib/teek.rb +540 -0
- data/sample/calculator.rb +260 -0
- data/sample/debug_demo.rb +45 -0
- data/sample/goldberg.rb +1803 -0
- data/sample/goldberg_helpers.rb +170 -0
- data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
- data/sample/minesweeper/minesweeper.rb +452 -0
- data/sample/threading_demo.rb +499 -0
- data/teek.gemspec +32 -0
- metadata +179 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4a6639b7742fe7c1be6b94ddb2e7ea7e5f65f66d4525931f5a916a92ebe45a90
|
|
4
|
+
data.tar.gz: 0ef9c6a423201a35123193b951577f6f1aa2e375cbabdd20d5d34a35bdd47335
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 252f099b49231033ba5b59d7108c79f920247e357d80ac236b2ead0772b09e5af9aa023855b91597d73586e1c9d78a16724f8d57d7a1d12cddb082ce35e23de7
|
|
7
|
+
data.tar.gz: bb50dfed0ede92e95c55a74a047a9382498605579c28c6a874cbd5837ceb5a4e13d94b1360446dca0af22ab556a35c9e9ea7919f10235fb4332f52c7f1529129
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present James Cook
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Teek
|
|
2
|
+
|
|
3
|
+
A Ruby interface to Tcl/Tk.
|
|
4
|
+
|
|
5
|
+
[API Documentation](https://jamescook.github.io/teek/)
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require 'teek'
|
|
11
|
+
|
|
12
|
+
app = Teek::App.new
|
|
13
|
+
|
|
14
|
+
app.show
|
|
15
|
+
app.tcl_eval('wm title . "Hello Teek"')
|
|
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,
|
|
22
|
+
# symbols pass through bare, and procs become callbacks
|
|
23
|
+
app.command('ttk::button', '.btn', text: 'Click me', command: proc {
|
|
24
|
+
app.command('.lbl', :configure, text: 'Clicked!')
|
|
25
|
+
})
|
|
26
|
+
app.command(:pack, '.btn', pady: 10)
|
|
27
|
+
|
|
28
|
+
app.mainloop
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Callbacks
|
|
32
|
+
|
|
33
|
+
Register Ruby procs as Tcl callbacks using `app.register_callback`:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
app = Teek::App.new
|
|
37
|
+
|
|
38
|
+
cb = app.register_callback(proc { |*args|
|
|
39
|
+
puts "clicked!"
|
|
40
|
+
})
|
|
41
|
+
app.tcl_eval("button .b -text Click -command {ruby_callback #{cb}}")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Stopping event propagation
|
|
45
|
+
|
|
46
|
+
In `bind` handlers, you can stop an event from propagating to subsequent binding tags by throwing `:teek_break`:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
cb = app.register_callback(proc { |*|
|
|
50
|
+
puts "handled - stop here"
|
|
51
|
+
throw :teek_break
|
|
52
|
+
})
|
|
53
|
+
app.tcl_eval("bind .entry <Key-Return> {ruby_callback #{cb}}")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This is equivalent to Tcl's `break` command in a bind script.
|
|
57
|
+
|
|
58
|
+
Two other control flow signals are available for advanced use:
|
|
59
|
+
|
|
60
|
+
- `throw :teek_continue` - skip remaining bind scripts for this event (Tcl `continue`)
|
|
61
|
+
- `throw :teek_return` - return from the current Tcl proc (Tcl `return`)
|
|
62
|
+
|
|
63
|
+
### Errors in callbacks
|
|
64
|
+
|
|
65
|
+
If a callback raises a Ruby exception, it becomes a Tcl error. The exception message is preserved and can be caught on the Tcl side with `catch`.
|
|
66
|
+
|
|
67
|
+
## List operations
|
|
68
|
+
|
|
69
|
+
Convert between Ruby arrays and Tcl list strings:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# Ruby array → Tcl list string (properly quoted)
|
|
73
|
+
Teek.make_list("hello world", "foo", "bar baz")
|
|
74
|
+
# => "{hello world} foo {bar baz}"
|
|
75
|
+
|
|
76
|
+
# Tcl list string → Ruby array
|
|
77
|
+
Teek.split_list("{hello world} foo {bar baz}")
|
|
78
|
+
# => ["hello world", "foo", "bar baz"]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Also available as `app.make_list` and `app.split_list` on an interpreter instance.
|
|
82
|
+
|
|
83
|
+
## Boolean conversion
|
|
84
|
+
|
|
85
|
+
Convert between Tcl boolean strings and Ruby booleans:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# Tcl boolean string → Ruby bool
|
|
89
|
+
Teek.tcl_to_bool("yes") # => true
|
|
90
|
+
Teek.tcl_to_bool("0") # => false
|
|
91
|
+
|
|
92
|
+
# Ruby bool → Tcl boolean string
|
|
93
|
+
Teek.bool_to_tcl(true) # => "1"
|
|
94
|
+
Teek.bool_to_tcl(nil) # => "0"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`tcl_to_bool` recognizes all Tcl boolean forms: `true`/`false`, `yes`/`no`, `on`/`off`, `1`/`0`, and numeric values (case-insensitive).
|
|
98
|
+
|
|
99
|
+
## Tcl Packages
|
|
100
|
+
|
|
101
|
+
Load external Tcl packages (BWidget, tkimg, etc.):
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
app.require_package('BWidget')
|
|
105
|
+
app.require_package('BWidget', '1.9') # with version constraint
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
For packages in non-standard locations:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
app.add_package_path('/path/to/packages')
|
|
112
|
+
app.require_package('mypackage')
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Query what's available:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
app.package_names # => ["Tk", "BWidget", ...]
|
|
119
|
+
app.package_present?('Tk') # => true
|
|
120
|
+
app.package_versions('Tk') # => ["9.0.1"]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Debugger
|
|
124
|
+
|
|
125
|
+
Pass `debug: true` to open a debugger window alongside your app:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
app = Teek::App.new(debug: true)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Or set the `TEEK_DEBUG` environment variable to enable it without changing code.
|
|
132
|
+
|
|
133
|
+
The debugger provides three tabs:
|
|
134
|
+
|
|
135
|
+
- **Widgets** — live tree of all widgets with a detail panel showing configuration
|
|
136
|
+
- **Variables** — all global Tcl variables with search/filter, auto-refreshes every second
|
|
137
|
+
- **Watches** — right-click or double-click a variable to watch it; tracks last 50 values with timestamps
|
|
138
|
+
|
|
139
|
+
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`.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
require 'rake/testtask'
|
|
3
|
+
require 'rake/clean'
|
|
4
|
+
|
|
5
|
+
# Documentation tasks - all doc gems are in docs_site/Gemfile
|
|
6
|
+
namespace :docs do
|
|
7
|
+
desc "Install docs dependencies (docs_site/Gemfile)"
|
|
8
|
+
task :setup do
|
|
9
|
+
Dir.chdir('docs_site') do
|
|
10
|
+
Bundler.with_unbundled_env { sh 'bundle install' }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
task :yard_clean do
|
|
15
|
+
FileUtils.rm_rf('doc')
|
|
16
|
+
FileUtils.rm_rf('docs_site/_api')
|
|
17
|
+
FileUtils.rm_rf('docs_site/_site')
|
|
18
|
+
FileUtils.rm_rf('docs_site/.jekyll-cache')
|
|
19
|
+
FileUtils.rm_f('docs_site/assets/js/search-data.json')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "Generate YARD JSON (uses docs_site/Gemfile)"
|
|
23
|
+
task yard_json: :yard_clean do
|
|
24
|
+
Bundler.with_unbundled_env do
|
|
25
|
+
sh 'BUNDLE_GEMFILE=docs_site/Gemfile bundle exec yard doc'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "Generate API docs (YARD JSON -> HTML)"
|
|
30
|
+
task yard: :yard_json do
|
|
31
|
+
Bundler.with_unbundled_env do
|
|
32
|
+
sh 'BUNDLE_GEMFILE=docs_site/Gemfile bundle exec ruby docs_site/build_api_docs.rb'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "Bless recordings from recordings/ into docs_site/assets/recordings/"
|
|
37
|
+
task :bless_recordings do
|
|
38
|
+
require 'fileutils'
|
|
39
|
+
src = 'recordings'
|
|
40
|
+
dest = 'docs_site/assets/recordings'
|
|
41
|
+
FileUtils.mkdir_p(dest)
|
|
42
|
+
videos = Dir.glob("#{src}/*.{mp4,webm}")
|
|
43
|
+
if videos.empty?
|
|
44
|
+
puts "No recordings in #{src}/ to bless."
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
videos.each do |path|
|
|
48
|
+
FileUtils.cp(path, dest)
|
|
49
|
+
puts " #{File.basename(path)} -> #{dest}/"
|
|
50
|
+
end
|
|
51
|
+
puts "Blessed #{videos.size} recording(s)."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc "Generate recordings gallery page"
|
|
55
|
+
task :recordings do
|
|
56
|
+
sh 'ruby docs_site/build_recordings.rb'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
desc "Generate full docs site (YARD + Jekyll)"
|
|
60
|
+
task generate: [:yard, :recordings] do
|
|
61
|
+
Dir.chdir('docs_site') do
|
|
62
|
+
Bundler.with_unbundled_env { sh 'bundle exec jekyll build' }
|
|
63
|
+
end
|
|
64
|
+
puts "Docs generated in docs_site/_site/"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "Serve docs locally"
|
|
68
|
+
task serve: [:yard, :recordings] do
|
|
69
|
+
Dir.chdir('docs_site') do
|
|
70
|
+
Bundler.with_unbundled_env { sh 'bundle exec jekyll serve' }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Aliases for convenience
|
|
76
|
+
task doc: 'docs:yard'
|
|
77
|
+
task yard: 'docs:yard'
|
|
78
|
+
|
|
79
|
+
# Compiling on macOS with Homebrew:
|
|
80
|
+
#
|
|
81
|
+
# Tcl/Tk 9.0:
|
|
82
|
+
# rake clean && rake compile -- --with-tcltkversion=9.0 \
|
|
83
|
+
# --with-tcl-lib=$(brew --prefix tcl-tk)/lib \
|
|
84
|
+
# --with-tcl-include=$(brew --prefix tcl-tk)/include/tcl-tk \
|
|
85
|
+
# --with-tk-lib=$(brew --prefix tcl-tk)/lib \
|
|
86
|
+
# --with-tk-include=$(brew --prefix tcl-tk)/include/tcl-tk \
|
|
87
|
+
# --without-X11
|
|
88
|
+
#
|
|
89
|
+
# Tcl/Tk 8.6:
|
|
90
|
+
# rake clean && rake compile -- --with-tcltkversion=8.6 \
|
|
91
|
+
# --with-tcl-lib=$(brew --prefix tcl-tk@8)/lib \
|
|
92
|
+
# --with-tcl-include=$(brew --prefix tcl-tk@8)/include \
|
|
93
|
+
# --with-tk-lib=$(brew --prefix tcl-tk@8)/lib \
|
|
94
|
+
# --with-tk-include=$(brew --prefix tcl-tk@8)/include \
|
|
95
|
+
# --without-X11
|
|
96
|
+
|
|
97
|
+
# Clean up extconf cached config files
|
|
98
|
+
CLEAN.include('ext/teek/config_list')
|
|
99
|
+
CLOBBER.include('tmp', 'lib/*.bundle', 'lib/*.so', 'ext/**/*.o', 'ext/**/*.bundle', 'ext/**/*.bundle.dSYM')
|
|
100
|
+
|
|
101
|
+
# Clean coverage artifacts before test runs to prevent accumulation
|
|
102
|
+
CLEAN.include('coverage/.resultset.json', 'coverage/results')
|
|
103
|
+
|
|
104
|
+
# Conditionally load rake-compiler
|
|
105
|
+
if Gem::Specification.find_all_by_name('rake-compiler').any?
|
|
106
|
+
require 'rake/extensiontask'
|
|
107
|
+
Rake::ExtensionTask.new do |ext|
|
|
108
|
+
ext.name = 'tcltklib'
|
|
109
|
+
ext.ext_dir = 'ext/teek'
|
|
110
|
+
ext.lib_dir = 'lib'
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
desc "Clear stale coverage artifacts"
|
|
115
|
+
task :clean_coverage do
|
|
116
|
+
require 'fileutils'
|
|
117
|
+
FileUtils.rm_f('coverage/.resultset.json')
|
|
118
|
+
FileUtils.rm_rf('coverage/results')
|
|
119
|
+
FileUtils.mkdir_p('coverage/results')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
namespace :coverage do
|
|
123
|
+
desc "Collate coverage results from multiple test runs into a single report"
|
|
124
|
+
task :collate do
|
|
125
|
+
require 'simplecov'
|
|
126
|
+
require 'simplecov_json_formatter'
|
|
127
|
+
require_relative 'test/simplecov_config'
|
|
128
|
+
|
|
129
|
+
result_files = Dir['coverage/results/*/.resultset.json']
|
|
130
|
+
if result_files.empty?
|
|
131
|
+
puts "No coverage results found in coverage/results/"
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
puts "Collating coverage from: #{result_files.map { |f| File.dirname(f).split('/').last }.join(', ')}"
|
|
136
|
+
|
|
137
|
+
SimpleCov.collate(result_files) do
|
|
138
|
+
coverage_dir 'coverage'
|
|
139
|
+
formatter SimpleCov::Formatter::MultiFormatter.new([
|
|
140
|
+
SimpleCov::Formatter::HTMLFormatter,
|
|
141
|
+
SimpleCov::Formatter::JSONFormatter
|
|
142
|
+
])
|
|
143
|
+
SimpleCovConfig.apply_filters(self)
|
|
144
|
+
SimpleCovConfig.apply_groups(self)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
puts "Coverage report generated: coverage/index.html, coverage/coverage.json"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
desc "Full coverage pipeline: collate results"
|
|
151
|
+
task :full => :collate
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Rake::TestTask.new(:test) do |t|
|
|
155
|
+
t.libs << 'test'
|
|
156
|
+
t.test_files = FileList['test/**/test_*.rb']
|
|
157
|
+
t.verbose = true
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
task test: [:compile, :clean_coverage]
|
|
161
|
+
|
|
162
|
+
def detect_platform
|
|
163
|
+
case RUBY_PLATFORM
|
|
164
|
+
when /darwin/ then 'darwin'
|
|
165
|
+
when /linux/ then 'linux'
|
|
166
|
+
when /mingw|mswin/ then 'windows'
|
|
167
|
+
else 'unknown'
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
task :default => :compile
|
|
172
|
+
|
|
173
|
+
# Docker tasks for local testing and CI
|
|
174
|
+
namespace :docker do
|
|
175
|
+
DOCKERFILE = 'Dockerfile.ci-test'
|
|
176
|
+
DOCKER_LABEL = 'project=teek'
|
|
177
|
+
|
|
178
|
+
def docker_image_name(tcl_version, ruby_version = nil)
|
|
179
|
+
ruby_version ||= ruby_version_from_env
|
|
180
|
+
base = tcl_version == '8.6' ? 'teek-ci-test-8' : 'teek-ci-test-9'
|
|
181
|
+
ruby_version == '4.0' ? base : "#{base}-ruby#{ruby_version}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def tcl_version_from_env
|
|
185
|
+
version = ENV.fetch('TCL_VERSION', '9.0')
|
|
186
|
+
unless ['8.6', '9.0'].include?(version)
|
|
187
|
+
abort "Invalid TCL_VERSION='#{version}'. Must be '8.6' or '9.0'."
|
|
188
|
+
end
|
|
189
|
+
version
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def ruby_version_from_env
|
|
193
|
+
ENV.fetch('RUBY_VERSION', '4.0')
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
desc "Build Docker image (TCL_VERSION=9.0|8.6, RUBY_VERSION=3.4|4.0|...)"
|
|
197
|
+
task :build do
|
|
198
|
+
tcl_version = tcl_version_from_env
|
|
199
|
+
ruby_version = ruby_version_from_env
|
|
200
|
+
image_name = docker_image_name(tcl_version, ruby_version)
|
|
201
|
+
|
|
202
|
+
verbose = ENV['VERBOSE'] || ENV['V']
|
|
203
|
+
quiet = !verbose
|
|
204
|
+
if quiet
|
|
205
|
+
puts "Building Docker image for Ruby #{ruby_version}, Tcl #{tcl_version}... (VERBOSE=1 for details)"
|
|
206
|
+
else
|
|
207
|
+
puts "Building Docker image for Ruby #{ruby_version}, Tcl #{tcl_version}..."
|
|
208
|
+
end
|
|
209
|
+
cmd = "docker build -f #{DOCKERFILE}"
|
|
210
|
+
cmd += " -q" if quiet
|
|
211
|
+
cmd += " --label #{DOCKER_LABEL}"
|
|
212
|
+
cmd += " --build-arg RUBY_VERSION=#{ruby_version}"
|
|
213
|
+
cmd += " --build-arg TCL_VERSION=#{tcl_version}"
|
|
214
|
+
cmd += " -t #{image_name} ."
|
|
215
|
+
|
|
216
|
+
sh cmd, verbose: !quiet
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
desc "Run tests in Docker (TCL_VERSION=9.0|8.6, RUBY_VERSION=3.4|4.0|..., TEST=path/to/test.rb)"
|
|
220
|
+
task test: :build do
|
|
221
|
+
tcl_version = tcl_version_from_env
|
|
222
|
+
ruby_version = ruby_version_from_env
|
|
223
|
+
image_name = docker_image_name(tcl_version, ruby_version)
|
|
224
|
+
|
|
225
|
+
require 'fileutils'
|
|
226
|
+
FileUtils.mkdir_p('coverage')
|
|
227
|
+
|
|
228
|
+
puts "Running tests in Docker (Ruby #{ruby_version}, Tcl #{tcl_version})..."
|
|
229
|
+
cmd = "docker run --rm --init"
|
|
230
|
+
cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
|
|
231
|
+
cmd += " -e TCL_VERSION=#{tcl_version}"
|
|
232
|
+
cmd += " -e TEST='#{ENV['TEST']}'" if ENV['TEST']
|
|
233
|
+
cmd += " -e TESTOPTS='#{ENV['TESTOPTS']}'" if ENV['TESTOPTS']
|
|
234
|
+
if ENV['COVERAGE'] == '1'
|
|
235
|
+
cmd += " -e COVERAGE=1"
|
|
236
|
+
cmd += " -e COVERAGE_NAME=#{ENV['COVERAGE_NAME'] || 'main'}"
|
|
237
|
+
end
|
|
238
|
+
cmd += " #{image_name}"
|
|
239
|
+
|
|
240
|
+
sh cmd
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
desc "Run interactive shell in Docker (TCL_VERSION=9.0|8.6, RUBY_VERSION=3.4|4.0|...)"
|
|
244
|
+
task shell: :build do
|
|
245
|
+
tcl_version = tcl_version_from_env
|
|
246
|
+
ruby_version = ruby_version_from_env
|
|
247
|
+
image_name = docker_image_name(tcl_version, ruby_version)
|
|
248
|
+
|
|
249
|
+
cmd = "docker run --rm --init -it"
|
|
250
|
+
cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
|
|
251
|
+
cmd += " -e TCL_VERSION=#{tcl_version}"
|
|
252
|
+
cmd += " #{image_name} bash"
|
|
253
|
+
|
|
254
|
+
sh cmd
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
desc "Remove dangling Docker images from teek builds"
|
|
258
|
+
task :prune do
|
|
259
|
+
sh "docker image prune -f --filter label=#{DOCKER_LABEL}"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
Rake::Task['docker:test'].enhance { Rake::Task['docker:prune'].invoke }
|
|
263
|
+
|
|
264
|
+
# Scan sample files for # teek-record magic comment
|
|
265
|
+
# Format: # teek-record: title=My Demo, codec=vp9
|
|
266
|
+
def find_recordable_samples
|
|
267
|
+
Dir['sample/**/*.rb'].filter_map do |path|
|
|
268
|
+
first_lines = File.read(path, 500)
|
|
269
|
+
match = first_lines.match(/^#\s*teek-record(?::\s*(.+))?$/)
|
|
270
|
+
next unless match
|
|
271
|
+
|
|
272
|
+
options = {}
|
|
273
|
+
if match[1]
|
|
274
|
+
match[1].split(',').each do |pair|
|
|
275
|
+
key, value = pair.strip.split('=', 2)
|
|
276
|
+
options[key.strip] = value&.strip if key
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
options['sample'] = path
|
|
280
|
+
options
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
desc "Record demos in Docker (TCL_VERSION=9.0|8.6, DEMO=sample/foo.rb)"
|
|
285
|
+
task record_demos: :build do
|
|
286
|
+
require 'fileutils'
|
|
287
|
+
FileUtils.mkdir_p('recordings')
|
|
288
|
+
|
|
289
|
+
demos = if ENV['DEMO']
|
|
290
|
+
find_recordable_samples.select { |d| d['sample'] == ENV['DEMO'] }
|
|
291
|
+
else
|
|
292
|
+
find_recordable_samples
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if demos.empty?
|
|
296
|
+
puts "No recordable samples found. Add '# teek-record' comment to samples."
|
|
297
|
+
next
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
demos.each do |demo|
|
|
301
|
+
sample = demo['sample']
|
|
302
|
+
codec = ENV['CODEC'] || demo['codec'] || 'x264'
|
|
303
|
+
name = demo['name']
|
|
304
|
+
|
|
305
|
+
puts
|
|
306
|
+
puts "Recording #{sample} (#{codec})..."
|
|
307
|
+
env = "CODEC=#{codec}"
|
|
308
|
+
env += " NAME=#{name}" if name
|
|
309
|
+
sh "#{env} ./scripts/docker-record.sh #{sample}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
puts "Done! Recordings in: recordings/"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
Rake::Task['docker:record_demos'].enhance { Rake::Task['docker:prune'].invoke }
|
|
316
|
+
end
|
data/ext/teek/extconf.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mkmf'
|
|
4
|
+
|
|
5
|
+
# Always use stubs - no option to disable
|
|
6
|
+
$CFLAGS << " -DUSE_TCL_STUBS -DUSE_TK_STUBS"
|
|
7
|
+
|
|
8
|
+
def find_tcltk
|
|
9
|
+
# Try pkg-config first
|
|
10
|
+
tcl_found = pkg_config('tcl') || pkg_config('tcl9.0') || pkg_config('tcl8.6')
|
|
11
|
+
tk_found = pkg_config('tk') || pkg_config('tk9.0') || pkg_config('tk8.6')
|
|
12
|
+
|
|
13
|
+
unless tcl_found && tk_found
|
|
14
|
+
# Manual search paths
|
|
15
|
+
tcl_dirs = [
|
|
16
|
+
'/opt/homebrew/opt/tcl-tk',
|
|
17
|
+
'/usr/local/opt/tcl-tk',
|
|
18
|
+
'/usr/local',
|
|
19
|
+
'/usr'
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
tcl_dirs.each do |dir|
|
|
23
|
+
inc = "#{dir}/include"
|
|
24
|
+
lib = "#{dir}/lib"
|
|
25
|
+
# Check for tcl-tk subdirectory (Homebrew layout)
|
|
26
|
+
if File.exist?("#{inc}/tcl-tk/tcl.h")
|
|
27
|
+
inc = "#{inc}/tcl-tk"
|
|
28
|
+
end
|
|
29
|
+
if File.exist?("#{inc}/tcl.h") && File.exist?("#{inc}/tk.h")
|
|
30
|
+
$INCFLAGS << " -I#{inc}"
|
|
31
|
+
$LDFLAGS << " -L#{lib}"
|
|
32
|
+
break
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check for required headers
|
|
38
|
+
have_header('tcl.h') or abort "tcl.h not found"
|
|
39
|
+
have_header('tk.h') or abort "tk.h not found"
|
|
40
|
+
|
|
41
|
+
# Link against STUB libraries, not main libraries
|
|
42
|
+
# Try versioned stub names first, then unversioned
|
|
43
|
+
tcl_stub = have_library('tclstub9.0') ||
|
|
44
|
+
have_library('tclstub8.6') ||
|
|
45
|
+
have_library('tclstub')
|
|
46
|
+
|
|
47
|
+
tk_stub = have_library('tkstub9.0') ||
|
|
48
|
+
have_library('tkstub8.6') ||
|
|
49
|
+
have_library('tkstub')
|
|
50
|
+
|
|
51
|
+
# MSYS2/MinGW uses names without dots (tclstub86 instead of tclstub8.6)
|
|
52
|
+
if RbConfig::CONFIG['host_os'] =~ /mingw|mswin/
|
|
53
|
+
tcl_stub ||= have_library('tclstub90') || have_library('tclstub86')
|
|
54
|
+
tk_stub ||= have_library('tkstub90') || have_library('tkstub86')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# If stub libraries not found by simple name, try via pkg-config
|
|
58
|
+
unless tcl_stub
|
|
59
|
+
# pkg-config may have added them already via --libs
|
|
60
|
+
# Check if we can find the stubs table
|
|
61
|
+
if try_link(<<~CODE)
|
|
62
|
+
#define USE_TCL_STUBS
|
|
63
|
+
#include <tcl.h>
|
|
64
|
+
int main() { return 0; }
|
|
65
|
+
CODE
|
|
66
|
+
tcl_stub = true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
abort "Tcl stub library not found" unless tcl_stub
|
|
71
|
+
abort "Tk stub library not found" unless tk_stub
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
find_tcltk
|
|
75
|
+
|
|
76
|
+
# Source files for the extension
|
|
77
|
+
$srcs = ['tcltkbridge.c', 'tkphoto.c', 'tkfont.c', 'tkwin.c']
|
|
78
|
+
|
|
79
|
+
create_makefile('tcltklib')
|
data/ext/teek/stubs.h
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#include <tcl.h>
|
|
2
|
+
|
|
3
|
+
extern int ruby_open_tcl_dll(char *);
|
|
4
|
+
extern int ruby_open_tk_dll(void);
|
|
5
|
+
extern int ruby_open_tcltk_dll(char *);
|
|
6
|
+
extern int tcl_stubs_init_p(void);
|
|
7
|
+
extern int tk_stubs_init_p(void);
|
|
8
|
+
extern Tcl_Interp *ruby_tcl_create_ip_and_stubs_init(int*);
|
|
9
|
+
extern int ruby_tcl_stubs_init(void);
|
|
10
|
+
extern int ruby_tk_stubs_init(Tcl_Interp*);
|
|
11
|
+
extern int ruby_tk_stubs_safeinit(Tcl_Interp*);
|
|
12
|
+
extern int ruby_tcltk_stubs(void);
|
|
13
|
+
|
|
14
|
+
/* no error */
|
|
15
|
+
#define TCLTK_STUBS_OK (0)
|
|
16
|
+
|
|
17
|
+
/* return value of ruby_open_tcl_dll() */
|
|
18
|
+
#define NO_TCL_DLL (1)
|
|
19
|
+
#define NO_FindExecutable (2)
|
|
20
|
+
|
|
21
|
+
/* return value of ruby_open_tk_dll() */
|
|
22
|
+
#define NO_TK_DLL (-1)
|
|
23
|
+
|
|
24
|
+
/* status value of ruby_tcl_create_ip_and_stubs_init(st) */
|
|
25
|
+
#define NO_CreateInterp (3)
|
|
26
|
+
#define NO_DeleteInterp (4)
|
|
27
|
+
#define FAIL_CreateInterp (5)
|
|
28
|
+
#define FAIL_Tcl_InitStubs (6)
|
|
29
|
+
|
|
30
|
+
/* return value of ruby_tk_stubs_init() */
|
|
31
|
+
#define NO_Tk_Init (7)
|
|
32
|
+
#define FAIL_Tk_Init (8)
|
|
33
|
+
#define FAIL_Tk_InitStubs (9)
|