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
data/lib/rubytui/app.rb
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyTUI
|
|
4
|
+
# Optional Model-View-Update (Elm Architecture) framework.
|
|
5
|
+
# Provides a structured pattern for building TUI applications.
|
|
6
|
+
#
|
|
7
|
+
# @example Counter app
|
|
8
|
+
# class MyApp
|
|
9
|
+
# include RubyTUI::App
|
|
10
|
+
#
|
|
11
|
+
# def init
|
|
12
|
+
# { counter: 0 } # initial model
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# def update(model, msg)
|
|
16
|
+
# case msg
|
|
17
|
+
# when :increment then model.merge(counter: model[:counter] + 1)
|
|
18
|
+
# when :quit then throw(:quit)
|
|
19
|
+
# else model
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def view(model, frame)
|
|
24
|
+
# frame.buffer.set_string(0, 0, "Count: #{model[:counter]}")
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def handle_event(event)
|
|
28
|
+
# case event
|
|
29
|
+
# when KeyEvent
|
|
30
|
+
# case event.char
|
|
31
|
+
# when "+" then :increment
|
|
32
|
+
# when "q" then :quit
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# MyApp.new.run!
|
|
39
|
+
#
|
|
40
|
+
module App
|
|
41
|
+
# Extend the including class with {ClassMethods}.
|
|
42
|
+
#
|
|
43
|
+
# @param base [Class] the class that includes App
|
|
44
|
+
# @return [void]
|
|
45
|
+
def self.included(base)
|
|
46
|
+
base.extend(ClassMethods)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Class-level methods added to every class that includes {App}.
|
|
50
|
+
module ClassMethods
|
|
51
|
+
# Convenience: create and run the app
|
|
52
|
+
#
|
|
53
|
+
# Instantiates the class with no arguments and calls {App#run!}.
|
|
54
|
+
#
|
|
55
|
+
# @param opts [Hash{Symbol => Object}] keyword options forwarded to {App#run!}
|
|
56
|
+
# @return [void]
|
|
57
|
+
def run!(**opts)
|
|
58
|
+
new.run!(**opts)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Override: return the initial model (a Hash or any object)
|
|
63
|
+
#
|
|
64
|
+
# @return [Object] the initial model (default: empty Hash)
|
|
65
|
+
def init
|
|
66
|
+
{}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Override: given the current model and a message, return the next model.
|
|
70
|
+
# Call <tt>throw(:quit)</tt> from update to exit the app loop.
|
|
71
|
+
#
|
|
72
|
+
# @param model [Object] the current model
|
|
73
|
+
# @param msg [Object] non-nil message from {#handle_event} or {#handle_tick}
|
|
74
|
+
# @return [Object] the next model (default: +model+ unchanged)
|
|
75
|
+
def update(model, msg)
|
|
76
|
+
model
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Override: render the UI for the current model
|
|
80
|
+
#
|
|
81
|
+
# Called inside {Terminal#draw} once per loop iteration.
|
|
82
|
+
#
|
|
83
|
+
# @param model [Object] the current model
|
|
84
|
+
# @param frame [Frame] the frame to render into
|
|
85
|
+
# @return [void]
|
|
86
|
+
def view(model, frame)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Override: convert an Event into a message (or nil to ignore)
|
|
90
|
+
#
|
|
91
|
+
# @param event [Event] the event returned by {Input::Reader#poll}
|
|
92
|
+
# @return [Object, nil] message passed to {#update}, or nil to ignore
|
|
93
|
+
# (default: nil)
|
|
94
|
+
def handle_event(event)
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Optional: called once before the main loop with [terminal, reader]
|
|
99
|
+
#
|
|
100
|
+
# Called after {#init}, with the terminal already set up.
|
|
101
|
+
#
|
|
102
|
+
# @param terminal [Terminal] the initialized terminal
|
|
103
|
+
# @param reader [Input::Reader] the input reader
|
|
104
|
+
# @return [void]
|
|
105
|
+
def on_start(terminal, reader)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Optional: called once after the main loop exits
|
|
109
|
+
#
|
|
110
|
+
# Called before the terminal is restored.
|
|
111
|
+
#
|
|
112
|
+
# @param model [Object, nil] the result of the <tt>catch(:quit)</tt> block:
|
|
113
|
+
# the value passed as <tt>throw(:quit, value)</tt>, or nil for a bare
|
|
114
|
+
# <tt>throw(:quit)</tt>
|
|
115
|
+
# @return [void]
|
|
116
|
+
def on_stop(model)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Run the application.
|
|
120
|
+
#
|
|
121
|
+
# The main loop renders, polls for events, and dispatches messages.
|
|
122
|
+
# Call <tt>throw(:quit)</tt> from {#update} to exit the loop cleanly.
|
|
123
|
+
#
|
|
124
|
+
# Wraps {RubyTUI.run} (raw mode, hidden cursor, alternate screen), so
|
|
125
|
+
# the terminal is restored when the loop exits or raises. Each
|
|
126
|
+
# iteration draws via {#view}, waits up to +tick_ms+ for an event
|
|
127
|
+
# (passed through {#handle_event}), then calls {#handle_tick}; non-nil
|
|
128
|
+
# messages from either are passed to {#update}.
|
|
129
|
+
#
|
|
130
|
+
# @param mouse [Boolean] enable mouse event reporting (default: false)
|
|
131
|
+
# @param tick_ms [Integer] event poll timeout per loop iteration, in
|
|
132
|
+
# milliseconds (default: 50)
|
|
133
|
+
# @param output [IO] output stream (default: $stdout)
|
|
134
|
+
# @param input [IO] input stream (default: $stdin)
|
|
135
|
+
# @return [void]
|
|
136
|
+
def run!(mouse: false, tick_ms: 50, output: $stdout, input: $stdin)
|
|
137
|
+
RubyTUI.run(output: output, input: input, mouse: mouse) do |terminal, reader|
|
|
138
|
+
model = init
|
|
139
|
+
on_start(terminal, reader)
|
|
140
|
+
|
|
141
|
+
model = catch(:quit) do
|
|
142
|
+
loop do
|
|
143
|
+
terminal.draw do |frame|
|
|
144
|
+
view(model, frame)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
event = reader.poll(timeout_ms: tick_ms)
|
|
148
|
+
|
|
149
|
+
if event
|
|
150
|
+
msg = handle_event(event)
|
|
151
|
+
model = update(model, msg) if msg
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
tick_msg = handle_tick(model)
|
|
155
|
+
model = update(model, tick_msg) if tick_msg
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
on_stop(model)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Override: return a message on each tick, or nil
|
|
164
|
+
#
|
|
165
|
+
# Called once per loop iteration after polling, whether or not an
|
|
166
|
+
# event arrived.
|
|
167
|
+
#
|
|
168
|
+
# @param model [Object] the current model
|
|
169
|
+
# @return [Object, nil] message passed to {#update}, or nil (default: nil)
|
|
170
|
+
def handle_tick(model)
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyTUI
|
|
4
|
+
# Backend interface contract.
|
|
5
|
+
# A backend translates buffer diffs into terminal output.
|
|
6
|
+
#
|
|
7
|
+
# Including classes must override every method that raises
|
|
8
|
+
# +NotImplementedError+; the image-related methods have defaults.
|
|
9
|
+
#
|
|
10
|
+
# @see AnsiBackend
|
|
11
|
+
# @see TestBackend
|
|
12
|
+
module Backend
|
|
13
|
+
# Write changed cells to the terminal.
|
|
14
|
+
# @param changes [Array<Array(Integer, Integer, Cell)>] array of [x, y, cell]
|
|
15
|
+
# @return [void]
|
|
16
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
17
|
+
def draw(changes)
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Hide the cursor
|
|
22
|
+
#
|
|
23
|
+
# @return [void]
|
|
24
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
25
|
+
def hide_cursor
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Show the cursor
|
|
30
|
+
#
|
|
31
|
+
# @return [void]
|
|
32
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
33
|
+
def show_cursor
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Move cursor to position
|
|
38
|
+
#
|
|
39
|
+
# @param x [Integer] zero-based column
|
|
40
|
+
# @param y [Integer] zero-based row
|
|
41
|
+
# @return [void]
|
|
42
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
43
|
+
def move_cursor(x, y)
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Clear the entire screen
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
51
|
+
def clear
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Enter alternate screen buffer
|
|
56
|
+
#
|
|
57
|
+
# @return [void]
|
|
58
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
59
|
+
def enter_alternate_screen
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Leave alternate screen buffer
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
67
|
+
def leave_alternate_screen
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Enable raw mode (no echo, no line buffering)
|
|
72
|
+
#
|
|
73
|
+
# @return [void]
|
|
74
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
75
|
+
def enable_raw_mode
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Disable raw mode
|
|
80
|
+
#
|
|
81
|
+
# @return [void]
|
|
82
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
83
|
+
def disable_raw_mode
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get terminal size as [width, height]
|
|
88
|
+
#
|
|
89
|
+
# @return [Array(Integer, Integer)] [width, height] in cells
|
|
90
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
91
|
+
def size
|
|
92
|
+
raise NotImplementedError
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Flush output
|
|
96
|
+
#
|
|
97
|
+
# @return [void]
|
|
98
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
99
|
+
def flush
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Enable mouse event reporting
|
|
104
|
+
#
|
|
105
|
+
# @return [void]
|
|
106
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
107
|
+
def enable_mouse
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Disable mouse event reporting
|
|
112
|
+
#
|
|
113
|
+
# @return [void]
|
|
114
|
+
# @raise [NotImplementedError] unless overridden by the including class
|
|
115
|
+
def disable_mouse
|
|
116
|
+
raise NotImplementedError
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Draw an image using the Kitty graphics protocol.
|
|
120
|
+
# No-op by default; backends that support images override it.
|
|
121
|
+
# @param placement [Hash{Symbol => Object}] image placement data with keys
|
|
122
|
+
# +:x+, +:y+, +:cols+, +:rows+, +:data+, +:pixel_width+, +:pixel_height+, +:format+,
|
|
123
|
+
# +:content_hash+, +:src_x+, +:src_y+, +:src_w+, +:src_h+
|
|
124
|
+
# @return [void]
|
|
125
|
+
def draw_kitty_image(placement)
|
|
126
|
+
# no-op by default (not all backends support images)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Delete a Kitty graphics protocol image.
|
|
130
|
+
# No-op by default.
|
|
131
|
+
# @param placement [Hash{Symbol => Object}] the image placement to delete
|
|
132
|
+
# @return [void]
|
|
133
|
+
def delete_kitty_image(placement)
|
|
134
|
+
# no-op by default
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Delete all Kitty graphics protocol images.
|
|
138
|
+
# No-op by default.
|
|
139
|
+
# @return [void]
|
|
140
|
+
def clear_all_images!
|
|
141
|
+
# no-op by default
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Query terminal for cell pixel dimensions.
|
|
145
|
+
# The default implementation returns [9, 18] without querying.
|
|
146
|
+
# @return [Array(Integer, Integer)] cell dimensions [width_px, height_px]
|
|
147
|
+
def query_cell_pixel_size
|
|
148
|
+
[9, 18]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Cached cell pixel dimensions; the default implementation returns nil.
|
|
152
|
+
#
|
|
153
|
+
# @return [Array(Integer, Integer), nil] cached cell dimensions
|
|
154
|
+
def cell_pixel_size
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|