teek 0.1.3 → 0.1.4

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -0
  3. data/Rakefile +120 -22
  4. data/ext/teek/extconf.rb +19 -1
  5. data/ext/teek/tcltkbridge.c +38 -2
  6. data/ext/teek/tcltkbridge.h +3 -0
  7. data/ext/teek/tkdrop.c +66 -0
  8. data/ext/teek/tkdrop.h +26 -0
  9. data/ext/teek/tkdrop_macos.m +141 -0
  10. data/ext/teek/tkdrop_win.c +232 -0
  11. data/ext/teek/tkdrop_x11.c +337 -0
  12. data/ext/teek/tkwin.c +42 -0
  13. data/lib/teek/platform.rb +29 -0
  14. data/lib/teek/version.rb +1 -1
  15. data/lib/teek.rb +49 -3
  16. data/teek.gemspec +3 -2
  17. metadata +7 -53
  18. data/sample/calculator.rb +0 -255
  19. data/sample/debug_demo.rb +0 -43
  20. data/sample/gamepad_viewer/assets/controller.png +0 -0
  21. data/sample/gamepad_viewer/gamepad_viewer.rb +0 -554
  22. data/sample/goldberg.rb +0 -1803
  23. data/sample/goldberg_helpers.rb +0 -170
  24. data/sample/optcarrot/thwaite.nes +0 -0
  25. data/sample/optcarrot/vendor/optcarrot/apu.rb +0 -856
  26. data/sample/optcarrot/vendor/optcarrot/config.rb +0 -257
  27. data/sample/optcarrot/vendor/optcarrot/cpu.rb +0 -1162
  28. data/sample/optcarrot/vendor/optcarrot/driver.rb +0 -144
  29. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +0 -14
  30. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +0 -105
  31. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +0 -153
  32. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +0 -14
  33. data/sample/optcarrot/vendor/optcarrot/nes.rb +0 -105
  34. data/sample/optcarrot/vendor/optcarrot/opt.rb +0 -168
  35. data/sample/optcarrot/vendor/optcarrot/pad.rb +0 -92
  36. data/sample/optcarrot/vendor/optcarrot/palette.rb +0 -65
  37. data/sample/optcarrot/vendor/optcarrot/ppu.rb +0 -1468
  38. data/sample/optcarrot/vendor/optcarrot/rom.rb +0 -143
  39. data/sample/optcarrot/vendor/optcarrot.rb +0 -14
  40. data/sample/optcarrot.rb +0 -354
  41. data/sample/paint/assets/bucket.png +0 -0
  42. data/sample/paint/assets/cursor.png +0 -0
  43. data/sample/paint/assets/eraser.png +0 -0
  44. data/sample/paint/assets/pencil.png +0 -0
  45. data/sample/paint/assets/spray.png +0 -0
  46. data/sample/paint/layer.rb +0 -255
  47. data/sample/paint/layer_manager.rb +0 -179
  48. data/sample/paint/paint_demo.rb +0 -837
  49. data/sample/paint/sparse_pixel_buffer.rb +0 -202
  50. data/sample/sdl2_demo.rb +0 -318
  51. data/sample/threading_demo.rb +0 -494
  52. data/sample/yam/assets/MINESWEEPER_0.png +0 -0
  53. data/sample/yam/assets/MINESWEEPER_1.png +0 -0
  54. data/sample/yam/assets/MINESWEEPER_2.png +0 -0
  55. data/sample/yam/assets/MINESWEEPER_3.png +0 -0
  56. data/sample/yam/assets/MINESWEEPER_4.png +0 -0
  57. data/sample/yam/assets/MINESWEEPER_5.png +0 -0
  58. data/sample/yam/assets/MINESWEEPER_6.png +0 -0
  59. data/sample/yam/assets/MINESWEEPER_7.png +0 -0
  60. data/sample/yam/assets/MINESWEEPER_8.png +0 -0
  61. data/sample/yam/assets/MINESWEEPER_F.png +0 -0
  62. data/sample/yam/assets/MINESWEEPER_M.png +0 -0
  63. data/sample/yam/assets/MINESWEEPER_X.png +0 -0
  64. data/sample/yam/assets/click.wav +0 -0
  65. data/sample/yam/assets/explosion.wav +0 -0
  66. data/sample/yam/assets/flag.wav +0 -0
  67. data/sample/yam/assets/music.mp3 +0 -0
  68. data/sample/yam/assets/sweep.wav +0 -0
  69. data/sample/yam/yam.rb +0 -587
data/lib/teek.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'tcltklib'
4
4
  require_relative 'teek/version'
