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,135 +1,133 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- # View is the base class for all screen view implementations. It provides assign injection (via `initialize`),
6
- # rendering hooks, layout composition helpers (`row`, `column`, `render_component`, `yield_content`),
7
- # and access to controller theme, style, and focus state from within views.
8
- class View
9
- # Initializes the view with named assigns injected as instance-local accessor methods via
10
- # `define_singleton_method`. Called when a controller instantiates a view for rendering.
11
- def initialize(**assigns)
12
- @assigns = assigns
13
- define_assign_readers
14
- end
4
+ # View is the base class for all screen view implementations. It provides assign injection (via `initialize`),
5
+ # rendering hooks, layout composition helpers (`row`, `column`, `render_component`, `yield_content`),
6
+ # and access to controller theme, style, and focus state from within views.
7
+ class View
8
+ # Initializes the view with named assigns injected as instance-local accessor methods via
9
+ # `define_singleton_method`. Called when a controller instantiates a view for rendering.
10
+ def initialize(**assigns)
11
+ @assigns = assigns
12
+ define_assign_readers
13
+ end
15
14
 
16
- # Returns all view assigns as a hash, used by layouts to compose the full template (content + screen + controller).
17
- def layout_assigns
18
- assigns
19
- end
15
+ # Returns all view assigns as a hash, used by layouts to compose the full template (content + screen + controller).
16
+ def layout_assigns
17
+ assigns
18
+ end
20
19
 
21
- # Renders the view's body. Default is empty — subclasses override to return visible text.
22
- def render
23
- ""
24
- end
20
+ # Renders the view's body. Default is empty — subclasses override to return visible text.
21
+ def render
22
+ ""
23
+ end
25
24
 
26
- # Delegates focus checking to the controller in assigns, allowing views to determine which slot (sidebar, content) has focus.
27
- def focused?(slot)
28
- ctrl = assigns[:focus_controller] || assigns[:controller]
29
- ctrl ? ctrl.focused?(slot) : false
30
- end
25
+ # Delegates focus checking to the controller in assigns, allowing views to determine which slot (sidebar, content) has focus.
26
+ def focused?(slot)
27
+ ctrl = assigns[:focus_controller] || assigns[:controller]
28
+ ctrl ? ctrl.focused?(slot) : false
29
+ end
31
30
 
32
- private
31
+ private
33
32
 
34
- attr_reader :assigns
33
+ attr_reader :assigns
35
34
 
36
- # Returns the shared UI style configuration used by components and views for visual rendering (colors, borders).
37
- def style
38
- UI.style
39
- end
35
+ # Returns the shared UI style configuration used by components and views for visual rendering (colors, borders).
36
+ def style
37
+ UI.style
38
+ end
40
39
 
41
- # Returns the active theme: uses `theme` from assigns or controller, falling back to `UI::Theme.default`.
42
- def theme
43
- assigns[:theme] || assigns[:controller]&.theme || UI::Theme.default
44
- end
40
+ # Returns the active theme: uses `theme` from assigns or controller, falling back to `UI::Theme.default`.
41
+ def theme
42
+ assigns[:theme] || assigns[:controller]&.theme || UI::Theme.default
43
+ end
45
44
 
46
- # Outputs styled text through the view's rendering pipeline. Accepts a named `style:` for inline formatting.
47
- # Appends the rendered value to the output buffer and returns it.
48
- def text(value, style: nil)
49
- rendered = apply_style(value.to_s, style)
50
- append_to_buffer(rendered)
51
- rendered
52
- end
45
+ # Outputs styled text through the view's rendering pipeline. Accepts a named `style:` for inline formatting.
46
+ # Appends the rendered value to the output buffer and returns it.
47
+ def text(value, style: nil)
48
+ rendered = apply_style(value.to_s, style)
49
+ append_to_buffer(rendered)
50
+ rendered
51
+ end
53
52
 
54
- # Renders a box with optional styling. Accepts an inline block for complex content or a plain value.
55
- # Used for bordered containers and field groups in views.
56
- def box(value = nil, style: nil, &)
57
- content = block_given? ? capture(&) : value.to_s
58
- apply_style(content, style)
59
- end
53
+ # Renders a box with optional styling. Accepts an inline block for complex content or a plain value.
54
+ # Used for bordered containers and field groups in views.
55
+ def box(value = nil, style: nil, &)
56
+ content = block_given? ? capture(&) : value.to_s
57
+ apply_style(content, style)
58
+ end
60
59
 
