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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +139 -0
  5. data/Rakefile +316 -0
  6. data/ext/teek/extconf.rb +79 -0
  7. data/ext/teek/stubs.h +33 -0
  8. data/ext/teek/tcl9compat.h +211 -0
  9. data/ext/teek/tcltkbridge.c +1597 -0
  10. data/ext/teek/tcltkbridge.h +42 -0
  11. data/ext/teek/tkfont.c +218 -0
  12. data/ext/teek/tkphoto.c +477 -0
  13. data/ext/teek/tkwin.c +144 -0
  14. data/lib/teek/background_none.rb +158 -0
  15. data/lib/teek/background_ractor4x.rb +410 -0
  16. data/lib/teek/background_thread.rb +272 -0
  17. data/lib/teek/debugger.rb +742 -0
  18. data/lib/teek/demo_support.rb +150 -0
  19. data/lib/teek/ractor_support.rb +246 -0
  20. data/lib/teek/version.rb +5 -0
  21. data/lib/teek.rb +540 -0
  22. data/sample/calculator.rb +260 -0
  23. data/sample/debug_demo.rb +45 -0
  24. data/sample/goldberg.rb +1803 -0
  25. data/sample/goldberg_helpers.rb +170 -0
  26. data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
  27. data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
  28. data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
  29. data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
  30. data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
  31. data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
  32. data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
  33. data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
  34. data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
  35. data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
  36. data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
  37. data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
  38. data/sample/minesweeper/minesweeper.rb +452 -0
  39. data/sample/threading_demo.rb +499 -0
  40. data/teek.gemspec +32 -0
  41. 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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in teek.gemspec
4
+ gemspec
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
@@ -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)