5
+ require_relative 'teek/platform'
5
6
  require_relative 'teek/ractor_support'
6
7
  require_relative 'teek/widget'
7
8
  require_relative 'teek/photo'
@@ -242,8 +243,25 @@ module Teek
242
243
  # @return [String] the Tcl result
243
244
  def command(cmd, *args, **kwargs)
244
245
  parts = [cmd.to_s]
245
- args.each do |arg|
246
- parts << tcl_value(arg)
246
+ i = 0
247
+ while i < args.length
248
+ arg = args[i]
249
+ if arg.is_a?(Proc)
250
+ id = @interp.register_callback(arg)
251
+ subs = []
252
+ while i + 1 < args.length && args[i + 1].is_a?(String) && args[i + 1].start_with?('%')
253
+ subs << args[i + 1]
254
+ i += 1
255
+ end
256
+ parts << if subs.empty?
257
+ "{ruby_callback #{id}}"
258
+ else
259
+ "{ruby_callback #{id} #{subs.join(' ')}}"
260
+ end
261
+ else
262
+ parts << tcl_value(arg)
263
+ end
264
+ i += 1
247
265
  end
248
266
  kwargs.each do |key, value|
249
267
  parts << "-#{key}"
@@ -592,6 +610,7 @@ module Teek
592
610
  button: '%b', # mouse button number
593
611
  mouse_wheel: '%D', # mousewheel delta
594
612
  type: '%T', # event type
613
+ data: '%d', # virtual event data (Tk 8.6+)
595
614
  }.freeze
596
615
 
597
616
  def bind(widget, event, *subs, &block)
@@ -613,6 +632,22 @@ module Teek
613
632
  @interp.tcl_eval("bind #{widget} #{event_str} {}")
614
633
  end
615
634
 
635
+ # Register a widget as a file drop target.
636
+ # After registration, dropping files onto the widget generates a single
637
+ # +<<DropFile>>+ virtual event with all file paths as a Tcl list in the
638
+ # event data. Use {#split_list} to convert to a Ruby array.
639
+ # @param widget [String] Tk widget path (e.g., ".", ".frame")
640
+ # @return [void]
641
+ # @example
642
+ # app.register_drop_target('.')
643
+ # app.bind('.', '<<DropFile>>', :data) do |data|
644
+ # paths = app.split_list(data)
645
+ # puts "Dropped #{paths.length} file(s): #{paths.inspect}"
646
+ # end
647
+ def register_drop_target(widget)
648
+ @interp.register_drop_target(widget.to_s)
649
+ end
650
+
616
651
  # Get the macOS window appearance. No-op (returns +nil+) on non-macOS.
617
652
  # @example
618
653
  # app.appearance # => "aqua", "darkaqua", or "auto"
@@ -760,7 +795,18 @@ module Teek
760
795
  when Array
761
796
  "{#{value.map { |v| tcl_value(v) }.join(' ')}}"
762
797
  else
