thaum 0.1.0 → 0.2.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 +4 -4
- data/README.md +106 -14
- data/examples/checkbox.rb +89 -0
- data/examples/counter.rb +50 -0
- data/examples/hello_world.rb +28 -0
- data/examples/layout_demo.rb +138 -0
- data/examples/modal.rb +76 -0
- data/examples/mouse.rb +60 -0
- data/examples/octagram_picker.rb +224 -0
- data/examples/picker.rb +150 -0
- data/examples/progress_bar.rb +90 -0
- data/examples/scroll_view.rb +64 -0
- data/examples/select.rb +64 -0
- data/examples/spinner.rb +66 -0
- data/examples/status_bar.rb +65 -0
- data/examples/stopwatch.rb +84 -0
- data/examples/table.rb +196 -0
- data/examples/tabs.rb +112 -0
- data/examples/text.rb +101 -0
- data/examples/theme_picker.rb +95 -0
- data/examples/todo.rb +242 -0
- data/lib/thaum/action.rb +30 -0
- data/lib/thaum/app.rb +87 -0
- data/lib/thaum/color.rb +97 -0
- data/lib/thaum/concerns/context_update.rb +40 -0
- data/lib/thaum/concerns/focus.rb +53 -0
- data/lib/thaum/concerns/layout.rb +349 -0
- data/lib/thaum/concerns/modal.rb +102 -0
- data/lib/thaum/concerns/tab_navigation.rb +97 -0
- data/lib/thaum/dispatch.rb +149 -0
- data/lib/thaum/escape_parser.rb +265 -0
- data/lib/thaum/event.rb +13 -0
- data/lib/thaum/events.rb +28 -0
- data/lib/thaum/hit_test.rb +28 -0
- data/lib/thaum/input_reader.rb +46 -0
- data/lib/thaum/key_event.rb +13 -0
- data/lib/thaum/keys.rb +55 -0
- data/lib/thaum/minitest.rb +64 -0
- data/lib/thaum/octagram.rb +76 -0
- data/lib/thaum/painter.rb +49 -0
- data/lib/thaum/rect.rb +5 -0
- data/lib/thaum/rendering/box_drawing.rb +186 -0
- data/lib/thaum/rendering/buffer.rb +84 -0
- data/lib/thaum/rendering/canvas.rb +219 -0
- data/lib/thaum/rendering/cell.rb +11 -0
- data/lib/thaum/rendering/renderer.rb +98 -0
- data/lib/thaum/rendering/style.rb +13 -0
- data/lib/thaum/run_loop.rb +182 -0
- data/lib/thaum/seq.rb +91 -0
- data/lib/thaum/sigil.rb +41 -0
- data/lib/thaum/sigils/button.rb +47 -0
- data/lib/thaum/sigils/checkbox.rb +57 -0
- data/lib/thaum/sigils/progress_bar.rb +65 -0
- data/lib/thaum/sigils/scroll_view.rb +115 -0
- data/lib/thaum/sigils/select.rb +56 -0
- data/lib/thaum/sigils/spinner.rb +39 -0
- data/lib/thaum/sigils/status_bar.rb +89 -0
- data/lib/thaum/sigils/table.rb +156 -0
- data/lib/thaum/sigils/tabs.rb +59 -0
- data/lib/thaum/sigils/text.rb +22 -0
- data/lib/thaum/sigils/text_input.rb +86 -0
- data/lib/thaum/terminal.rb +46 -0
- data/lib/thaum/themes.rb +267 -0
- data/lib/thaum/tree.rb +16 -0
- data/lib/thaum/version.rb +1 -1
- data/lib/thaum.rb +64 -1
- metadata +114 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edf87796b15b377e1fb9630d63a89c488c3ef127afe5f86b00748ffe40ea6bc2
|
|
4
|
+
data.tar.gz: 4a3b2b5883f17d2a3050dc9ca0e07802c722757b4f27516ea92b73eafea53b44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd4dac7b5e97854e8347f382101690d15216450e64de3626ac561629a763811ce6557ece95968f41cf6dfc3bd3c44357df68592dfaf16f83d3fa9b88596824c2
|
|
7
|
+
data.tar.gz: d7855d4542eb89480e215f8dd42acd9c15e444db41a9fe83acc7f1cb9f6b0d4baa02803352454767f4ce3fa8f6894063a6f29f5395db031e750d069497dbe34e
|
data/README.md
CHANGED
|
@@ -1,39 +1,131 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
15
|
+
Compose full-screen TUI apps from a few parts:
|
|
4
16
|
|
|
5
|
-
|
|
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
|
-
|
|
22
|
+
Features:
|
|
8
23
|
|
|
9
|
-
|
|
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
|
-
|
|
33
|
+
## Installation
|
|
12
34
|
|
|
13
35
|
```bash
|
|
14
|
-
bundle add
|
|
36
|
+
bundle add thaum
|
|
15
37
|
```
|
|
16
38
|
|
|
17
|
-
|
|
39
|
+
Or:
|
|
18
40
|
|
|
19
41
|
```bash
|
|
20
|
-
gem install
|
|
42
|
+
gem install thaum
|
|
21
43
|
```
|
|
22
44
|
|
|
45
|
+
Requires Ruby 3.2 or newer.
|
|
46
|
+
|
|
23
47
|
## Usage
|
|
24
48
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
123
|
+
Bug reports and pull requests are welcome at https://github.com/urug/thaum.
|
|
36
124
|
|
|
37
125
|
## License
|
|
38
126
|
|
|
39
|
-
|
|
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)
|
data/examples/counter.rb
ADDED
|
@@ -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)
|