charming 0.1.2 → 0.1.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/charming/application.rb +3 -3
  3. data/lib/charming/controller/class_methods.rb +2 -2
  4. data/lib/charming/controller/command_palette.rb +2 -2
  5. data/lib/charming/controller/rendering.rb +2 -2
  6. data/lib/charming/controller/session_state.rb +1 -1
  7. data/lib/charming/generators/component_generator.rb +1 -1
  8. data/lib/charming/generators/templates/app/application.template +1 -1
  9. data/lib/charming/generators/templates/app/layout.template +3 -6
  10. data/lib/charming/generators/templates/app/view.template +1 -1
  11. data/lib/charming/generators/templates/component/component.rb.template +1 -1
  12. data/lib/charming/generators/templates/screen/view.rb.template +1 -1
  13. data/lib/charming/generators/templates/view/view.rb.template +1 -1
  14. data/lib/charming/internal/renderer/differential.rb +13 -5
  15. data/lib/charming/internal/terminal/tty_backend.rb +22 -2
  16. data/lib/charming/presentation/component.rb +3 -5
  17. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  18. data/lib/charming/presentation/components/command_palette.rb +94 -96
  19. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  20. data/lib/charming/presentation/components/empty_state.rb +47 -49
  21. data/lib/charming/presentation/components/form/builder.rb +52 -54
  22. data/lib/charming/presentation/components/form/confirm.rb +49 -51
  23. data/lib/charming/presentation/components/form/field.rb +94 -96
  24. data/lib/charming/presentation/components/form/input.rb +53 -55
  25. data/lib/charming/presentation/components/form/note.rb +27 -29
  26. data/lib/charming/presentation/components/form/select.rb +84 -86
  27. data/lib/charming/presentation/components/form/textarea.rb +67 -69
  28. data/lib/charming/presentation/components/form.rb +120 -122
  29. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  30. data/lib/charming/presentation/components/list.rb +123 -125
  31. data/lib/charming/presentation/components/markdown.rb +21 -23
  32. data/lib/charming/presentation/components/modal.rb +46 -48
  33. data/lib/charming/presentation/components/progressbar.rb +51 -53
  34. data/lib/charming/presentation/components/spinner.rb +40 -42
  35. data/lib/charming/presentation/components/table.rb +109 -111
  36. data/lib/charming/presentation/components/text_area.rb +219 -221
  37. data/lib/charming/presentation/components/text_input.rb +120 -122
  38. data/lib/charming/presentation/components/viewport.rb +218 -220
  39. data/lib/charming/presentation/layout/builder.rb +64 -66
  40. data/lib/charming/presentation/layout/overlay.rb +48 -50
  41. data/lib/charming/presentation/layout/pane.rb +122 -118
  42. data/lib/charming/presentation/layout/rect.rb +14 -16
  43. data/lib/charming/presentation/layout/screen_layout.rb +40 -42
  44. data/lib/charming/presentation/layout/split.rb +101 -103
  45. data/lib/charming/presentation/layout.rb +28 -30
  46. data/lib/charming/presentation/markdown/block_renderers.rb +94 -96
  47. data/lib/charming/presentation/markdown/inline_renderers.rb +52 -54
  48. data/lib/charming/presentation/markdown/render_context.rb +12 -14
  49. data/lib/charming/presentation/markdown/renderer.rb +84 -86
  50. data/lib/charming/presentation/markdown/syntax_highlighter.rb +57 -59
  51. data/lib/charming/presentation/markdown.rb +4 -6
  52. data/lib/charming/presentation/template_view.rb +22 -24
  53. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  54. data/lib/charming/presentation/templates.rb +47 -49
  55. data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
  56. data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
  57. data/lib/charming/presentation/ui/border.rb +24 -26
  58. data/lib/charming/presentation/ui/border_painter.rb +37 -39
  59. data/lib/charming/presentation/ui/canvas.rb +59 -61
  60. data/lib/charming/presentation/ui/style.rb +173 -175
  61. data/lib/charming/presentation/ui/theme.rb +133 -135
  62. data/lib/charming/presentation/ui/width.rb +12 -14
  63. data/lib/charming/presentation/ui.rb +69 -71
  64. data/lib/charming/presentation/view.rb +103 -105
  65. data/lib/charming/runtime.rb +23 -10
  66. data/lib/charming/version.rb +1 -1
  67. data/lib/charming.rb +3 -2
  68. metadata +2 -1
