charming 0.1.2 → 0.1.4

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/lib/charming/application.rb +22 -5
  3. data/lib/charming/cli.rb +3 -3
  4. data/lib/charming/controller/class_methods.rb +2 -2
  5. data/lib/charming/controller/command_palette.rb +2 -2
  6. data/lib/charming/controller/component_dispatching.rb +47 -3
  7. data/lib/charming/controller/focus.rb +123 -0
  8. data/lib/charming/controller/focus_management.rb +1 -1
  9. data/lib/charming/controller/rendering.rb +6 -17
  10. data/lib/charming/controller/session_state.rb +12 -1
  11. data/lib/charming/controller.rb +11 -2
  12. data/lib/charming/database/commands.rb +106 -0
  13. data/lib/charming/generators/component_generator.rb +1 -1
  14. data/lib/charming/generators/database_installer.rb +154 -0
  15. data/lib/charming/generators/model_generator.rb +2 -10
  16. data/lib/charming/generators/name.rb +1 -1
  17. data/lib/charming/generators/templates/app/application.template +1 -1
  18. data/lib/charming/generators/templates/app/layout.template +3 -6
  19. data/lib/charming/generators/templates/app/view.template +1 -1
  20. data/lib/charming/generators/templates/component/component.rb.template +1 -1
  21. data/lib/charming/generators/templates/screen/view.rb.template +1 -1
  22. data/lib/charming/generators/templates/view/view.rb.template +1 -1
  23. data/lib/charming/generators/view_generator.rb +1 -1
  24. data/lib/charming/internal/renderer/differential.rb +13 -5
  25. data/lib/charming/internal/terminal/tty_backend.rb +22 -2
  26. data/lib/charming/presentation/component.rb +3 -5
  27. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  28. data/lib/charming/presentation/components/command_palette.rb +94 -96
  29. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  30. data/lib/charming/presentation/components/empty_state.rb +47 -49
  31. data/lib/charming/presentation/components/form/builder.rb +52 -54
  32. data/lib/charming/presentation/components/form/confirm.rb +49 -51
  33. data/lib/charming/presentation/components/form/field.rb +94 -96
  34. data/lib/charming/presentation/components/form/input.rb +53 -55
  35. data/lib/charming/presentation/components/form/note.rb +27 -29
  36. data/lib/charming/presentation/components/form/select.rb +84 -86
  37. data/lib/charming/presentation/components/form/textarea.rb +67 -69
  38. data/lib/charming/presentation/components/form.rb +120 -122
  39. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  40. data/lib/charming/presentation/components/list.rb +123 -125
  41. data/lib/charming/presentation/components/markdown.rb +21 -23
  42. data/lib/charming/presentation/components/modal.rb +46 -48
  43. data/lib/charming/presentation/components/progressbar.rb +51 -53
  44. data/lib/charming/presentation/components/spinner.rb +40 -42
  45. data/lib/charming/presentation/components/table.rb +109 -111
  46. data/lib/charming/presentation/components/text_area.rb +219 -221
  47. data/lib/charming/presentation/components/text_input.rb +120 -122
  48. data/lib/charming/presentation/components/viewport.rb +218 -220
  49. data/lib/charming/presentation/layout/builder.rb +64 -66
  50. data/lib/charming/presentation/layout/overlay.rb +48 -50
  51. data/lib/charming/presentation/layout/pane.rb +129 -118
  52. data/lib/charming/presentation/layout/rect.rb +19 -16
  53. data/lib/charming/presentation/layout/screen_layout.rb +47 -42
  54. data/lib/charming/presentation/layout/split.rb +107 -102
  55. data/lib/charming/presentation/layout.rb +28 -30
  56. data/lib/charming/presentation/markdown/render_context.rb +30 -14
  57. data/lib/charming/presentation/markdown/renderer.rb +302 -79
  58. data/lib/charming/presentation/markdown/style_config.rb +215 -0
  59. data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -59
  60. data/lib/charming/presentation/markdown.rb +4 -6
  61. data/lib/charming/presentation/template_view.rb +22 -24
  62. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  63. data/lib/charming/presentation/templates.rb +47 -49
  64. data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
  65. data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
  66. data/lib/charming/presentation/ui/border.rb +24 -26
  67. data/lib/charming/presentation/ui/border_painter.rb +37 -39
  68. data/lib/charming/presentation/ui/canvas.rb +59 -61
  69. data/lib/charming/presentation/ui/style.rb +173 -175
  70. data/lib/charming/presentation/ui/theme.rb +133 -135
  71. data/lib/charming/presentation/ui/width.rb +12 -14
  72. data/lib/charming/presentation/ui.rb +69 -71
  73. data/lib/charming/presentation/view.rb +110 -105
  74. data/lib/charming/router.rb +3 -8
  75. data/lib/charming/runtime.rb +25 -10
  76. data/lib/charming/version.rb +1 -1
  77. data/lib/charming.rb +5 -4
  78. metadata +43 -9
  79. data/lib/charming/database_commands.rb +0 -103
  80. data/lib/charming/database_installer.rb +0 -152
  81. data/lib/charming/focus.rb +0 -121
  82. data/lib/charming/presentation/markdown/block_renderers.rb +0 -120
  83. data/lib/charming/presentation/markdown/inline_renderers.rb +0 -68
