tuile 0.4.0 → 0.6.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 +28 -0
- data/README.md +150 -4
- data/examples/file_commander.rb +4 -3
- data/examples/sampler.rb +1 -0
- data/lib/tuile/ansi.rb +4 -3
- data/lib/tuile/color.rb +249 -0
- data/lib/tuile/component/button.rb +9 -5
- data/lib/tuile/component/label.rb +44 -16
- data/lib/tuile/component/list.rb +29 -19
- data/lib/tuile/component/picker_window.rb +2 -2
- data/lib/tuile/component/popup.rb +11 -1
- data/lib/tuile/component/text_area.rb +1 -2
- data/lib/tuile/component/text_field.rb +1 -2
- data/lib/tuile/component/text_input.rb +10 -15
- data/lib/tuile/component/text_view.rb +696 -58
- data/lib/tuile/component/window.rb +70 -16
- data/lib/tuile/component.rb +74 -5
- data/lib/tuile/event_queue.rb +130 -11
- data/lib/tuile/fake_event_queue.rb +69 -0
- data/lib/tuile/fake_screen.rb +8 -0
- data/lib/tuile/keys.rb +10 -0
- data/lib/tuile/screen.rb +98 -4
- data/lib/tuile/sizing.rb +59 -0
- data/lib/tuile/styled_string.rb +28 -61
- data/lib/tuile/terminal_background.rb +137 -0
- data/lib/tuile/theme.rb +202 -0
- data/lib/tuile/theme_def.rb +85 -0
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile.rb +0 -1
- data/sig/tuile.rbs +1160 -93
- metadata +6 -15
data/lib/tuile/component/list.rb
CHANGED
|
@@ -14,14 +14,9 @@ module Tuile
|
|
|
14
14
|
#
|
|
15
15
|
# Cursor is supported; call {#cursor=} to change cursor behavior. The
|
|
16
16
|
# cursor responds to arrows, `jk`, Home/End, Ctrl+U/D and scrolls the
|
|
17
|
-
# list automatically. The cursor highlight overlays
|
|
18
|
-
# while preserving each span's foreground color.
|
|
17
|
+
# list automatically. The cursor highlight overlays
|
|
18
|
+
# {Theme#active_bg_color} while preserving each span's foreground color.
|
|
19
19
|
class List < Component
|
|
20
|
-
# 256-color SGR index for the cursor-row background highlight. Matches
|
|
21
|
-
# what `Rainbow(...).bg(:darkslategray)` emits.
|
|
22
|
-
# @return [Integer]
|
|
23
|
-
CURSOR_BG = 59
|
|
24
|
-
|
|
25
20
|
def initialize
|
|
26
21
|
super
|
|
27
22
|
@lines = []
|
|
@@ -135,11 +130,11 @@ module Tuile
|
|
|
135
130
|
raise TypeError, "expected Array, got #{lines.inspect}" unless lines.is_a? Array
|
|
136
131
|
|
|
137
132
|
@lines = parse_input_lines(lines)
|
|
138
|
-
@content_size = nil
|
|
139
133
|
rebuild_padded_lines
|
|
140
134
|
update_top_line_if_auto_scroll
|
|
141
135
|
notify_cursor_changed
|
|
142
136
|
invalidate
|
|
137
|
+
self.content_size = compute_content_size
|
|
143
138
|
end
|
|
144
139
|
|
|
145
140
|
# Without a block, returns the current lines. With a block, fully
|
|
@@ -181,20 +176,11 @@ module Tuile
|
|
|
181
176
|
screen.check_locked
|
|
182
177
|
new_lines = parse_input_lines(lines)
|
|
183
178
|
@lines += new_lines
|
|
184
|
-
@content_size = nil
|
|
185
179
|
@padded_lines += new_lines.map { |line| pad_to_row(line) }
|
|
186
180
|
update_top_line_if_auto_scroll
|
|
187
181
|
notify_cursor_changed
|
|
188
182
|
invalidate
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# @return [Size]
|
|
192
|
-
def content_size
|
|
193
|
-
@content_size ||= begin
|
|
194
|
-
content_w = @lines.map(&:display_width).max || 0
|
|
195
|
-
width = @lines.empty? ? 0 : content_w + 2
|
|
196
|
-
Size.new(width, @lines.size)
|
|
197
|
-
end
|
|
183
|
+
grow_content_size(new_lines)
|
|
198
184
|
end
|
|
199
185
|
|
|
200
186
|
def focusable? = true
|
|
@@ -508,6 +494,30 @@ module Tuile
|
|
|
508
494
|
|
|
509
495
|
private
|
|
510
496
|
|
|
497
|
+
# Natural size from scratch: longest line's display width plus the two
|
|
498
|
+
# single-space gutters {#pad_to_row} adds, × line count. An empty list
|
|
499
|
+
# is {Size::ZERO} (no gutters for no content).
|
|
500
|
+
# @return [Size]
|
|
501
|
+
def compute_content_size
|
|
502
|
+
content_w = @lines.map(&:display_width).max || 0
|
|
503
|
+
width = @lines.empty? ? 0 : content_w + 2
|
|
504
|
+
Size.new(width, @lines.size)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Incremental {#content_size} update for appends: folds just the
|
|
508
|
+
# appended lines into the running maximum, keeping {#add_lines}
|
|
509
|
+
# O(appended) instead of re-scanning the whole list (LogWindow appends
|
|
510
|
+
# a line per log statement).
|
|
511
|
+
# @param appended [Array<StyledString>] the just-appended lines
|
|
512
|
+
# (already concatenated onto {@lines}).
|
|
513
|
+
# @return [void]
|
|
514
|
+
def grow_content_size(appended)
|
|
515
|
+
return if appended.empty?
|
|
516
|
+
|
|
517
|
+
appended_w = appended.map(&:display_width).max + 2
|
|
518
|
+
self.content_size = Size.new([content_size.width, appended_w].max, @lines.size)
|
|
519
|
+
end
|
|
520
|
+
|
|
511
521
|
# Coerces and flattens a list of input entries into trimmed
|
|
512
522
|
# {StyledString} lines. Each entry becomes a {StyledString} (String
|
|
513
523
|
# via {StyledString.parse}, StyledString passed through, anything else
|
|
@@ -731,7 +741,7 @@ module Tuile
|
|
|
731
741
|
def paintable_line(index, row_in_viewport, scrollbar)
|
|
732
742
|
base = index < @lines.size ? @padded_lines[index] : @blank_padded
|
|
733
743
|
is_cursor = (active? || @show_cursor_when_inactive) && index < @lines.size && @cursor.position == index
|
|
734
|
-
styled = is_cursor ? base.with_bg(
|
|
744
|
+
styled = is_cursor ? base.with_bg(screen.theme.active_bg_color) : base
|
|
735
745
|
out = styled.to_ansi
|
|
736
746
|
out += scrollbar.scrollbar_char(row_in_viewport) if scrollbar
|
|
737
747
|
out
|
|
@@ -37,7 +37,7 @@ module Tuile
|
|
|
37
37
|
@options = options.map { Option.new(it[0], it[1]) }
|
|
38
38
|
@block = block
|
|
39
39
|
list = Component::List.new
|
|
40
|
-
list.lines = @options.map { "#{it.key} #{
|
|
40
|
+
list.lines = @options.map { "#{it.key} #{screen.theme.hint(it.caption)}" }
|
|
41
41
|
list.cursor = Component::List::Cursor.new
|
|
42
42
|
list.on_item_chosen = ->(index, _line) { select_option(@options[index].key) }
|
|
43
43
|
self.content = list
|
|
@@ -65,7 +65,7 @@ module Tuile
|
|
|
65
65
|
|
|
66
66
|
# @return [String]
|
|
67
67
|
def keyboard_hint
|
|
68
|
-
@options.map { "#{it.key} #{
|
|
68
|
+
@options.map { "#{it.key} #{screen.theme.hint(it.caption)}" }.join(" ")
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
# Opens a picker as a popup. Picking an option fires `block`, then
|
|
@@ -81,10 +81,20 @@ module Tuile
|
|
|
81
81
|
update_rect unless new_content.nil?
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Re-sizes (and recenters, when open) whenever the wrapped content's
|
|
85
|
+
# natural size changes — e.g. a {Label}'s `text=`, a {List}'s
|
|
86
|
+
# `add_line`, or a nested {Window} whose own content grew (the window
|
|
87
|
+
# recomputes its {Component#content_size} and the change bubbles here).
|
|
88
|
+
# @param _child [Component]
|
|
89
|
+
# @return [void]
|
|
90
|
+
def on_child_content_size_changed(_child)
|
|
91
|
+
update_rect
|
|
92
|
+
end
|
|
93
|
+
|
|
84
94
|
# Hint for the status bar: own "q Close" plus the wrapped content's hint.
|
|
85
95
|
# @return [String]
|
|
86
96
|
def keyboard_hint
|
|
87
|
-
prefix = "q #{
|
|
97
|
+
prefix = "q #{screen.theme.hint("Close")}"
|
|
88
98
|
child_hint = @content&.keyboard_hint.to_s
|
|
89
99
|
child_hint.empty? ? prefix : "#{prefix} #{child_hint}"
|
|
90
100
|
end
|
|
@@ -70,7 +70,6 @@ module Tuile
|
|
|
70
70
|
def repaint
|
|
71
71
|
return if rect.empty?
|
|
72
72
|
|
|
73
|
-
bg = active? ? ACTIVE_BG_SGR : INACTIVE_BG_SGR
|
|
74
73
|
rows = display_rows
|
|
75
74
|
(0...rect.height).each do |screen_row|
|
|
76
75
|
row_idx = screen_row + @top_display_row
|
|
@@ -81,7 +80,7 @@ module Tuile
|
|
|
81
80
|
chunk = @text[r[:start], r[:length]] || ""
|
|
82
81
|
chunk + (" " * (rect.width - r[:length]))
|
|
83
82
|
end
|
|
84
|
-
screen.print TTY::Cursor.move_to(rect.left, rect.top + screen_row),
|
|
83
|
+
screen.print TTY::Cursor.move_to(rect.left, rect.top + screen_row), background(line)
|
|
85
84
|
end
|
|
86
85
|
end
|
|
87
86
|
|
|
@@ -59,9 +59,8 @@ module Tuile
|
|
|
59
59
|
def repaint
|
|
60
60
|
return if rect.empty?
|
|
61
61
|
|
|
62
|
-
bg = active? ? ACTIVE_BG_SGR : INACTIVE_BG_SGR
|
|
63
62
|
padded = @text + (" " * (rect.width - @text.length))
|
|
64
|
-
screen.print TTY::Cursor.move_to(rect.left, rect.top),
|
|
63
|
+
screen.print TTY::Cursor.move_to(rect.left, rect.top), background(padded)
|
|
65
64
|
end
|
|
66
65
|
|
|
67
66
|
protected
|
|
@@ -88,21 +88,6 @@ module Tuile
|
|
|
88
88
|
invalidate
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
# 256-color SGR for the focused-button highlight (matches what
|
|
92
|
-
# `Rainbow(...).bg(:darkslategray)` emits, which is what
|
|
93
|
-
# {Component::Button#repaint} uses for its focused state).
|
|
94
|
-
# @return [String]
|
|
95
|
-
ACTIVE_BG_SGR = "\e[48;5;59m"
|
|
96
|
-
# 256-color SGR for the unfocused field's "well": index 238 sits in
|
|
97
|
-
# the grayscale ramp (~#444444), bright enough to stand out against
|
|
98
|
-
# non-pure-black terminal themes (Gruvbox/Solarized/OneDark base
|
|
99
|
-
# backgrounds sit in the #1d–#2d range), and still distinctly darker
|
|
100
|
-
# than the active highlight at index 59 (~#5f5f5f). Rainbow's
|
|
101
|
-
# RGB-to-256 mapping snaps everything dark to palette index 16
|
|
102
|
-
# (terminal black), so we emit the escape directly to reach the ramp.
|
|
103
|
-
# @return [String]
|
|
104
|
-
INACTIVE_BG_SGR = "\e[48;5;238m"
|
|
105
|
-
|
|
106
91
|
# Handles a key. Returns false when the component is inactive. Otherwise
|
|
107
92
|
# first runs the {Component#handle_key} shortcut search via `super`, then
|
|
108
93
|
# delegates to {#handle_text_input_key}.
|
|
@@ -117,6 +102,16 @@ module Tuile
|
|
|
117
102
|
|
|
118
103
|
protected
|
|
119
104
|
|
|
105
|
+
# Renders `text` on the field's background well, looked up from the
|
|
106
|
+
# current {Screen#theme} at paint time: {Theme#active_bg} when this
|
|
107
|
+
# input is on the active (focus) chain, {Theme#input_bg} otherwise —
|
|
108
|
+
# visibly a field either way, distinctly highlighted when active.
|
|
109
|
+
# @param text [String]
|
|
110
|
+
# @return [String] ANSI-rendered text.
|
|
111
|
+
def background(text)
|
|
112
|
+
active? ? screen.theme.active_bg(text) : screen.theme.input_bg(text)
|
|
113
|
+
end
|
|
114
|
+
|
|
120
115
|
# Input filter for {#text=}. Subclasses override to truncate or reject
|
|
121
116
|
# invalid input. Default coerces to String.
|
|
122
117
|
# @param new_text [String]
|