@@ -1,266 +1,264 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- # TextArea is a multi-line text editor component. Supports character insertion (with
7
- # newline insertion via Shift+Enter or Ctrl+J), cursor movement (left/right/up/down,
8
- # home/end, page up/down), deletion (backspace/delete), and scrolling for long buffers.
9
- # Vertical movement preserves a "preferred column" so left/right navigation feels stable.
10
- class TextArea < Component
11
- # The current text value, cursor byte offset, top-visible row offset, and remembered
12
- # column for vertical navigation, respectively.
13
- attr_reader :value, :cursor, :offset, :preferred_column
14
-
15
- # *value* is the initial text. *placeholder* is shown when the value is empty. *width* and
16
- # *height* constrain the rendered output. *cursor* defaults to the end of the value.
17
- # *offset* is the top-visible row. *preferred_column* is the column to resume at on
18
- # vertical movement (defaults to the current column on first use).
19
- def initialize(value: "", placeholder: "", width: nil, height: nil, cursor: nil, offset: 0, preferred_column: nil)
20
- super()
21
- @value = value.dup
22
- @placeholder = placeholder
23
- @width = width
24
- @height = height
25
- @cursor = cursor || @value.length
26
- @offset = offset
27
- @preferred_column = preferred_column
28
- clamp_position
29
- ensure_cursor_visible
30
- end
4
+ module Components
5
+ # TextArea is a multi-line text editor component. Supports character insertion (with
6
+ # newline insertion via Shift+Enter or Ctrl+J), cursor movement (left/right/up/down,
7
+ # home/end, page up/down), deletion (backspace/delete), and scrolling for long buffers.
8
+ # Vertical movement preserves a "preferred column" so left/right navigation feels stable.
9
+ class TextArea < Component
10
+ # The current text value, cursor byte offset, top-visible row offset, and remembered
11
+ # column for vertical navigation, respectively.
12
+ attr_reader :value, :cursor, :offset, :preferred_column
13
+
14
+ # *value* is the initial text. *placeholder* is shown when the value is empty. *width* and
15
+ # *height* constrain the rendered output. *cursor* defaults to the end of the value.
16
+ # *offset* is the top-visible row. *preferred_column* is the column to resume at on
17
+ # vertical movement (defaults to the current column on first use).
18
+ def initialize(value: "", placeholder: "", width: nil, height: nil, cursor: nil, offset: 0, preferred_column: nil)
19
+ super()
20
+ @value = value.dup
21
+ @placeholder = placeholder
22
+ @width = width
23
+ @height = height
24
+ @cursor = cursor || @value.length
25
+ @offset = offset
26
+ @preferred_column = preferred_column
27
+ clamp_position
28
+ ensure_cursor_visible
29
+ end
31
30
 