@@ -1,211 +1,209 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module UI
6
- # Style is an immutable builder for terminal text styling. Every method returns a new
7
- # Style instance with the requested attribute added, so styles can be safely chained and
8
- # shared across views. `render(value)` applies the accumulated style to a string.
9
- class Style
10
- ATTRIBUTES = ANSICodes::ATTRIBUTES
11
-
12
- COLORS = ANSICodes::COLORS
13
-
14
- # Initializes a new style with an optional options hash. Recognized keys: `:attributes`
15
- # (array of attribute symbols), `:padding` ([top, right, bottom, left]), `:align`
16
- # (`:left`/`:right`/`:center`), and any of `:foreground`, `:background`, `:border`,
17
- # `:border_sides`, `:border_foreground`, `:width`, `:height`.
18
- def initialize(options = {})
19
- @options = {
20
- attributes: [],
21
- padding: [0, 0, 0, 0],
22
- align: :left
23
- }.merge(options)
24
- end
4
+ module UI
5
+ # Style is an immutable builder for terminal text styling. Every method returns a new
6
+ # Style instance with the requested attribute added, so styles can be safely chained and
7
+ # shared across views. `render(value)` applies the accumulated style to a string.
8
+ class Style
9
+ ATTRIBUTES = ANSICodes::ATTRIBUTES
10
+
11
+ COLORS = ANSICodes::COLORS
12
+
13
+ # Initializes a new style with an optional options hash. Recognized keys: `:attributes`
14
+ # (array of attribute symbols), `:padding` ([top, right, bottom, left]), `:align`
15
+ # (`:left`/`:right`/`:center`), and any of `:foreground`, `:background`, `:border`,
16
+ # `:border_sides`, `:border_foreground`, `:width`, `:height`.
17
+ def initialize(options = {})
18
+ @options = {
19
+ attributes: [],
20
+ padding: [0, 0, 0, 0],
21
+ align: :left
22
+ }.merge(options)
23
+ end
25
24
 
26
- # Returns a new Style with the foreground *color* set. *color* is a color name (":red"),
27
- # 256-color index (integer), or hex string ("#rrggbb").
28
- def foreground(color)
29
- with(foreground: color)
30
- end
31
- alias_method :fg, :foreground
25
+ # Returns a new Style with the foreground *color* set. *color* is a color name (":red"),
26
+ # 256-color index (integer), or hex string ("#rrggbb").
27
+ def foreground(color)
28
+ with(foreground: color)
29
+ end
30
+ alias_method :fg, :foreground
32
31
 
33
- # Returns a new Style with the background *color* set.
34
- def background(color)
35
- with(background: color)
36
- end
37
- alias_method :bg, :background
38
-
39
- # Attribute methods (bold, italic, underline, …) are defined dynamically by the
40
- # metaprogramming loop below. Each toggles a single text attribute on the style.
41
- ATTRIBUTES.each_key do |attribute|
42
- define_method(attribute) do
43
- with(attributes: (@options.fetch(:attributes) + [attribute]).uniq)
44
- end
45
- end
32
+ # Returns a new Style with the background *color* set.
33
+ def background(color)
34
+ with(background: color)
35
+ end
36
+ alias_method :bg, :background
46
37
 