61
- # Joins items horizontally (side-by-side) using the UI rendering engine. Supports a `gap:` parameter.
62
- def row(*items, gap: 0)
63
- UI.join_horizontal(*items, gap: gap)
64
- end
60
+ # Joins items horizontally (side-by-side) using the UI rendering engine. Supports a `gap:` parameter.
61
+ def row(*items, gap: 0)
62
+ UI.join_horizontal(*items, gap: gap)
63
+ end
65
64
 
66
- # Stacks items vertically using the UI rendering engine. Supports a `gap:` parameter for spacing.
67
- def column(*items, gap: 0)
68
- UI.join_vertical(*items, gap: gap)
69
- end
65
+ # Stacks items vertically using the UI rendering engine. Supports a `gap:` parameter for spacing.
66
+ def column(*items, gap: 0)
67
+ UI.join_vertical(*items, gap: gap)
68
+ end
70
69
 
71
- # Renders a component (e.g., a ProgressBar, Spinner, Modal) and returns its string output.
72
- def render_component(component)
73
- component.render.to_s
74
- end
70
+ # Renders a component (e.g., a ProgressBar, Spinner, Modal) and returns its string output.
71
+ def render_component(component)
72
+ component.render.to_s
73
+ end
75
74
 
76
- # Renders a partial view component. An alias for `render_component` used in layout templates.
77
- def render_partial(partial)
78
- render_component(partial)
79
- end
75
+ # Renders a partial view component. An alias for `render_component` used in layout templates.
76
+ def render_partial(partial)
77
+ render_component(partial)
78
+ end
80
79
 
81
- # Builds a declarative layout tree for the current terminal screen and renders it.
82
- def screen_layout(background: nil, &)
83
- layout = Layout::Builder.build(screen: layout_screen, view: self, background: background, &)
84
- register_layout_focus(layout)
85
- layout.render
86
- end
80
+ # Builds a declarative layout tree for the current terminal screen and renders it.
81
+ def screen_layout(background: nil, &)
82
+ layout = Layout::Builder.build(screen: layout_screen, view: self, background: background, &)
83
+ register_layout_focus(layout)
84
+ layout.render
85
+ end
87
86
 
88
- # Yields the layout's `content` slot — used by view templates to inject their body into a layout wrapper (e.g., sidebar).
89
- def yield_content
90
- assigns.fetch(:content, "")
91
- end
87
+ # Yields the layout's `content` slot — used by view templates to inject their body into a layout wrapper (e.g., sidebar).
88
+ def yield_content
89
+ assigns.fetch(:content, "")
90
+ end
92
91
 
93
- # Evaluates a block in the view's context with a clean output buffer. Captures text written via `text`/`box`
94
- # and returns joined content. Resets buffer afterward for parent rendering.
95
- def capture(&)
96
- previous_buffer = @output_buffer
97
- @output_buffer = []
98
- result = instance_eval(&)
99
- @output_buffer.empty? ? result.to_s : @output_buffer.join("\n")
100
- ensure
101
- @output_buffer = previous_buffer
102
- end
92
+ # Evaluates a block in the view's context with a clean output buffer. Captures text written via `text`/`box`
93
+ # and returns joined content. Resets buffer afterward for parent rendering.
94
+ def capture(&)
95
+ previous_buffer = @output_buffer
96
+ @output_buffer = []
97
+ result = instance_eval(&)
98
+ @output_buffer.empty? ? result.to_s : @output_buffer.join("\n")
99
+ ensure
100
+ @output_buffer = previous_buffer
101
+ end
103
102
 
104
- # Appends a value to the current output buffer (if one is active). Used by rendering helpers.
105
- def append_to_buffer(value)
106
- @output_buffer << value if @output_buffer
107
- end
103
+ # Appends a value to the current output buffer (if one is active). Used by rendering helpers.
104
+ def append_to_buffer(value)
105
+ @output_buffer << value if @output_buffer
106
+ end
108
107
 
109
- # Applies a style object's `render` method to a string, returning styled output or raw text when style is nil.
110
- def apply_style(value, style_object)
111
- style_object ? style_object.render(value) : value
112
- end
108
+ # Applies a style object's `render` method to a string, returning styled output or raw text when style is nil.
109
+ def apply_style(value, style_object)
110
+ style_object ? style_object.render(value) : value
111
+ end
113
112
 
114
- # Dynamically defines read-only accessor methods for each assign key as singleton methods on self.
115
- # Skips keys where the view already responds (controller methods take precedence).
116
- def define_assign_readers
117
- assigns.each_key do |name|
118
- next if respond_to?(name, true)
113
+ # Dynamically defines read-only accessor methods for each assign key as singleton methods on self.
114
+ # Skips keys where the view already responds (controller methods take precedence).
115
+ def define_assign_readers
116
+ assigns.each_key do |name|
117
+ next if respond_to?(name, true)
119
118
 
