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
@@ -0,0 +1,260 @@
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.command(:wm, 'title', '.', 'Calculator')
27
+ @app.command(:wm, 'resizable', '.', 0, 0)
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.command(:set, '::display', '0')
35
+ @app.command('ttk::entry', '.display',
36
+ textvariable: '::display',
37
+ justify: :right,
38
+ state: :readonly,
39
+ font: '{TkDefaultFont} 24')
40
+ @app.command(:grid, '.display', row: 0, column: 0, columnspan: 4,
41
+ sticky: :ew, padx: 4, pady: 4, ipady: 8)
42
+
43
+ build_buttons
44
+ end
45
+
46
+ def build_buttons
47
+ # Row 1: C, +/-, %, /
48
+ button('C', 1, 0, style: :accent) { clear }
49
+ button('+/-', 1, 1, style: :accent) { negate }
50
+ button('%', 1, 2, style: :accent) { percent }
51
+ button('/', 1, 3, style: :op) { set_op(:/) }
52
+
53
+ # Row 2: 7, 8, 9, *
54
+ button('7', 2, 0) { digit('7') }
55
+ button('8', 2, 1) { digit('8') }
56
+ button('9', 2, 2) { digit('9') }
57
+ button('*', 2, 3, style: :op) { set_op(:*) }
58
+
59
+ # Row 3: 4, 5, 6, -
60
+ button('4', 3, 0) { digit('4') }
61
+ button('5', 3, 1) { digit('5') }
62
+ button('6', 3, 2) { digit('6') }
63
+ button('-', 3, 3, style: :op) { set_op(:-) }
64
+
65
+ # Row 4: 1, 2, 3, +
66
+ button('1', 4, 0) { digit('1') }
67
+ button('2', 4, 1) { digit('2') }
68
+ button('3', 4, 2) { digit('3') }
69
+ button('+', 4, 3, style: :op) { set_op(:+) }
70
+
71
+ # Row 5: 0 (wide), ., =
72
+ button('0', 5, 0, colspan: 2) { digit('0') }
73
+ button('.', 5, 2) { decimal }
74
+ button('=', 5, 3, style: :op) { equals }
75
+
76
+ # Make columns equal width
77
+ 4.times { |c| @app.command(:grid, 'columnconfigure', '.', c, weight: 1, minsize: 60) }
78
+ end
79
+
80
+ # --- UI helpers ---
81
+
82
+ # Click a button by its label (for demo/testing).
83
+ # In recording mode, shows the pressed visual state briefly before invoking.
84
+ def click(label, recording: false)
85
+ path = @buttons[label]
86
+ return unless path
87
+ if recording
88
+ @app.command(path, 'state', 'pressed')
89
+ @app.after(80) {
90
+ @app.command(path, 'state', '!pressed')
91
+ @app.command(path, 'invoke')
92
+ }
93
+ else
94
+ @app.command(path, 'invoke')
95
+ end
96
+ end
97
+
98
+ def button(text, row, col, style: :num, colspan: 1, &action)
99
+ @btn_id = (@btn_id || 0) + 1
100
+ @buttons ||= {}
101
+ path = ".btn_#{@btn_id}"
102
+ @buttons[text] = path
103
+ @app.command('ttk::button', path, text: text, style: 'Calc.TButton',
104
+ command: proc { |*| action.call })
105
+ @app.command(:grid, path, row: row, column: col, columnspan: colspan,
106
+ sticky: :nsew, padx: 2, pady: 2)
107
+ end
108
+
109
+ def update_display
110
+ @app.command(:set, '::display', @display_value)
111
+ end
112
+
113
+ # --- Calculator logic ---
114
+
115
+ def digit(d)
116
+ if @reset_on_next
117
+ @display_value = '0'
118
+ @reset_on_next = false
119
+ end
120
+ if @display_value == '0' && d != '0'
121
+ @display_value = d
122
+ elsif @display_value != '0'
123
+ @display_value += d
124
+ end
125
+ update_display
126
+ end
127
+
128
+ def decimal
129
+ @display_value = '0' if @reset_on_next
130
+ @reset_on_next = false
131
+ unless @display_value.include?('.')
132
+ @display_value += '.'
133
+ end
134
+ update_display
135
+ end
136
+
137
+ def clear
138
+ @display_value = '0'
139
+ @pending_op = nil
140
+ @accumulator = nil
141
+ @reset_on_next = false
142
+ update_display
143
+ end
144
+
145
+ def negate
146
+ if @display_value.start_with?('-')
147
+ @display_value = @display_value[1..]
148
+ elsif @display_value != '0'
149
+ @display_value = "-#{@display_value}"
150
+ end
151
+ update_display
152
+ end
153
+
154
+ def percent
155
+ @display_value = (current_value / 100.0).to_s
156
+ @reset_on_next = true
157
+ update_display
158
+ end
159
+
160
+ def set_op(op)
161
+ evaluate if @pending_op && !@reset_on_next
162
+ @accumulator = current_value
163
+ @pending_op = op
164
+ @reset_on_next = true
165
+ end
166
+
167
+ def equals
168
+ evaluate
169
+ @pending_op = nil
170
+ end
171
+
172
+ def run
173
+ @app.mainloop
174
+ end
175
+
176
+ private
177
+
178
+ def current_value
179
+ @display_value.to_f
180
+ end
181
+
182
+ def evaluate
183
+ return unless @pending_op && @accumulator
184
+
185
+ b = current_value
186
+ result = case @pending_op
187
+ when :+ then @accumulator + b
188
+ when :- then @accumulator - b
189
+ when :* then @accumulator * b
190
+ when :/ then b.zero? ? Float::NAN : @accumulator / b
191
+ end
192
+
193
+ @accumulator = result
194
+ @display_value = format_result(result)
195
+ @reset_on_next = true
196
+ update_display
197
+ end
198
+
199
+ def format_result(val)
200
+ return 'Error' if val.nil? || val.nan? || val.infinite?
201
+ val == val.to_i ? val.to_i.to_s : val.to_s
202
+ end
203
+ end
204
+
205
+ calc = Calculator.new
206
+
207
+ # Automated demo support (testing and recording)
208
+ require_relative '../lib/teek/demo_support'
209
+ TeekDemo.app = calc.app
210
+
211
+ if TeekDemo.recording?
212
+ calc.app.set_window_geometry('+0+0')
213
+ calc.app.tcl_eval('. configure -cursor none')
214
+ TeekDemo.signal_recording_ready
215
+ end
216
+
217
+ if TeekDemo.active?
218
+ TeekDemo.after_idle {
219
+ d = TeekDemo.method(:delay)
220
+ app = calc.app
221
+ rec = TeekDemo.recording?
222
+
223
+ # Exercises all four operations, landing on 42.
224
+ # 8 * 9 = 72, / 3 = 24, + 25 = 49, - 7 = 42
225
+ steps = [
226
+ -> { calc.click('8', recording: rec) },
227
+ -> { calc.click('*', recording: rec) },
228
+ -> { calc.click('9', recording: rec) },
229
+ -> { calc.click('=', recording: rec) }, # 72
230
+ nil,
231
+ -> { calc.click('/', recording: rec) },
232
+ -> { calc.click('3', recording: rec) },
233
+ -> { calc.click('=', recording: rec) }, # 24
234
+ nil,
235
+ -> { calc.click('+', recording: rec) },
236
+ -> { calc.click('2', recording: rec) },
237
+ -> { calc.click('5', recording: rec) },
238
+ -> { calc.click('=', recording: rec) }, # 49
239
+ nil,
240
+ -> { calc.click('-', recording: rec) },
241
+ -> { calc.click('7', recording: rec) },
242
+ -> { calc.click('=', recording: rec) }, # 42
243
+ nil, nil,
244
+ -> { TeekDemo.finish },
245
+ ]
246
+
247
+ run_step = nil
248
+ i = 0
249
+ run_step = proc {
250
+ steps[i]&.call
251
+ i += 1
252
+ if i < steps.length
253
+ app.after(d.call(test: 1, record: 250)) { run_step.call }
254
+ end
255
+ }
256
+ run_step.call
257
+ }
258
+ end
259
+
260
+ calc.run
@@ -0,0 +1,45 @@
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
+ app.tcl_eval('ttk::frame .f')
17
+ app.tcl_eval('pack .f -fill both -expand 1 -padx 10 -pady 10')
18
+
19
+ app.tcl_eval('ttk::label .f.lbl -text "Hello from the app"')
20
+ app.tcl_eval('pack .f.lbl -pady 5')
21
+
22
+ app.tcl_eval('ttk::entry .f.ent')
23
+ app.tcl_eval('pack .f.ent -pady 5')
24
+
25
+ # Button that creates more widgets dynamically
26
+ counter = 0
27
+ cb = app.register_callback(proc { |*|
28
+ counter += 1
29
+ app.tcl_eval("ttk::button .f.btn#{counter} -text {Button #{counter}}")
30
+ app.tcl_eval("pack .f.btn#{counter} -pady 2")
31
+ })
32
+ app.tcl_eval("ttk::button .f.add -text {Add Widget} -command {ruby_callback #{cb}}")
33
+ app.tcl_eval('pack .f.add -pady 5')
34
+
35
+ # Button to destroy last widget
36
+ rm_cb = app.register_callback(proc { |*|
37
+ if counter > 0
38
+ app.destroy(".f.btn#{counter}")
39
+ counter -= 1
40
+ end
41
+ })
42
+ app.tcl_eval("ttk::button .f.rm -text {Remove Widget} -command {ruby_callback #{rm_cb}}")
43
+ app.tcl_eval('pack .f.rm -pady 5')
44
+
45
+ app.mainloop