47
- # Returns a new Style with the padding set. Accepts 1, 2, or 4 values following CSS-style
48
- # shorthand: 1 all sides, 2 [vertical, horizontal], 4 [top, right, bottom, left].
49
- def padding(*values)
50
- with(padding: expand_box_values(values))
38
+ # Attribute methods (bold, italic, underline, …) are defined dynamically by the
39
+ # metaprogramming loop below. Each toggles a single text attribute on the style.
40
+ ATTRIBUTES.each_key do |attribute|
41
+ define_method(attribute) do
42
+ with(attributes: (@options.fetch(:attributes) + [attribute]).uniq)
51
43
  end
44
+ end
52
45
 
53
- # Returns a new Style with the border set. *style* is a border name (e.g., :normal,
54
- # :rounded). *sides* optionally restricts the border to specific sides. *foreground*
55
- # sets the border color.
56
- def border(style = :normal, sides: nil, foreground: nil)
57
- with(border: style, border_sides: sides, border_foreground: foreground)
58
- end
46
+ # Returns a new Style with the padding set. Accepts 1, 2, or 4 values following CSS-style
47
+ # shorthand: 1 → all sides, 2 [vertical, horizontal], 4 [top, right, bottom, left].
48
+ def padding(*values)
49
+ with(padding: expand_box_values(values))
50
+ end
59
51
 
60
- # Returns a new Style that fixes the rendered width to *value* (in display columns).
61
- def width(value)
62
- with(width: value)
63
- end
52
+ # Returns a new Style with the border set. *style* is a border name (e.g., :normal,
53
+ # :rounded). *sides* optionally restricts the border to specific sides. *foreground*
54
+ # sets the border color.
55
+ def border(style = :normal, sides: nil, foreground: nil)
56
+ with(border: style, border_sides: sides, border_foreground: foreground)
57
+ end
64
58
 
65
- # Returns a new Style that fixes the rendered height to *value* (in rows).
66
- def height(value)
67
- with(height: value)
68
- end
59
+ # Returns a new Style that fixes the rendered width to *value* (in display columns).
60
+ def width(value)
61
+ with(width: value)
62
+ end
69
63
 
70
- # Returns a new Style with horizontal alignment set (`:left`, `:right`, or `:center`).
71
- def align(value)
72
- with(align: value)
73
- end
64
+ # Returns a new Style that fixes the rendered height to *value* (in rows).
65
+ def height(value)
66
+ with(height: value)
67
+ end
74
68
 
75
- # Applies the configured style to *value* and returns the styled string. Steps:
76
- # 1. wrap to `:width`, 2. align horizontally, 3. expand to `:height`, 4. apply padding,
77
- # 5. paint border, 6. emit ANSI attribute/foreground/background escapes.
78
- def render(value)
79
- lines = apply_dimensions(value.to_s.lines(chomp: true))
80
- lines = apply_padding(lines)
81
- lines = apply_border(lines)
82
- apply_ansi(lines.join("\n"))
83
- end
69
+ # Returns a new Style with horizontal alignment set (`:left`, `:right`, or `:center`).
70
+ def align(value)
71
+ with(align: value)
72
+ end
84
73
 
85
- private
74
+ # Applies the configured style to *value* and returns the styled string. Steps:
75
+ # 1. wrap to `:width`, 2. align horizontally, 3. expand to `:height`, 4. apply padding,
76
+ # 5. paint border, 6. emit ANSI attribute/foreground/background escapes.
77
+ def render(value)
78
+ lines = apply_dimensions(value.to_s.lines(chomp: true))
79
+ lines = apply_padding(lines)
80
+ lines = apply_border(lines)
81
+ apply_ansi(lines.join("\n"))
82
+ end
86
83
 
87
- # Returns a copy of self with *changes* merged into the options hash.
88
- def with(changes)
89
- self.class.new(@options.merge(changes))
90
- end
84
+ private
91
85
 
92
- # Wraps each line to the target width and applies horizontal alignment, then expands
93
- # to the target height.
94
- def apply_dimensions(lines)
95
- content_width = target_content_width(lines)
96
- dimensioned = lines.map { |line| align_line(fit_line(line, content_width), content_width) }
97
- apply_height(dimensioned, content_width)
98
- end
86
+ # Returns a copy of self with *changes* merged into the options hash.
87
+ def with(changes)
88
+ self.class.new(@options.merge(changes))
89
+ end
99
90
 