32
- # Routes key events to the appropriate cursor/text mutation. Returns :handled when the
33
- # event was consumed, nil otherwise.
34
- def handle_key(event)
35
- key = Charming.key_of(event)
36
- return :handled if newline_event?(event) && insert("\n")
37
- return :handled if character_event?(event) && insert(event.char)
38
-
39
- case key
40
- when :left then move_left
41
- when :right then move_right
42
- when :up then move_up
43
- when :down then move_down
44
- when :home then move_home
45
- when :end then move_end
46
- when :backspace then delete_before_cursor
47
- when :delete then delete_at_cursor
48
- when :page_up then page_up
49
- when :page_down then page_down
50
- else return nil
51
- end
52
-
53
- :handled
54
- end
31
+ # Routes key events to the appropriate cursor/text mutation. Returns :handled when the
32
+ # event was consumed, nil otherwise.
33
+ def handle_key(event)
34
+ key = Charming.key_of(event)
35
+ return :handled if newline_event?(event) && insert("\n")
36
+ return :handled if character_event?(event) && insert(event.char)
37
+
38
+ case key
39
+ when :left then move_left
40
+ when :right then move_right
41
+ when :up then move_up
42
+ when :down then move_down
43
+ when :home then move_home
44
+ when :end then move_end
45
+ when :backspace then delete_before_cursor
46
+ when :delete then delete_at_cursor
47
+ when :page_up then page_up
48
+ when :page_down then page_down
49
+ else return nil
50
+ end
51
+
52
+ :handled
53
+ end
55
54
 
56
- # Renders the visible portion of the text buffer (scrolled to `offset`), with each
57
- # visible line either clipped to `width` or padded to it.
58
- def render
59
- visible_lines.map { |line| render_line(line) }.join("\n")
60
- end
55
+ # Renders the visible portion of the text buffer (scrolled to `offset`), with each
56
+ # visible line either clipped to `width` or padded to it.
57
+ def render
58
+ visible_lines.map { |line| render_line(line) }.join("\n")
59
+ end
61
60
 
62
- private
61
+ private
63
62
 
64
- attr_reader :placeholder, :width, :height
63
+ attr_reader :placeholder, :width, :height
65
64
 
66
- # True when the event represents an explicit newline request: Shift+Enter or Ctrl+J.
67
- def newline_event?(event)
68
- key = Charming.key_of(event)
69
- return true if key == :enter && event.respond_to?(:shift) && event.shift
70
- return true if key == :j && event.respond_to?(:ctrl) && event.ctrl
65
+ # True when the event represents an explicit newline request: Shift+Enter or Ctrl+J.
66
+ def newline_event?(event)
67
+ key = Charming.key_of(event)
68
+ return true if key == :enter && event.respond_to?(:shift) && event.shift
69
+ return true if key == :j && event.respond_to?(:ctrl) && event.ctrl
71
70
 
72
- false
73
- end
71
+ false
72
+ end
74
73
 
75
- # True when *event* carries a single printable character.
76
- def character_event?(event)
77
- event.respond_to?(:char) && event.char && event.char.length == 1 && printable?(event.char)
78
- end
74
+ # True when *event* carries a single printable character.
75
+ def character_event?(event)
76
+ event.respond_to?(:char) && event.char && event.char.length == 1 && printable?(event.char)
77
+ end
79
78
 
80
- # True when *char* is not a control character.
81
- def printable?(char)
82
- !char.match?(/[[:cntrl:]]/)
83
- end
79
+ # True when *char* is not a control character.
80
+ def printable?(char)
81
+ !char.match?(/[[:cntrl:]]/)
82
+ end
84
83
 
85
- # Inserts *text* at the cursor, advances the cursor by its length, resets the preferred
86
- # column, and ensures the cursor remains visible.
87
- def insert(text)
88
- @value = value[0...cursor].to_s + text + value[cursor..].to_s
89
- @cursor += text.length
90
- reset_preferred_column
91
- ensure_cursor_visible
92
- end
84
+ # Inserts *text* at the cursor, advances the cursor by its length, resets the preferred
85
+ # column, and ensures the cursor remains visible.
86
+ def insert(text)
87
+ @value = value[0...cursor].to_s + text + value[cursor..].to_s
88
+ @cursor += text.length
89
+ reset_preferred_column
90
+ ensure_cursor_visible
91
+ end
93
92
 
