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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +574 -0
  4. data/lib/rubytui/app.rb +174 -0
  5. data/lib/rubytui/backend.rb +158 -0
  6. data/lib/rubytui/backends/ansi_backend.rb +391 -0
  7. data/lib/rubytui/backends/test_backend.rb +223 -0
  8. data/lib/rubytui/buffer.rb +400 -0
  9. data/lib/rubytui/cell.rb +55 -0
  10. data/lib/rubytui/color.rb +153 -0
  11. data/lib/rubytui/color_mode.rb +203 -0
  12. data/lib/rubytui/errors.rb +13 -0
  13. data/lib/rubytui/event.rb +161 -0
  14. data/lib/rubytui/frame.rb +70 -0
  15. data/lib/rubytui/input/key.rb +93 -0
  16. data/lib/rubytui/input/parser.rb +231 -0
  17. data/lib/rubytui/input/reader.rb +119 -0
  18. data/lib/rubytui/layout/constraint.rb +83 -0
  19. data/lib/rubytui/layout/flex.rb +16 -0
  20. data/lib/rubytui/layout/layout.rb +205 -0
  21. data/lib/rubytui/modifier.rb +67 -0
  22. data/lib/rubytui/rect.rb +126 -0
  23. data/lib/rubytui/stateful_widget.rb +20 -0
  24. data/lib/rubytui/style.rb +143 -0
  25. data/lib/rubytui/symbols.rb +88 -0
  26. data/lib/rubytui/terminal.rb +218 -0
  27. data/lib/rubytui/text/line.rb +67 -0
  28. data/lib/rubytui/text/span.rb +34 -0
  29. data/lib/rubytui/text/text.rb +82 -0
  30. data/lib/rubytui/unicode.rb +162 -0
  31. data/lib/rubytui/version.rb +6 -0
  32. data/lib/rubytui/widget.rb +20 -0
  33. data/lib/rubytui/widgets/async_image.rb +248 -0
  34. data/lib/rubytui/widgets/block.rb +260 -0
  35. data/lib/rubytui/widgets/canvas.rb +248 -0
  36. data/lib/rubytui/widgets/chart.rb +224 -0
  37. data/lib/rubytui/widgets/gauge.rb +139 -0
  38. data/lib/rubytui/widgets/image.rb +330 -0
  39. data/lib/rubytui/widgets/input_field.rb +245 -0
  40. data/lib/rubytui/widgets/list.rb +186 -0
  41. data/lib/rubytui/widgets/paragraph.rb +181 -0
  42. data/lib/rubytui/widgets/popup.rb +140 -0
  43. data/lib/rubytui/widgets/scrollbar.rb +175 -0
  44. data/lib/rubytui/widgets/sparkline.rb +86 -0
  45. data/lib/rubytui/widgets/table.rb +231 -0
  46. data/lib/rubytui/widgets/tabs.rb +90 -0
  47. data/lib/rubytui.rb +389 -0
  48. metadata +89 -0
@@ -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