rubytui 1.2.3
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +574 -0
- data/lib/rubytui/app.rb +174 -0
- data/lib/rubytui/backend.rb +158 -0
- data/lib/rubytui/backends/ansi_backend.rb +391 -0
- data/lib/rubytui/backends/test_backend.rb +223 -0
- data/lib/rubytui/buffer.rb +400 -0
- data/lib/rubytui/cell.rb +55 -0
- data/lib/rubytui/color.rb +153 -0
- data/lib/rubytui/color_mode.rb +203 -0
- data/lib/rubytui/errors.rb +13 -0
- data/lib/rubytui/event.rb +161 -0
- data/lib/rubytui/frame.rb +70 -0
- data/lib/rubytui/input/key.rb +93 -0
- data/lib/rubytui/input/parser.rb +231 -0
- data/lib/rubytui/input/reader.rb +119 -0
- data/lib/rubytui/layout/constraint.rb +83 -0
- data/lib/rubytui/layout/flex.rb +16 -0
- data/lib/rubytui/layout/layout.rb +205 -0
- data/lib/rubytui/modifier.rb +67 -0
- data/lib/rubytui/rect.rb +126 -0
- data/lib/rubytui/stateful_widget.rb +20 -0
- data/lib/rubytui/style.rb +143 -0
- data/lib/rubytui/symbols.rb +88 -0
- data/lib/rubytui/terminal.rb +218 -0
- data/lib/rubytui/text/line.rb +67 -0
- data/lib/rubytui/text/span.rb +34 -0
- data/lib/rubytui/text/text.rb +82 -0
- data/lib/rubytui/unicode.rb +162 -0
- data/lib/rubytui/version.rb +6 -0
- data/lib/rubytui/widget.rb +20 -0
- data/lib/rubytui/widgets/async_image.rb +248 -0
- data/lib/rubytui/widgets/block.rb +260 -0
- data/lib/rubytui/widgets/canvas.rb +248 -0
- data/lib/rubytui/widgets/chart.rb +224 -0
- data/lib/rubytui/widgets/gauge.rb +139 -0
- data/lib/rubytui/widgets/image.rb +330 -0
- data/lib/rubytui/widgets/input_field.rb +245 -0
- data/lib/rubytui/widgets/list.rb +186 -0
- data/lib/rubytui/widgets/paragraph.rb +181 -0
- data/lib/rubytui/widgets/popup.rb +140 -0
- data/lib/rubytui/widgets/scrollbar.rb +175 -0
- data/lib/rubytui/widgets/sparkline.rb +86 -0
- data/lib/rubytui/widgets/table.rb +231 -0
- data/lib/rubytui/widgets/tabs.rb +90 -0
- data/lib/rubytui.rb +389 -0
- metadata +89 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyTUI
|
|
4
|
+
# Detects and manages terminal color capabilities.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# ColorMode.force(ColorMode::COLOR256)
|
|
8
|
+
# ColorMode.downgrade(Color.rgb(255, 128, 0)) # => Color.indexed(214)
|
|
9
|
+
module ColorMode
|
|
10
|
+
# Basic 16-color ANSI palette.
|
|
11
|
+
BASIC16 = :basic16
|
|
12
|
+
# 256-color indexed palette.
|
|
13
|
+
COLOR256 = :color256
|
|
14
|
+
# 24-bit RGB truecolor.
|
|
15
|
+
TRUECOLOR = :truecolor
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# Detect terminal color capability from environment variables.
|
|
19
|
+
#
|
|
20
|
+
# Returns {TRUECOLOR} when +COLORTERM+ is +truecolor+ or +24bit+, +TERM+
|
|
21
|
+
# contains +24bit+ or +truecolor+, or +TERM_PROGRAM+ is one of
|
|
22
|
+
# +iterm.app+, +hyper+, +wezterm+, +alacritty+, +kitty+;
|
|
23
|
+
# {COLOR256} when +TERM+ contains +256color+ or +COLORTERM+ is +256+;
|
|
24
|
+
# otherwise {BASIC16}. All comparisons are case-insensitive. The result
|
|
25
|
+
# is cached until {.reset!}, and {.force} overrides it.
|
|
26
|
+
#
|
|
27
|
+
# @return [Symbol] the detected (or forced) color mode
|
|
28
|
+
def detect
|
|
29
|
+
return @detected if defined?(@detected)
|
|
30
|
+
|
|
31
|
+
@detected = detect_from_env
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Reset cached detection (for testing)
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
def reset!
|
|
38
|
+
remove_instance_variable(:@detected) if defined?(@detected)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Force a specific color mode
|
|
42
|
+
#
|
|
43
|
+
# Replaces the cached value returned by {.detect} until {.reset!}.
|
|
44
|
+
#
|
|
45
|
+
# @param mode [Symbol] color mode to report, normally {BASIC16},
|
|
46
|
+
# {COLOR256} or {TRUECOLOR}
|
|
47
|
+
# @return [Symbol] +mode+
|
|
48
|
+
def force(mode)
|
|
49
|
+
@detected = mode
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Downgrade a Color to fit the current terminal capability.
|
|
53
|
+
#
|
|
54
|
+
# {TRUECOLOR} (or an unrecognized mode) returns +color+ unchanged.
|
|
55
|
+
# {COLOR256} maps RGB colors to the nearest indexed color. {BASIC16}
|
|
56
|
+
# maps RGB and indexed colors to the nearest named color. Colors that
|
|
57
|
+
# already fit the mode are returned unchanged.
|
|
58
|
+
#
|
|
59
|
+
# @param color [Color, nil] color to convert
|
|
60
|
+
# @param mode [Symbol] target color mode (default: {.detect})
|
|
61
|
+
# @return [Color, nil] the converted color, or nil if +color+ is nil
|
|
62
|
+
def downgrade(color, mode = detect)
|
|
63
|
+
return color if color.nil?
|
|
64
|
+
|
|
65
|
+
case mode
|
|
66
|
+
when TRUECOLOR
|
|
67
|
+
color # no downgrade needed
|
|
68
|
+
when COLOR256
|
|
69
|
+
downgrade_to_256(color)
|
|
70
|
+
when BASIC16
|
|
71
|
+
downgrade_to_16(color)
|
|
72
|
+
else
|
|
73
|
+
color
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def detect_from_env
|
|
80
|
+
colorterm = ENV["COLORTERM"]&.downcase
|
|
81
|
+
term = ENV["TERM"]&.downcase || ""
|
|
82
|
+
term_program = ENV["TERM_PROGRAM"]&.downcase || ""
|
|
83
|
+
|
|
84
|
+
# Truecolor detection
|
|
85
|
+
if colorterm == "truecolor" || colorterm == "24bit"
|
|
86
|
+
return TRUECOLOR
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if term.include?("24bit") || term.include?("truecolor")
|
|
90
|
+
return TRUECOLOR
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Known truecolor terminals
|
|
94
|
+
if %w[iterm.app hyper wezterm alacritty kitty].include?(term_program)
|
|
95
|
+
return TRUECOLOR
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 256 color detection
|
|
99
|
+
if term.include?("256color") || colorterm == "256"
|
|
100
|
+
return COLOR256
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Default to basic 16
|
|
104
|
+
BASIC16
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Convert RGB/truecolor to nearest 256-color index
|
|
108
|
+
def downgrade_to_256(color)
|
|
109
|
+
case color.kind
|
|
110
|
+
when :rgb
|
|
111
|
+
r, g, b = color.value
|
|
112
|
+
idx = rgb_to_256(r, g, b)
|
|
113
|
+
Color.indexed(idx)
|
|
114
|
+
else
|
|
115
|
+
color # :named and :indexed already fit
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Convert any color to nearest basic 16 color
|
|
120
|
+
def downgrade_to_16(color)
|
|
121
|
+
case color.kind
|
|
122
|
+
when :rgb
|
|
123
|
+
r, g, b = color.value
|
|
124
|
+
rgb_to_named16(r, g, b)
|
|
125
|
+
when :indexed
|
|
126
|
+
indexed_to_named16(color.value)
|
|
127
|
+
else
|
|
128
|
+
color # :named already fits
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Map RGB to nearest 256-color palette index
|
|
133
|
+
def rgb_to_256(r, g, b)
|
|
134
|
+
# Check grayscale ramp first (indices 232-255)
|
|
135
|
+
if r == g && g == b
|
|
136
|
+
return 16 if r < 8
|
|
137
|
+
return 231 if r > 248
|
|
138
|
+
return (((r - 8).to_f / 247) * 24).round + 232
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Map to 6x6x6 color cube (indices 16-231)
|
|
142
|
+
ri = (r.to_f / 255 * 5).round
|
|
143
|
+
gi = (g.to_f / 255 * 5).round
|
|
144
|
+
bi = (b.to_f / 255 * 5).round
|
|
145
|
+
16 + (36 * ri) + (6 * gi) + bi
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Map RGB to nearest named 16 color
|
|
149
|
+
def rgb_to_named16(r, g, b)
|
|
150
|
+
# Simple luminance-based mapping to basic 16
|
|
151
|
+
brightness = (r * 299 + g * 587 + b * 114) / 1000
|
|
152
|
+
bright = brightness > 128
|
|
153
|
+
|
|
154
|
+
# Determine dominant channel
|
|
155
|
+
max = [r, g, b].max
|
|
156
|
+
if max == 0
|
|
157
|
+
return bright ? Color::BRIGHT_BLACK : Color::BLACK
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
rn = r.to_f / max
|
|
161
|
+
gn = g.to_f / max
|
|
162
|
+
bn = b.to_f / max
|
|
163
|
+
|
|
164
|
+
if rn > 0.6 && gn > 0.6 && bn > 0.6
|
|
165
|
+
bright ? Color::WHITE : Color::BRIGHT_BLACK
|
|
166
|
+
elsif rn > 0.6 && gn > 0.6
|
|
167
|
+
bright ? Color::BRIGHT_YELLOW : Color::YELLOW
|
|
168
|
+
elsif rn > 0.6 && bn > 0.6
|
|
169
|
+
bright ? Color::BRIGHT_MAGENTA : Color::MAGENTA
|
|
170
|
+
elsif gn > 0.6 && bn > 0.6
|
|
171
|
+
bright ? Color::BRIGHT_CYAN : Color::CYAN
|
|
172
|
+
elsif rn > 0.5
|
|
173
|
+
bright ? Color::BRIGHT_RED : Color::RED
|
|
174
|
+
elsif gn > 0.5
|
|
175
|
+
bright ? Color::BRIGHT_GREEN : Color::GREEN
|
|
176
|
+
elsif bn > 0.5
|
|
177
|
+
bright ? Color::BRIGHT_BLUE : Color::BLUE
|
|
178
|
+
else
|
|
179
|
+
bright ? Color::BRIGHT_BLACK : Color::BLACK
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Map 256-color index to nearest named 16 color
|
|
184
|
+
def indexed_to_named16(idx)
|
|
185
|
+
if idx < 16
|
|
186
|
+
# Already a basic color
|
|
187
|
+
Color.new(:named, idx).freeze
|
|
188
|
+
elsif idx >= 232
|
|
189
|
+
# Grayscale ramp
|
|
190
|
+
level = (idx - 232) * 10 + 8
|
|
191
|
+
level > 128 ? Color::WHITE : Color::BRIGHT_BLACK
|
|
192
|
+
else
|
|
193
|
+
# 6x6x6 cube -> RGB -> named16
|
|
194
|
+
idx -= 16
|
|
195
|
+
b = (idx % 6) * 51
|
|
196
|
+
g = ((idx / 6) % 6) * 51
|
|
197
|
+
r = ((idx / 36) % 6) * 51
|
|
198
|
+
rgb_to_named16(r, g, b)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyTUI
|
|
4
|
+
# Base class for all RubyTUI-specific errors.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
# Error category for terminal-related failures.
|
|
7
|
+
class TerminalError < Error; end
|
|
8
|
+
# Error category for layout-related failures.
|
|
9
|
+
class LayoutError < Error; end
|
|
10
|
+
# Raised by {Widgets::Image} when given image data in a format the Kitty
|
|
11
|
+
# graphics protocol path does not accept (JPEG, WebP, GIF or BMP).
|
|
12
|
+
class UnsupportedImageFormat < Error; end
|
|
13
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyTUI
|
|
4
|
+
# Base class for input events: {KeyEvent}, {MouseEvent} and {ResizeEvent}.
|
|
5
|
+
class Event
|
|
6
|
+
# Base class for all events
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# A keyboard event: key code, optional character and modifier flags.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# event = KeyEvent.new(code: Input::Key::CHAR, char: "q", modifiers: Input::Key::CTRL)
|
|
13
|
+
# event.ctrl? # => true
|
|
14
|
+
# event.to_s # => "KeyEvent(Ctrl+q)"
|
|
15
|
+
class KeyEvent < Event
|
|
16
|
+
attr_reader :code, :char, :modifiers
|
|
17
|
+
|
|
18
|
+
# @param code [Symbol] key code, one of the {Input::Key} key-code constants
|
|
19
|
+
# (e.g. {Input::Key::ENTER}, {Input::Key::CHAR})
|
|
20
|
+
# @param char [String, nil] the character, for {Input::Key::CHAR} events (default: nil)
|
|
21
|
+
# @param modifiers [Integer] bitmask of {Input::Key::SHIFT}, {Input::Key::ALT} and
|
|
22
|
+
# {Input::Key::CTRL} (default: {Input::Key::NONE})
|
|
23
|
+
def initialize(code:, char: nil, modifiers: Input::Key::NONE)
|
|
24
|
+
@code = code
|
|
25
|
+
@char = char
|
|
26
|
+
@modifiers = modifiers
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Boolean] true if the {Input::Key::CTRL} flag is set
|
|
30
|
+
def ctrl? = (@modifiers & Input::Key::CTRL) != 0
|
|
31
|
+
# @return [Boolean] true if the {Input::Key::ALT} flag is set
|
|
32
|
+
def alt? = (@modifiers & Input::Key::ALT) != 0
|
|
33
|
+
# @return [Boolean] true if the {Input::Key::SHIFT} flag is set
|
|
34
|
+
def shift? = (@modifiers & Input::Key::SHIFT) != 0
|
|
35
|
+
|
|
36
|
+
# Value equality: same code, character and modifiers.
|
|
37
|
+
#
|
|
38
|
+
# @param other [Object] object to compare
|
|
39
|
+
# @return [Boolean] true if +other+ is a KeyEvent with equal {#code}, {#char}
|
|
40
|
+
# and {#modifiers}
|
|
41
|
+
def ==(other)
|
|
42
|
+
other.is_a?(KeyEvent) && @code == other.code &&
|
|
43
|
+
@char == other.char && @modifiers == other.modifiers
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Human-readable form: modifier names, then the character (or the key
|
|
47
|
+
# code when there is no character), joined by <tt>+</tt>.
|
|
48
|
+
#
|
|
49
|
+
# @return [String] e.g. <tt>KeyEvent(Ctrl+q)</tt> or <tt>KeyEvent(enter)</tt>
|
|
50
|
+
def to_s
|
|
51
|
+
parts = []
|
|
52
|
+
parts << "Ctrl" if ctrl?
|
|
53
|
+
parts << "Alt" if alt?
|
|
54
|
+
parts << "Shift" if shift?
|
|
55
|
+
parts << (@char || @code.to_s)
|
|
56
|
+
"KeyEvent(#{parts.join("+")})"
|
|
57
|
+
end
|
|
58
|
+
alias_method :inspect, :to_s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A mouse event: button, action, cell position and modifier flags.
|
|
62
|
+
#
|
|
63
|
+
# {Input::Parser} produces these from SGR (mode 1006) and legacy
|
|
64
|
+
# (<tt>ESC [ M</tt>) mouse reports, with 0-based cell coordinates.
|
|
65
|
+
class MouseEvent < Event
|
|
66
|
+
# @!group Mouse Buttons
|
|
67
|
+
|
|
68
|
+
# Left button.
|
|
69
|
+
LEFT = :left
|
|
70
|
+
# Middle button.
|
|
71
|
+
MIDDLE = :middle
|
|
72
|
+
# Right button.
|
|
73
|
+
RIGHT = :right
|
|
74
|
+
# Scroll wheel up (reported with action {PRESS}).
|
|
75
|
+
SCROLL_UP = :scroll_up
|
|
76
|
+
# Scroll wheel down (reported with action {PRESS}).
|
|
77
|
+
SCROLL_DOWN = :scroll_down
|
|
78
|
+
# No specific button (legacy-protocol release, or motion with no button held).
|
|
79
|
+
NONE = :none
|
|
80
|
+
|
|
81
|
+
# @!endgroup
|
|
82
|
+
|
|
83
|
+
# @!group Mouse Actions
|
|
84
|
+
|
|
85
|
+
# Button pressed; also used for scroll-wheel events.
|
|
86
|
+
PRESS = :press
|
|
87
|
+
# Button released.
|
|
88
|
+
RELEASE = :release
|
|
89
|
+
# Pointer motion reported by the terminal (motion flag set).
|
|
90
|
+
DRAG = :drag
|
|
91
|
+
# Pointer motion with no button held. Currently never emitted by {Input::Parser}.
|
|
92
|
+
MOVE = :move
|
|
93
|
+
|
|
94
|
+
# @!endgroup
|
|
95
|
+
|
|
96
|
+
attr_reader :button, :action, :x, :y, :modifiers
|
|
97
|
+
|
|
98
|
+
# @param button [Symbol] button constant ({LEFT}, {MIDDLE}, {RIGHT},
|
|
99
|
+
# {SCROLL_UP}, {SCROLL_DOWN} or {NONE})
|
|
100
|
+
# @param action [Symbol] action constant ({PRESS}, {RELEASE}, {DRAG} or {MOVE})
|
|
101
|
+
# @param x [Integer] column (0-based cell coordinate)
|
|
102
|
+
# @param y [Integer] row (0-based cell coordinate)
|
|
103
|
+
# @param modifiers [Integer] bitmask of {Input::Key::SHIFT}, {Input::Key::ALT} and
|
|
104
|
+
# {Input::Key::CTRL} (default: {Input::Key::NONE})
|
|
105
|
+
def initialize(button:, action:, x:, y:, modifiers: Input::Key::NONE)
|
|
106
|
+
@button = button
|
|
107
|
+
@action = action
|
|
108
|
+
@x = x
|
|
109
|
+
@y = y
|
|
110
|
+
@modifiers = modifiers
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @return [Boolean] true if the {Input::Key::CTRL} flag is set
|
|
114
|
+
def ctrl? = (@modifiers & Input::Key::CTRL) != 0
|
|
115
|
+
# @return [Boolean] true if the {Input::Key::ALT} flag is set
|
|
116
|
+
def alt? = (@modifiers & Input::Key::ALT) != 0
|
|
117
|
+
# @return [Boolean] true if the {Input::Key::SHIFT} flag is set
|
|
118
|
+
def shift? = (@modifiers & Input::Key::SHIFT) != 0
|
|
119
|
+
|
|
120
|
+
# Value equality: same button, action, position and modifiers.
|
|
121
|
+
#
|
|
122
|
+
# @param other [Object] object to compare
|
|
123
|
+
# @return [Boolean] true if +other+ is a MouseEvent with equal {#button},
|
|
124
|
+
# {#action}, {#x}, {#y} and {#modifiers}
|
|
125
|
+
def ==(other)
|
|
126
|
+
other.is_a?(MouseEvent) && @button == other.button &&
|
|
127
|
+
@action == other.action && @x == other.x && @y == other.y &&
|
|
128
|
+
@modifiers == other.modifiers
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Human-readable form: modifier names, then <tt>action(button)</tt>,
|
|
132
|
+
# joined by <tt>+</tt>, followed by the position.
|
|
133
|
+
#
|
|
134
|
+
# @return [String] e.g. <tt>MouseEvent(press(left) @10,5)</tt>
|
|
135
|
+
def to_s
|
|
136
|
+
parts = []
|
|
137
|
+
parts << "Ctrl" if ctrl?
|
|
138
|
+
parts << "Alt" if alt?
|
|
139
|
+
parts << "Shift" if shift?
|
|
140
|
+
parts << "#{@action}(#{@button})"
|
|
141
|
+
"MouseEvent(#{parts.join("+")} @#{@x},#{@y})"
|
|
142
|
+
end
|
|
143
|
+
alias_method :inspect, :to_s
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# A terminal resize event carrying the new size in cells.
|
|
147
|
+
class ResizeEvent < Event
|
|
148
|
+
attr_reader :width, :height
|
|
149
|
+
|
|
150
|
+
# @param width [Integer] new width in columns
|
|
151
|
+
# @param height [Integer] new height in rows
|
|
152
|
+
def initialize(width:, height:)
|
|
153
|
+
@width = width
|
|
154
|
+
@height = height
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @return [String] human-readable form, e.g. <tt>ResizeEvent(80x24)</tt>
|
|
158
|
+
def to_s = "ResizeEvent(#{@width}x#{@height})"
|
|
159
|
+
alias_method :inspect, :to_s
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyTUI
|
|
4
|
+
# Rendering context yielded by {Terminal#draw} and {RubyTUI.render_once}.
|
|
5
|
+
#
|
|
6
|
+
# Frame wraps a {Buffer} and provides methods for rendering widgets.
|
|
7
|
+
# It is the primary interface for placing widgets during a draw call.
|
|
8
|
+
#
|
|
9
|
+
# @example Rendering widgets in a frame
|
|
10
|
+
# terminal.draw do |frame|
|
|
11
|
+
# frame.render_widget(block, frame.area)
|
|
12
|
+
# frame.render_stateful_widget(list, chunks[1], list_state)
|
|
13
|
+
# end
|
|
14
|
+
class Frame
|
|
15
|
+
# @return [Buffer] the buffer to render into
|
|
16
|
+
attr_reader :buffer
|
|
17
|
+
# @return [Rect] the total renderable area
|
|
18
|
+
attr_reader :area
|
|
19
|
+
|
|
20
|
+
# @param buffer [Buffer]
|
|
21
|
+
# @param area [Rect]
|
|
22
|
+
def initialize(buffer, area)
|
|
23
|
+
@buffer = buffer
|
|
24
|
+
@area = area
|
|
25
|
+
@cursor_position = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Render a widget into a specific area of this frame.
|
|
29
|
+
#
|
|
30
|
+
# The widget must implement <tt>render(area, buf)</tt> (the {Widget} protocol).
|
|
31
|
+
#
|
|
32
|
+
# @param widget [#render] any object responding to <tt>render(area, buf)</tt>
|
|
33
|
+
# @param area [Rect] the area to render into
|
|
34
|
+
# @return [void]
|
|
35
|
+
def render_widget(widget, area)
|
|
36
|
+
widget.render(area, @buffer)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Render a stateful widget with its state object.
|
|
40
|
+
#
|
|
41
|
+
# The widget must implement <tt>render(area, buf, state)</tt> (the {StatefulWidget} protocol).
|
|
42
|
+
#
|
|
43
|
+
# @param widget [#render] any object responding to <tt>render(area, buf, state)</tt>
|
|
44
|
+
# @param area [Rect] the area to render into
|
|
45
|
+
# @param state [Object] the widget's external state
|
|
46
|
+
# @return [void]
|
|
47
|
+
def render_stateful_widget(widget, area, state)
|
|
48
|
+
widget.render(area, @buffer, state)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Request cursor placement at a specific position.
|
|
52
|
+
#
|
|
53
|
+
# Used by widgets like {Widgets::InputField} to position the terminal cursor.
|
|
54
|
+
#
|
|
55
|
+
# @param x [Integer] column position
|
|
56
|
+
# @param y [Integer] row position
|
|
57
|
+
# @return [void]
|
|
58
|
+
def set_cursor_position(x, y)
|
|
59
|
+
@cursor_position = [x, y]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get the requested cursor position.
|
|
63
|
+
#
|
|
64
|
+
# @return [Array(Integer, Integer), nil] [x, y] pair, or nil if not set
|
|
65
|
+
def cursor_position = @cursor_position
|
|
66
|
+
|
|
67
|
+
# @return [Rect] the total renderable area (alias for {#area})
|
|
68
|
+
def size = @area
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyTUI
|
|
4
|
+
# Terminal input handling: key constants, byte-sequence {Parser} and
|
|
5
|
+
# non-blocking {Reader}.
|
|
6
|
+
module Input
|
|
7
|
+
# Key codes and modifier flags used by {KeyEvent} and {MouseEvent}.
|
|
8
|
+
#
|
|
9
|
+
# Key codes are Symbols stored in <tt>KeyEvent#code</tt>; modifier flags are
|
|
10
|
+
# Integer bits OR'd together in the +modifiers+ of {KeyEvent} and {MouseEvent}.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# case event.code
|
|
14
|
+
# when Input::Key::ENTER then submit
|
|
15
|
+
# when Input::Key::CHAR then insert(event.char)
|
|
16
|
+
# end
|
|
17
|
+
module Key
|
|
18
|
+
# @!group Key Codes
|
|
19
|
+
|
|
20
|
+
# Character key; the character itself is in <tt>KeyEvent#char</tt>.
|
|
21
|
+
CHAR = :char
|
|
22
|
+
# Enter / Return.
|
|
23
|
+
ENTER = :enter
|
|
24
|
+
# Escape.
|
|
25
|
+
ESCAPE = :escape
|
|
26
|
+
# Backspace.
|
|
27
|
+
BACKSPACE = :backspace
|
|
28
|
+
# Delete (forward delete).
|
|
29
|
+
DELETE = :delete
|
|
30
|
+
# Tab.
|
|
31
|
+
TAB = :tab
|
|
32
|
+
# Shift+Tab (back-tab).
|
|
33
|
+
BACKTAB = :backtab # Shift+Tab
|
|
34
|
+
# Up arrow.
|
|
35
|
+
UP = :up
|
|
36
|
+
# Down arrow.
|
|
37
|
+
DOWN = :down
|
|
38
|
+
# Left arrow.
|
|
39
|
+
LEFT = :left
|
|
40
|
+
# Right arrow.
|
|
41
|
+
RIGHT = :right
|
|
42
|
+
# Home.
|
|
43
|
+
HOME = :home
|
|
44
|
+
# End.
|
|
45
|
+
END_KEY = :end
|
|
46
|
+
# Page Up.
|
|
47
|
+
PAGE_UP = :page_up
|
|
48
|
+
# Page Down.
|
|
49
|
+
PAGE_DOWN = :page_down
|
|
50
|
+
# Insert.
|
|
51
|
+
INSERT = :insert
|
|
52
|
+
# Function key F1.
|
|
53
|
+
F1 = :f1
|
|
54
|
+
# Function key F2.
|
|
55
|
+
F2 = :f2
|
|
56
|
+
# Function key F3.
|
|
57
|
+
F3 = :f3
|
|
58
|
+
# Function key F4.
|
|
59
|
+
F4 = :f4
|
|
60
|
+
# Function key F5.
|
|
61
|
+
F5 = :f5
|
|
62
|
+
# Function key F6.
|
|
63
|
+
F6 = :f6
|
|
64
|
+
# Function key F7.
|
|
65
|
+
F7 = :f7
|
|
66
|
+
# Function key F8.
|
|
67
|
+
F8 = :f8
|
|
68
|
+
# Function key F9.
|
|
69
|
+
F9 = :f9
|
|
70
|
+
# Function key F10.
|
|
71
|
+
F10 = :f10
|
|
72
|
+
# Function key F11.
|
|
73
|
+
F11 = :f11
|
|
74
|
+
# Function key F12.
|
|
75
|
+
F12 = :f12
|
|
76
|
+
|
|
77
|
+
# @!endgroup
|
|
78
|
+
|
|
79
|
+
# @!group Modifier Flags
|
|
80
|
+
|
|
81
|
+
# No modifier keys held.
|
|
82
|
+
NONE = 0
|
|
83
|
+
# Shift modifier bit.
|
|
84
|
+
SHIFT = 0b001
|
|
85
|
+
# Alt (Meta / Option) modifier bit.
|
|
86
|
+
ALT = 0b010
|
|
87
|
+
# Ctrl modifier bit.
|
|
88
|
+
CTRL = 0b100
|
|
89
|
+
|
|
90
|
+
# @!endgroup
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|