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
|
@@ -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
|