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,133 +1,138 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Layout
6
- # Split divides a parent Rect among its child nodes horizontally or vertically.
7
- # Children with a configured `width`/`height` are placed at that fixed size; children
8
- # without a fixed size share the remaining space according to their `grow` weight.
9
- class Split
10
- # The fixed width/height of the split (when set) and the grow weight for the split itself.
11
- attr_reader :width, :height, :grow
12
-
13
- # *direction* is `:horizontal` or `:vertical`. *gap* (in cells) separates children.
14
- # *width*/*height* are optional fixed dimensions for the split as a whole.
15
- # *grow* is the weight for distributing remaining space (used when this Split is a
16
- # child of another Split).
17
- def initialize(direction:, gap: 0, width: nil, height: nil, grow: nil)
18
- @direction = direction.to_sym
19
- @gap = gap.to_i
20
- @width = width
21
- @height = height
22
- @grow = grow
23
- @children = []
24
- end
4
+ module Layout
5
+ # Split divides a parent Rect among its child nodes horizontally or vertically.
6
+ # Children with a configured `width`/`height` are placed at that fixed size; children
7
+ # without a fixed size share the remaining space according to their `grow` weight.
8
+ class Split
9
+ # The fixed width/height of the split (when set) and the grow weight for the split itself.
10
+ attr_reader :width, :height, :grow
11
+
12
+ # *direction* is `:horizontal` or `:vertical`. *gap* (in cells) separates children.
13
+ # *width*/*height* are optional fixed dimensions for the split as a whole.
14
+ # *grow* is the weight for distributing remaining space (used when this Split is a
15
+ # child of another Split).
16
+ def initialize(direction:, gap: 0, width: nil, height: nil, grow: nil)
17
+ @direction = direction.to_sym
18
+ @gap = gap.to_i
19
+ @width = width
20
+ @height = height
21
+ @grow = grow
22
+ @children = []
23
+ end
25
24
 
26
- # Appends *node* (a child Split or Pane) to this Split.
27
- def add_child(node)
28
- children << node
29
- end
25
+ # Appends *node* (a child Split or Pane) to this Split.
26
+ def add_child(node)
27
+ children << node
28
+ end
30
29
 
31
- # Returns the flattened list of focusable names from all child nodes.
32
- def focusable_names
33
- children.flat_map(&:focusable_names)
34
- end
30
+ # Returns the flattened list of focusable names from all child nodes.
31
+ def focusable_names
32
+ children.flat_map(&:focusable_names)
33
+ end
35
34
 
36
- # Renders each child into its own sub-rect, then overlays them on a blank canvas
37
- # of the parent's dimensions.
38
- def render(rect)
39
- frame = UI.place("", width: rect.width, height: rect.height)
35
+ # Returns all named pane mouse targets in this split, preserving declaration order.
36
+ def mouse_targets(rect)
37
+ child_rects(rect).zip(children).flat_map do |child_rect, child|
38
+ child.respond_to?(:mouse_targets) ? child.mouse_targets(child_rect) : []
39
+ end
40
+ end
40
41
 
41
- child_rects(rect).zip(children).each do |child_rect, child|
42
- frame = UI.overlay(frame, child.render(child_rect), top: child_rect.y - rect.y, left: child_rect.x - rect.x)
43
- end
42
+ # Renders each child into its own sub-rect, then overlays them on a blank canvas
43
+ # of the parent's dimensions.
44
+ def render(rect)
45
+ frame = UI.place("", width: rect.width, height: rect.height)
44
46
 
45
- frame
47
+ child_rects(rect).zip(children).each do |child_rect, child|
48
+ frame = UI.overlay(frame, child.render(child_rect), top: child_rect.y - rect.y, left: child_rect.x - rect.x)
46
49
  end
47
50
 
48
- private
49
-
50
- # The split direction (`:horizontal` or `:vertical`) and the inter-child gap.
51
- attr_reader :direction, :gap, :children
51
+ frame
52
+ end
52
53
 
53
- # Returns an array of child rects sized according to each child's fixed dimensions
54
- # and grow weights. Raises ArgumentError when *direction* is neither horizontal nor vertical.
55
- def child_rects(rect)
56
- return horizontal_rects(rect) if direction == :horizontal
57
- return vertical_rects(rect) if direction == :vertical
54
+ private
58
55
 
59
- raise ArgumentError, "unknown split direction: #{direction.inspect}"
60
- end
56
+ # The split direction (`:horizontal` or `:vertical`) and the inter-child gap.
57
+ attr_reader :direction, :gap, :children
61
58
 
62
- # Computes per-child rects for a horizontal split.
63
- def horizontal_rects(rect)
64
- sizes = child_sizes(axis: :horizontal, available: rect.width)
65
- left = rect.x
59
+ # Returns an array of child rects sized according to each child's fixed dimensions
60
+ # and grow weights. Raises ArgumentError when *direction* is neither horizontal nor vertical.
61
+ def child_rects(rect)
62
+ return horizontal_rects(rect) if direction == :horizontal
63
+ return vertical_rects(rect) if direction == :vertical
66
64
 
