charming 0.1.1 → 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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/charming/application.rb +14 -3
  4. data/lib/charming/cli.rb +23 -0
  5. data/lib/charming/controller/class_methods.rb +115 -0
  6. data/lib/charming/controller/command_palette.rb +135 -0
  7. data/lib/charming/controller/component_dispatching.rb +81 -0
  8. data/lib/charming/controller/dispatching.rb +60 -0
  9. data/lib/charming/controller/focus_management.rb +30 -0
  10. data/lib/charming/controller/rendering.rb +127 -0
  11. data/lib/charming/controller/session_state.rb +41 -0
  12. data/lib/charming/controller/sidebar_navigation.rb +111 -0
  13. data/lib/charming/controller.rb +35 -559
  14. data/lib/charming/database_commands.rb +16 -0
  15. data/lib/charming/database_installer.rb +27 -0
  16. data/lib/charming/focus.rb +58 -2
  17. data/lib/charming/generators/app_file_generator.rb +13 -0
  18. data/lib/charming/generators/app_generator.rb +123 -47
  19. data/lib/charming/generators/base.rb +26 -0
  20. data/lib/charming/generators/component_generator.rb +10 -10
  21. data/lib/charming/generators/controller_generator.rb +22 -11
  22. data/lib/charming/generators/model_generator.rb +38 -29
  23. data/lib/charming/generators/name.rb +10 -0
  24. data/lib/charming/generators/screen_generator.rb +78 -32
  25. data/lib/charming/generators/templates/app/Gemfile.template +5 -0
  26. data/lib/charming/generators/templates/app/README.md.template +9 -0
  27. data/lib/charming/generators/templates/app/Rakefile.template +3 -0
  28. data/lib/charming/generators/templates/app/application.template +13 -0
  29. data/lib/charming/generators/templates/app/application_controller.template +19 -0
  30. data/lib/charming/generators/templates/app/application_record.template +7 -0
  31. data/lib/charming/generators/templates/app/application_state.template +6 -0
  32. data/lib/charming/generators/templates/app/database_config.template +12 -0
  33. data/lib/charming/generators/templates/app/executable.template +7 -0
  34. data/lib/charming/generators/templates/app/gemspec.template +6 -0
  35. data/lib/charming/generators/templates/app/home_controller.template +6 -0
  36. data/lib/charming/generators/templates/app/home_state.template +7 -0
  37. data/lib/charming/generators/templates/app/keep.template +0 -0
  38. data/lib/charming/generators/templates/app/layout.template +110 -0
  39. data/lib/charming/generators/templates/app/root_file.template +20 -0
  40. data/lib/charming/generators/templates/app/routes.template +5 -0
  41. data/lib/charming/generators/templates/app/seeds.template +1 -0
  42. data/lib/charming/generators/templates/app/spec_controller.template +17 -0
  43. data/lib/charming/generators/templates/app/spec_helper.template +3 -0
  44. data/lib/charming/generators/templates/app/spec_state.template +17 -0
  45. data/lib/charming/generators/templates/app/spec_view.template +16 -0
  46. data/lib/charming/generators/templates/app/version.template +5 -0
  47. data/lib/charming/generators/templates/app/view.template +21 -0
  48. data/lib/charming/generators/templates/component/component.rb.template +9 -0
  49. data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
  50. data/lib/charming/generators/templates/model/migration.rb.template +9 -0
  51. data/lib/charming/generators/templates/model/model.rb.template +6 -0
  52. data/lib/charming/generators/templates/model/spec.rb.template +9 -0
  53. data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
  54. data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
  55. data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
  56. data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
  57. data/lib/charming/generators/templates/screen/state.rb.template +7 -0
  58. data/lib/charming/generators/templates/screen/view.rb.template +11 -0
  59. data/lib/charming/generators/templates/view/view.rb.template +11 -0
  60. data/lib/charming/generators/view_generator.rb +19 -3
  61. data/lib/charming/internal/renderer/differential.rb +25 -2
  62. data/lib/charming/internal/renderer/full_repaint.rb +6 -0
  63. data/lib/charming/internal/terminal/adapter.rb +29 -3
  64. data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
  65. data/lib/charming/internal/terminal/memory_backend.rb +28 -1
  66. data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
  67. data/lib/charming/internal/terminal/tty_backend.rb +65 -115
  68. data/lib/charming/presentation/component.rb +3 -5
  69. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  70. data/lib/charming/presentation/components/command_palette.rb +94 -96
  71. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  72. data/lib/charming/presentation/components/empty_state.rb +47 -36
  73. data/lib/charming/presentation/components/form/builder.rb +52 -40
  74. data/lib/charming/presentation/components/form/confirm.rb +50 -39
  75. data/lib/charming/presentation/components/form/field.rb +94 -71
  76. data/lib/charming/presentation/components/form/input.rb +61 -49
  77. data/lib/charming/presentation/components/form/note.rb +27 -20
  78. data/lib/charming/presentation/components/form/select.rb +84 -63
  79. data/lib/charming/presentation/components/form/textarea.rb +67 -53
  80. data/lib/charming/presentation/components/form.rb +120 -93
  81. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  82. data/lib/charming/presentation/components/list.rb +123 -97
  83. data/lib/charming/presentation/components/markdown.rb +21 -17
  84. data/lib/charming/presentation/components/modal.rb +55 -43
  85. data/lib/charming/presentation/components/progressbar.rb +61 -50
  86. data/lib/charming/presentation/components/spinner.rb +35 -27
  87. data/lib/charming/presentation/components/table.rb +108 -85
  88. data/lib/charming/presentation/components/text_area.rb +219 -173
  89. data/lib/charming/presentation/components/text_input.rb +120 -98
  90. data/lib/charming/presentation/components/viewport.rb +218 -168
  91. data/lib/charming/presentation/layout/builder.rb +84 -0
  92. data/lib/charming/presentation/layout/overlay.rb +55 -0
  93. data/lib/charming/presentation/layout/pane.rb +149 -0
  94. data/lib/charming/presentation/layout/rect.rb +21 -0
  95. data/lib/charming/presentation/layout/screen_layout.rb +58 -0
  96. data/lib/charming/presentation/layout/split.rb +132 -0
  97. data/lib/charming/presentation/layout.rb +28 -30
  98. data/lib/charming/presentation/markdown/block_renderers.rb +118 -0
  99. data/lib/charming/presentation/markdown/inline_renderers.rb +66 -0
  100. data/lib/charming/presentation/markdown/render_context.rb +20 -0
  101. data/lib/charming/presentation/markdown/renderer.rb +82 -174
  102. data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -44
  103. data/lib/charming/presentation/markdown.rb +4 -3
  104. data/lib/charming/presentation/template_view.rb +22 -17
  105. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  106. data/lib/charming/presentation/templates.rb +47 -32
  107. data/lib/charming/presentation/ui/ansi_codes.rb +87 -0
  108. data/lib/charming/presentation/ui/ansi_slicer.rb +92 -0
  109. data/lib/charming/presentation/ui/border.rb +24 -26
  110. data/lib/charming/presentation/ui/border_painter.rb +56 -0
  111. data/lib/charming/presentation/ui/canvas.rb +80 -0
  112. data/lib/charming/presentation/ui/style.rb +170 -205
  113. data/lib/charming/presentation/ui/theme.rb +133 -135
  114. data/lib/charming/presentation/ui/width.rb +12 -14
  115. data/lib/charming/presentation/ui.rb +70 -213
  116. data/lib/charming/presentation/view.rb +107 -92
  117. data/lib/charming/runtime.rb +25 -10
  118. data/lib/charming/tasks/inline_executor.rb +9 -0
  119. data/lib/charming/tasks/task.rb +3 -0
  120. data/lib/charming/tasks/threaded_executor.rb +12 -0
  121. data/lib/charming/version.rb +1 -1
  122. data/lib/charming.rb +16 -2
  123. metadata +60 -10
  124. data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -90
  125. data/lib/charming/generators/app_generator/basic_templates.rb +0 -81
  126. data/lib/charming/generators/app_generator/component_templates.rb +0 -36
  127. data/lib/charming/generators/app_generator/controller_template.rb +0 -60
  128. data/lib/charming/generators/app_generator/database_templates.rb +0 -45
  129. data/lib/charming/generators/app_generator/layout_template.rb +0 -66
  130. data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -69
  131. data/lib/charming/generators/app_generator/state_templates.rb +0 -30
  132. data/lib/charming/generators/app_generator/view_template.rb +0 -84
