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,742 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# Live inspector for Teek applications. Opens a Toplevel window with three
|
|
5
|
+
# tabs: Widgets (tree + config), Variables (searchable list), and Watches
|
|
6
|
+
# (tracked variable history).
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# app = Teek::App.new(debug: true)
|
|
10
|
+
#
|
|
11
|
+
# Can also be enabled via the +TEEK_DEBUG+ environment variable.
|
|
12
|
+
class Debugger
|
|
13
|
+
# Prefix for all debugger widget paths
|
|
14
|
+
TOP = ".teek_debug"
|
|
15
|
+
NB = "#{TOP}.nb"
|
|
16
|
+
WATCH_HISTORY_SIZE = 50
|
|
17
|
+
|
|
18
|
+
attr_reader :interp
|
|
19
|
+
|
|
20
|
+
def initialize(app)
|
|
21
|
+
@app = app
|
|
22
|
+
@interp = app.interp
|
|
23
|
+
@watches = {}
|
|
24
|
+
|
|
25
|
+
app.command(:toplevel, TOP)
|
|
26
|
+
app.command(:wm, 'title', TOP, 'Teek Debugger')
|
|
27
|
+
app.command(:wm, 'geometry', TOP, '400x500')
|
|
28
|
+
|
|
29
|
+
# Don't let closing the debugger kill the app
|
|
30
|
+
close_proc = proc { |*| app.command(:wm, 'withdraw', TOP) }
|
|
31
|
+
app.command(:wm, 'protocol', TOP, 'WM_DELETE_WINDOW', close_proc)
|
|
32
|
+
|
|
33
|
+
setup_ui
|
|
34
|
+
sync_widget_tree
|
|
35
|
+
start_auto_refresh
|
|
36
|
+
|
|
37
|
+
# Start behind the main app window
|
|
38
|
+
app.command(:lower, TOP, '.')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def show
|
|
42
|
+
@app.show(TOP)
|
|
43
|
+
@app.command(:raise, TOP)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def hide
|
|
47
|
+
@app.hide(TOP)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Called by App when a widget is created
|
|
51
|
+
def on_widget_created(path, cls)
|
|
52
|
+
tree = "#{NB}.widgets.tree"
|
|
53
|
+
return unless @app.command(:winfo, 'exists', tree) == "1"
|
|
54
|
+
return if @app.command(tree, 'exists', path) == "1"
|
|
55
|
+
|
|
56
|
+
ensure_parent_exists(path)
|
|
57
|
+
parent_id = parent_tree_id(path)
|
|
58
|
+
name = tk_basename(path)
|
|
59
|
+
|
|
60
|
+
@app.command(tree, 'insert', parent_id, 'end',
|
|
61
|
+
id: path, text: name, values: Teek.make_list(path, cls))
|
|
62
|
+
@app.command(tree, 'item', parent_id, open: 1)
|
|
63
|
+
rescue Teek::TclError => e
|
|
64
|
+
$stderr.puts "teek debugger: on_widget_created(#{path}): #{e.message}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Called by App when a widget is destroyed
|
|
68
|
+
def on_widget_destroyed(path)
|
|
69
|
+
tree = "#{NB}.widgets.tree"
|
|
70
|
+
return unless @app.command(:winfo, 'exists', tree) == "1"
|
|
71
|
+
return unless @app.command(tree, 'exists', path) == "1"
|
|
72
|
+
|
|
73
|
+
@app.command(tree, 'delete', path)
|
|
74
|
+
rescue Teek::TclError => e
|
|
75
|
+
$stderr.puts "teek debugger: on_widget_destroyed(#{path}): #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def setup_ui
|
|
81
|
+
@app.command('ttk::notebook', NB)
|
|
82
|
+
@app.command(:pack, NB, fill: :both, expand: 1)
|
|
83
|
+
|
|
84
|
+
setup_widget_tree_tab
|
|
85
|
+
setup_variables_tab
|
|
86
|
+
setup_watches_tab
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ── Widgets tab ──────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def setup_widget_tree_tab
|
|
92
|
+
tree = "#{NB}.widgets.tree"
|
|
93
|
+
|
|
94
|
+
@app.command('ttk::frame', "#{NB}.widgets")
|
|
95
|
+
@app.command(NB, 'add', "#{NB}.widgets", text: 'Widgets')
|
|
96
|
+
|
|
97
|
+
@app.command('ttk::treeview', tree,
|
|
98
|
+
columns: 'path class', show: 'tree headings', selectmode: :browse)
|
|
99
|
+
@app.command(tree, 'heading', '#0', text: 'Name')
|
|
100
|
+
@app.command(tree, 'heading', 'path', text: 'Path')
|
|
101
|
+
@app.command(tree, 'heading', 'class', text: 'Class')
|
|
102
|
+
@app.command(tree, 'column', '#0', width: 150)
|
|
103
|
+
@app.command(tree, 'column', 'path', width: 150)
|
|
104
|
+
@app.command(tree, 'column', 'class', width: 100)
|
|
105
|
+
|
|
106
|
+
@app.command('ttk::scrollbar', "#{NB}.widgets.vsb",
|
|
107
|
+
orient: :vertical, command: "#{tree} yview")
|
|
108
|
+
@app.command(tree, 'configure', yscrollcommand: "#{NB}.widgets.vsb set")
|
|
109
|
+
|
|
110
|
+
@app.command(:pack, "#{NB}.widgets.vsb", side: :right, fill: :y)
|
|
111
|
+
@app.command(:pack, tree, fill: :both, expand: 1)
|
|
112
|
+
|
|
113
|
+
# Root item for "."
|
|
114
|
+
@app.command(tree, 'insert', '', 'end',
|
|
115
|
+
id: '.', text: '.', values: '. Tk', open: 1)
|
|
116
|
+
|
|
117
|
+
setup_detail_panel
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def setup_detail_panel
|
|
121
|
+
tree = "#{NB}.widgets.tree"
|
|
122
|
+
detail = "#{NB}.widgets.detail"
|
|
123
|
+
detail_text = "#{detail}.text"
|
|
124
|
+
|
|
125
|
+
@app.command('ttk::frame', detail)
|
|
126
|
+
@app.command(:pack, detail, side: :bottom, fill: :x)
|
|
127
|
+
@app.command(:text, detail_text,
|
|
128
|
+
height: 6, wrap: :word, state: :disabled, font: 'TkFixedFont')
|
|
129
|
+
@app.command(:pack, detail_text, fill: :x)
|
|
130
|
+
|
|
131
|
+
# Repack tree to not overlap detail
|
|
132
|
+
@app.command(:pack, detail, before: tree, side: :bottom, fill: :x)
|
|
133
|
+
|
|
134
|
+
select_proc = proc { |*| on_tree_select }
|
|
135
|
+
@app.command(tree, 'configure', cursor: 'hand2')
|
|
136
|
+
@app.command(:bind, tree, '<<TreeviewSelect>>', select_proc)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def on_tree_select
|
|
140
|
+
tree = "#{NB}.widgets.tree"
|
|
141
|
+
detail_text = "#{NB}.widgets.detail.text"
|
|
142
|
+
|
|
143
|
+
sel = @app.command(tree, 'selection')
|
|
144
|
+
return if sel.empty?
|
|
145
|
+
|
|
146
|
+
path = sel
|
|
147
|
+
begin
|
|
148
|
+
config = @app.command(path, 'configure')
|
|
149
|
+
lines = Teek.split_list(config).map { |item|
|
|
150
|
+
parts = Teek.split_list(item)
|
|
151
|
+
next if parts.size < 5
|
|
152
|
+
" #{parts[0]} = #{parts[4]}"
|
|
153
|
+
}.compact.join("\n")
|
|
154
|
+
detail = "#{path}\n#{lines}"
|
|
155
|
+
rescue Teek::TclError
|
|
156
|
+
detail = "#{path}\n (widget no longer exists)"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
@app.command(detail_text, 'configure', state: :normal)
|
|
160
|
+
@app.command(detail_text, 'delete', '1.0', 'end')
|
|
161
|
+
@app.command(detail_text, 'insert', '1.0', detail)
|
|
162
|
+
@app.command(detail_text, 'configure', state: :disabled)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# ── Variables tab ────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def setup_variables_tab
|
|
168
|
+
vars_tree = "#{NB}.vars.tree"
|
|
169
|
+
|
|
170
|
+
@app.command('ttk::frame', "#{NB}.vars")
|
|
171
|
+
@app.command(NB, 'add', "#{NB}.vars", text: 'Variables')
|
|
172
|
+
|
|
173
|
+
# Toolbar: search entry + refresh button
|
|
174
|
+
@app.command('ttk::frame', "#{NB}.vars.toolbar")
|
|
175
|
+
@app.command(:pack, "#{NB}.vars.toolbar", fill: :x, padx: 2, pady: 2)
|
|
176
|
+
|
|
177
|
+
@app.command('ttk::label', "#{NB}.vars.toolbar.lbl", text: 'Filter:')
|
|
178
|
+
@app.command(:pack, "#{NB}.vars.toolbar.lbl", side: :left)
|
|
179
|
+
|
|
180
|
+
@app.command('ttk::entry', "#{NB}.vars.toolbar.search")
|
|
181
|
+
@app.command(:pack, "#{NB}.vars.toolbar.search",
|
|
182
|
+
side: :left, fill: :x, expand: 1, padx: 4)
|
|
183
|
+
|
|
184
|
+
refresh_proc = proc { |*| refresh_variables }
|
|
185
|
+
@app.command('ttk::button', "#{NB}.vars.toolbar.refresh",
|
|
186
|
+
text: 'Refresh', command: refresh_proc)
|
|
187
|
+
@app.command(:pack, "#{NB}.vars.toolbar.refresh", side: :right)
|
|
188
|
+
|
|
189
|
+
# Filter on keypress
|
|
190
|
+
filter_proc = proc { |*| filter_variables }
|
|
191
|
+
@app.command(:bind, "#{NB}.vars.toolbar.search", '<KeyRelease>', filter_proc)
|
|
192
|
+
|
|
193
|
+
# Treeview: name, value, type
|
|
194
|
+
@app.command('ttk::treeview', vars_tree,
|
|
195
|
+
columns: 'value type', show: 'tree headings', selectmode: :browse)
|
|
196
|
+
@app.command(vars_tree, 'heading', '#0', text: 'Name')
|
|
197
|
+
@app.command(vars_tree, 'heading', 'value', text: 'Value')
|
|
198
|
+
@app.command(vars_tree, 'heading', 'type', text: 'Type')
|
|
199
|
+
@app.command(vars_tree, 'column', '#0', width: 150)
|
|
200
|
+
@app.command(vars_tree, 'column', 'value', width: 200)
|
|
201
|
+
@app.command(vars_tree, 'column', 'type', width: 50)
|
|
202
|
+
|
|
203
|
+
@app.command('ttk::scrollbar', "#{NB}.vars.vsb",
|
|
204
|
+
orient: :vertical, command: "#{vars_tree} yview")
|
|
205
|
+
@app.command(vars_tree, 'configure',
|
|
206
|
+
yscrollcommand: "#{NB}.vars.vsb set")
|
|
207
|
+
|
|
208
|
+
@app.command(:pack, "#{NB}.vars.vsb", side: :right, fill: :y)
|
|
209
|
+
@app.command(:pack, vars_tree, fill: :both, expand: 1)
|
|
210
|
+
|
|
211
|
+
# Right-click context menu
|
|
212
|
+
@app.command(:menu, "#{NB}.vars.ctx", tearoff: 0)
|
|
213
|
+
watch_proc = proc { |*| watch_selected_variable }
|
|
214
|
+
@app.command("#{NB}.vars.ctx", 'add', 'command',
|
|
215
|
+
label: 'Watch', command: watch_proc)
|
|
216
|
+
|
|
217
|
+
@app.command(:bind, vars_tree, '<Button-3>', proc { |*|
|
|
218
|
+
# Select the row under cursor, then show context menu
|
|
219
|
+
@app.tcl_eval("
|
|
220
|
+
set item [#{vars_tree} identify item [winfo pointerx #{vars_tree}] [winfo pointery #{vars_tree}]]
|
|
221
|
+
if {$item ne {}} { #{vars_tree} selection set $item }
|
|
222
|
+
")
|
|
223
|
+
@app.tcl_eval("tk_popup #{NB}.vars.ctx [winfo pointerx .] [winfo pointery .]")
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
# Double-click to watch
|
|
227
|
+
@app.command(:bind, vars_tree, '<Double-1>', proc { |*| watch_selected_variable })
|
|
228
|
+
|
|
229
|
+
refresh_variables
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def refresh_variables
|
|
233
|
+
new_data = fetch_variables
|
|
234
|
+
return if new_data == @var_data
|
|
235
|
+
|
|
236
|
+
vars_tree = "#{NB}.vars.tree"
|
|
237
|
+
|
|
238
|
+
# Preserve selection and scroll
|
|
239
|
+
sel = @app.command(vars_tree, 'selection') rescue ""
|
|
240
|
+
scroll = Teek.split_list(@app.command(vars_tree, 'yview')) rescue nil
|
|
241
|
+
|
|
242
|
+
@var_data = new_data
|
|
243
|
+
filter_variables
|
|
244
|
+
|
|
245
|
+
# Restore selection if item still exists
|
|
246
|
+
unless sel.empty?
|
|
247
|
+
if @app.command(vars_tree, 'exists', sel) == "1"
|
|
248
|
+
@app.command(vars_tree, 'selection', 'set', sel)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Restore scroll position
|
|
253
|
+
if scroll && scroll.size == 2
|
|
254
|
+
@app.command(vars_tree, 'yview', 'moveto', scroll[0])
|
|
255
|
+
end
|
|
256
|
+
rescue Teek::TclError => e
|
|
257
|
+
$stderr.puts "teek debugger: refresh_variables: #{e.message}"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def fetch_variables
|
|
261
|
+
vars = {}
|
|
262
|
+
names = Teek.split_list(@app.command(:info, 'globals'))
|
|
263
|
+
names.sort.each do |name|
|
|
264
|
+
is_array = @app.command(:array, 'exists', name) == "1"
|
|
265
|
+
if is_array
|
|
266
|
+
begin
|
|
267
|
+
elements = Teek.split_list(@app.command(:array, 'get', name))
|
|
268
|
+
pairs = elements.each_slice(2).to_a
|
|
269
|
+
vars[name] = { type: "array", value: "(#{pairs.size} elements)", elements: pairs }
|
|
270
|
+
rescue Teek::TclError
|
|
271
|
+
vars[name] = { type: "array", value: "(error reading)" }
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
begin
|
|
275
|
+
val = @app.command(:set, name)
|
|
276
|
+
vars[name] = { type: "scalar", value: val }
|
|
277
|
+
rescue Teek::TclError
|
|
278
|
+
vars[name] = { type: "?", value: "(error reading)" }
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
vars
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def filter_variables
|
|
286
|
+
return unless @var_data
|
|
287
|
+
|
|
288
|
+
vars_tree = "#{NB}.vars.tree"
|
|
289
|
+
pattern = @app.command("#{NB}.vars.toolbar.search", 'get').downcase
|
|
290
|
+
|
|
291
|
+
# Clear tree — uses Tcl command substitution
|
|
292
|
+
@app.tcl_eval("#{vars_tree} delete [#{vars_tree} children {}]")
|
|
293
|
+
|
|
294
|
+
@var_data.each do |name, info|
|
|
295
|
+
next unless pattern.empty? ||
|
|
296
|
+
name.downcase.include?(pattern) ||
|
|
297
|
+
info[:value].downcase.include?(pattern)
|
|
298
|
+
|
|
299
|
+
display_val = info[:value]
|
|
300
|
+
display_val = display_val[0, 200] + "..." if display_val.size > 200
|
|
301
|
+
|
|
302
|
+
item_id = "v:#{name}"
|
|
303
|
+
@app.command(vars_tree, 'insert', '', 'end',
|
|
304
|
+
id: item_id, text: name,
|
|
305
|
+
values: Teek.make_list(display_val, info[:type]))
|
|
306
|
+
|
|
307
|
+
# For arrays, add child items for each element
|
|
308
|
+
next unless info[:type] == "array" && info[:elements]
|
|
309
|
+
info[:elements].each do |key, val|
|
|
310
|
+
next unless pattern.empty? ||
|
|
311
|
+
key.downcase.include?(pattern) ||
|
|
312
|
+
val.downcase.include?(pattern)
|
|
313
|
+
el_val = val.size > 200 ? val[0, 200] + "..." : val
|
|
314
|
+
@app.command(vars_tree, 'insert', item_id, 'end',
|
|
315
|
+
id: "v:#{name}:#{key}", text: "#{name}(#{key})",
|
|
316
|
+
values: Teek.make_list(el_val, ''))
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
rescue Teek::TclError => e
|
|
320
|
+
$stderr.puts "teek debugger: filter_variables: #{e.message}"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def update_variables_incremental
|
|
324
|
+
return unless @var_data
|
|
325
|
+
|
|
326
|
+
vars_tree = "#{NB}.vars.tree"
|
|
327
|
+
pattern = @app.command("#{NB}.vars.toolbar.search", 'get').downcase
|
|
328
|
+
|
|
329
|
+
# Build set of vars that match the current filter
|
|
330
|
+
visible = {}
|
|
331
|
+
@var_data.each do |name, info|
|
|
332
|
+
next unless pattern.empty? ||
|
|
333
|
+
name.downcase.include?(pattern) ||
|
|
334
|
+
info[:value].downcase.include?(pattern)
|
|
335
|
+
visible[name] = info
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Walk current tree items: update existing, mark deleted
|
|
339
|
+
current_ids = Teek.split_list(@app.command(vars_tree, 'children', ''))
|
|
340
|
+
in_tree = {}
|
|
341
|
+
|
|
342
|
+
current_ids.each do |item_id|
|
|
343
|
+
name = item_id.sub(/\Av:/, '')
|
|
344
|
+
in_tree[name] = item_id
|
|
345
|
+
|
|
346
|
+
if visible.key?(name)
|
|
347
|
+
info = visible[name]
|
|
348
|
+
display_val = info[:value]
|
|
349
|
+
display_val = display_val[0, 200] + "..." if display_val.size > 200
|
|
350
|
+
|
|
351
|
+
@app.command(vars_tree, 'item', item_id,
|
|
352
|
+
text: name,
|
|
353
|
+
values: Teek.make_list(display_val, info[:type]))
|
|
354
|
+
|
|
355
|
+
if info[:type] == "array" && info[:elements]
|
|
356
|
+
update_array_children(vars_tree, item_id, name, info[:elements], pattern)
|
|
357
|
+
else
|
|
358
|
+
# Remove leftover children (e.g. was array, now scalar)
|
|
359
|
+
children = Teek.split_list(@app.command(vars_tree, 'children', item_id))
|
|
360
|
+
children.each { |c| @app.command(vars_tree, 'delete', c) } unless children.empty?
|
|
361
|
+
end
|
|
362
|
+
else
|
|
363
|
+
# Mark as deleted in-place
|
|
364
|
+
current_text = @app.command(vars_tree, 'item', item_id, '-text')
|
|
365
|
+
unless current_text.include?("(deleted)")
|
|
366
|
+
@app.command(vars_tree, 'item', item_id,
|
|
367
|
+
text: "(deleted) #{name}",
|
|
368
|
+
values: Teek.make_list("", ""))
|
|
369
|
+
children = Teek.split_list(@app.command(vars_tree, 'children', item_id))
|
|
370
|
+
children.each { |c| @app.command(vars_tree, 'delete', c) }
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Append new vars not yet in tree
|
|
376
|
+
visible.each do |name, info|
|
|
377
|
+
next if in_tree.key?(name)
|
|
378
|
+
|
|
379
|
+
display_val = info[:value]
|
|
380
|
+
display_val = display_val[0, 200] + "..." if display_val.size > 200
|
|
381
|
+
|
|
382
|
+
item_id = "v:#{name}"
|
|
383
|
+
@app.command(vars_tree, 'insert', '', 'end',
|
|
384
|
+
id: item_id, text: name,
|
|
385
|
+
values: Teek.make_list(display_val, info[:type]))
|
|
386
|
+
|
|
387
|
+
next unless info[:type] == "array" && info[:elements]
|
|
388
|
+
info[:elements].each do |key, val|
|
|
389
|
+
next unless pattern.empty? ||
|
|
390
|
+
key.downcase.include?(pattern) ||
|
|
391
|
+
val.downcase.include?(pattern)
|
|
392
|
+
el_val = val.size > 200 ? val[0, 200] + "..." : val
|
|
393
|
+
@app.command(vars_tree, 'insert', item_id, 'end',
|
|
394
|
+
id: "v:#{name}:#{key}", text: "#{name}(#{key})",
|
|
395
|
+
values: Teek.make_list(el_val, ''))
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
rescue Teek::TclError => e
|
|
399
|
+
$stderr.puts "teek debugger: update_variables_incremental: #{e.message}"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def update_array_children(vars_tree, parent_id, name, elements, pattern)
|
|
403
|
+
current_children = Teek.split_list(@app.command(vars_tree, 'children', parent_id))
|
|
404
|
+
child_set = {}
|
|
405
|
+
current_children.each { |c| child_set[c] = true }
|
|
406
|
+
|
|
407
|
+
seen = {}
|
|
408
|
+
elements.each do |key, val|
|
|
409
|
+
next unless pattern.empty? ||
|
|
410
|
+
key.downcase.include?(pattern) ||
|
|
411
|
+
val.downcase.include?(pattern)
|
|
412
|
+
|
|
413
|
+
el_id = "v:#{name}:#{key}"
|
|
414
|
+
el_val = val.size > 200 ? val[0, 200] + "..." : val
|
|
415
|
+
seen[el_id] = true
|
|
416
|
+
|
|
417
|
+
if child_set.key?(el_id)
|
|
418
|
+
@app.command(vars_tree, 'item', el_id,
|
|
419
|
+
text: "#{name}(#{key})",
|
|
420
|
+
values: Teek.make_list(el_val, ''))
|
|
421
|
+
else
|
|
422
|
+
@app.command(vars_tree, 'insert', parent_id, 'end',
|
|
423
|
+
id: el_id, text: "#{name}(#{key})",
|
|
424
|
+
values: Teek.make_list(el_val, ''))
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Remove children no longer in the array
|
|
429
|
+
(child_set.keys - seen.keys).each do |old_id|
|
|
430
|
+
@app.command(vars_tree, 'delete', old_id)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def watch_selected_variable
|
|
435
|
+
vars_tree = "#{NB}.vars.tree"
|
|
436
|
+
sel = @app.command(vars_tree, 'selection')
|
|
437
|
+
return if sel.empty?
|
|
438
|
+
|
|
439
|
+
name = @app.command(vars_tree, 'item', sel, '-text')
|
|
440
|
+
return if name.empty?
|
|
441
|
+
|
|
442
|
+
# Strip array(key) back to just the array name
|
|
443
|
+
name = name.sub(/\(.*\)\z/, '')
|
|
444
|
+
add_watch(name)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# ── Watches tab ──────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
def setup_watches_tab
|
|
450
|
+
watch_tree = "#{NB}.watches.tree"
|
|
451
|
+
|
|
452
|
+
@app.command('ttk::frame', "#{NB}.watches")
|
|
453
|
+
@app.command(NB, 'add', "#{NB}.watches", text: 'Watches')
|
|
454
|
+
|
|
455
|
+
# Toolbar: refresh button
|
|
456
|
+
@app.command('ttk::frame', "#{NB}.watches.toolbar")
|
|
457
|
+
@app.command(:pack, "#{NB}.watches.toolbar", fill: :x, padx: 2, pady: 2)
|
|
458
|
+
|
|
459
|
+
refresh_proc = proc { |*| refresh_watches }
|
|
460
|
+
@app.command('ttk::button', "#{NB}.watches.toolbar.refresh",
|
|
461
|
+
text: 'Refresh', command: refresh_proc)
|
|
462
|
+
@app.command(:pack, "#{NB}.watches.toolbar.refresh", side: :right)
|
|
463
|
+
|
|
464
|
+
# Help label — shown when no watches exist
|
|
465
|
+
@app.command('ttk::label', "#{NB}.watches.help",
|
|
466
|
+
text: "Right-click a variable in the Variables tab to watch it.",
|
|
467
|
+
foreground: 'gray50')
|
|
468
|
+
@app.command(:pack, "#{NB}.watches.help", expand: 1)
|
|
469
|
+
|
|
470
|
+
# Treeview: name, current value, changes count
|
|
471
|
+
@app.command('ttk::treeview', watch_tree,
|
|
472
|
+
columns: 'value changes', show: 'tree headings', selectmode: :browse)
|
|
473
|
+
@app.command(watch_tree, 'heading', '#0', text: 'Name')
|
|
474
|
+
@app.command(watch_tree, 'heading', 'value', text: 'Value')
|
|
475
|
+
@app.command(watch_tree, 'heading', 'changes', text: 'Changes')
|
|
476
|
+
@app.command(watch_tree, 'column', '#0', width: 120)
|
|
477
|
+
@app.command(watch_tree, 'column', 'value', width: 200)
|
|
478
|
+
@app.command(watch_tree, 'column', 'changes', width: 60)
|
|
479
|
+
|
|
480
|
+
@app.command('ttk::scrollbar', "#{NB}.watches.vsb",
|
|
481
|
+
orient: :vertical, command: "#{watch_tree} yview")
|
|
482
|
+
@app.command(watch_tree, 'configure',
|
|
483
|
+
yscrollcommand: "#{NB}.watches.vsb set")
|
|
484
|
+
|
|
485
|
+
# Don't pack tree yet — help label is shown first
|
|
486
|
+
|
|
487
|
+
# Detail panel for history
|
|
488
|
+
@app.command(:text, "#{NB}.watches.history",
|
|
489
|
+
height: 8, wrap: :word, state: :disabled, font: 'TkFixedFont')
|
|
490
|
+
|
|
491
|
+
# Selection shows history
|
|
492
|
+
select_proc = proc { |*| on_watch_select }
|
|
493
|
+
@app.command(:bind, watch_tree, '<<TreeviewSelect>>', select_proc)
|
|
494
|
+
|
|
495
|
+
# Right-click to unwatch
|
|
496
|
+
@app.command(:menu, "#{NB}.watches.ctx", tearoff: 0)
|
|
497
|
+
unwatch_proc = proc { |*| unwatch_selected }
|
|
498
|
+
@app.command("#{NB}.watches.ctx", 'add', 'command',
|
|
499
|
+
label: 'Unwatch', command: unwatch_proc)
|
|
500
|
+
|
|
501
|
+
@app.command(:bind, watch_tree, '<Button-3>', proc { |*|
|
|
502
|
+
@app.tcl_eval("
|
|
503
|
+
set item [#{watch_tree} identify item [winfo pointerx #{watch_tree}] [winfo pointery #{watch_tree}]]
|
|
504
|
+
if {$item ne {}} { #{watch_tree} selection set $item }
|
|
505
|
+
")
|
|
506
|
+
@app.tcl_eval("tk_popup #{NB}.watches.ctx [winfo pointerx .] [winfo pointery .]")
|
|
507
|
+
})
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def add_watch(name)
|
|
511
|
+
return if @watches.key?(name)
|
|
512
|
+
|
|
513
|
+
# Register Tcl trace on the variable
|
|
514
|
+
cb_id = @app.register_callback(proc { |var_name, index, *|
|
|
515
|
+
record_watch(var_name, index)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
@app.tcl_eval("trace add variable #{Teek.make_list(name)} write {ruby_callback #{cb_id}}")
|
|
519
|
+
|
|
520
|
+
@watches[name] = { cb_id: cb_id, values: [] }
|
|
521
|
+
|
|
522
|
+
# Capture current value
|
|
523
|
+
record_watch(name, nil)
|
|
524
|
+
|
|
525
|
+
update_watches_ui
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def remove_watch(name)
|
|
529
|
+
info = @watches.delete(name)
|
|
530
|
+
return unless info
|
|
531
|
+
|
|
532
|
+
# Remove Tcl trace
|
|
533
|
+
@app.tcl_eval(
|
|
534
|
+
"trace remove variable #{Teek.make_list(name)} write {ruby_callback #{info[:cb_id]}}"
|
|
535
|
+
)
|
|
536
|
+
@app.unregister_callback(info[:cb_id])
|
|
537
|
+
|
|
538
|
+
update_watches_ui
|
|
539
|
+
rescue Teek::TclError => e
|
|
540
|
+
$stderr.puts "teek debugger: remove_watch(#{name}): #{e.message}"
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def record_watch(name, index)
|
|
544
|
+
watch = @watches[name]
|
|
545
|
+
return unless watch
|
|
546
|
+
|
|
547
|
+
val = begin
|
|
548
|
+
if index && !index.empty?
|
|
549
|
+
@app.tcl_eval("set #{Teek.make_list(name)}(#{index})")
|
|
550
|
+
else
|
|
551
|
+
@app.command(:set, name)
|
|
552
|
+
end
|
|
553
|
+
rescue Teek::TclError
|
|
554
|
+
"(undefined)"
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
entry = { value: val, time: Time.now }
|
|
558
|
+
watch[:values] << entry
|
|
559
|
+
watch[:values].shift if watch[:values].size > WATCH_HISTORY_SIZE
|
|
560
|
+
|
|
561
|
+
# Update this row in-place
|
|
562
|
+
update_watch_row(name, watch)
|
|
563
|
+
rescue Teek::TclError => e
|
|
564
|
+
$stderr.puts "teek debugger: record_watch(#{name}): #{e.message}"
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def update_watch_row(name, info)
|
|
568
|
+
watch_tree = "#{NB}.watches.tree"
|
|
569
|
+
item_id = "watch_#{name}"
|
|
570
|
+
|
|
571
|
+
current = info[:values].last
|
|
572
|
+
display_val = current ? current[:value] : ""
|
|
573
|
+
display_val = display_val[0, 200] + "..." if display_val.size > 200
|
|
574
|
+
|
|
575
|
+
if @app.command(watch_tree, 'exists', item_id) == "1"
|
|
576
|
+
@app.command(watch_tree, 'item', item_id,
|
|
577
|
+
values: Teek.make_list(display_val, info[:values].size.to_s))
|
|
578
|
+
else
|
|
579
|
+
@app.command(watch_tree, 'insert', '', 'end',
|
|
580
|
+
id: item_id, text: name,
|
|
581
|
+
values: Teek.make_list(display_val, info[:values].size.to_s))
|
|
582
|
+
end
|
|
583
|
+
rescue Teek::TclError => e
|
|
584
|
+
$stderr.puts "teek debugger: update_watch_row(#{name}): #{e.message}"
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def refresh_watches
|
|
588
|
+
@watches.each do |name, info|
|
|
589
|
+
# Re-read current value
|
|
590
|
+
val = begin
|
|
591
|
+
@app.command(:set, name)
|
|
592
|
+
rescue Teek::TclError
|
|
593
|
+
"(undefined)"
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
current = info[:values].last
|
|
597
|
+
if current.nil? || current[:value] != val
|
|
598
|
+
info[:values] << { value: val, time: Time.now }
|
|
599
|
+
info[:values].shift if info[:values].size > WATCH_HISTORY_SIZE
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
update_watch_row(name, info)
|
|
603
|
+
end
|
|
604
|
+
rescue Teek::TclError => e
|
|
605
|
+
$stderr.puts "teek debugger: refresh_watches: #{e.message}"
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def update_watches_ui
|
|
609
|
+
watch_tree = "#{NB}.watches.tree"
|
|
610
|
+
help = "#{NB}.watches.help"
|
|
611
|
+
history = "#{NB}.watches.history"
|
|
612
|
+
|
|
613
|
+
# Update tab label with count
|
|
614
|
+
label = @watches.empty? ? "Watches" : "Watches (#{@watches.size})"
|
|
615
|
+
@app.command(NB, 'tab', "#{NB}.watches", text: label)
|
|
616
|
+
|
|
617
|
+
if @watches.empty?
|
|
618
|
+
@app.command(:pack, 'forget', watch_tree) rescue nil
|
|
619
|
+
@app.command(:pack, 'forget', "#{NB}.watches.vsb") rescue nil
|
|
620
|
+
@app.command(:pack, 'forget', history) rescue nil
|
|
621
|
+
@app.command(:pack, help, expand: 1)
|
|
622
|
+
else
|
|
623
|
+
@app.command(:pack, 'forget', help) rescue nil
|
|
624
|
+
@app.command(:pack, "#{NB}.watches.vsb", side: :right, fill: :y)
|
|
625
|
+
@app.command(:pack, watch_tree, fill: :both, expand: 1)
|
|
626
|
+
@app.command(:pack, history, side: :bottom, fill: :x, before: watch_tree)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
refresh_watches
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def on_watch_select
|
|
633
|
+
watch_tree = "#{NB}.watches.tree"
|
|
634
|
+
history_w = "#{NB}.watches.history"
|
|
635
|
+
|
|
636
|
+
sel = @app.command(watch_tree, 'selection')
|
|
637
|
+
return if sel.empty?
|
|
638
|
+
|
|
639
|
+
name = @app.command(watch_tree, 'item', sel, '-text')
|
|
640
|
+
info = @watches[name]
|
|
641
|
+
return unless info
|
|
642
|
+
|
|
643
|
+
lines = info[:values].reverse.map { |e|
|
|
644
|
+
ts = e[:time].strftime("%H:%M:%S.%L")
|
|
645
|
+
val = e[:value]
|
|
646
|
+
val = val[0, 100] + "..." if val.size > 100
|
|
647
|
+
" [#{ts}] #{val}"
|
|
648
|
+
}.join("\n")
|
|
649
|
+
text = "#{name} (#{info[:values].size} changes)\n#{lines}"
|
|
650
|
+
|
|
651
|
+
@app.command(history_w, 'configure', state: :normal)
|
|
652
|
+
@app.command(history_w, 'delete', '1.0', 'end')
|
|
653
|
+
@app.command(history_w, 'insert', '1.0', text)
|
|
654
|
+
@app.command(history_w, 'configure', state: :disabled)
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def unwatch_selected
|
|
658
|
+
watch_tree = "#{NB}.watches.tree"
|
|
659
|
+
sel = @app.command(watch_tree, 'selection')
|
|
660
|
+
return if sel.empty?
|
|
661
|
+
|
|
662
|
+
name = @app.command(watch_tree, 'item', sel, '-text')
|
|
663
|
+
remove_watch(name)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# ── Auto-refresh ─────────────────────────────────────────
|
|
667
|
+
|
|
668
|
+
def start_auto_refresh
|
|
669
|
+
@auto_refresh_id = @app.after(1000) do
|
|
670
|
+
auto_refresh_tick
|
|
671
|
+
start_auto_refresh
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def auto_refresh_tick
|
|
676
|
+
vars_tree = "#{NB}.vars.tree"
|
|
677
|
+
|
|
678
|
+
new_data = fetch_variables
|
|
679
|
+
unless new_data == @var_data
|
|
680
|
+
# Preserve selection and scroll
|
|
681
|
+
sel = @app.command(vars_tree, 'selection') rescue ""
|
|
682
|
+
scroll = Teek.split_list(@app.command(vars_tree, 'yview')) rescue nil
|
|
683
|
+
|
|
684
|
+
@var_data = new_data
|
|
685
|
+
update_variables_incremental
|
|
686
|
+
|
|
687
|
+
# Restore selection if item still exists
|
|
688
|
+
unless sel.empty?
|
|
689
|
+
if @app.command(vars_tree, 'exists', sel) == "1"
|
|
690
|
+
@app.command(vars_tree, 'selection', 'set', sel)
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# Restore scroll position
|
|
695
|
+
if scroll && scroll.size == 2
|
|
696
|
+
@app.command(vars_tree, 'yview', 'moveto', scroll[0])
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
refresh_watches
|
|
701
|
+
rescue Teek::TclError => e
|
|
702
|
+
$stderr.puts "teek debugger: auto-refresh error: #{e.message}"
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# ── Widget tree helpers ──────────────────────────────────
|
|
706
|
+
|
|
707
|
+
def sync_widget_tree
|
|
708
|
+
@app.widgets.sort_by { |path, _| path.count('.') }.each do |path, info|
|
|
709
|
+
on_widget_created(path, info[:class])
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def parent_tree_id(path)
|
|
714
|
+
last_dot = path.rindex('.')
|
|
715
|
+
return '.' if last_dot.nil? || last_dot == 0
|
|
716
|
+
path[0...last_dot]
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def tk_basename(path)
|
|
720
|
+
last_dot = path.rindex('.')
|
|
721
|
+
return path if last_dot.nil?
|
|
722
|
+
path[(last_dot + 1)..]
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def ensure_parent_exists(path)
|
|
726
|
+
tree = "#{NB}.widgets.tree"
|
|
727
|
+
parent = parent_tree_id(path)
|
|
728
|
+
return if parent == '.'
|
|
729
|
+
return if @app.command(tree, 'exists', parent) == "1"
|
|
730
|
+
|
|
731
|
+
ensure_parent_exists(parent)
|
|
732
|
+
name = tk_basename(parent)
|
|
733
|
+
cls = begin
|
|
734
|
+
@app.command(:winfo, 'class', parent)
|
|
735
|
+
rescue Teek::TclError
|
|
736
|
+
"?"
|
|
737
|
+
end
|
|
738
|
+
@app.command(tree, 'insert', parent_tree_id(parent), 'end',
|
|
739
|
+
id: parent, text: name, values: Teek.make_list(parent, cls), open: 1)
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
end
|