67
- sizes.map do |width|
68
- child_rect = Rect.new(x: left, y: rect.y, width: width, height: rect.height)
69
- left += width + gap
70
- child_rect
71
- end
72
- end
65
+ raise ArgumentError, "unknown split direction: #{direction.inspect}"
66
+ end
73
67
 
74
- # Computes per-child rects for a vertical split.
75
- def vertical_rects(rect)
76
- sizes = child_sizes(axis: :vertical, available: rect.height)
77
- top = rect.y
68
+ # Computes per-child rects for a horizontal split.
69
+ def horizontal_rects(rect)
70
+ sizes = child_sizes(axis: :horizontal, available: rect.width)
71
+ left = rect.x
78
72
 
79
- sizes.map do |height|
80
- child_rect = Rect.new(x: rect.x, y: top, width: rect.width, height: height)
81
- top += height + gap
82
- child_rect
83
- end
73
+ sizes.map do |width|
74
+ child_rect = Rect.new(x: left, y: rect.y, width: width, height: rect.height)
75
+ left += width + gap
76
+ child_rect
84
77
  end
78
+ end
85
79
 
86
- # Computes the size of each child along the *axis* given the *available* cells.
87
- # Subtracts the total gap, allocates fixed sizes first, and distributes the remainder
88
- # among flexible (non-fixed) children by their grow weights.
89
- def child_sizes(axis:, available:)
90
- gap_size = gap * [children.length - 1, 0].max
91
- available_for_children = [available - gap_size, 0].max
92
- fixed = children.map { |child| fixed_size(child, axis) }
93
- flexible_indexes = fixed.each_index.select { |index| fixed[index].nil? }
94
- sizes = fixed.map { |size| size&.to_i }
95
- remaining = [available_for_children - sizes.compact.sum, 0].max
96
-
97
- distribute_remaining(sizes, flexible_indexes, remaining)
98
- end
80
+ # Computes per-child rects for a vertical split.
81
+ def vertical_rects(rect)
82
+ sizes = child_sizes(axis: :vertical, available: rect.height)
83
+ top = rect.y
99
84
 
100
- # Returns the fixed size of *child* along *axis* (`:horizontal` reads width, `:vertical` reads height).
101
- def fixed_size(child, axis)
102
- (axis == :horizontal) ? child.width : child.height
85
+ sizes.map do |height|
86
+ child_rect = Rect.new(x: rect.x, y: top, width: rect.width, height: height)
87
+ top += height + gap
88
+ child_rect
103
89
  end
90
+ end
104
91
 
105
- # Distributes the *remaining* cells across *flexible_indexes* by grow weight, with the
106
- # last flexible child absorbing any rounding remainder.
107
- def distribute_remaining(sizes, flexible_indexes, remaining)
108
- return sizes.map { |size| size || 0 } if flexible_indexes.empty?
92
+ # Computes the size of each child along the *axis* given the *available* cells.
93
+ # Subtracts the total gap, allocates fixed sizes first, and distributes the remainder
94
+ # among flexible (non-fixed) children by their grow weights.
95
+ def child_sizes(axis:, available:)
96
+ gap_size = gap * [children.length - 1, 0].max
97
+ available_for_children = [available - gap_size, 0].max
98
+ fixed = children.map { |child| fixed_size(child, axis) }
99
+ flexible_indexes = fixed.each_index.select { |index| fixed[index].nil? }
100
+ sizes = fixed.map { |size| size&.to_i }
101
+ remaining = [available_for_children - sizes.compact.sum, 0].max
102
+
103
+ distribute_remaining(sizes, flexible_indexes, remaining)
104
+ end
105
+
106
+ # Returns the fixed size of *child* along *axis* (`:horizontal` reads width, `:vertical` reads height).
107
+ def fixed_size(child, axis)
108
+ (axis == :horizontal) ? child.width : child.height
109
+ end
109
110
 
110
- total_grow = flexible_indexes.sum { |index| grow_weight(children[index]) }
111
- used = 0
111
+ # Distributes the *remaining* cells across *flexible_indexes* by grow weight, with the
112
+ # last flexible child absorbing any rounding remainder.
113
+ def distribute_remaining(sizes, flexible_indexes, remaining)
114
+ return sizes.map { |size| size || 0 } if flexible_indexes.empty?
112
115
 
113
- flexible_indexes.each_with_index do |index, flexible_index|
114
- size = if flexible_index == flexible_indexes.length - 1
115
- remaining - used
116
- else
117
- (remaining * grow_weight(children[index]) / total_grow).floor
118
- end
116
+ total_grow = flexible_indexes.sum { |index| grow_weight(children[index]) }
117
+ used = 0
119
118
 
120
- sizes[index] = size
121
- used += size
119
+ flexible_indexes.each_with_index do |index, flexible_index|
120
+ size = if flexible_index == flexible_indexes.length - 1
121
+ remaining - used
122
+ else
123
+ (remaining * grow_weight(children[index]) / total_grow).floor
122
124
  end
123
125
 
124
- sizes
126
+ sizes[index] = size
127
+ used += size
125
128
  end