@@ -1,118 +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
- # Yields the layout's `content` slot used by view templates to inject their body into a layout wrapper (e.g., sidebar).
82
- def yield_content
83
- assigns.fetch(:content, "")
84
- 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
85
86
 
86
- # Evaluates a block in the view's context with a clean output buffer. Captures text written via `text`/`box`
87
- # and returns joined content. Resets buffer afterward for parent rendering.
88
- def capture(&)
89
- previous_buffer = @output_buffer
90
- @output_buffer = []
91
- result = instance_eval(&)
92
- @output_buffer.empty? ? result.to_s : @output_buffer.join("\n")
93
- ensure
94
- @output_buffer = previous_buffer
95
- 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
96
91
 
97
- # Appends a value to the current output buffer (if one is active). Used by rendering helpers.
98
- def append_to_buffer(value)
99
- @output_buffer << value if @output_buffer
100
- 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
101
102
 
102
- # Applies a style object's `render` method to a string, returning styled output or raw text when style is nil.
103
- def apply_style(value, style_object)
104
- style_object ? style_object.render(value) : value
105
- 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
106
107
 
107
- # Dynamically defines read-only accessor methods for each assign key as singleton methods on self.
108
- # Skips keys where the view already responds (controller methods take precedence).
109
- def define_assign_readers
110
- assigns.each_key do |name|
111
- next if respond_to?(name, true)
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
112
112
 
