thaum 0.1.0 → 0.2.1

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +106 -14
  4. data/examples/checkbox.rb +89 -0
  5. data/examples/counter.rb +50 -0
  6. data/examples/hello_world.rb +28 -0
  7. data/examples/layout_demo.rb +138 -0
  8. data/examples/modal.rb +76 -0
  9. data/examples/mouse.rb +60 -0
  10. data/examples/octagram_picker.rb +224 -0
  11. data/examples/picker.rb +150 -0
  12. data/examples/progress_bar.rb +90 -0
  13. data/examples/scroll_view.rb +64 -0
  14. data/examples/select.rb +64 -0
  15. data/examples/spinner.rb +66 -0
  16. data/examples/status_bar.rb +65 -0
  17. data/examples/stopwatch.rb +84 -0
  18. data/examples/table.rb +196 -0
  19. data/examples/tabs.rb +112 -0
  20. data/examples/text.rb +101 -0
  21. data/examples/theme_picker.rb +95 -0
  22. data/examples/todo.rb +242 -0
  23. data/lib/thaum/action.rb +48 -0
  24. data/lib/thaum/app.rb +87 -0
  25. data/lib/thaum/color.rb +104 -0
  26. data/lib/thaum/concerns/context_update.rb +40 -0
  27. data/lib/thaum/concerns/focus.rb +53 -0
  28. data/lib/thaum/concerns/modal.rb +102 -0
  29. data/lib/thaum/concerns/tab_navigation.rb +97 -0
  30. data/lib/thaum/dispatch.rb +149 -0
  31. data/lib/thaum/escape_parser.rb +265 -0
  32. data/lib/thaum/event.rb +13 -0
  33. data/lib/thaum/events.rb +28 -0
  34. data/lib/thaum/hit_test.rb +28 -0
  35. data/lib/thaum/input_reader.rb +115 -0
  36. data/lib/thaum/key_event.rb +13 -0
  37. data/lib/thaum/keys.rb +55 -0
  38. data/lib/thaum/layout.rb +347 -0
  39. data/lib/thaum/minitest.rb +64 -0
  40. data/lib/thaum/octagram.rb +76 -0
  41. data/lib/thaum/painter.rb +49 -0
  42. data/lib/thaum/rect.rb +5 -0
  43. data/lib/thaum/rendering/box_drawing.rb +186 -0
  44. data/lib/thaum/rendering/buffer.rb +84 -0
  45. data/lib/thaum/rendering/canvas.rb +221 -0
  46. data/lib/thaum/rendering/cell.rb +11 -0
  47. data/lib/thaum/rendering/renderer.rb +98 -0
  48. data/lib/thaum/rendering/style.rb +13 -0
  49. data/lib/thaum/run_loop.rb +182 -0
  50. data/lib/thaum/seq.rb +91 -0
  51. data/lib/thaum/sigil.rb +41 -0
  52. data/lib/thaum/sigils/button.rb +47 -0
  53. data/lib/thaum/sigils/checkbox.rb +57 -0
  54. data/lib/thaum/sigils/progress_bar.rb +65 -0
  55. data/lib/thaum/sigils/scroll_view.rb +115 -0
  56. data/lib/thaum/sigils/select.rb +56 -0
  57. data/lib/thaum/sigils/spinner.rb +39 -0
  58. data/lib/thaum/sigils/status_bar.rb +89 -0
  59. data/lib/thaum/sigils/table.rb +156 -0
  60. data/lib/thaum/sigils/tabs.rb +59 -0
  61. data/lib/thaum/sigils/text.rb +22 -0
  62. data/lib/thaum/sigils/text_input.rb +86 -0
  63. data/lib/thaum/terminal.rb +46 -0
  64. data/lib/thaum/themes.rb +267 -0
  65. data/lib/thaum/tree.rb +16 -0
  66. data/lib/thaum/version.rb +1 -1
  67. data/lib/thaum.rb +64 -1
  68. metadata +115 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 887cd877416743a99c037ed2da42612f1f4d6343516dacf56aef1d85b45cd36f