100
- # Returns the target content width: the explicit :width if set, otherwise the natural
101
- # max display width of the lines.
102
- def target_content_width(lines)
103
- explicit_width = @options[:width]
104
- natural_width = lines.map { |line| Width.measure(line) }.max || 0
105
- explicit_width || natural_width
106
- end
91
+ # Wraps each line to the target width and applies horizontal alignment, then expands
92
+ # to the target height.
93
+ def apply_dimensions(lines)
94
+ content_width = target_content_width(lines)
95
+ dimensioned = lines.map { |line| align_line(fit_line(line, content_width), content_width) }
96
+ apply_height(dimensioned, content_width)
97
+ end
107
98
 
108
- # Clips *line* to *width* display columns, preserving ANSI styling where possible.
109
- def fit_line(line, width)
110
- return line if Width.measure(line) <= width
99
+ # Returns the target content width: the explicit :width if set, otherwise the natural
100
+ # max display width of the lines.
101
+ def target_content_width(lines)
102
+ explicit_width = @options[:width]
103
+ natural_width = lines.map { |line| Width.measure(line) }.max || 0
104
+ explicit_width || natural_width
105
+ end
111
106
 
112
- UI.visible_slice(line, 0, width)
113
- end
107
+ # Clips *line* to *width* display columns, preserving ANSI styling where possible.
108
+ def fit_line(line, width)
109
+ return line if Width.measure(line) <= width
110
+
111
+ UI.visible_slice(line, 0, width)
112
+ end
114
113
 
115
- # Truncates or pads the lines array to *height* rows, filling with blank rows.
116
- def apply_height(lines, width)
117
- height = @options[:height]
118
- return lines unless height
114
+ # Truncates or pads the lines array to *height* rows, filling with blank rows.
115
+ def apply_height(lines, width)
116
+ height = @options[:height]
117
+ return lines unless height
119
118
 
120
- visible = lines.first(height)
121
- visible + Array.new([height - visible.length, 0].max) { " " * width }
122
- end
119
+ visible = lines.first(height)
120
+ visible + Array.new([height - visible.length, 0].max) { " " * width }
121
+ end
123
122
 
124
- # Applies padding by prepending/appending blank rows (vertical) and indenting each
125
- # line (horizontal).
126
- def apply_padding(lines)
127
- top, right, bottom, left = @options.fetch(:padding)
128
- inner_width = lines.map { |line| Width.measure(line) }.max || 0
129
- empty = " " * (left + inner_width + right)
130
- padded = lines.map do |line|
131
- pad_line(line, inner_width, left, right)
132
- end
133
-
134
- Array.new(top, empty) + padded + Array.new(bottom, empty)
123
+ # Applies padding by prepending/appending blank rows (vertical) and indenting each
124
+ # line (horizontal).
125
+ def apply_padding(lines)
126
+ top, right, bottom, left = @options.fetch(:padding)
127
+ inner_width = lines.map { |line| Width.measure(line) }.max || 0
128
+ empty = " " * (left + inner_width + right)
129
+ padded = lines.map do |line|
130
+ pad_line(line, inner_width, left, right)
135
131
  end
136
132
 
137
- # Paints the configured border around the lines, when :border is set.
138
- def apply_border(lines)
139
- border_name = @options[:border]
140
- return lines unless border_name
133
+ Array.new(top, empty) + padded + Array.new(bottom, empty)
134
+ end
141
135
 
142
- border_painter(border_name).paint(lines, content_width(lines))
143
- end
136
+ # Paints the configured border around the lines, when :border is set.
137
+ def apply_border(lines)
138
+ border_name = @options[:border]
139
+ return lines unless border_name
144
140
 
145
- # Pads a single line to *inner_width*, with *left* and *right* padding spaces.
146
- def pad_line(line, inner_width, left, right)
147
- (" " * left) + line + (" " * (inner_width - Width.measure(line) + right))
148
- end
141
+ border_painter(border_name).paint(lines, content_width(lines))
142
+ end
149
143
 