113
- define_singleton_method(name) { assigns.fetch(name) }
114
- end
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)
118
+
119
+ define_singleton_method(name) { assigns.fetch(name) }
115
120
  end
116
121
  end
122
+
123
+ def layout_screen
124
+ assigns[:screen] || assigns[:controller]&.screen || Charming::Screen.new(width: 80, height: 24)
125
+ end
126
+
127
+ def register_layout_focus(layout)
128
+ return unless assigns[:controller]
129
+
130
+ assigns[:controller].focus.define_layout(layout.focusable_names)
131
+ end
117
132
  end
118
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)
@@ -75,6 +77,8 @@ module Charming
75
77
  controller(event: event).dispatch_mouse
76
78
  end
77
79
 
80
+ # Instantiates a fresh controller for the active route, passing the application, current *event*,
81
+ # route params, screen dimensions, and route object. Called by every dispatch path.
78
82
  def controller(event: nil)
79
83
  @route.controller_class.new(application: @application, event: event, params: @route.params, screen: screen, route: @route)
80
84
  end
@@ -91,8 +95,12 @@ module Charming
91
95
  end
92
96
 
93
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.
94
100
  def dispatch_resize(event)
95
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)
96
104
  dispatch(@route.action, event: event)
97
105
  end
98
106
 
@@ -181,6 +189,13 @@ module Charming
181
189
  @backend.install_resize_handler if @backend.respond_to?(:install_resize_handler)
182
190
  end
183
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
+
184
199
  # Restores terminal state: reinstalls any previous resize handler, shows
185
200
  # the cursor, and leaves the alternative screen buffer.
186
201
  def restore_terminal
@@ -2,22 +2,31 @@
2
2
 
3
3
  module Charming
4
4
  module Tasks
5
+ # InlineExecutor runs submitted tasks synchronously on the calling thread, pushing
6
+ # the resulting TaskEvent directly into the runtime's *queue*. Used for testing and
7
+ # for environments where spawning background threads is undesirable.
5
8
  class InlineExecutor
9
+ # *queue* is the thread-safe Queue (typically `runtime.@task_queue`) into which
10
+ # completed TaskEvents are pushed.
6
11
  def initialize(queue)
7
12
  @queue = queue
8
13
  end
9
14
 
15
+ # Wraps *block* in a Task, invokes it immediately, and pushes the resulting
16
+ # TaskEvent (value or error) onto the queue. Returns nil.
10
17
  def submit(name, &block)
11
18
  task = Task.new(name: name.to_sym, block: block)
12
19
  @queue << run(task)
13
20
  nil
14
21
  end
15
22
 
23
+ # No-op stub for the shutdown contract; nothing to join since tasks run on the caller.
16
24
  def shutdown(timeout: 0.0)
17
25
  end
18
26
 
19
27
  private
20
28
 
29
+ # Invokes the task's block and wraps the result (or raised exception) in a TaskEvent.
21
30
  def run(task)
22
31
  Events::TaskEvent.new(name: task.name, value: task.call)
23
32
  rescue StandardError, ScriptError => e
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Charming
4
4
  module Tasks
5
+ # Task is the unit of work submitted to a task executor. It pairs a *name* (used by
6
+ # `on_task` handlers to route the result) with a *block* to invoke on the executor.
5
7
  Task = Data.define(:name, :block) do
8
+ # Invokes the task's block in the executor's thread and returns its value (or raises).
6
9
  def call = block.call
7
10
  end
8
11
  end
