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,499 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # teek-record: title=Concurrency Demo - File Hasher
4
+
5
+ # Concurrency Demo - File Hasher
6
+ #
7
+ # Compares concurrency modes:
8
+ # - None: Direct execution, UI frozen. Shows what happens without background work.
9
+ # - None+update: Synchronous but with forced UI updates so progress is visible.
10
+ # - Thread: Background thread with GVL overhead. Enables Pause, UI stays responsive.
11
+ # - Ractor: True parallelism (separate GVL). Best throughput. (Ruby 4.x+ only)
12
+
13
+ require 'teek'
14
+ require 'teek/background_none'
15
+ require 'digest'
16
+ require 'tmpdir'
17
+
18
+ # Register :none mode for demo
19
+ Teek::BackgroundWork.register_background_mode(:none, Teek::BackgroundNone::BackgroundWork)
20
+
21
+ RACTOR_AVAILABLE = Teek::BackgroundWork::RACTOR_SUPPORTED
22
+
23
+ class ThreadingDemo
24
+ attr_reader :app
25
+
26
+ ALGORITHMS = %w[SHA256 SHA512 SHA384 SHA1 MD5].freeze
27
+
28
+ MODES = if RACTOR_AVAILABLE
29
+ ['None', 'None+update', 'Thread', 'Ractor'].freeze
30
+ else
31
+ ['None', 'None+update', 'Thread'].freeze
32
+ end
33
+
34
+ def initialize
35
+ @app = Teek::App.new
36
+ @running = false
37
+ @paused = false
38
+ @stop_requested = false
39
+ @background_task = nil
40
+
41
+ build_ui
42
+ collect_files
43
+
44
+ @app.update
45
+ w = @app.command(:winfo, 'width', '.')
46
+ h = @app.command(:winfo, 'height', '.')
47
+ @app.command(:wm, 'geometry', '.', "#{w}x#{h}+0+0")
48
+ @app.command(:wm, 'resizable', '.', 1, 1)
49
+
50
+ close_proc = proc { |*|
51
+ @background_task&.close
52
+ @app.command(:destroy, '.')
53
+ }
54
+ @app.command(:wm, 'protocol', '.', 'WM_DELETE_WINDOW', close_proc)
55
+ end
56
+
57
+ def build_ui
58
+ @app.show
59
+ @app.command(:wm, 'title', '.', 'Concurrency Demo - File Hasher')
60
+ @app.command(:wm, 'minsize', '.', 600, 400)
61
+
62
+ # Tcl variables for widget bindings
63
+ @app.command(:set, '::chunk_size', 3)
64
+ @app.command(:set, '::algorithm', 'SHA256')
65
+ @app.command(:set, '::mode', 'Thread')
66
+ @app.command(:set, '::allow_pause', 0)
67
+ @app.command(:set, '::progress', 0)
68
+
69
+ ractor_note = RACTOR_AVAILABLE ? "Ractor: true parallel." : "(Ractor available on Ruby 4.x+)"
70
+ @app.command('ttk::label', '.desc',
71
+ text: "File hasher demo - compares concurrency modes.\n" \
72
+ "None: UI frozen. None+update: progress visible, pause works. " \
73
+ "Thread: responsive, GVL shared. #{ractor_note}",
74
+ justify: :left)
75
+ @app.command(:pack, '.desc', fill: :x, padx: 10, pady: 10)
76
+
77
+ build_controls
78
+ build_statusbar
79
+ build_log
80
+ end
81
+
82
+ def build_controls
83
+ @app.command('ttk::frame', '.ctrl')
84
+ @app.command(:pack, '.ctrl', fill: :x, padx: 10, pady: 5)
85
+
86
+ @app.command('ttk::button', '.ctrl.start',
87
+ text: 'Start', command: proc { |*| start_hashing })
88
+ @app.command(:pack, '.ctrl.start', side: :left)
89
+
90
+ @app.command('ttk::button', '.ctrl.pause',
91
+ text: 'Pause', state: :disabled, command: proc { |*| toggle_pause })
92
+ @app.command(:pack, '.ctrl.pause', side: :left, padx: 5)
93
+
94
+ @app.command('ttk::button', '.ctrl.reset',
95
+ text: 'Reset', command: proc { |*| reset })
96
+ @app.command(:pack, '.ctrl.reset', side: :left)
97
+
98
+ @app.command('ttk::label', '.ctrl.algo_lbl', text: 'Algorithm:')
99
+ @app.command(:pack, '.ctrl.algo_lbl', side: :left, padx: 10)
100
+
101
+ @app.command('ttk::combobox', '.ctrl.algo',
102
+ textvariable: '::algorithm',
103
+ values: Teek.make_list(*ALGORITHMS),
104
+ width: 8,
105
+ state: :readonly)
106
+ @app.command(:pack, '.ctrl.algo', side: :left)
107
+
108
+ @app.command('ttk::label', '.ctrl.batch_lbl', text: 'Batch:')
109
+ @app.command(:pack, '.ctrl.batch_lbl', side: :left, padx: 10)
110
+
111
+ @app.command('ttk::label', '.ctrl.batch_val', text: '3', width: 3)
112
+ @app.command(:pack, '.ctrl.batch_val', side: :left)
113
+
114
+ @app.command('ttk::scale', '.ctrl.scale',
115
+ orient: :horizontal,
116
+ from: 1,
117
+ to: 100,
118
+ length: 100,
119
+ variable: '::chunk_size',
120
+ command: proc { |v, *| @app.command('.ctrl.batch_val', 'configure', text: v.to_f.round.to_s) })
121
+ @app.command(:pack, '.ctrl.scale', side: :left, padx: 5)
122
+
123
+ @app.command('ttk::label', '.ctrl.mode_lbl', text: 'Mode:')
124
+ @app.command(:pack, '.ctrl.mode_lbl', side: :left, padx: 10)
125
+
126
+ @app.command('ttk::combobox', '.ctrl.mode',
127
+ textvariable: '::mode',
128
+ values: Teek.make_list(*MODES),
129
+ width: 10,
130
+ state: :readonly)
131
+ @app.command(:pack, '.ctrl.mode', side: :left)
132
+
133
+ @app.command('ttk::checkbutton', '.ctrl.pause_chk',
134
+ text: 'Allow Pause',
135
+ variable: '::allow_pause')
136
+ @app.command(:pack, '.ctrl.pause_chk', side: :left, padx: 10)
137
+ end
138
+
139
+ def build_statusbar
140
+ @app.command('ttk::frame', '.status')
141
+ @app.command(:pack, '.status', side: :bottom, fill: :x, padx: 5, pady: 5)
142
+
143
+ # Progress section (left)
144
+ @app.command('ttk::frame', '.status.progress', relief: :sunken, borderwidth: 2)
145
+ @app.command(:pack, '.status.progress', side: :left, fill: :x, expand: 1, padx: 2)
146
+
147
+ @app.command('ttk::progressbar', '.status.progress.bar',
148
+ orient: :horizontal,
149
+ length: 200,
150
+ mode: :determinate,
151
+ variable: '::progress',
152
+ maximum: 100)
153
+ @app.command(:pack, '.status.progress.bar', side: :left, padx: 5, pady: 4)
154
+
155
+ @app.command('ttk::label', '.status.progress.status', text: 'Ready', width: 20, anchor: :w)
156
+ @app.command(:pack, '.status.progress.status', side: :left, padx: 10)
157
+
158
+ @app.command('ttk::label', '.status.progress.file', text: '', width: 28, anchor: :w)
159
+ @app.command(:pack, '.status.progress.file', side: :left, padx: 5)
160
+
161
+ # Info section (right)
162
+ @app.command('ttk::frame', '.status.info', relief: :sunken, borderwidth: 2)
163
+ @app.command(:pack, '.status.info', side: :right, padx: 2)
164
+
165
+ @app.command('ttk::label', '.status.info.files', text: '', width: 12, anchor: :e)
166
+ @app.command(:pack, '.status.info.files', side: :left, padx: 8, pady: 4)
167
+
168
+ @app.command('ttk::separator', '.status.info.sep', orient: :vertical)
169
+ @app.command(:pack, '.status.info.sep', side: :left, fill: :y, pady: 4)
170
+
171
+ @app.command('ttk::label', '.status.info.ruby', text: "Ruby #{RUBY_VERSION}", anchor: :e)
172
+ @app.command(:pack, '.status.info.ruby', side: :left, padx: 8, pady: 4)
173
+ end
174
+
175
+ def build_log
176
+ @app.command('ttk::labelframe', '.log', text: 'Output')
177
+ @app.command(:pack, '.log', fill: :both, expand: 1, padx: 10, pady: 5)
178
+
179
+ @app.command('ttk::frame', '.log.f')
180
+ @app.command(:pack, '.log.f', fill: :both, expand: 1, padx: 5, pady: 5)
181
+ @app.command(:pack, 'propagate', '.log.f', 0)
182
+
183
+ @app.command(:text, '.log.f.text', width: 80, height: 15, wrap: :none)
184
+ @app.command(:pack, '.log.f.text', side: :left, fill: :both, expand: 1)
185
+
186
+ @app.command('ttk::scrollbar', '.log.f.vsb', orient: :vertical, command: '.log.f.text yview')
187
+ @app.command('.log.f.text', 'configure', yscrollcommand: '.log.f.vsb set')
188
+ @app.command(:pack, '.log.f.vsb', side: :right, fill: :y)
189
+ end
190
+
191
+ def collect_files
192
+ base = File.exist?('/app') ? '/app' : Dir.pwd
193
+ @files = Dir.glob("#{base}/**/*", File::FNM_DOTMATCH).select { |f| File.file?(f) }
194
+ @files.reject! { |f| f.include?('/.git/') }
195
+ @files.sort!
196
+
197
+ max_files = ARGV.find { |a| a.start_with?('--max-files=') }&.split('=')&.last&.to_i
198
+ max_files ||= ENV['DEMO_MAX_FILES']&.to_i
199
+ max_files ||= 5 if ENV['TK_READY_PORT'] # test mode -- don't hash 200+ files
200
+ @files = @files.first(max_files) if max_files && max_files > 0
201
+
202
+ @app.command('.status.info.files', 'configure', text: "#{@files.size} files")
203
+ end
204
+
205
+ def current_mode
206
+ @app.command(:set, '::mode')
207
+ end
208
+
209
+ def get_var(name)
210
+ @app.command(:set, name)
211
+ end
212
+
213
+ def set_var(name, value)
214
+ @app.command(:set, name, value)
215
+ end
216
+
217
+ def set_combo_enabled(path)
218
+ # ttk state: must clear disabled AND set readonly in one call
219
+ @app.tcl_eval("#{path} state {!disabled readonly}")
220
+ end
221
+
222
+ def start_hashing
223
+ @running = true
224
+ @paused = false
225
+ @stop_requested = false
226
+
227
+ @app.command('.ctrl.start', 'state', 'disabled')
228
+ @app.command('.ctrl.algo', 'state', 'disabled')
229
+ @app.command('.ctrl.mode', 'state', 'disabled')
230
+ @app.command('.log.f.text', 'delete', '1.0', 'end')
231
+ set_var('::progress', 0)
232
+ @app.command('.status.progress.status', 'configure', text: 'Hashing...')
233
+
234
+ if get_var('::allow_pause').to_i == 1
235
+ @app.command('.ctrl.pause', 'state', '!disabled')
236
+ else
237
+ @app.command('.ctrl.pause', 'state', 'disabled')
238
+ end
239
+
240
+ @app.command(:wm, 'resizable', '.', 0, 0) unless current_mode == 'Ractor'
241
+
242
+ @metrics = {
243
+ start_time: Process.clock_gettime(Process::CLOCK_MONOTONIC),
244
+ ui_update_count: 0,
245
+ ui_update_total_ms: 0.0,
246
+ total: @files.size,
247
+ files_done: 0,
248
+ mode: current_mode
249
+ }
250
+
251
+ mode_sym = case current_mode
252
+ when 'None', 'None+update' then :none
253
+ else current_mode.downcase.to_sym
254
+ end
255
+ start_background_work(mode_sym)
256
+ end
257
+
258
+ def toggle_pause
259
+ @paused = !@paused
260
+ @app.command('.ctrl.pause', 'configure', text: @paused ? 'Resume' : 'Pause')
261
+ @app.command('.status.progress.status', 'configure', text: @paused ? 'Paused' : 'Hashing...')
262
+ @app.command(:wm, 'resizable', '.', @paused ? 1 : 0, @paused ? 1 : 0)
263
+ if @paused
264
+ set_combo_enabled('.ctrl.mode')
265
+ else
266
+ @app.command('.ctrl.mode', 'state', 'disabled')
267
+ end
268
+
269
+ if @background_task
270
+ @paused ? @background_task.pause : @background_task.resume
271
+ end
272
+
273
+ write_metrics("PAUSED") if @paused && @metrics
274
+ end
275
+
276
+ def reset
277
+ @stop_requested = true
278
+ @paused = false
279
+ @running = false
280
+
281
+ @background_task&.stop
282
+ @background_task = nil
283
+
284
+ @app.command('.ctrl.start', 'state', '!disabled')
285
+ @app.command('.ctrl.pause', 'state', 'disabled')
286
+ @app.command('.ctrl.pause', 'configure', text: 'Pause')
287
+ set_combo_enabled('.ctrl.algo')
288
+ set_combo_enabled('.ctrl.mode')
289
+ @app.command(:wm, 'resizable', '.', 1, 1)
290
+ @app.command('.log.f.text', 'delete', '1.0', 'end')
291
+ set_var('::progress', 0)
292
+ @app.command('.status.progress.status', 'configure', text: 'Ready')
293
+ @app.command('.status.progress.file', 'configure', text: '')
294
+
295
+ set_var('::mode', 'Thread')
296
+ set_var('::algorithm', 'SHA256')
297
+ set_var('::chunk_size', 3)
298
+ @app.command('.ctrl.batch_val', 'configure', text: '3')
299
+ set_var('::allow_pause', 0)
300
+ end
301
+
302
+ def write_metrics(status = "DONE")
303
+ return unless @metrics
304
+ m = @metrics
305
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - m[:start_time]
306
+ dir = File.writable?(__dir__) ? __dir__ : Dir.tmpdir
307
+ File.open(File.join(dir, 'threading_demo_metrics.log'), 'a') do |f|
308
+ f.puts "=" * 60
309
+ f.puts "Status: #{status} at #{Time.now}"
310
+ f.puts "Mode: #{m[:mode]}"
311
+ f.puts "Algorithm: #{get_var('::algorithm')}"
312
+ f.puts "Files processed: #{m[:files_done]}/#{m[:total]}"
313
+ chunk = [get_var('::chunk_size').to_f.round, 1].max
314
+ f.puts "Batch size: #{chunk}"
315
+ f.puts "-" * 40
316
+ f.puts "Elapsed: #{elapsed.round(3)}s"
317
+ f.puts "UI updates: #{m[:ui_update_count]}"
318
+ f.puts "UI update total: #{m[:ui_update_total_ms].round(1)}ms" if m[:ui_update_total_ms]
319
+ f.puts "UI update avg: #{(m[:ui_update_total_ms] / m[:ui_update_count]).round(2)}ms" if m[:ui_update_count] > 0 && m[:ui_update_total_ms]
320
+ f.puts "Files/sec: #{(m[:files_done] / elapsed).round(1)}" if elapsed > 0
321
+ f.puts
322
+ end
323
+ end
324
+
325
+ def finish_hashing
326
+ write_metrics("DONE") unless @stop_requested
327
+ return if @stop_requested
328
+
329
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @metrics[:start_time]
330
+ files_per_sec = (@metrics[:files_done] / elapsed).round(1)
331
+ @app.command('.status.progress.status', 'configure',
332
+ text: "Done #{elapsed.round(2)}s (#{files_per_sec}/s)")
333
+ @app.command('.status.progress.file', 'configure', text: '')
334
+ @app.command('.ctrl.start', 'state', '!disabled')
335
+ @app.command('.ctrl.pause', 'state', 'disabled')
336
+ set_combo_enabled('.ctrl.algo')
337
+ set_combo_enabled('.ctrl.mode')
338
+ @app.command(:wm, 'resizable', '.', 1, 1)
339
+ @running = false
340
+ end
341
+
342
+ # ─────────────────────────────────────────────────────────────
343
+ # All modes use unified Teek::BackgroundWork API
344
+ # ─────────────────────────────────────────────────────────────
345
+
346
+ def start_background_work(mode)
347
+ ui_mode = current_mode
348
+
349
+ files = @files.dup
350
+ algo_name = get_var('::algorithm')
351
+ chunk_size = [get_var('::chunk_size').to_f.round, 1].max
352
+ base_dir = Dir.pwd
353
+ allow_pause = get_var('::allow_pause').to_i == 1
354
+
355
+ work_data = {
356
+ files: files,
357
+ algo_name: algo_name,
358
+ chunk_size: chunk_size,
359
+ base_dir: base_dir,
360
+ allow_pause: allow_pause
361
+ }
362
+
363
+ if mode == :ractor
364
+ work_data = Ractor.make_shareable({
365
+ files: Ractor.make_shareable(files.freeze),
366
+ algo_name: algo_name.freeze,
367
+ chunk_size: chunk_size,
368
+ base_dir: base_dir.freeze,
369
+ allow_pause: allow_pause
370
+ })
371
+ end
372
+
373
+ # Each progress value has unique log text — don't drop any
374
+ Teek::BackgroundWork.drop_intermediate = false
375
+
376
+ @background_task = Teek::BackgroundWork.new(@app, work_data, mode: mode) do |task, data|
377
+ algo_class = Digest.const_get(data[:algo_name])
378
+ total = data[:files].size
379
+ pending = []
380
+
381
+ data[:files].each_with_index do |path, index|
382
+ if data[:allow_pause] && pending.empty?
383
+ task.check_pause
384
+ end
385
+
386
+ begin
387
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
388
+ hash = algo_class.file(path).hexdigest
389
+ dt = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
390
+ short_path = path.sub(%r{^/app/}, '').sub(data[:base_dir] + '/', '')
391
+ pending << "#{short_path}: #{hash} #{dt < 0.01 ? format('%.5fs', dt) : format('%.2fs', dt)}\n"
392
+ rescue StandardError => e
393
+ short_path = path.sub(%r{^/app/}, '').sub(data[:base_dir] + '/', '')
394
+ pending << "#{short_path}: ERROR - #{e.message}\n"
395
+ end
396
+
397
+ is_last = index == total - 1
398
+ if pending.size >= data[:chunk_size] || is_last
399
+ task.yield({
400
+ index: index,
401
+ total: total,
402
+ updates: pending.join
403
+ })
404
+ pending = []
405
+ end
406
+ end
407
+ end
408
+
409
+ @background_task.on_progress do |msg|
410
+ ui_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
411
+
412
+ @app.command('.log.f.text', 'insert', 'end', msg[:updates])
413
+ @app.command('.log.f.text', 'see', 'end')
414
+ pct = ((msg[:index] + 1).to_f / msg[:total] * 100).round
415
+ set_var('::progress', pct)
416
+ @app.command('.status.progress.status', 'configure',
417
+ text: "Hashing... #{msg[:index] + 1}/#{msg[:total]}")
418
+
419
+ @metrics[:ui_update_count] += 1
420
+ @metrics[:ui_update_total_ms] += (Process.clock_gettime(Process::CLOCK_MONOTONIC) - ui_start) * 1000
421
+ @metrics[:files_done] = msg[:index] + 1
422
+
423
+ @app.update if ui_mode == 'None+update'
424
+ end.on_done do
425
+ @background_task = nil
426
+ finish_hashing
427
+ end
428
+ end
429
+
430
+ def run
431
+ @app.mainloop
432
+ end
433
+ end
434
+
435
+ demo = ThreadingDemo.new
436
+
437
+ # Automated demo support (testing and recording)
438
+ require_relative '../lib/teek/demo_support'
439
+ TeekDemo.app = demo.app
440
+
441
+ if TeekDemo.recording?
442
+ demo.app.set_window_geometry('+0+0')
443
+ demo.app.tcl_eval('. configure -cursor none')
444
+ TeekDemo.signal_recording_ready
445
+ end
446
+
447
+ if TeekDemo.active?
448
+ TeekDemo.after_idle {
449
+ demo.app.after(100) {
450
+ app = demo.app
451
+
452
+ # Set batch size high for fast processing
453
+ app.command(:set, '::chunk_size', 100)
454
+
455
+ # Test matrix: [mode, pause_enabled]
456
+ tests = [['None', false], ['None+update', false], ['Thread', false]]
457
+ tests << ['Ractor', false] if RACTOR_AVAILABLE
458
+ # Quick mode for smoke tests
459
+ tests = [['Thread', false]] if ARGV.include?('--quick') || TeekDemo.testing?
460
+
461
+ test_index = 0
462
+
463
+ run_next_test = proc do
464
+ if test_index < tests.size
465
+ mode, pause = tests[test_index]
466
+
467
+ # Configure mode and pause
468
+ app.command(:set, '::mode', mode)
469
+ app.command(:set, '::allow_pause', pause ? 1 : 0)
470
+
471
+ # Start hashing
472
+ app.after(100) { app.command('.ctrl.start', 'invoke') }
473
+
474
+ # Wait for completion
475
+ check_done = proc do
476
+ if demo.instance_variable_get(:@running)
477
+ app.after(200, &check_done)
478
+ else
479
+ test_index += 1
480
+ if test_index < tests.size
481
+ app.after(200) {
482
+ app.command('.ctrl.reset', 'invoke')
483
+ app.after(200, &run_next_test)
484
+ }
485
+ else
486
+ app.after(200) { TeekDemo.finish }
487
+ end
488
+ end
489
+ end
490
+ app.after(500, &check_done)
491
+ end
492
+ end
493
+
494
+ run_next_test.call
495
+ }
496
+ }
497
+ end
498
+
499
+ demo.run
data/teek.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ require_relative "lib/teek/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "teek"
5
+ spec.version = Teek::VERSION
6
+ spec.authors = ["James Cook"]
7
+ spec.email = ["jcook.rubyist@gmail.com"]
8
+
9
+ spec.summary = %q{Small and simple Tk interface (8.6+ support)}
10
+ spec.description = %q{Tk interface}
11
+ spec.homepage = "https://github.com/jamescook/teek"
12
+ spec.licenses = ["MIT"]
13
+
14
+ spec.files = Dir.glob("{lib,ext,exe,sample}/**/*").select { |f|
15
+ File.file?(f) && f !~ /\.(bundle|so|o|log)$/
16
+ } + %w[Rakefile LICENSE README.md teek.gemspec Gemfile]
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+ spec.extensions = ["ext/teek/extconf.rb"]
21
+ spec.required_ruby_version = ">= 3.2"
22
+
23
+ spec.add_development_dependency "rake", "~> 13.0"
24
+ spec.add_development_dependency "rake-compiler", "~> 1.0"
25
+ spec.add_development_dependency "simplecov", "~> 0.22"
26
+ spec.add_development_dependency "minitest", "~> 6.0"
27
+ spec.add_development_dependency "method_source", "~> 1.0"
28
+ spec.add_development_dependency "prism", "~> 1.0" # stdlib in Ruby 3.3+, gem for 3.2
29
+ spec.add_development_dependency "base64" # stdlib until Ruby 3.4, now bundled gem
30
+
31
+ spec.metadata["msys2_mingw_dependencies"] = "teek"
32
+ end