4
- data.tar.gz: ca949330c9cfc15e625e59703e9dfb42448debd8991509ac98c4993cbef5361f
3
+ metadata.gz: 2a57f1cd2382c79243a8c7cb1de2294e73c63cc8c1ac5d3aa53550a1804b8e18
4
+ data.tar.gz: 15c723d96a1820f1685ebbfec62f1a8be56f497b77136a8641ebfc12dba9b8d3
5
5
  SHA512:
6
- metadata.gz: 43df40cb0447321b87854589d9e813ac7b87037c648b6a345ba0d1232a33586b092007db22c8400bdbd3fe540557fb2419ae124e89fb4952707c266c5548a1dd
7
- data.tar.gz: 4b899c569dae2b3f447eb98516da64c3d32efffe6166c9013d695fd5d9d2ae25811c92669b28928ed0fba968f882ebc276674d5d827fc3abc4aa072f7a7ebbfb
6
+ metadata.gz: a745ca6c770a9ed3fe5bacf49bbf5e0e857e3c7f343717e6383dfbf79fe88b82db5071f8fbbc58ef5c3f303d0b69a4dcb97e018bfadb50474a2fa12e0a225d41
7
+ data.tar.gz: 3d0645793d58cb962df5dcbb00f561fcb7dcb41a63b6045d026821f81a36a88ae31e3f13bede63a2572a4e22201f9707c85a129d0f0a9ca7d20b4ecec95dd2ea
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.1] - 2026-06-17
10
+
11
+ ### Fixed
12
+
13
+ - `InputReader#stop` no longer blocks ~1s on quit; the reader thread is interrupted instead of left to time out.
14
+ - Escape sequences split across read boundaries are now coalesced instead of being mis-parsed as a stray Escape plus garbage keys. `InputReader#read_chunk` now extends the read whenever a chunk ends mid-sequence (CSI/SS3/SGR-mouse or a bare ESC), bounded by a timeout and an extend cap.
15
+ - Centered/right-aligned wrapped text drawn at an x-offset is now positioned against the offset content area instead of the full canvas width.
16
+
17
+ ### Added
18
+
19
+ - Honor the `NO_COLOR` environment variable — when set and non-empty, color output is disabled.
20
+
21
+ ### Changed
22
+
23
+ - Moved `Thaum::Concerns::Layout` back to `Thaum::Layout`. Includes in `App`, `Octagram`, and downstream code should reference the top-level module.
24
+ - `Thaum::Action` raises a clear `Thaum::Error` when a background method is called outside a running app, instead of a `NoMethodError` on nil.
data/README.md CHANGED
@@ -1,39 +1,131 @@
1
- # Thaum
1
+ ```
2
+ ╭─────────────────────────────────────────────────────╮
3
+ │ ▒▒▒▒▒▒▒▒╗ ▒▒╗ ▒▒╗ ▒▒▒▒▒╗ ▒▒╗ ▒▒╗ ▒▒▒╗ ▒▒▒╗ │
4
+ │ ╚══▒▒╔══╝ ▒▒║ ▒▒║ ▒▒╔══▒▒╗ ▒▒║ ▒▒║ ▒▒▒▒╗ ▒▒▒▒║ │
5
+ │ ▒▒║ ▒▒▒▒▒▒▒║ ▒▒▒▒▒▒▒║ ▒▒║ ▒▒║ ▒▒╔▒▒▒▒╔▒▒║ │
6
+ │ ▒▒║ ▒▒╔══▒▒║ ▒▒╔══▒▒║ ▒▒║ ▒▒║ ▒▒║╚▒▒╔╝▒▒║ │
7
+ │ ▒▒║ ▒▒║ ▒▒║ ▒▒║ ▒▒║ ╚▒▒▒▒▒▒╔╝ ▒▒║ ╚═╝ ▒▒║ │
8
+ │ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ │
9
+ ╰─────────────────────────────────────────────────────╯
10
+ ```
11
+ > A Thaum is the basic unit of magical strength. It has been universally established as the amount of magic needed to create one small white pigeon or three normal-sized billiard balls. [*](#Footnotes)
12
+ >
13
+ > — Terry Pratchett
2
14
 
3
- TODO: Delete this and the text below, and describe your gem
15
+ Compose full-screen TUI apps from a few parts:
4
16
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/thaum`. To experiment with that code, run `bin/console` for an interactive prompt.
17
+ - **App** top-level container that owns layout, focus, and event handling
18
+ - **Layout** — vertical/horizontal split DSL (`region(height: 3)`, `region(width: :fill)`, …)
19
+ - **Sigil** — focusable, renderable leaf component (`Text`, `TextInput`, `Select`, `Button`, `ScrollView`, `Table`, `Spinner`, `ProgressBar`, `Checkbox`, `Tabs`)
20
+ - **Octagram** — composite that contains sigils and presents itself as a distributable component
6
21
 
7
- ## Installation
22
+ Features:
8
23
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
24
+ - Buffer-diffing renderer with synchronized output (`\e[?2026h`)
25
+ - Truecolor with automatic degradation to 256 / 16 / no-color based on `$COLORTERM` and `$TERM`
26
+ - Themes (8 built-in palettes — Solarized, Gruvbox, Catppuccin, …)
27
+ - Event dispatch: keys, paste, ticks, resize, suspend/resume
28
+ - Focus with Tab/Shift-Tab cycling and overridable `focus_order`
29
+ - Background work via `Thaum::Action` (concurrent-ruby thread pool)
30
+ - Box-drawing junction merging — adjacent borders resolve to the correct corner/tee glyph automatically, across light/heavy/double weights
31
+ - Snapshot testing via `thaum/minitest`
10
32
 
11
- Install the gem and add to the application's Gemfile by executing:
33
+ ## Installation
12
34
 
13
35
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
36
+ bundle add thaum
15
37
  ```
16
38
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
39
+ Or:
18
40
 
19
41
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
42
+ gem install thaum
21
43
  ```
22
44
 
45
+ Requires Ruby 3.2 or newer.
46
+
23
47
  ## Usage
24
48
 
25
- TODO: Write usage instructions here
49
+ The `Hello World` example:
50
+
51
+ ```ruby
52
+ require "thaum"
53
+
54
+ class HelloWorldApp
55
+ include Thaum::App
56
+
57
+ def on_key(event)
58
+ quit if event.key == :escape
59
+ end
60
+
61
+ def initialize
62
+ @greeting = Thaum::Text.new(content: "Hello World!", align: :center)
63
+ end
64
+
65
+ def partition
66
+ vertical do
67
+ region(height: :fill) { @greeting }
68
+ end
69
+ end
70
+ end
71
+
72
+ Thaum.run(HelloWorldApp.new)
73
+
74
+ ```
75
+
76
+ ## Examples
77
+
78
+ The `examples/` directory has a runnable demo for every shipped sigil plus a few composed apps:
79
+
80
+ ```bash
81
+ bundle exec ruby examples/counter.rb # minimal app
82
+ bundle exec ruby examples/picker.rb # filter-as-you-type list selector
83
+ bundle exec ruby examples/todo.rb # TextInput + Select + Button
84
+ bundle exec ruby examples/stopwatch.rb # on_tick driven
85
+ bundle exec ruby examples/theme_picker.rb # cycle the built-in themes
86
+ bundle exec ruby examples/layout_demo.rb # nested Layout DSL + border junctions
87
+ bundle exec ruby examples/octagram_picker.rb # picker packaged as an Octagram
88
+ ```
89
+
90
+ Per-sigil demos: `text.rb`, `select.rb`, `checkbox.rb`, `tabs.rb`, `spinner.rb`, `progress_bar.rb`, `scroll_view.rb`, `table.rb`.
91
+
92
+ ## Snapshot testing
93
+
94
+ ```ruby
95
+ require "thaum/minitest"
96
+
97
+ class MySigilTest < Minitest::Test
98
+ def test_renders_greeting
99
+ buffer = Thaum::Rendering::Buffer.new(width: 20, height: 3)
100
+ canvas = Thaum::Rendering::Canvas.new(buffer: buffer, rect: Thaum::Rect.new(x: 0, y: 0, width: 20, height: 3))
101
+ MySigil.new.render(canvas: canvas, theme: Thaum::Themes::DEFAULT)
102
+ assert_snapshot(buffer, "my_sigil/greeting")
103
+ end
104
+ end
105
+ ```
106
+
107
+ First run writes `test/snapshots/my_sigil/greeting.txt` (or `.ans` if the output contains ANSI styling) and passes with a warning. Re-run with `UPDATE_SNAPSHOTS=1` to refresh existing snapshots.
26
108
 
27
109
  ## Development
28
110
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
111
+ ```bash
112
+ bin/setup # bundle install
113
+ rake test # run the test suite (Minitest)
114
+ rake rubocop # lint
115
+ rake # both
116
+ bin/console # IRB with thaum required
117
+ ```
30
118
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
119
+ To install locally: `bundle exec rake install`. To cut a release: bump `lib/thaum/version.rb`, then `bundle exec rake release`.
32
120
 
33
121
  ## Contributing
34
122
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/thaum.
123
+ Bug reports and pull requests are welcome at https://github.com/urug/thaum.
36
124
 
37
125
  ## License
38
126
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
127
+ MIT. See [LICENSE.txt](LICENSE.txt).
128
+
129
+ ### Footnotes
130
+
131
+ \* Also, a terminal window
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/checkbox.rb
4
+ #
5
+ # Demonstrates Thaum::Checkbox. Four checkboxes (one indeterminate),
6
+ # Tab between them, Space or Enter to toggle. A status line shows the
7
+ # current state. Press esc to quit.
8
+
9
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
10
+ require "thaum"
11
+
12
+ class Status
13
+ include Thaum::Sigil
14
+
15
+ def initialize(boxes)
16
+ @boxes = boxes
17
+ end
18
+
19
+ def focusable? = false
20
+
21
+ def render(canvas:, theme:)
22
+ canvas.fill(bg: theme.bar_bg)
23
+ summary = @boxes.map { |b| state(b) }.join(" ")
24
+ canvas.text(content: " #{summary}", fg: theme.dim)
25
+ end
26
+
27
+ private
28
+
29
+ def state(box)
30
+ mark = if box.indeterminate?
31
+ "-"
32
+ else
33
+ box.checked? ? "✓" : " "
34
+ end
35
+ "[#{mark}] #{box.instance_variable_get(:@label)}"
36
+ end
37
+ end
38
+
39
+ class Hint
40
+ include Thaum::Sigil
41
+
42
+ def focusable? = false
43
+
44
+ def render(canvas:, theme:)
45
+ canvas.fill(bg: theme.bar_bg)
46
+ canvas.text(content: " tab next space/enter toggles esc quits", fg: theme.dim)
47
+ end
48
+ end
49
+
50
+ class CheckboxApp
51
+ include Thaum::App
52
+
53
+ def initialize
54
+ @ssl = Thaum::Checkbox.new(label: "Enable SSL", checked: true)
55
+ @notify = Thaum::Checkbox.new(label: "Email me updates")
56
+ @beta = Thaum::Checkbox.new(label: "Beta features", indeterminate: true)
57
+ @newsletter = Thaum::Checkbox.new(label: "Newsletter (twice a month)")
58
+ @status = Status.new([@ssl, @notify, @beta, @newsletter])
59
+ @hint = Hint.new
60
+ end
61
+
62
+ def theme = Thaum::Themes::CATPPUCCIN_MOCHA
63
+
64
+ def on_key(event)
65
+ quit if event.key == :escape
66
+ end
67
+
68
+ # Checkbox::ChangedEvent bubbles up on every toggle. The Status sigil reads
69
+ # each Checkbox's state at render time, so we don't need to do anything
70
+ # — but acknowledging the event keeps stderr clean.
71
+ def on_event(event)
72
+ return if event.is_a?(Thaum::Checkbox::ChangedEvent)
73
+
74
+ super
75
+ end
76
+
77
+ def partition
78
+ vertical do
79
+ region(height: 1) { @ssl }
80
+ region(height: 1) { @notify }
81
+ region(height: 1) { @beta }
82
+ region(height: 1) { @newsletter }
83
+ region(height: :fill) { @status }
84
+ region(height: 1) { @hint }
85
+ end
86
+ end
87
+ end
88
+
89
+ Thaum.run(CheckboxApp.new)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/counter.rb
4
+
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ require "thaum"
7
+
8
+ class CounterSigil
9
+ include Thaum::Sigil
10
+
11
+ def initialize
12
+ @count = 0
13
+ end
14
+
15
+ def on_key(event)
16
+ case event.key
17
+ when :up then @count += 1
18
+ when :down then @count -= 1
19
+ else emit(event) # Pass unhandled events up to the app
20
+ end
21
+ end
22
+
23
+ def render(canvas:, theme:)
24
+ y = canvas.height / 2
25
+
26
+ canvas.fill(bg: theme.bg)
27
+ canvas.text(content: "Count: #{@count}", fg: theme.fg, align: :center, y: y)
28
+ canvas.text(content: "↑ / ↓ to change escape to quit", fg: theme.dim, align: :center, y: y + 2)
29
+ end
30
+ end
31
+
32
+ class CounterApp
33
+ include Thaum::App
34
+
35
+ def initialize
36
+ @counter = CounterSigil.new
37
+ end
38
+
39
+ def on_key(event)
40
+ quit if event.key == :escape
41
+ end
42
+
43
+ def partition
44
+ vertical do
45
+ region(height: :fill) { @counter }
46
+ end
47
+ end
48
+ end
49
+
50
+ Thaum.run(CounterApp.new)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/hello_world.rb
4
+ # A minimal "Hello World" app. Press esc to quit.
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
7
+
8
+ require "thaum"
9
+
10
+ class HelloWorldApp
11
+ include Thaum::App
12
+
13
+ def on_key(event)
14
+ quit if event.key == :escape
15
+ end
16
+
17
+ def initialize
18
+ @greeting = Thaum::Text.new(content: "Hello World!", align: :center)
19
+ end
20
+
21
+ def partition
22
+ vertical do
23
+ region(height: :fill) { @greeting }
24
+ end
25
+ end
26
+ end
27
+
28
+ Thaum.run(HelloWorldApp.new)
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/layout_demo.rb
4
+ #
5
+ # Two demos in one screen:
6
+ #
7
+ # 1. OUTER LAYOUT — the App.partition uses several region settings:
8
+ # vertical with fixed heights (h: 3, h: 1) + a fill row
9
+ # horizontal with mixed (w: 12, w: :fill, w: "20%", min: 18)
10
+ # header row split with (w: "30%", w: :fill, w: 10)
11
+ # Each region holds a flat-color HeaderItem so you can see what
12
+ # Layout placed where.
13
+ #
14
+ # 2. INNER GRID — a single Grid sigil fills the center cell and draws
15
+ # multiple bordered panels on overlapping rects. The cells where
16
+ # panels meet are shared, and Buffer#set merges box-drawing glyphs,
17
+ # so all the internal junctions resolve to ┬ ├ ┤ ┴ ┼ automatically.
18
+ #
19
+ # Press esc to quit.
20
+
21
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
22
+ require "thaum"
23
+
24
+ class HeaderItem
25
+ include Thaum::Sigil
26
+
27
+ def initialize(label:, color:)
28
+ @label = label
29
+ @color = color
30
+ end
31
+
32
+ def focusable? = false
33
+
34
+ def render(canvas:, theme:)
35
+ canvas.fill(bg: @color)
36
+ y = [canvas.height / 2, 0].max
37
+ canvas.text(content: @label, align: :center, y: y, fg: theme.bg, bg: @color)
38
+ end
39
+ end
40
+
41
+ class Grid
42
+ include Thaum::Sigil
43
+
44
+ def focusable? = false
45
+
46
+ # All panels are described relative to the grid's own (0,0). Adjacent
47
+ # rects share their edge column or row by one cell so Buffer#set's
48
+ # merge produces a junction instead of two parallel lines.
49
+ def render(canvas:, theme:)
50
+ canvas.fill(bg: theme.bg)
51
+ panels = panel_panels(w: canvas.width, h: canvas.height)
52
+ panels.each { |pair| border(canvas: canvas, rect: pair.first, theme: theme) }
53
+ panels.each { |pair| label(canvas: canvas, rect: pair.first, label: pair.last, theme: theme) } # rubocop:disable Style/CombinableLoops
54
+ end
55
+
56
+ private
57
+
58
+ def panel_panels(w:, h:)
59
+ th, mh, my, by, bh = band_offsets(h)
60
+ half = (w / 2.0).ceil + 1 # +1 so the two middle cells share a column
61
+ [
62
+ [panel(x: 0, y: 0, ww: w, hh: th), "title"],
63
+ [panel(x: 0, y: my, ww: half, hh: mh), "list"],
64
+ [panel(x: half - 1, y: my, ww: w - half + 1, hh: mh), "preview"],
65
+ [panel(x: 0, y: by, ww: w, hh: bh), "status"]
66
+ ]
67
+ end
68
+
69
+ def band_offsets(h)
70
+ top_h = [(h * 0.30).round, 3].max
71
+ mid_h = [(h * 0.40).round, 3].max
72
+ mid_y = top_h - 1
73
+ bot_y = mid_y + mid_h - 1
74
+ [top_h, mid_h, mid_y, bot_y, h - bot_y]
75
+ end
76
+
77
+ def panel(x:, y:, ww:, hh:) = Thaum::Rect.new(x: x, y: y, width: ww, height: hh)
78
+
79
+ def border(canvas:, rect:, theme:)
80
+ canvas.sub(rect: rect).border(fg: theme.border)
81
+ end
82
+
83
+ def label(canvas:, rect:, label:, theme:)
84
+ inner = canvas.sub(
85
+ rect: Thaum::Rect.new(x: rect.x + 1, y: rect.y + 1, width: rect.width - 2, height: rect.height - 2)
86
+ )
87
+ inner.text(content: label, fg: theme.dim)
88
+ end
89
+ end
90
+
91
+ class LayoutDemoApp
92
+ include Thaum::App
93
+
94
+ def initialize
95
+ t = Thaum::Themes::SOLARIZED_LIGHT
96
+ @title = HeaderItem.new(label: "Layout DSL — region settings demo", color: t.accent)
97
+ @status = HeaderItem.new(label: "h:3/h:fill/h:1 outer · w:12/w::fill/w:'20%' min:18 middle", color: t.selection)
98
+ @clock = HeaderItem.new(label: "12:34", color: t.border)
99
+ @nav = HeaderItem.new(label: "w: 12", color: t.pressed)
100
+ @aside = HeaderItem.new(label: "w: '20%'\nmin: 18", color: t.bar_bg)
101
+ @grid = Grid.new
102
+ @hint = HeaderItem.new(label: " esc to quit — borders inside the grid merge into junctions", color: t.dim)
103
+ end
104
+
105
+ def theme = Thaum::Themes::SOLARIZED_LIGHT
106
+
107
+ def on_key(event)
108
+ quit if event.key == :escape
109
+ end
110
+
111
+ def partition
112
+ vertical do
113
+ region(height: 3) { header }
114
+ region(height: :fill) { middle }
115
+ region(height: 1) { @hint }
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def header
122
+ horizontal do
123
+ region(width: "30%") { @title }
124
+ region(width: :fill) { @status }
125
+ region(width: 10) { @clock }
126
+ end
127
+ end
128
+
129
+ def middle
130
+ horizontal do
131
+ region(width: 12) { @nav }
132
+ region(width: :fill) { @grid }
133
+ region(width: "20%", min: 18) { @aside }
134
+ end
135
+ end
136
+ end
137
+
138
+ Thaum.run(LayoutDemoApp.new)
data/examples/modal.rb ADDED
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/modal.rb
4
+ #
5
+ # Press "m" to open a modal. Inside the modal, press Escape (framework-
6
+ # intercepted) or "y"/"n" to dismiss. Outside the modal, Escape quits.
7
+
8
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
9
+ require "thaum"
10
+
11
+ class Background
12
+ include Thaum::Sigil
13
+
14
+ def focusable? = false
15
+
16
+ def render(canvas:, theme:)
17
+ canvas.fill(bg: theme.bg)
18
+ canvas.text(content: "Press m to open a modal, esc to quit.", fg: theme.fg, align: :center, y: canvas.height / 2)
19
+ canvas.text(content: "(Modal can be Escape-dismissed.)", fg: theme.dim, align: :center, y: (canvas.height / 2) + 2)
20
+ end
21
+ end
22
+
23
+ class Confirm
24
+ include Thaum::Sigil
25
+
26
+ DismissedEvent = Thaum::Event.define(:choice)
27
+
28
+ def render(canvas:, theme:)
29
+ inner = canvas.border(style: :rounded, fg: theme.warning_fg, bg: theme.bg)
30
+ inner.fill(bg: theme.bg)
31
+ inner.text(content: "Are you sure?", fg: theme.warning_fg, align: :center, y: 1)
32
+ inner.text(content: "[y]es [n]o Esc cancels", fg: theme.muted_fg, align: :center, y: 3)
33
+ end
34
+
35
+ def on_key(event)
36
+ case event.key
37
+ when "y", "n" then emit(DismissedEvent.new(choice: event.key))
38
+ else emit(event)
39
+ end
40
+ end
41
+ end
42
+
43
+ class ModalApp
44
+ include Thaum::App
45
+
46
+ def initialize
47
+ @choice_display = Thaum::Text.new(content: "Modal choice: none", align: :center)
48
+ @background = Background.new
49
+ end
50
+
51
+ def partition
52
+ vertical do
53
+ region(height: 1) { @choice_display }
54
+ region(height: :fill) { @background }
55
+ end
56
+ end
57
+
58
+ def on_key(event)
59
+ case event.key
60
+ when :escape then quit
61
+ when "m" then show_modal(sigil: Confirm.new, width: 36, height: 7)
62
+ end
63
+ end
64
+
65
+ def on_event(event)
66
+ case event
67
+ when Confirm::DismissedEvent
68
+ @choice_display.content = "Modal choice: #{event.choice}"
69
+ hide_modal
70
+ else
71
+ super
72
+ end
73
+ end
74
+ end
75
+
76
+ Thaum.run(ModalApp.new)
data/examples/mouse.rb ADDED
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Usage: bundle exec ruby examples/mouse.rb
4
+ #
5
+ # Click in either pane to bump its counter. Scroll wheel anywhere bumps
6
+ # the global tally. Press esc to quit.
7
+
8
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
9
+ require "thaum"
10
+
11
+ class ClickPane
12
+ include Thaum::Sigil
13
+
14
+ attr_reader :clicks
15
+
16
+ def initialize(label)
17
+ @label = label
18
+ @clicks = 0
19
+ end
20
+
21
+ def on_mouse(event)
22
+ case event.action
23
+ when :press then @clicks += 1
24
+ end
25
+ end
26
+
27
+ def render(canvas:, theme:)
28
+ canvas.fill(bg: focused? ? theme.pressed : theme.bg)
29
+ center = canvas.height / 2
30
+ canvas.text(content: @label, fg: theme.fg, align: :center, y: center - 1)
31
+ canvas.text(content: "clicks: #{@clicks}", fg: theme.fg, align: :center, y: center + 1)
32
+ end
33
+ end
34
+
35
+ class MouseApp
36
+ include Thaum::App
37
+
38
+ def initialize
39
+ @left = ClickPane.new("LEFT")
40
+ @right = ClickPane.new("RIGHT")
41
+ @scrolls = 0
42
+ end
43
+
44
+ def on_key(event)
45
+ quit if event.key == :escape
46
+ end
47
+
48
+ def on_mouse(event)
49
+ @scrolls += 1 if event.action == :scroll
50
+ end
51
+
52
+ def partition
53
+ horizontal do
54
+ region(width: :fill) { @left }
55
+ region(width: :fill) { @right }
56
+ end
57
+ end
58
+ end
59
+
60
+ Thaum.run(MouseApp.new)