@@ -2,13 +2,22 @@
2
2
 
3
3
  module Charming
4
4
  module Tasks
5
+ # ThreadedExecutor runs submitted tasks on background Ruby threads. Each submission
6
+ # creates a new thread that invokes the block and pushes the resulting TaskEvent
7
+ # onto the shared *queue*. Threads are tracked so `shutdown` can wait (or kill)
8
+ # in-flight work.
5
9
  class ThreadedExecutor
10
+ # *queue* is the thread-safe Queue (typically `runtime.@task_queue`) into which
11
+ # completed TaskEvents are pushed.
6
12
  def initialize(queue)
7
13
  @queue = queue
8
14
  @threads = []
9
15
  @mutex = Mutex.new
10
16
  end
11
17
 
18
+ # Wraps *block* in a Task and spawns a new thread to invoke it. The thread's
19
+ # return value (or rescued exception) is pushed onto the queue as a TaskEvent.
20
+ # Returns nil immediately.
12
21
  def submit(name, &block)
13
22
  task = Task.new(name: name.to_sym, block: block)
14
23
  thread = Thread.new(task) { |t| @queue << run(t) }
@@ -16,6 +25,8 @@ module Charming
16
25
  nil
17
26
  end
18
27
 
28
+ # Waits up to *timeout* seconds for in-flight threads to finish, then kills any
29
+ # remaining live threads. Used by Runtime during teardown.
19
30
  def shutdown(timeout: 0.0)
20
31
  threads = @mutex.synchronize { @threads.dup }
21
32
  threads.each { |thread| thread.join(timeout) }
@@ -29,6 +40,7 @@ module Charming
29
40
 
30
41
  private
31
42
 
43
+ # Invokes the task's block and wraps the result (or rescued exception) in a TaskEvent.
32
44
  def run(task)
33
45
  Events::TaskEvent.new(name: task.name, value: task.call)
34
46
  rescue StandardError, ScriptError => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/charming.rb CHANGED
@@ -6,23 +6,37 @@ loader = Zeitwerk::Loader.for_gem
6
6
  loader.inflector.inflect(
7
7
  "cli" => "CLI",
8
8
  "ui" => "UI",
9
+ "ansi_codes" => "ANSICodes",
10
+ "ansi_slicer" => "ANSISlicer",
11
+ "border_painter" => "BorderPainter",
12
+ "block_renderers" => "BlockRenderer",
13
+ "inline_renderers" => "InlineRenderer",
14
+ "render_context" => "RenderContext",
9
15
  "erb_handler" => "ErbHandler",
16
+ "key_normalizer" => "KeyNormalizer",
17
+ "mouse_parser" => "MouseParser",
10
18
  "tty_backend" => "TTYBackend"
11
19
  )
20
+ loader.collapse("#{__dir__}/charming/presentation")
12
21
  loader.setup
13
22
 
14
23
  module Charming
24
+ # Base error class for all Charming-specific exceptions (used by templates, generators, runtime, etc.).
15
25
  class Error < StandardError; end
16
26
 
27
+ # Entry point for running a Charming application. Instantiates a Runtime for *application* and starts
28
+ # the event loop. *backend* defaults to TTYBackend; tests pass MemoryBackend directly via `Charming::Runtime.new`.
17
29
  def self.run(application, backend: nil)
18
30
  Runtime.new(application, backend: backend).run
19
31
  end
20
32
 
33
+ # Returns the normalized key symbol for an event-like object — `event.key` when the object responds
34
+ # to it, otherwise `event.to_sym`. Lets components treat raw strings and KeyEvent objects uniformly.
21
35
  def self.key_of(event)
22
36
  key = event.respond_to?(:key) ? event.key : event
23
37
  key.to_sym
24
38
  end
25
39
  end
26
40
 
27
- Charming::Presentation::Templates.register ".tui.erb", Charming::Presentation::Templates::ErbHandler
28
- 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.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - pando
@@ -206,6 +206,14 @@ files:
206
206
  - lib/charming/application_state.rb
207
207
  - lib/charming/cli.rb
208
208
  - lib/charming/controller.rb
209
+ - lib/charming/controller/class_methods.rb
210
+ - lib/charming/controller/command_palette.rb
211
+ - lib/charming/controller/component_dispatching.rb
212
+ - lib/charming/controller/dispatching.rb
213
+ - lib/charming/controller/focus_management.rb
214
+ - lib/charming/controller/rendering.rb
215
+ - lib/charming/controller/session_state.rb
216
+ - lib/charming/controller/sidebar_navigation.rb
209
217
  - lib/charming/database_commands.rb