94
- # Moves the cursor one character left.
95
- def move_left
96
- @cursor -= 1 if cursor.positive?
97
- reset_preferred_column
98
- ensure_cursor_visible
99
- end
93
+ # Moves the cursor one character left.
94
+ def move_left
95
+ @cursor -= 1 if cursor.positive?
96
+ reset_preferred_column
97
+ ensure_cursor_visible
98
+ end
100
99
 
101
- # Moves the cursor one character right.
102
- def move_right
103
- @cursor += 1 if cursor < value.length
104
- reset_preferred_column
105
- ensure_cursor_visible
106
- end
100
+ # Moves the cursor one character right.
101
+ def move_right
102
+ @cursor += 1 if cursor < value.length
103
+ reset_preferred_column
104
+ ensure_cursor_visible
105
+ end
107
106
 
108
- # Moves the cursor up one line while preserving the preferred column.
109
- def move_up
110
- move_vertical(-1)
111
- end
107
+ # Moves the cursor up one line while preserving the preferred column.
108
+ def move_up
109
+ move_vertical(-1)
110
+ end
112
111
 
113
- # Moves the cursor down one line while preserving the preferred column.
114
- def move_down
115
- move_vertical(+1)
116
- end
112
+ # Moves the cursor down one line while preserving the preferred column.
113
+ def move_down
114
+ move_vertical(+1)
115
+ end
117
116
 
118
- # Moves the cursor to the start of the current line.
119
- def move_home
120
- row, = cursor_position
121
- @cursor = line_start(row)
122
- reset_preferred_column
123
- ensure_cursor_visible
124
- end
117
+ # Moves the cursor to the start of the current line.
118
+ def move_home
119
+ row, = cursor_position
120
+ @cursor = line_start(row)
121
+ reset_preferred_column
122
+ ensure_cursor_visible
123
+ end
125
124
 
126
- # Moves the cursor to the end of the current line.
127
- def move_end
128
- row, = cursor_position
129
- @cursor = line_start(row) + line_length(row)
130
- reset_preferred_column
131
- ensure_cursor_visible
132
- end
125
+ # Moves the cursor to the end of the current line.
126
+ def move_end
127
+ row, = cursor_position
128
+ @cursor = line_start(row) + line_length(row)
129
+ reset_preferred_column
130
+ ensure_cursor_visible
131
+ end
133
132
 
134
- # Deletes the character before the cursor (backspace behavior).
135
- def delete_before_cursor
136
- return if cursor.zero?
133
+ # Deletes the character before the cursor (backspace behavior).
134
+ def delete_before_cursor
135
+ return if cursor.zero?
137
136
 
138
- @value = value[0...(cursor - 1)].to_s + value[cursor..].to_s
139
- @cursor -= 1
140
- reset_preferred_column
141
- ensure_cursor_visible
142
- end
137
+ @value = value[0...(cursor - 1)].to_s + value[cursor..].to_s
138
+ @cursor -= 1
139
+ reset_preferred_column
140
+ ensure_cursor_visible
141
+ end
143
142
 
144
- # Deletes the character at the cursor (delete-key behavior).
145
- def delete_at_cursor
146
- return if cursor >= value.length
143
+ # Deletes the character at the cursor (delete-key behavior).
144
+ def delete_at_cursor
145
+ return if cursor >= value.length
147
146
 
148
- @value = value[0...cursor].to_s + value[(cursor + 1)..].to_s
149
- reset_preferred_column
150
- ensure_cursor_visible
151
- end
147
+ @value = value[0...cursor].to_s + value[(cursor + 1)..].to_s
148
+ reset_preferred_column
149
+ ensure_cursor_visible
150
+ end
152
151
 
153
- # Scrolls the buffer up by one viewport height.
154
- def page_up
155
- @offset -= viewport_height
156
- clamp_offset
157
- end
152
+ # Scrolls the buffer up by one viewport height.
153
+ def page_up
154
+ @offset -= viewport_height
155
+ clamp_offset
156
+ end
158
157
 