120
- define_singleton_method(name) { assigns.fetch(name) }
121
- end
119
+ define_singleton_method(name) { assigns.fetch(name) }
122
120
  end
121
+ end
123
122
 
124
- def layout_screen
125
- assigns[:screen] || assigns[:controller]&.screen || Charming::Screen.new(width: 80, height: 24)
126
- end
123
+ def layout_screen
124
+ assigns[:screen] || assigns[:controller]&.screen || Charming::Screen.new(width: 80, height: 24)
125
+ end
127
126
 
128
- def register_layout_focus(layout)
129
- return unless assigns[:controller]
127
+ def register_layout_focus(layout)
128
+ return unless assigns[:controller]
130
129
 
131
- assigns[:controller].focus.define_layout(layout.focusable_names)
132
- end
130
+ assigns[:controller].focus.define_layout(layout.focusable_names)
133
131
  end
134
132
  end
135
133
  end
@@ -26,19 +26,21 @@ module Charming
26
26
  # restores terminal state on exit.
27
27
  def run
28
28
  setup_terminal
29
- render(resolve_response(dispatch(@route.action)))
30
- loop do
31
- event = next_task_event || next_timer_event || @backend.read_event(timeout: read_timeout)
32
- next unless event
29
+ with_raw_input do
30
+ render(resolve_response(dispatch(@route.action)))
31
+ loop do
32
+ event = next_task_event || next_timer_event || @backend.read_event(timeout: read_timeout)
33
+ next unless event
33
34
 
34
- response = dispatch_event(event)
35
- next unless response
36
- break if response.quit?
35
+ response = dispatch_event(event)
36
+ next unless response
37
+ break if response.quit?
37
38
 
38
- response = resolve_response(response)
39
- break if response.quit?
39
+ response = resolve_response(response)
40
+ break if response.quit?
40
41
 
41
- render(response)
42
+ render(response)
43
+ end
42
44
  end
43
45
  ensure
44
46
  @task_executor&.shutdown(timeout: 0.0)
@@ -93,8 +95,12 @@ module Charming
93
95
  end
94
96
 
95
97
  # Dispatches a resize event: updates screen dimensions and re-renders the current action.
98
+ # The renderer's cached previous frame is invalidated and the backend is cleared so the
99
+ # new-dimension frame paints onto a clean alt-screen instead of overlaying stale rows.
96
100
  def dispatch_resize(event)
97
101
  @screen = Screen.new(width: event.width, height: event.height)
102
+ @renderer.invalidate if @renderer.respond_to?(:invalidate)
103
+ @backend.clear if @backend.respond_to?(:clear)
98
104
  dispatch(@route.action, event: event)
99
105
  end
100
106
 
@@ -183,6 +189,13 @@ module Charming
183
189
  @backend.install_resize_handler if @backend.respond_to?(:install_resize_handler)
184
190
  end
185
191
 
192
+ # Keeps input raw/no-echo across rendering and dispatch, not just during reads.
193
+ def with_raw_input(&block)
194
+ return yield unless @backend.respond_to?(:with_raw_input)
195
+
196
+ @backend.with_raw_input(&block)
197
+ end
198
+
186
199
  # Restores terminal state: reinstalls any previous resize handler, shows
187
200
  # the cursor, and leaves the alternative screen buffer.
188
201
  def restore_terminal
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/charming.rb CHANGED
@@ -17,6 +17,7 @@ loader.inflector.inflect(
17
17
  "mouse_parser" => "MouseParser",
18
18
  "tty_backend" => "TTYBackend"
19
19
  )
20
+ loader.collapse("#{__dir__}/charming/presentation")
20
21
  loader.setup
21
22
 
22
23
  module Charming
@@ -37,5 +38,5 @@ module Charming
37
38
  end
38
39
  end
39
40
 
40
- Charming::Presentation::Templates.register ".tui.erb", Charming::Presentation::Templates::ErbHandler
41
- Charming::Presentation::Templates.register ".txt.erb", Charming::Presentation::Templates::ErbHandler
41
+ Charming::Templates.register ".tui.erb", Charming::Templates::ErbHandler
42
+ Charming::Templates.register ".txt.erb", Charming::Templates::ErbHandler
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: charming
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - pando
@@ -277,6 +277,7 @@ files:
277
277
  - lib/charming/presentation/component.rb
278
278
  - lib/charming/presentation/components/activity_indicator.rb
279
279
  - lib/charming/presentation/components/command_palette.rb
280
+ - lib/charming/presentation/components/command_palette_modal.rb
280
281
  - lib/charming/presentation/components/empty_state.rb
281
282
  - lib/charming/presentation/components/form.rb
282
283
  - lib/charming/presentation/components/form/builder.rb