210
218
  - lib/charming/database_installer.rb
211
219
  - lib/charming/events/key_event.rb
@@ -216,15 +224,6 @@ files:
216
224
  - lib/charming/focus.rb
217
225
  - lib/charming/generators/app_file_generator.rb
218
226
  - lib/charming/generators/app_generator.rb
219
- - lib/charming/generators/app_generator/app_spec_templates.rb
220
- - lib/charming/generators/app_generator/basic_templates.rb
221
- - lib/charming/generators/app_generator/component_templates.rb
222
- - lib/charming/generators/app_generator/controller_template.rb
223
- - lib/charming/generators/app_generator/database_templates.rb
224
- - lib/charming/generators/app_generator/layout_template.rb
225
- - lib/charming/generators/app_generator/screen_spec_templates.rb
226
- - lib/charming/generators/app_generator/state_templates.rb
227
- - lib/charming/generators/app_generator/view_template.rb
228
227
  - lib/charming/generators/base.rb
229
228
  - lib/charming/generators/component_generator.rb
230
229
  - lib/charming/generators/controller_generator.rb
@@ -232,15 +231,53 @@ files:
232
231
  - lib/charming/generators/model_generator.rb
233
232
  - lib/charming/generators/name.rb
234
233
  - lib/charming/generators/screen_generator.rb
234
+ - lib/charming/generators/templates/app/Gemfile.template
235
+ - lib/charming/generators/templates/app/README.md.template
236
+ - lib/charming/generators/templates/app/Rakefile.template
237
+ - lib/charming/generators/templates/app/application.template
238
+ - lib/charming/generators/templates/app/application_controller.template
239
+ - lib/charming/generators/templates/app/application_record.template
240
+ - lib/charming/generators/templates/app/application_state.template
241
+ - lib/charming/generators/templates/app/database_config.template
242
+ - lib/charming/generators/templates/app/executable.template
243
+ - lib/charming/generators/templates/app/gemspec.template
244
+ - lib/charming/generators/templates/app/home_controller.template
245
+ - lib/charming/generators/templates/app/home_state.template
246
+ - lib/charming/generators/templates/app/keep.template
247
+ - lib/charming/generators/templates/app/layout.template
248
+ - lib/charming/generators/templates/app/root_file.template
249
+ - lib/charming/generators/templates/app/routes.template
250
+ - lib/charming/generators/templates/app/seeds.template
251
+ - lib/charming/generators/templates/app/spec_controller.template
252
+ - lib/charming/generators/templates/app/spec_helper.template
253
+ - lib/charming/generators/templates/app/spec_state.template
254
+ - lib/charming/generators/templates/app/spec_view.template
255
+ - lib/charming/generators/templates/app/version.template
256
+ - lib/charming/generators/templates/app/view.template
257
+ - lib/charming/generators/templates/component/component.rb.template
258
+ - lib/charming/generators/templates/controller/controller.rb.template
259
+ - lib/charming/generators/templates/model/migration.rb.template
260
+ - lib/charming/generators/templates/model/model.rb.template
261
+ - lib/charming/generators/templates/model/spec.rb.template
262
+ - lib/charming/generators/templates/screen/controller.rb.template
263
+ - lib/charming/generators/templates/screen/spec_controller.rb.template
264
+ - lib/charming/generators/templates/screen/spec_state.rb.template
265
+ - lib/charming/generators/templates/screen/spec_view.rb.template
266
+ - lib/charming/generators/templates/screen/state.rb.template
267
+ - lib/charming/generators/templates/screen/view.rb.template
268
+ - lib/charming/generators/templates/view/view.rb.template
235
269
  - lib/charming/generators/view_generator.rb
236
270
  - lib/charming/internal/renderer/differential.rb
237
271
  - lib/charming/internal/renderer/full_repaint.rb
238
272
  - lib/charming/internal/terminal/adapter.rb
273
+ - lib/charming/internal/terminal/key_normalizer.rb
239
274
  - lib/charming/internal/terminal/memory_backend.rb
275
+ - lib/charming/internal/terminal/mouse_parser.rb
240
276
  - lib/charming/internal/terminal/tty_backend.rb
241
277
  - lib/charming/presentation/component.rb
242
278
  - lib/charming/presentation/components/activity_indicator.rb
243
279
  - lib/charming/presentation/components/command_palette.rb
