tuile 0.11.0 → 0.13.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/CHANGELOG.md +77 -0
- data/DECISIONS.md +1970 -14
- data/README.md +136 -491
- data/TERMINOLOGY.md +70 -0
- data/book/01-first-app.md +22 -17
- data/book/02-repaint.md +19 -6
- data/book/03-layout.md +12 -11
- data/book/05-focus.md +133 -18
- data/book/06-theming.md +6 -3
- data/book/07-components.md +498 -38
- data/book/08-testing.md +18 -4
- data/book/README.md +7 -5
- data/examples/file_commander.rb +27 -20
- data/examples/hello_world.rb +17 -5
- data/examples/sampler.rb +422 -66
- data/ideas/arrow-key-navigation.md +16 -0
- data/ideas/new-components.md +16 -10
- data/lib/tuile/ansi.rb +10 -0
- data/lib/tuile/buffer.rb +7 -7
- data/lib/tuile/component/abstract_string_field.rb +36 -0
- data/lib/tuile/component/button.rb +1 -1
- data/lib/tuile/component/checkbox.rb +1 -1
- data/lib/tuile/component/checkbox_group.rb +31 -26
- data/lib/tuile/component/combo_box.rb +13 -8
- data/lib/tuile/component/info_window.rb +1 -1
- data/lib/tuile/component/label.rb +14 -14
- data/lib/tuile/component/list.rb +313 -216
- data/lib/tuile/component/list_dropdown.rb +100 -10
- data/lib/tuile/component/menu_bar/cascade.rb +255 -0
- data/lib/tuile/component/menu_bar.rb +582 -0
- data/lib/tuile/component/notification.rb +320 -0
- data/lib/tuile/component/picker_window.rb +3 -8
- data/lib/tuile/component/popup.rb +83 -19
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/radio_group.rb +32 -30
- data/lib/tuile/component/select.rb +10 -8
- data/lib/tuile/component/tab_sheet.rb +242 -0
- data/lib/tuile/component/tabs.rb +528 -0
- data/lib/tuile/component/text_area/wrapped_text.rb +320 -0
- data/lib/tuile/component/text_area.rb +84 -277
- data/lib/tuile/component/text_field.rb +24 -7
- data/lib/tuile/component/text_view.rb +197 -180
- data/lib/tuile/component/window.rb +8 -8
- data/lib/tuile/component.rb +43 -18
- data/lib/tuile/event_queue.rb +25 -1
- data/lib/tuile/fake_screen.rb +14 -0
- data/lib/tuile/keys.rb +65 -0
- data/lib/tuile/screen.rb +95 -78
- data/lib/tuile/screen_pane.rb +109 -27
- data/lib/tuile/styled_string.rb +52 -12
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +6 -6
- data/sig/tuile.rbs +2307 -516
- metadata +9 -3
- data/mise.toml +0 -2
data/book/README.md
CHANGED
|
@@ -66,17 +66,19 @@ one, not to fill an outline.
|
|
|
66
66
|
`focusable?`, and the three-rung order in which a keystroke is offered
|
|
67
67
|
to the tree — Tab, global shortcuts, then `handle_key` delivered to
|
|
68
68
|
focus and bubbling up its ancestors. Why scope-wide keys (pane jumps, a
|
|
69
|
-
form's default button) belong on an ancestor,
|
|
70
|
-
|
|
69
|
+
form's default button) belong on an ancestor, why a paste rides its own
|
|
70
|
+
path rather than the ladder, and how to write a status line over
|
|
71
|
+
`on_focus_changed` — Tuile draws none for you.
|
|
71
72
|
6. **[Theming](06-theming.md).** Semantic color tokens read at paint
|
|
72
73
|
time, opt-in component backgrounds that inherit down the tree
|
|
73
74
|
(`bg_color`), light/dark auto-detection at startup and live OS
|
|
74
75
|
appearance flips, pairing variants in a `ThemeDef`, app-specific custom
|
|
75
76
|
tokens, and rebuilding theme-derived content in `on_theme_changed`.
|
|
76
77
|
7. **[The component library](07-components.md).** A narrative tour of
|
|
77
|
-
the shipped toolbox —
|
|
78
|
-
|
|
79
|
-
*when and why* you reach for each.
|
|
78
|
+
the shipped toolbox — the text inputs and views, the value fields, the
|
|
79
|
+
selectors, Button, ProgressBar, Window, TabSheet, MenuBar, Popup and the
|
|
80
|
+
window conveniences — framed around *when and why* you reach for each.
|
|
81
|
+
Signatures stay in the rdoc.
|
|
80
82
|
8. **[Testing a Tuile app](08-testing.md).** The testing approach:
|
|
81
83
|
`FakeScreen`, asserting against the painted buffer, driving
|
|
82
84
|
invalidation, and PTY-based end-to-end tests of runnable scripts.
|
data/examples/file_commander.rb
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
# Tuile two-pane file commander. Two windows side by side, each showing a
|
|
5
5
|
# directory listing. Tab switches active pane; arrows / jk move the cursor;
|
|
6
6
|
# Enter descends into a directory (no-op on a regular file); Backspace
|
|
7
|
-
# ascends to the parent. The header label shows the active pane's cwd
|
|
7
|
+
# ascends to the parent. The header label shows the active pane's cwd, and a
|
|
8
|
+
# static status line spells out the keys — Tuile draws no status bar and
|
|
9
|
+
# reserves no row, so both are ordinary children of the layout.
|
|
8
10
|
# Unreadable directories surface an InfoWindow. Layout follows the
|
|
9
11
|
# terminal on resize (WINCH) — the framework dispatches a TTYSizeEvent and
|
|
10
12
|
# the layout's `rect=` rebuilds the geometry.
|
|
@@ -33,6 +35,7 @@ module FileCommanderExample
|
|
|
33
35
|
def initialize(start_dir)
|
|
34
36
|
super()
|
|
35
37
|
self.cursor = Tuile::Component::List::Cursor.new
|
|
38
|
+
self.renderer = ->(entry) { Rainbow(entry[:display]).color(TYPE_COLORS[entry[:type]]) }
|
|
36
39
|
@cwd = File.expand_path(start_dir)
|
|
37
40
|
@on_cwd_changed = nil
|
|
38
41
|
load_entries
|
|
@@ -60,8 +63,8 @@ module FileCommanderExample
|
|
|
60
63
|
|
|
61
64
|
private
|
|
62
65
|
|
|
63
|
-
def descend(_index,
|
|
64
|
-
target = File.expand_path(File.join(@cwd,
|
|
66
|
+
def descend(_index, entry)
|
|
67
|
+
target = File.expand_path(File.join(@cwd, entry[:name]))
|
|
65
68
|
change_to(target) if File.directory?(target)
|
|
66
69
|
end
|
|
67
70
|
|
|
@@ -75,7 +78,7 @@ module FileCommanderExample
|
|
|
75
78
|
@cwd = path
|
|
76
79
|
load_entries
|
|
77
80
|
self.cursor = Tuile::Component::List::Cursor.new
|
|
78
|
-
self.
|
|
81
|
+
self.scroll_top_row = 0
|
|
79
82
|
@on_cwd_changed&.call
|
|
80
83
|
rescue SystemCallError => e
|
|
81
84
|
@cwd = previous
|
|
@@ -89,7 +92,7 @@ module FileCommanderExample
|
|
|
89
92
|
{ name: name, type: classify(path), display: is_dir ? "#{name}/" : name, dir_first: is_dir ? 0 : 1 }
|
|
90
93
|
end
|
|
91
94
|
entries.sort_by! { |e| [e[:dir_first], e[:name].downcase] }
|
|
92
|
-
self.
|
|
95
|
+
self.items = entries
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
# Classify by symlink first so a symlink-to-dir still reads as a link.
|
|
@@ -106,18 +109,6 @@ module FileCommanderExample
|
|
|
106
109
|
end
|
|
107
110
|
end
|
|
108
111
|
|
|
109
|
-
# A pane window that advertises navigation shortcuts in the status bar.
|
|
110
|
-
# The active window's `keyboard_hint` is rendered by {Tuile::Screen}
|
|
111
|
-
# alongside the global `q` quit hint, so all the user-facing controls
|
|
112
|
-
# land in one place.
|
|
113
|
-
class PaneWindow < Tuile::Component::Window
|
|
114
|
-
def keyboard_hint
|
|
115
|
-
"Tab #{screen.theme.hint("Switch")} " \
|
|
116
|
-
"Enter #{screen.theme.hint("Open")} " \
|
|
117
|
-
"Bksp #{screen.theme.hint("Up")}"
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
112
|
# Top-level layout. Header label on the first row, two side-by-side
|
|
122
113
|
# windows below. `rect=` re-runs on the initial mount and on every WINCH,
|
|
123
114
|
# so the split tracks the terminal size automatically.
|
|
@@ -127,19 +118,34 @@ module FileCommanderExample
|
|
|
127
118
|
@header = Tuile::Component::Label.new
|
|
128
119
|
add(@header)
|
|
129
120
|
|
|
130
|
-
@left_window =
|
|
121
|
+
@left_window = Tuile::Component::Window.new
|
|
131
122
|
@left_list = DirList.new(left_dir)
|
|
132
123
|
@left_list.on_cwd_changed = method(:refresh_header)
|
|
133
124
|
@left_window.content = @left_list
|
|
134
125
|
@left_window.scrollbar = true
|
|
135
126
|
add(@left_window)
|
|
136
127
|
|
|
137
|
-
@right_window =
|
|
128
|
+
@right_window = Tuile::Component::Window.new
|
|
138
129
|
@right_list = DirList.new(right_dir)
|
|
139
130
|
@right_list.on_cwd_changed = method(:refresh_header)
|
|
140
131
|
@right_window.content = @right_list
|
|
141
132
|
@right_window.scrollbar = true
|
|
142
133
|
add(@right_window)
|
|
134
|
+
|
|
135
|
+
# The status line. Every key here works in both panes, so the row never
|
|
136
|
+
# changes and nothing needs to watch focus — a status line is only worth
|
|
137
|
+
# wiring to Tuile::Screen#on_focus_changed= when its text actually varies
|
|
138
|
+
# with the focused component. `theme.hint` bakes its colors in, so the
|
|
139
|
+
# one thing this label does watch is a light/dark flip.
|
|
140
|
+
@status = Tuile::Component::Label.new
|
|
141
|
+
render_status = lambda do
|
|
142
|
+
t = screen.theme
|
|
143
|
+
@status.text = "q #{t.hint("quit")} Tab #{t.hint("Switch")} " \
|
|
144
|
+
"Enter #{t.hint("Open")} Bksp #{t.hint("Up")}"
|
|
145
|
+
end
|
|
146
|
+
render_status.call
|
|
147
|
+
@status.on_theme_changed = render_status
|
|
148
|
+
add(@status)
|
|
143
149
|
end
|
|
144
150
|
|
|
145
151
|
attr_reader :left_window
|
|
@@ -149,8 +155,9 @@ module FileCommanderExample
|
|
|
149
155
|
return if rect.empty?
|
|
150
156
|
|
|
151
157
|
@header.rect = Tuile::Rect.new(rect.left, rect.top, rect.width, 1)
|
|
158
|
+
@status.rect = Tuile::Rect.new(rect.left, rect.top + rect.height - 1, rect.width, 1)
|
|
152
159
|
body_top = rect.top + 1
|
|
153
|
-
body_height = [rect.height -
|
|
160
|
+
body_height = [rect.height - 2, 0].max
|
|
154
161
|
half = rect.width / 2
|
|
155
162
|
@left_window.rect = Tuile::Rect.new(rect.left, body_top, half, body_height)
|
|
156
163
|
@right_window.rect = Tuile::Rect.new(rect.left + half, body_top,
|
data/examples/hello_world.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Tuile hello-world. A Window wrapping a Label
|
|
4
|
+
# Tuile hello-world. A Window wrapping a Label, over a status line the app
|
|
5
|
+
# owns — Tuile draws no chrome of its own and reserves no row.
|
|
5
6
|
#
|
|
6
7
|
# Run from the gem root:
|
|
7
8
|
# bundle exec ruby -Ilib examples/hello_world.rb
|
|
@@ -14,12 +15,23 @@ require "tuile"
|
|
|
14
15
|
# Tuile::Screen.instance during invalidate/repaint hooks.
|
|
15
16
|
screen = Tuile::Screen.new
|
|
16
17
|
|
|
17
|
-
label = Tuile::Component::Label.new("Hello, world!")
|
|
18
|
-
|
|
19
18
|
window = Tuile::Component::Window.new("Tuile")
|
|
20
|
-
window.content =
|
|
19
|
+
window.content = Tuile::Component::Label.new("Hello, world!")
|
|
20
|
+
|
|
21
|
+
# The status line. `theme.hint` styles the *description* half of a "key what"
|
|
22
|
+
# pair, and bakes the color in — so the label rebuilds itself from
|
|
23
|
+
# `on_theme_changed` to follow a light/dark flip.
|
|
24
|
+
status = Tuile::Component::Label.new
|
|
25
|
+
render_status = -> { status.text = "q #{screen.theme.hint("quit")}" }
|
|
26
|
+
render_status.call
|
|
27
|
+
status.on_theme_changed = render_status
|
|
28
|
+
|
|
29
|
+
# One row for the status line, everything else to the window.
|
|
30
|
+
root = Tuile::Component::Layout::Vertical.new
|
|
31
|
+
root.add(window, Tuile::Component::Layout::Expand[1])
|
|
32
|
+
root.add(status, Tuile::Component::Layout::Fixed[1])
|
|
21
33
|
|
|
22
|
-
screen.content =
|
|
34
|
+
screen.content = root
|
|
23
35
|
window.focus
|
|
24
36
|
begin
|
|
25
37
|
screen.run_event_loop
|