150
- # Builds a BorderPainter configured for the current border options.
151
- def border_painter(border_name)
152
- BorderPainter.new(
153
- border: Border.fetch(border_name),
154
- sides: @options[:border_sides],
155
- foreground: @options[:border_foreground],
156
- background: @options[:background]
157
- )
158
- end
144
+ # Pads a single line to *inner_width*, with *left* and *right* padding spaces.
145
+ def pad_line(line, inner_width, left, right)
146
+ (" " * left) + line + (" " * (inner_width - Width.measure(line) + right))
147
+ end
159
148
 
160
- # Returns the natural display width of the longest line in *lines*.
161
- def content_width(lines)
162
- lines.map { |line| Width.measure(line) }.max || 0
163
- end
149
+ # Builds a BorderPainter configured for the current border options.
150
+ def border_painter(border_name)
151
+ BorderPainter.new(
152
+ border: Border.fetch(border_name),
153
+ sides: @options[:border_sides],
154
+ foreground: @options[:border_foreground],
155
+ background: @options[:background]
156
+ )
157
+ end
164
158
 
165
- # Applies the active ANSI attribute/foreground/background codes to *value*.
166
- def apply_ansi(value)
167
- ansi_codes_obj.apply(value)
168
- end
159
+ # Returns the natural display width of the longest line in *lines*.
160
+ def content_width(lines)
161
+ lines.map { |line| Width.measure(line) }.max || 0
162
+ end
169
163
 
170
- # The list of active ANSI escape sequence strings (attribute + foreground + background).
171
- def ansi_codes
172
- ansi_codes_obj.codes
173
- end
164
+ # Applies the active ANSI attribute/foreground/background codes to *value*.
165
+ def apply_ansi(value)
166
+ ansi_codes_obj.apply(value)
167
+ end
174
168
 
175
- # Builds an ANSICodes object from the active attributes, foreground, and background.
176
- def ansi_codes_obj
177
- ANSICodes.new(
178
- attributes: @options.fetch(:attributes),
179
- foreground: @options[:foreground],
180
- background: @options[:background]
181
- )
182
- end
169
+ # The list of active ANSI escape sequence strings (attribute + foreground + background).
170
+ def ansi_codes
171
+ ansi_codes_obj.codes
172
+ end
173
+
174
+ # Builds an ANSICodes object from the active attributes, foreground, and background.
175
+ def ansi_codes_obj
176
+ ANSICodes.new(
177
+ attributes: @options.fetch(:attributes),
178
+ foreground: @options[:foreground],
179
+ background: @options[:background]
180
+ )
181
+ end
182
+
183
+ # Pads *line* on the left or right (or both, for :center) according to :align.
184
+ def align_line(line, width)
185
+ remaining = width - Width.measure(line)
186
+ return line if remaining <= 0
183
187
 
184
- # Pads *line* on the left or right (or both, for :center) according to :align.
185
- def align_line(line, width)
186
- remaining = width - Width.measure(line)
187
- return line if remaining <= 0
188
-
189
- case @options.fetch(:align)
190
- when :right
191
- (" " * remaining) + line
192
- when :center
193
- left = remaining / 2
194
- (" " * left) + line + (" " * (remaining - left))
195
- else
196
- line + (" " * remaining)
197
- end
188
+ case @options.fetch(:align)
189
+ when :right
190
+ (" " * remaining) + line
191
+ when :center
192
+ left = remaining / 2
193
+ (" " * left) + line + (" " * (remaining - left))
194
+ else
195
+ line + (" " * remaining)
198
196
  end
197
+ end
199
198
 
200
- # Normalizes 1/2/4 padding value arguments into a [top, right, bottom, left] array.
201
- def expand_box_values(values)
202
- case values.length
203
- when 1 then [values[0], values[0], values[0], values[0]]
204
- when 2 then [values[0], values[1], values[0], values[1]]
205
- when 4 then values
206
- else
207
- raise ArgumentError, "padding expects 1, 2, or 4 values"
208
- end
199
+ # Normalizes 1/2/4 padding value arguments into a [top, right, bottom, left] array.
200
+ def expand_box_values(values)
201
+ case values.length
202
+ when 1 then [values[0], values[0], values[0], values[0]]
203
+ when 2 then [values[0], values[1], values[0], values[1]]
204
+ when 4 then values
205
+ else
206
+ raise ArgumentError, "padding expects 1, 2, or 4 values"
209
207
  end
210
208
  end
211
209
  end