280
+ - lib/charming/presentation/components/command_palette_modal.rb
244
281
  - lib/charming/presentation/components/empty_state.rb
245
282
  - lib/charming/presentation/components/form.rb
246
283
  - lib/charming/presentation/components/form/builder.rb
@@ -261,14 +298,27 @@ files:
261
298
  - lib/charming/presentation/components/text_input.rb
262
299
  - lib/charming/presentation/components/viewport.rb
263
300
  - lib/charming/presentation/layout.rb
301
+ - lib/charming/presentation/layout/builder.rb
302
+ - lib/charming/presentation/layout/overlay.rb
303
+ - lib/charming/presentation/layout/pane.rb
304
+ - lib/charming/presentation/layout/rect.rb
305
+ - lib/charming/presentation/layout/screen_layout.rb
306
+ - lib/charming/presentation/layout/split.rb
264
307
  - lib/charming/presentation/markdown.rb
308
+ - lib/charming/presentation/markdown/block_renderers.rb
309
+ - lib/charming/presentation/markdown/inline_renderers.rb
310
+ - lib/charming/presentation/markdown/render_context.rb
265
311
  - lib/charming/presentation/markdown/renderer.rb
266
312
  - lib/charming/presentation/markdown/syntax_highlighter.rb
267
313
  - lib/charming/presentation/template_view.rb
268
314
  - lib/charming/presentation/templates.rb
269
315
  - lib/charming/presentation/templates/erb_handler.rb
270
316
  - lib/charming/presentation/ui.rb
317
+ - lib/charming/presentation/ui/ansi_codes.rb
318
+ - lib/charming/presentation/ui/ansi_slicer.rb
271
319
  - lib/charming/presentation/ui/border.rb
320
+ - lib/charming/presentation/ui/border_painter.rb
321
+ - lib/charming/presentation/ui/canvas.rb
272
322
  - lib/charming/presentation/ui/style.rb
273
323
  - lib/charming/presentation/ui/theme.rb
274
324
  - lib/charming/presentation/ui/themes/phosphor.json
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Generators
5
- class AppGenerator
6
- module AppSpecTemplates
7
- def spec_state
8
- %(# frozen_string_literal: true
9
-
10
- require "#{app_name.snake_name}"
11
-
12
- RSpec.describe #{app_name.class_name}::HomeState do
13
- describe "#title" do
14
- it "has the correct default string value" do
15
- instance = described_class.new
16
- expect(instance.title).to eq("#{app_name.class_name}")
17
- end
18
-
19
- it "accepts overridden title values" do
20
- instance = described_class.new(title: "Alternative")
21
- expect(instance.title).to eq("Alternative")
22
- end
23
- end
24
- end
25
- )
26
- end
27
-
28
- def spec_controller
29
- %(# frozen_string_literal: true
30
-
31
- require "#{app_name.snake_name}"
32
-
33
- RSpec.describe #{app_name.class_name}::HomeController do
34
- let(:application) { #{app_name.class_name}::Application.new }
35
-
36
- subject(:controller) { described_class.new(application: application) }
37
-
38
- describe "#show" do
39
- it "renders the view with the state" do
40
- response = controller.dispatch(:show)
41
-
42
- expect(response).to respond_to(:body)
43
- end
44
- end
45
- end
46
- )
47
- end
48
-
49
- def spec_view
50
- %(# frozen_string_literal: true
51
-
52
- require "#{app_name.snake_name}"
53
-
54
- RSpec.describe "home/show template" do
55
- describe "#render" do
56
- it "renders the state title" do
57
- template = Charming::Presentation::Templates.resolve("home/show", root: #{app_name.class_name}::Application.root)
58
- view = Charming::Presentation::TemplateView.new(
59
- template: template,
60
- namespace: #{app_name.class_name},
61
- home: double(title: "#{app_name.class_name}"),
62
- theme: #{app_name.class_name}::Application.new.theme
63
- )
64
-
65
- expect(view.render).to include("#{app_name.class_name}")
66
- end
67
- end
68
- end
69
- )
70
- end
71
-
72
- def spec_component
73
- %(# frozen_string_literal: true
74
-
75
- require "#{app_name.snake_name}"
76
-
77
- RSpec.describe #{app_name.class_name}::AppFrameComponent do
78
- describe "#render" do
79
- it "returns a string" do
80
- component = described_class.new(title: "#{app_name.class_name}")
81
- expect(component.render).to be_a(String)
82
- end
83
- end
84
- end
85
- )
86
- end
87
- end
88
- end
89
- end
90
- end