159
- # Scrolls the buffer down by one viewport height.
160
- def page_down
161
- @offset += viewport_height
162
- clamp_offset
163
- end
158
+ # Scrolls the buffer down by one viewport height.
159
+ def page_down
160
+ @offset += viewport_height
161
+ clamp_offset
162
+ end
164
163
 
165
- # Moves the cursor vertically by *delta* rows. Stays within the line count and uses
166
- # `preferred_column` so up/down movement feels stable on short lines.
167
- def move_vertical(delta)
168
- row, column = cursor_position
169
- target_row = (row + delta).clamp(0, lines.length - 1)
170
- @preferred_column ||= column
171
- @cursor = line_start(target_row) + [@preferred_column, line_length(target_row)].min
172
- ensure_cursor_visible
173
- end
164
+ # Moves the cursor vertically by *delta* rows. Stays within the line count and uses
165
+ # `preferred_column` so up/down movement feels stable on short lines.
166
+ def move_vertical(delta)
167
+ row, column = cursor_position
168
+ target_row = (row + delta).clamp(0, lines.length - 1)
169
+ @preferred_column ||= column
170
+ @cursor = line_start(target_row) + [@preferred_column, line_length(target_row)].min
171
+ ensure_cursor_visible
172
+ end
174
173
 
175
- # Sets the preferred column to the current column (called when horizontal movement happens).
176
- def reset_preferred_column
177
- @preferred_column = cursor_position.last
178
- end
174
+ # Sets the preferred column to the current column (called when horizontal movement happens).
175
+ def reset_preferred_column
176
+ @preferred_column = cursor_position.last
177
+ end
179
178
 
180
- # Returns the cursor's current position as `[row, column]`, where row is the zero-based
181
- # line index and column is the character offset within that line.
182
- def cursor_position
183
- before = value[0...cursor].to_s
184
- row = before.count("\n")
185
- last_newline = before.rindex("\n")
186
- column = last_newline ? before.length - last_newline - 1 : before.length
187
- [row, column]
188
- end
179
+ # Returns the cursor's current position as `[row, column]`, where row is the zero-based
180
+ # line index and column is the character offset within that line.
181
+ def cursor_position
182
+ before = value[0...cursor].to_s
183
+ row = before.count("\n")
184
+ last_newline = before.rindex("\n")
185
+ column = last_newline ? before.length - last_newline - 1 : before.length
186
+ [row, column]
187
+ end
189
188
 
190
- # Returns the byte offset where line *row* begins in the value.
191
- def line_start(row)
192
- lines.first(row).sum(&:length) + row
193
- end
189
+ # Returns the byte offset where line *row* begins in the value.
190
+ def line_start(row)
191
+ lines.first(row).sum(&:length) + row
192
+ end
194
193
 
195
- # Returns the character length of the line at *row* (empty string when row is past the end).
196
- def line_length(row)
197
- lines.fetch(row, "").length
198
- end
194
+ # Returns the character length of the line at *row* (empty string when row is past the end).
195
+ def line_length(row)
196
+ lines.fetch(row, "").length
197
+ end
199
198
 
200
- # Splits the value into an array of lines (preserving trailing empty lines).
201
- def lines
202
- value.empty? ? [""] : value.split("\n", -1)
203
- end
199
+ # Splits the value into an array of lines (preserving trailing empty lines).
200
+ def lines
201
+ value.empty? ? [""] : value.split("\n", -1)
202
+ end
204
203
 
205
- # Returns the rendered lines (with cursor marker inserted) before viewport slicing.
206
- def rendered_lines
207
- return [cursor_marker + placeholder] if value.empty?
204
+ # Returns the rendered lines (with cursor marker inserted) before viewport slicing.
205
+ def rendered_lines
206
+ return [cursor_marker + placeholder] if value.empty?
208
207
 
209
- (value[0...cursor].to_s + cursor_marker + value[cursor..].to_s).split("\n", -1)
210
- end
208
+ (value[0...cursor].to_s + cursor_marker + value[cursor..].to_s).split("\n", -1)
209
+ end
211
210
 