763
- "{#{value}}"
798
+ tcl_quote_string(value.to_s)
799
+ end
800
+ end
801
+
802
+ # Brace-quote a string for Tcl, falling back to double-quote quoting
803
+ # when the string ends with a backslash (Tcl treats \} as an escaped
804
+ # brace, preventing the closing brace from terminating the group).
805
+ def tcl_quote_string(s)
806
+ if s.end_with?('\\')
807
+ '"' + s.gsub(/[\\\[\]$"]/) { |c| "\\#{c}" } + '"'
808
+ else
809
+ "{#{s}}"
764
810
  end
765
811
  end
766
812
  end
data/teek.gemspec CHANGED
@@ -11,8 +11,9 @@ Gem::Specification.new do |spec|
11
11
  spec.homepage = "https://github.com/jamescook/teek"
12
12
  spec.licenses = ["MIT"]
13
13
 
14
- spec.files = Dir.glob("{lib,ext,exe,sample}/**/*").select { |f|
15
- File.file?(f) && f !~ /\.(bundle|so|o|log)$/
14
+ spec.files = Dir.glob("{lib,ext,exe}/**/*").select { |f|
15
+ File.file?(f) && f !~ /\.(bundle|so|o|log)$/ &&
16
+ !f.include?('.dSYM/') && File.basename(f) != 'Makefile'
16
17
  } + %w[Rakefile LICENSE README.md teek.gemspec Gemfile]
17
18
  spec.bindir = "exe"
18
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teek
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Cook
@@ -124,6 +124,11 @@ files:
124
124
  - ext/teek/tcl9compat.h
125
125
  - ext/teek/tcltkbridge.c
126
126
  - ext/teek/tcltkbridge.h
127
+ - ext/teek/tkdrop.c
128
+ - ext/teek/tkdrop.h
129
+ - ext/teek/tkdrop_macos.m
130
+ - ext/teek/tkdrop_win.c
131
+ - ext/teek/tkdrop_x11.c
127
132
  - ext/teek/tkeventsource.c
128
133
  - ext/teek/tkfont.c
129
134
  - ext/teek/tkphoto.c
@@ -136,61 +141,10 @@ files:
136
141
  - lib/teek/demo_support.rb
137
142
  - lib/teek/method_coverage_service.rb
138
143
  - lib/teek/photo.rb
144
+ - lib/teek/platform.rb
139
145
  - lib/teek/ractor_support.rb
140
146
  - lib/teek/version.rb
141
147
  - lib/teek/widget.rb
142
- - sample/calculator.rb
143
- - sample/debug_demo.rb
144
- - sample/gamepad_viewer/assets/controller.png
145
- - sample/gamepad_viewer/gamepad_viewer.rb
146
- - sample/goldberg.rb
147
- - sample/goldberg_helpers.rb
148
- - sample/optcarrot.rb
149
- - sample/optcarrot/thwaite.nes
150
- - sample/optcarrot/vendor/optcarrot.rb
151
- - sample/optcarrot/vendor/optcarrot/apu.rb
152
- - sample/optcarrot/vendor/optcarrot/config.rb
153
- - sample/optcarrot/vendor/optcarrot/cpu.rb
154
- - sample/optcarrot/vendor/optcarrot/driver.rb
155
- - sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb
156
- - sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb
157
- - sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb
158
- - sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb
159
- - sample/optcarrot/vendor/optcarrot/nes.rb
160
- - sample/optcarrot/vendor/optcarrot/opt.rb
161
- - sample/optcarrot/vendor/optcarrot/pad.rb
162
- - sample/optcarrot/vendor/optcarrot/palette.rb
163
- - sample/optcarrot/vendor/optcarrot/ppu.rb
164
- - sample/optcarrot/vendor/optcarrot/rom.rb
165
- - sample/paint/assets/bucket.png
166
- - sample/paint/assets/cursor.png
167
- - sample/paint/assets/eraser.png
168
- - sample/paint/assets/pencil.png
169
- - sample/paint/assets/spray.png
170
- - sample/paint/layer.rb
171
- - sample/paint/layer_manager.rb
172
- - sample/paint/paint_demo.rb
173
- - sample/paint/sparse_pixel_buffer.rb
174
- - sample/sdl2_demo.rb
175
- - sample/threading_demo.rb
176
- - sample/yam/assets/MINESWEEPER_0.png
177
- - sample/yam/assets/MINESWEEPER_1.png
178
- - sample/yam/assets/MINESWEEPER_2.png
179
- - sample/yam/assets/MINESWEEPER_3.png
180
- - sample/yam/assets/MINESWEEPER_4.png
181
- - sample/yam/assets/MINESWEEPER_5.png
182
- - sample/yam/assets/MINESWEEPER_6.png
183
- - sample/yam/assets/MINESWEEPER_7.png
184
- - sample/yam/assets/MINESWEEPER_8.png
185
- - sample/yam/assets/MINESWEEPER_F.png
186
- - sample/yam/assets/MINESWEEPER_M.png
187
- - sample/yam/assets/MINESWEEPER_X.png
188
- - sample/yam/assets/click.wav
189
- - sample/yam/assets/explosion.wav
190
- - sample/yam/assets/flag.wav
191
- - sample/yam/assets/music.mp3
192
- - sample/yam/assets/sweep.wav
193
- - sample/yam/yam.rb
194
148
  - teek.gemspec
195
149
  homepage: https://github.com/jamescook/teek
196
150
  licenses:
data/sample/calculator.rb DELETED
@@ -1,255 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- # teek-record: title=Calculator
4
-
5
- # Calculator - A simple desktop calculator
6
- #
7
- # Run: ruby -Ilib sample/calculator.rb
8
-
9
- require 'teek'
10
-
11
- class Calculator
12
- attr_reader :app
13
-
14
- def initialize
15
- @app = Teek::App.new
16
- @display_value = '0'
17
- @pending_op = nil
18
- @accumulator = nil
19
- @reset_on_next = false
20
-
21
- build_ui
22
- end
23
-
24
- def build_ui
25
- @app.show
26
- @app.set_window_title('Calculator')
27
- @app.set_window_resizable(false, false)
28
-
29
- # Button style — use a larger font since macOS aqua theme
30
- # ignores vertical stretch; font size drives button height
31
- @app.tcl_eval('ttk::style configure Calc.TButton -font {{TkDefaultFont} 18}')
32
-
33
- # Display
34
- @app.set_variable('::display', '0')
35
- @display = @app.create_widget('ttk::entry', textvariable: '::display',
36
- justify: :right, state: :readonly, font: '{TkDefaultFont} 24')
37
- @display.grid(row: 0, column: 0, columnspan: 4,
38
- sticky: :ew, padx: 4, pady: 4, ipady: 8)
39
-
40
- build_buttons
41
- end
42
-
43
- def build_buttons
44
- # Row 1: C, +/-, %, /
45
- button('C', 1, 0, style: :accent) { clear }
46
- button('+/-', 1, 1, style: :accent) { negate }
47
- button('%', 1, 2, style: :accent) { percent }
48
- button('/', 1, 3, style: :op) { set_op(:/) }
49
-
50
- # Row 2: 7, 8, 9, *
51
- button('7', 2, 0) { digit('7') }
52
- button('8', 2, 1) { digit('8') }
53
- button('9', 2, 2) { digit('9') }
54
- button('*', 2, 3, style: :op) { set_op(:*) }
55
-
56
- # Row 3: 4, 5, 6, -
57
- button('4', 3, 0) { digit('4') }
58
- button('5', 3, 1) { digit('5') }
59
- button('6', 3, 2) { digit('6') }
60
- button('-', 3, 3, style: :op) { set_op(:-) }
61
-
62
- # Row 4: 1, 2, 3, +
63
- button('1', 4, 0) { digit('1') }
64
- button('2', 4, 1) { digit('2') }
65
- button('3', 4, 2) { digit('3') }
66
- button('+', 4, 3, style: :op) { set_op(:+) }
67
-
68
- # Row 5: 0 (wide), ., =
69
- button('0', 5, 0, colspan: 2) { digit('0') }
70
- button('.', 5, 2) { decimal }
71
- button('=', 5, 3, style: :op) { equals }
72
-
73
- # Make columns equal width
74
- 4.times { |c| @app.command(:grid, 'columnconfigure', '.', c, weight: 1, minsize: 60) }
75
- end
76
-
77
- # --- UI helpers ---
78
-
79
- # Click a button by its label (for demo/testing).
80
- # In recording mode, shows the pressed visual state briefly before invoking.
81
- def click(label, recording: false)
82
- widget = @buttons[label]
83
- return unless widget
84
- if recording
85
- widget.command('state', 'pressed')
86
- @app.after(80) {
87
- widget.command('state', '!pressed')
88
- widget.command(:invoke)
89
- }
90
- else
91
- widget.command(:invoke)
92
- end
93
- end
94
-
95
- def button(text, row, col, style: :num, colspan: 1, &action)
96
- @buttons ||= {}
97
- widget = @app.create_widget('ttk::button', text: text, style: 'Calc.TButton',
98
- command: proc { |*| action.call })
99
- @buttons[text] = widget
100
- widget.grid(row: row, column: col, columnspan: colspan,
101
- sticky: :nsew, padx: 2, pady: 2)
102
- end
103
-
104
- def update_display
105
- @app.set_variable('::display', @display_value)
106
- end
107
-
108
- # --- Calculator logic ---
109
-
110
- def digit(d)
111
- if @reset_on_next
112
- @display_value = '0'
113
- @reset_on_next = false
114
- end
115
- if @display_value == '0' && d != '0'
116
- @display_value = d
117
- elsif @display_value != '0'
118
- @display_value += d
119
- end
120
- update_display
121
- end
122
-
123
- def decimal
124
- @display_value = '0' if @reset_on_next
125
- @reset_on_next = false
126
- unless @display_value.include?('.')
127
- @display_value += '.'
128
- end
129
- update_display
130
- end
131
-
132
- def clear
133
- @display_value = '0'
134
- @pending_op = nil
135
- @accumulator = nil
136
- @reset_on_next = false
137
- update_display
138
- end
139
-
140
- def negate
141
- if @display_value.start_with?('-')
142
- @display_value = @display_value[1..]
143
- elsif @display_value != '0'
144
- @display_value = "-#{@display_value}"
145
- end
146
- update_display
147
- end
148
-
149
- def percent
150
- @display_value = (current_value / 100.0).to_s
151
- @reset_on_next = true
152
- update_display
153
- end
154
-
155
- def set_op(op)
156
- evaluate if @pending_op && !@reset_on_next
157
- @accumulator = current_value
158
- @pending_op = op
159
- @reset_on_next = true
160
- end
161
-
162
- def equals
163
- evaluate
164
- @pending_op = nil
165
- end
166
-
167
- def run
168
- @app.mainloop
169
- end
170
-
171
- private
172
-
173
- def current_value
174
- @display_value.to_f
175
- end
176
-
177
- def evaluate
178
- return unless @pending_op && @accumulator
179
-
180
- b = current_value
181
- result = case @pending_op
182
- when :+ then @accumulator + b
183
- when :- then @accumulator - b
184
- when :* then @accumulator * b
185
- when :/ then b.zero? ? Float::NAN : @accumulator / b
186
- end
187
-
188
- @accumulator = result
189
- @display_value = format_result(result)
190
- @reset_on_next = true
191
- update_display
192
- end
193
-
194
- def format_result(val)
195
- return 'Error' if val.nil? || val.nan? || val.infinite?
196
- val == val.to_i ? val.to_i.to_s : val.to_s
197
- end
198
- end
199
-
200
- calc = Calculator.new
201
-
202
- # Automated demo support (testing and recording)
203
- require_relative '../lib/teek/demo_support'
204
- TeekDemo.app = calc.app
205
-
206
- if TeekDemo.recording?
207
- calc.app.set_window_geometry('+0+0')
208
- calc.app.tcl_eval('. configure -cursor none')
209
- TeekDemo.signal_recording_ready
210
- end
211
-
212
- if TeekDemo.active?
213
- TeekDemo.after_idle {
214
- d = TeekDemo.method(:delay)
215
- app = calc.app
216
- rec = TeekDemo.recording?
217
-
218
- # Exercises all four operations, landing on 42.
219
- # 8 * 9 = 72, / 3 = 24, + 25 = 49, - 7 = 42
220
- steps = [
221
- -> { calc.click('8', recording: rec) },
222
- -> { calc.click('*', recording: rec) },
223
- -> { calc.click('9', recording: rec) },
224
- -> { calc.click('=', recording: rec) }, # 72
225
- nil,
226
- -> { calc.click('/', recording: rec) },
227
- -> { calc.click('3', recording: rec) },
228
- -> { calc.click('=', recording: rec) }, # 24
229
- nil,
230
- -> { calc.click('+', recording: rec) },
231
- -> { calc.click('2', recording: rec) },
232
- -> { calc.click('5', recording: rec) },
233
- -> { calc.click('=', recording: rec) }, # 49
234
- nil,
235
- -> { calc.click('-', recording: rec) },
236
- -> { calc.click('7', recording: rec) },
237
- -> { calc.click('=', recording: rec) }, # 42
238
- nil, nil,
239
- -> { TeekDemo.finish },
240
- ]
241
-
242
- run_step = nil
243
- i = 0
244
- run_step = proc {
245
- steps[i]&.call
246
- i += 1
247
- if i < steps.length
248
- app.after(d.call(test: 1, record: 250)) { run_step.call }
249
- end
250
- }
251
- run_step.call
252
- }
253
- end
254
-
255
- calc.run
data/sample/debug_demo.rb DELETED
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Demo of the Teek debugger window.
4
- # Run: ruby -Ilib sample/debug_demo.rb
5
-
6
- require 'teek'
7
-
8
- app = Teek::App.new(debug: true)
9
-
10
- # Show the main app window
11
- app.show
12
- app.set_window_title('Debug Demo App')
13
- app.set_window_geometry('300x200')
14
-
15
- # Create some widgets
16
- frame = app.create_widget('ttk::frame')
17
- app.command(:pack, frame, fill: :both, expand: 1, padx: 10, pady: 10)
18
-
19
- lbl = app.create_widget('ttk::label', parent: frame, text: 'Hello from the app')
20
- app.command(:pack, lbl, pady: 5)
21
-
22
- ent = app.create_widget('ttk::entry', parent: frame)
23
- app.command(:pack, ent, pady: 5)
24
-
25
- # Button that creates more widgets dynamically
26
- dynamic_widgets = []
27
- add_btn = app.create_widget('ttk::button', parent: frame, text: 'Add Widget',
28
- command: proc { |*|
29
- btn = app.create_widget('ttk::button', parent: frame, text: "Button #{dynamic_widgets.size + 1}")
30
- app.command(:pack, btn, pady: 2)
31
- dynamic_widgets << btn
32
- })
33
- app.command(:pack, add_btn, pady: 5)
34
-
35
- # Button to destroy last widget
36
- rm_btn = app.create_widget('ttk::button', parent: frame, text: 'Remove Widget',
37
- command: proc { |*|
38
- widget = dynamic_widgets.pop
39
- widget&.destroy
40
- })
41
- app.command(:pack, rm_btn, pady: 5)
42
-
43
- app.mainloop