126
129
 
127
- # Returns the grow weight of *child*, defaulting to 1 when unset.
128
- def grow_weight(child)
129
- [child.grow.to_i, 1].max
130
- end
130
+ sizes
131
+ end
132
+
133
+ # Returns the grow weight of *child*, defaulting to 1 when unset.
134
+ def grow_weight(child)
135
+ [child.grow.to_i, 1].max
131
136
  end
132
137
  end
133
138
  end
@@ -1,43 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- # Layout contains generic screen-size math and composition helpers. It is
6
- # intentionally unaware of application shells such as sidebars or nav panes.
7
- module Layout
8
- module_function
4
+ # Layout contains generic screen-size math and composition helpers. It is
5
+ # intentionally unaware of application shells such as sidebars or nav panes.
6
+ module Layout
7
+ module_function
9
8
 
10
- def clamp_size(value, min: nil, max: nil)
11
- size = value.to_i
12
- size = [size, min].max if min
13
- size = [size, max].min if max
14
- size
15
- end
9
+ def clamp_size(value, min: nil, max: nil)
10
+ size = value.to_i
11
+ size = [size, min].max if min
12
+ size = [size, max].min if max
13
+ size
14
+ end
16
15
 
17
- def available_width(screen, reserved: 0, min: nil, max: nil)
18
- clamp_size(screen.width - reserved, min: min, max: max)
19
- end
16
+ def available_width(screen, reserved: 0, min: nil, max: nil)
17
+ clamp_size(screen.width - reserved, min: min, max: max)
18
+ end
20
19
 
21
- def available_height(screen, reserved: 0, min: nil, max: nil)
22
- clamp_size(screen.height - reserved, min: min, max: max)
23
- end
20
+ def available_height(screen, reserved: 0, min: nil, max: nil)
21
+ clamp_size(screen.height - reserved, min: min, max: max)
22
+ end
24
23
 
25
- def stack_or_row(*blocks, narrow:, gap: 0)
26
- if narrow
27
- UI.join_vertical(*blocks, gap: gap)
28
- else
29
- UI.join_horizontal(*blocks, gap: gap)
30
- end
24
+ def stack_or_row(*blocks, narrow:, gap: 0)
25
+ if narrow
26
+ UI.join_vertical(*blocks, gap: gap)
27
+ else
28
+ UI.join_horizontal(*blocks, gap: gap)
31
29
  end
30
+ end
32
31
 
33
- def selected_window_start(selected_index:, item_count:, window_size:)
34
- count = item_count.to_i
35
- size = [window_size.to_i, 1].max
36
- selected = selected_index.to_i.clamp(0, [count - 1, 0].max)
37
- max_start = [count - size, 0].max
32
+ def selected_window_start(selected_index:, item_count:, window_size:)
33
+ count = item_count.to_i
34
+ size = [window_size.to_i, 1].max
35
+ selected = selected_index.to_i.clamp(0, [count - 1, 0].max)
36
+ max_start = [count - size, 0].max
38
37
 
39
- (selected - size + 1).clamp(0, max_start)
40
- end
38
+ (selected - size + 1).clamp(0, max_start)
41
39
  end
42
40
  end
43
41
  end
@@ -1,21 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Markdown
6
- # RenderContext carries the state needed to render nested Markdown blocks: the current
7
- # list nesting depth (used for indentation) and the wrap width.
8
- RenderContext = Data.define(:list_depth, :width) do
9
- # Builds a new RenderContext with the given *width* and optional starting *list_depth*.
10
- def self.from(width:, list_depth: 0)
11
- new(list_depth: list_depth, width: width)
12
- end
4
+ module Markdown
5
+ # RenderContext carries render-time state that needs to flow down the Markdown AST.
6
+ RenderContext = Data.define(:width, :list_depth, :style, :current_style, :base_url, :source_lines) do
7
+ def self.from(width:, style:, base_url: nil, source_lines: [], list_depth: 0, current_style: nil)
8
+ new(
9
+ width: width,
10
+ list_depth: list_depth,
11
+ style: style,
12
+ current_style: current_style || style[:document],
13
+ base_url: base_url,
14
+ source_lines: source_lines
15
+ )
16
+ end
17
+
18
+ def with(width: self.width, list_depth: self.list_depth, current_style: self.current_style)
19
+ self.class.new(
20
+ width: width,
21
+ list_depth: list_depth,
22
+ style: style,
23
+ current_style: current_style,
24
+ base_url: base_url,
25
+ source_lines: source_lines
26
+ )
27
+ end
28
+
29
+ def nested_list(width: self.width)
30
+ with(width: width, list_depth: list_depth + 1)
31
+ end
13
32
 
14
- # Returns a derived context with the list depth incremented by *depth_increment*
15
- # and the wrap width overridden to *width* (defaults to the current width).
16
- def nested(depth_increment: 0, width: self.width)
17
- self.class.new(list_depth: list_depth + depth_increment, width: width)
18
- end
33
+ def inherit(style_name)
34
+ current_style.inherit_visual(style[style_name])
19
35
  end
20
36
  end
21
37
  end