212
- # Returns the lines that should be visible in the current viewport, padded to *height*
213
- # with empty strings when the buffer is shorter.
214
- def visible_lines
215
- ensure_cursor_visible
216
- rendered = rendered_lines.slice(offset, viewport_height) || []
217
- return rendered unless height
211
+ # Returns the lines that should be visible in the current viewport, padded to *height*
212
+ # with empty strings when the buffer is shorter.
213
+ def visible_lines
214
+ ensure_cursor_visible
215
+ rendered = rendered_lines.slice(offset, viewport_height) || []
216
+ return rendered unless height
218
217
 
219
- rendered + Array.new([height - rendered.length, 0].max, "")
220
- end
218
+ rendered + Array.new([height - rendered.length, 0].max, "")
219
+ end
221
220
 
222
- # Renders a single line, clipping to *width* and padding with spaces.
223
- def render_line(line)
224
- return line unless width
221
+ # Renders a single line, clipping to *width* and padding with spaces.
222
+ def render_line(line)
223
+ return line unless width
225
224
 
226
- clipped = UI.visible_slice(line, 0, width)
227
- clipped + (" " * [width - UI::Width.measure(clipped), 0].max)
228
- end
225
+ clipped = UI.visible_slice(line, 0, width)
226
+ clipped + (" " * [width - UI::Width.measure(clipped), 0].max)
227
+ end
229
228
 
230
- # Adjusts the top-visible offset so the cursor row is in view. Scrolling is performed
231
- # one row at a time when needed.
232
- def ensure_cursor_visible
233
- row, = cursor_position
234
- @offset = row if row < offset
235
- @offset = row - viewport_height + 1 if row >= offset + viewport_height
236
- clamp_offset
237
- end
229
+ # Adjusts the top-visible offset so the cursor row is in view. Scrolling is performed
230
+ # one row at a time when needed.
231
+ def ensure_cursor_visible
232
+ row, = cursor_position
233
+ @offset = row if row < offset
234
+ @offset = row - viewport_height + 1 if row >= offset + viewport_height
235
+ clamp_offset
236
+ end
238
237
 
239
- # Clamps the cursor and offset to valid bounds.
240
- def clamp_position
241
- @cursor = cursor.clamp(0, value.length)
242
- clamp_offset
243
- end
238
+ # Clamps the cursor and offset to valid bounds.
239
+ def clamp_position
240
+ @cursor = cursor.clamp(0, value.length)
241
+ clamp_offset
242
+ end
244
243
 
245
- # Clamps the offset to the valid range `[0, max_offset]`.
246
- def clamp_offset
247
- @offset = offset.clamp(0, max_offset)
248
- end
244
+ # Clamps the offset to the valid range `[0, max_offset]`.
245
+ def clamp_offset
246
+ @offset = offset.clamp(0, max_offset)
247
+ end
249
248
 
250
- # Returns the maximum allowed offset (so the bottom of the buffer is reachable).
251
- def max_offset
252
- [lines.length - viewport_height, 0].max
253
- end
249
+ # Returns the maximum allowed offset (so the bottom of the buffer is reachable).
250
+ def max_offset
251
+ [lines.length - viewport_height, 0].max
252
+ end
254
253
 
255
- # Returns the visible row count (the configured *height* or the buffer's line count).
256
- def viewport_height
257
- height || lines.length
258
- end
254
+ # Returns the visible row count (the configured *height* or the buffer's line count).
255
+ def viewport_height
256
+ height || lines.length
257
+ end
259
258
 
260
- # The literal character used to mark the cursor position in `rendered_lines`.
261
- def cursor_marker
262
- "|"
263
- end
259
+ # The literal character used to mark the cursor position in `rendered_lines`.
260
+ def cursor_marker
261
+ "|"
264
262
  end
265
263
  end
266
264
  end