charming 0.1.0 → 0.1.1

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -378
  3. data/lib/charming/application.rb +3 -3
  4. data/lib/charming/{application_model.rb → application_state.rb} +3 -3
  5. data/lib/charming/cli.rb +39 -3
  6. data/lib/charming/controller.rb +146 -24
  7. data/lib/charming/database_commands.rb +87 -0
  8. data/lib/charming/database_installer.rb +125 -0
  9. data/lib/charming/events/key_event.rb +15 -0
  10. data/lib/charming/events/mouse_event.rb +42 -0
  11. data/lib/charming/events/resize_event.rb +9 -0
  12. data/lib/charming/events/task_event.rb +19 -0
  13. data/lib/charming/events/timer_event.rb +9 -0
  14. data/lib/charming/generators/app_generator/app_spec_templates.rb +12 -8
  15. data/lib/charming/generators/app_generator/basic_templates.rb +14 -2
  16. data/lib/charming/generators/app_generator/component_templates.rb +1 -1
  17. data/lib/charming/generators/app_generator/controller_template.rb +3 -12
  18. data/lib/charming/generators/app_generator/database_templates.rb +45 -0
  19. data/lib/charming/generators/app_generator/layout_template.rb +51 -145
  20. data/lib/charming/generators/app_generator/screen_spec_templates.rb +7 -8
  21. data/lib/charming/generators/app_generator/{model_templates.rb → state_templates.rb} +5 -5
  22. data/lib/charming/generators/app_generator/view_template.rb +12 -18
  23. data/lib/charming/generators/app_generator.rb +37 -11
  24. data/lib/charming/generators/component_generator.rb +1 -1
  25. data/lib/charming/generators/controller_generator.rb +1 -4
  26. data/lib/charming/generators/model_generator.rb +119 -0
  27. data/lib/charming/generators/name.rb +0 -4
  28. data/lib/charming/generators/screen_generator.rb +14 -28
  29. data/lib/charming/generators/view_generator.rb +11 -14
  30. data/lib/charming/internal/renderer/differential.rb +2 -3
  31. data/lib/charming/internal/terminal/tty_backend.rb +25 -8
  32. data/lib/charming/presentation/component.rb +10 -0
  33. data/lib/charming/presentation/components/activity_indicator.rb +160 -0
  34. data/lib/charming/presentation/components/command_palette.rb +120 -0
  35. data/lib/charming/presentation/components/empty_state.rb +43 -0
  36. data/lib/charming/presentation/components/form/builder.rb +48 -0
  37. data/lib/charming/presentation/components/form/confirm.rb +56 -0
  38. data/lib/charming/presentation/components/form/field.rb +96 -0
  39. data/lib/charming/presentation/components/form/input.rb +57 -0
  40. data/lib/charming/presentation/components/form/note.rb +32 -0
  41. data/lib/charming/presentation/components/form/select.rb +89 -0
  42. data/lib/charming/presentation/components/form/textarea.rb +70 -0
  43. data/lib/charming/presentation/components/form.rb +127 -0
  44. data/lib/charming/presentation/components/keyboard_handler.rb +58 -0
  45. data/lib/charming/presentation/components/list.rb +104 -0
  46. data/lib/charming/presentation/components/markdown.rb +25 -0
  47. data/lib/charming/presentation/components/modal.rb +50 -0
  48. data/lib/charming/presentation/components/progressbar.rb +57 -0
  49. data/lib/charming/presentation/components/spinner.rb +39 -0
  50. data/lib/charming/presentation/components/table.rb +118 -0
  51. data/lib/charming/presentation/components/text_area.rb +219 -0
  52. data/lib/charming/presentation/components/text_input.rb +105 -0
  53. data/lib/charming/presentation/components/viewport.rb +220 -0
  54. data/lib/charming/presentation/layout.rb +43 -0
  55. data/lib/charming/presentation/markdown/renderer.rb +203 -0
  56. data/lib/charming/presentation/markdown/syntax_highlighter.rb +63 -0
  57. data/lib/charming/presentation/markdown.rb +8 -0
  58. data/lib/charming/presentation/template_view.rb +27 -0
  59. data/lib/charming/presentation/templates/erb_handler.rb +15 -0
  60. data/lib/charming/presentation/templates.rb +51 -0
  61. data/lib/charming/presentation/ui/border.rb +35 -0
  62. data/lib/charming/presentation/ui/style.rb +246 -0
  63. data/lib/charming/presentation/ui/theme.rb +180 -0
  64. data/lib/charming/{ui → presentation/ui}/themes/phosphor.json +2 -2
  65. data/lib/charming/presentation/ui/width.rb +26 -0
  66. data/lib/charming/presentation/ui.rb +232 -0
  67. data/lib/charming/presentation/view.rb +118 -0
  68. data/lib/charming/runtime.rb +7 -7
  69. data/lib/charming/screen.rb +5 -1
  70. data/lib/charming/tasks/inline_executor.rb +28 -0
  71. data/lib/charming/tasks/task.rb +9 -0
  72. data/lib/charming/{task_executor.rb → tasks/threaded_executor.rb} +4 -27
  73. data/lib/charming/version.rb +1 -1
  74. data/lib/charming.rb +4 -0
  75. metadata +114 -29
  76. data/lib/charming/component.rb +0 -8
  77. data/lib/charming/components/activity_indicator.rb +0 -158
  78. data/lib/charming/components/command_palette.rb +0 -118
  79. data/lib/charming/components/keyboard_handler.rb +0 -22
  80. data/lib/charming/components/list.rb +0 -105
  81. data/lib/charming/components/modal.rb +0 -48
  82. data/lib/charming/components/progressbar.rb +0 -55
  83. data/lib/charming/components/spinner.rb +0 -37
  84. data/lib/charming/components/table.rb +0 -115
  85. data/lib/charming/components/text_input.rb +0 -103
  86. data/lib/charming/components/viewport.rb +0 -191
  87. data/lib/charming/key_event.rb +0 -13
  88. data/lib/charming/mouse_event.rb +0 -40
  89. data/lib/charming/resize_event.rb +0 -7
  90. data/lib/charming/task.rb +0 -7
  91. data/lib/charming/task_event.rb +0 -17
  92. data/lib/charming/timer_event.rb +0 -7
  93. data/lib/charming/ui/border.rb +0 -33
  94. data/lib/charming/ui/style.rb +0 -244
  95. data/lib/charming/ui/theme.rb +0 -178
  96. data/lib/charming/ui/width.rb +0 -24
  97. data/lib/charming/ui.rb +0 -230
  98. data/lib/charming/view.rb +0 -116
  99. /data/lib/charming/{generators.rb → generators/error.rb} +0 -0
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
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
15
+
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
20
+
21
+ # Renders the view's body. Default is empty — subclasses override to return visible text.
22
+ def render
23
+ ""
24
+ end
25
+
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
31
+
32
+ private
33
+
34
+ attr_reader :assigns
35
+
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
40
+
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
45
+
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
53
+
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
60
+
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
65
+
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
70
+
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
75
+
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
80
+
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
85
+
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
96
+
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
101
+
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
106
+
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)
112
+
113
+ define_singleton_method(name) { assigns.fetch(name) }
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -76,16 +76,16 @@ module Charming
76
76
  end
77
77
 
78
78
  def controller(event: nil)
79
- @route.controller_class.new(application: @application, event: event, params: @route.params, screen: screen)
79
+ @route.controller_class.new(application: @application, event: event, params: @route.params, screen: screen, route: @route)
80
80
  end
81
81
 
82
82
  # Type-based dispatcher: routes resize, task, timer, mouse, and key events
83
83
  # to the appropriate handler. Falls back to key dispatch for unclassified events.
84
84
  def dispatch_event(event)
85
- return dispatch_resize(event) if event.is_a?(ResizeEvent)
86
- return dispatch_task(event) if event.is_a?(TaskEvent)
87
- return dispatch_timer(event) if event.is_a?(TimerEvent)
88
- return dispatch_mouse(event) if event.is_a?(MouseEvent)
85
+ return dispatch_resize(event) if event.is_a?(Events::ResizeEvent)
86
+ return dispatch_task(event) if event.is_a?(Events::TaskEvent)
87
+ return dispatch_timer(event) if event.is_a?(Events::TimerEvent)
88
+ return dispatch_mouse(event) if event.is_a?(Events::MouseEvent)
89
89
 
90
90
  dispatch_key(event)
91
91
  end
@@ -133,7 +133,7 @@ module Charming
133
133
 
134
134
  now = clock_now
135
135
  timer[:next_at] = now + timer.fetch(:binding).interval
136
- TimerEvent.new(name: timer.fetch(:binding).name, now: now)
136
+ Events::TimerEvent.new(name: timer.fetch(:binding).name, now: now)
137
137
  end
138
138
 
139
139
  # Pops a task event from the thread-safe queue if one is available.
@@ -161,7 +161,7 @@ module Charming
161
161
 
162
162
  # Constructs a task executor: supports explicit instances, callable factories, or the default Threaded executor.
163
163
  def build_task_executor(task_executor)
164
- return TaskExecutor::Threaded.new(@task_queue) unless task_executor
164
+ return Tasks::ThreadedExecutor.new(@task_queue) unless task_executor
165
165
  return task_executor if task_executor.respond_to?(:submit)
166
166
  return task_executor.call(@task_queue) if task_executor.respond_to?(:call) && !task_executor.respond_to?(:new)
167
167
 
@@ -4,5 +4,9 @@ module Charming
4
4
  # Screen represents the terminal viewport dimensions as a simple Data class.
5
5
  # The `width` and `height` values flow from the backend through the runtime
6
6
  # loop into every controller dispatch for layout calculations.
7
- Screen = Data.define(:width, :height)
7
+ Screen = Data.define(:width, :height) do
8
+ def narrow?(below:, min_height: nil)
9
+ width < below && (min_height.nil? || height >= min_height)
10
+ end
11
+ end
8
12
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Charming
4
+ module Tasks
5
+ class InlineExecutor
6
+ def initialize(queue)
7
+ @queue = queue
8
+ end
9
+
10
+ def submit(name, &block)
11
+ task = Task.new(name: name.to_sym, block: block)
12
+ @queue << run(task)
13
+ nil
14
+ end
15
+
16
+ def shutdown(timeout: 0.0)
17
+ end
18
+
19
+ private
20
+
21
+ def run(task)
22
+ Events::TaskEvent.new(name: task.name, value: task.call)
23
+ rescue StandardError, ScriptError => e
24
+ Events::TaskEvent.new(name: task.name, error: e)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Charming
4
+ module Tasks
5
+ Task = Data.define(:name, :block) do
6
+ def call = block.call
7
+ end
8
+ end
9
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module TaskExecutor
5
- class Threaded
4
+ module Tasks
5
+ class ThreadedExecutor
6
6
  def initialize(queue)
7
7
  @queue = queue
8
8
  @threads = []
@@ -30,32 +30,9 @@ module Charming
30
30
  private
31
31
 
32
32
  def run(task)
33
- TaskEvent.new(name: task.name, value: task.call)
33
+ Events::TaskEvent.new(name: task.name, value: task.call)
34
34
  rescue StandardError, ScriptError => e
35
- TaskEvent.new(name: task.name, error: e)
36
- end
37
- end
38
-
39
- class Inline
40
- def initialize(queue)
41
- @queue = queue
42
- end
43
-
44
- def submit(name, &block)
45
- task = Task.new(name: name.to_sym, block: block)
46
- @queue << run(task)
47
- nil
48
- end
49
-
50
- def shutdown(timeout: 0.0)
51
- end
52
-
53
- private
54
-
55
- def run(task)
56
- TaskEvent.new(name: task.name, value: task.call)
57
- rescue StandardError, ScriptError => e
58
- TaskEvent.new(name: task.name, error: e)
35
+ Events::TaskEvent.new(name: task.name, error: e)
59
36
  end
60
37
  end
61
38
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
data/lib/charming.rb CHANGED
@@ -6,6 +6,7 @@ loader = Zeitwerk::Loader.for_gem
6
6
  loader.inflector.inflect(
7
7
  "cli" => "CLI",
8
8
  "ui" => "UI",
9
+ "erb_handler" => "ErbHandler",
9
10
  "tty_backend" => "TTYBackend"
10
11
  )
11
12
  loader.setup
@@ -22,3 +23,6 @@ module Charming
22
23
  key.to_sym
23
24
  end
24
25
  end
26
+
27
+ Charming::Presentation::Templates.register ".tui.erb", Charming::Presentation::Templates::ErbHandler
28
+ Charming::Presentation::Templates.register ".txt.erb", Charming::Presentation::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.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - pando
@@ -29,6 +29,68 @@ dependencies:
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
31
  version: 8.1.2
32
+ - !ruby/object:Gem::Dependency
33
+ name: activerecord
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '8.1'
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 8.1.2
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '8.1'
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 8.1.2
52
+ - !ruby/object:Gem::Dependency
53
+ name: kramdown
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '2.5'
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '2.5'
66
+ - !ruby/object:Gem::Dependency
67
+ name: rouge
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '5.0'
73
+ type: :runtime
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '5.0'
80
+ - !ruby/object:Gem::Dependency
81
+ name: sqlite3
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '2.0'
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '2.0'
32
94
  - !ruby/object:Gem::Dependency
33
95
  name: tty-cursor
34
96
  requirement: !ruby/object:Gem::Requirement
@@ -141,35 +203,33 @@ files:
141
203
  - exe/charming
142
204
  - lib/charming.rb
143
205
  - lib/charming/application.rb
144
- - lib/charming/application_model.rb
206
+ - lib/charming/application_state.rb
145
207
  - lib/charming/cli.rb
146
- - lib/charming/component.rb
147
- - lib/charming/components/activity_indicator.rb
148
- - lib/charming/components/command_palette.rb
149
- - lib/charming/components/keyboard_handler.rb
150
- - lib/charming/components/list.rb
151
- - lib/charming/components/modal.rb
152
- - lib/charming/components/progressbar.rb
153
- - lib/charming/components/spinner.rb
154
- - lib/charming/components/table.rb
155
- - lib/charming/components/text_input.rb
156
- - lib/charming/components/viewport.rb
157
208
  - lib/charming/controller.rb
209
+ - lib/charming/database_commands.rb
210
+ - lib/charming/database_installer.rb
211
+ - lib/charming/events/key_event.rb
212
+ - lib/charming/events/mouse_event.rb
213
+ - lib/charming/events/resize_event.rb
214
+ - lib/charming/events/task_event.rb
215
+ - lib/charming/events/timer_event.rb
158
216
  - lib/charming/focus.rb
159
- - lib/charming/generators.rb
160
217
  - lib/charming/generators/app_file_generator.rb
161
218
  - lib/charming/generators/app_generator.rb
162
219
  - lib/charming/generators/app_generator/app_spec_templates.rb
163
220
  - lib/charming/generators/app_generator/basic_templates.rb
164
221
  - lib/charming/generators/app_generator/component_templates.rb
165
222
  - lib/charming/generators/app_generator/controller_template.rb
223
+ - lib/charming/generators/app_generator/database_templates.rb
166
224
  - lib/charming/generators/app_generator/layout_template.rb
167
- - lib/charming/generators/app_generator/model_templates.rb
168
225
  - lib/charming/generators/app_generator/screen_spec_templates.rb
226
+ - lib/charming/generators/app_generator/state_templates.rb
169
227
  - lib/charming/generators/app_generator/view_template.rb
170
228
  - lib/charming/generators/base.rb
171
229
  - lib/charming/generators/component_generator.rb
172
230
  - lib/charming/generators/controller_generator.rb
231
+ - lib/charming/generators/error.rb
232
+ - lib/charming/generators/model_generator.rb
173
233
  - lib/charming/generators/name.rb
174
234
  - lib/charming/generators/screen_generator.rb
175
235
  - lib/charming/generators/view_generator.rb
@@ -178,25 +238,50 @@ files:
178
238
  - lib/charming/internal/terminal/adapter.rb
179
239
  - lib/charming/internal/terminal/memory_backend.rb
180
240
  - lib/charming/internal/terminal/tty_backend.rb
181
- - lib/charming/key_event.rb
182
- - lib/charming/mouse_event.rb
183
- - lib/charming/resize_event.rb
241
+ - lib/charming/presentation/component.rb
242
+ - lib/charming/presentation/components/activity_indicator.rb
243
+ - lib/charming/presentation/components/command_palette.rb
244
+ - lib/charming/presentation/components/empty_state.rb
245
+ - lib/charming/presentation/components/form.rb
246
+ - lib/charming/presentation/components/form/builder.rb
247
+ - lib/charming/presentation/components/form/confirm.rb
248
+ - lib/charming/presentation/components/form/field.rb
249
+ - lib/charming/presentation/components/form/input.rb
250
+ - lib/charming/presentation/components/form/note.rb
251
+ - lib/charming/presentation/components/form/select.rb
252
+ - lib/charming/presentation/components/form/textarea.rb
253
+ - lib/charming/presentation/components/keyboard_handler.rb
254
+ - lib/charming/presentation/components/list.rb
255
+ - lib/charming/presentation/components/markdown.rb
256
+ - lib/charming/presentation/components/modal.rb
257
+ - lib/charming/presentation/components/progressbar.rb
258
+ - lib/charming/presentation/components/spinner.rb
259
+ - lib/charming/presentation/components/table.rb
260
+ - lib/charming/presentation/components/text_area.rb
261
+ - lib/charming/presentation/components/text_input.rb
262
+ - lib/charming/presentation/components/viewport.rb
263
+ - lib/charming/presentation/layout.rb
264
+ - lib/charming/presentation/markdown.rb
265
+ - lib/charming/presentation/markdown/renderer.rb
266
+ - lib/charming/presentation/markdown/syntax_highlighter.rb
267
+ - lib/charming/presentation/template_view.rb
268
+ - lib/charming/presentation/templates.rb
269
+ - lib/charming/presentation/templates/erb_handler.rb
270
+ - lib/charming/presentation/ui.rb
271
+ - lib/charming/presentation/ui/border.rb
272
+ - lib/charming/presentation/ui/style.rb
273
+ - lib/charming/presentation/ui/theme.rb
274
+ - lib/charming/presentation/ui/themes/phosphor.json
275
+ - lib/charming/presentation/ui/width.rb
276
+ - lib/charming/presentation/view.rb
184
277
  - lib/charming/response.rb
185
278
  - lib/charming/router.rb
186
279
  - lib/charming/runtime.rb
187
280
  - lib/charming/screen.rb
188
- - lib/charming/task.rb
189
- - lib/charming/task_event.rb
190
- - lib/charming/task_executor.rb
191
- - lib/charming/timer_event.rb
192
- - lib/charming/ui.rb
193
- - lib/charming/ui/border.rb
194
- - lib/charming/ui/style.rb
195
- - lib/charming/ui/theme.rb
196
- - lib/charming/ui/themes/phosphor.json
197
- - lib/charming/ui/width.rb
281
+ - lib/charming/tasks/inline_executor.rb
282
+ - lib/charming/tasks/task.rb
283
+ - lib/charming/tasks/threaded_executor.rb
198
284
  - lib/charming/version.rb
199
- - lib/charming/view.rb
200
285
  - sig/charming.rbs
201
286
  homepage: https://github.com/pandorocks/charming
202
287
  licenses:
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- # Component is the base class for all reusable terminal widgets. It inherits from View to gain assigns,
5
- # helper methods (text, box, row, column, etc.), and rendering via render.
6
- class Component < View
7
- end
8
- end
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Components
5
- # ActivityIndicator renders a color-gradient progress or loading indicator
6
- # as styled text. It produces a fixed-width row of characters whose colors
7
- # interpolate between two gradient endpoints (or cycle through a single
8
- # color). A label can be appended after the bar and an ellipsis that cycles
9
- # through frames, useful for "loading" state display. Call `tick` to advance
10
- # the frame counter, and call `render` to produce the styled output string.
11
- class ActivityIndicator < Component
12
- # Default character pool used for generating each position's character via stable hashing.
13
- DEFAULT_CHARS = "0123456789abcdefABCDEF~!@#$%^&*+=_".chars.freeze
14
-
15
- # The default two-color gradient applied across the bar width (red to cyan).
16
- # The cyan endpoint mirrors the Phosphor theme palette's "cyan" token so the bar
17
- # remains legible on Phosphor's dark navy background; gradient: accepts raw hex,
18
- # so callers using a different theme should pass their own endpoints.
19
- DEFAULT_GRADIENT = ["#ff0000", "#6FD0E3"].freeze
20
-
21
- # The default label color for ellipsis and text portions when no custom
22
- # label_style is provided.
23
- DEFAULT_LABEL_COLOR = "#cccccc"
24
-
25
- # Ellipsis frame sequence: four states cycle through "., "..", "...", and "" (empty).
26
- ELLIPSIS_FRAMES = [".", "..", "...", ""].freeze
27
-
28
- # Number of frames in the animation cycle before the indicator pattern repeats.
29
- FRAME_COUNT = 10
30
-
31
- # FNV-1a variant constants used by stable_hash for reproducible character selection per position.
32
- FNV_OFFSET = 2_166_136_261
33
- FNV_PRIME = 16_777_619
34
- FNV_MASK = 0xffffffff
35
-
36
- attr_reader :width, :label, :index, :seed, :chars, :gradient, :label_style
37
-
38
- # Initializes a new ActivityIndicator with configurable visual parameters.
39
- # width — Display width of the gradient bar in characters (minimum 1). Default: 10.
40
- # label — Optional text label shown adjacent to the indicator.
41
- # index — Initial frame index for the ellipsis/frame animations. Default: 0.
42
- # seed — Hash seed that determines which characters appear at each position.
43
- # chars — Character pool to draw from (default is DEFAULT_CHARS).
44
- # gradient — Two-element array of hex color strings ["#rrggbb", "#rrggbb"] for interpolation.
45
- # label_style — A Style object to use for rendering the label text; falls back to a gray foreground.
46
- def initialize(width: 10, label: nil, index: 0, seed: 0, chars: DEFAULT_CHARS,
47
- gradient: DEFAULT_GRADIENT, label_style: nil)
48
- super()
49
- raise ArgumentError, "chars cannot be empty" if chars.empty?
50
-
51
- @width = [width.to_i, 1].max
52
- @label = label
53
- @index = index.to_i
54
- @seed = seed
55
- @chars = chars.map(&:to_s)
56
- @gradient = gradient
57
- @label_style = label_style
58
- end
59
-
60
- # Advances the frame counter forward by +count+ steps, allowing the displayed pattern to change.
61
- # Accepts an integer count (converted via +to_i+). Returns self for chaining.
62
- def tick(count = 1)
63
- @index += count.to_i
64
- self
65
- end
66
-
67
- # Renders the activity indicator as a styled string. If a label was provided,
68
- # produces "bar ellipsis" alongside it; otherwise produces only the gradient bar.
69
- # Returns a formatted string suitable for terminal rendering.
70
- def render
71
- return indicator unless label
72
-
73
- "#{indicator} #{styled_label}#{styled_ellipsis}"
74
- end
75
-
76
- private
77
-
78
- # Renders the full gradient bar as an array of styled characters joined into a single string.
79
- # Each character at +position+ is selected by hashing together seed, frame, and position —
80
- # making the pattern stable across renders — then styled with the interpolated gradient color
81
- # at that position.
82
- def indicator
83
- Array.new(width) { |position| styled_char(position) }.join
84
- end
85
-
86
- # Selects a character for the bar at the given +position+, styles it with the gradient color
87
- # interpolated for that position, and returns the result as a formatted string via +render+.
88
- def styled_char(position)
89
- style.foreground(color_at(position)).render(char_at(position))
90
- end
91
-
92
- # Chooses a character from self.chars by hashing seed:frame:position together with a stable
93
- # FNV-1a hash. The resulting index is modulated against the character pool length, ensuring
94
- # reproducible output across renders.
95
- def char_at(position)
96
- chars.fetch(stable_hash("#{seed}:#{frame}:#{position}") % chars.length)
97
- end
98
-
99
- # Renders the label text in its own style (or fallback gray color) via a Style renderer call.
100
- def styled_label
101
- label_style_or_default.render(label.to_s)
102
- end
103
-
104
- # Renders an ellipsis frame (".", "..", "...", or empty) based on (index / 4) mod 4, styled with the label style.
105
- def styled_ellipsis
106
- label_style_or_default.render(ellipsis_frame)
107
- end
108
-
109
- # Returns the current ellipsis frame string: one of ".", "..", "...", "". Cycles through four frames per tick.
110
- def ellipsis_frame
111
- ELLIPSIS_FRAMES.fetch((index / 4) % ELLIPSIS_FRAMES.length)
112
- end
113
-
114
- # Returns the label style if set, otherwise produces a gray foreground style for fallback rendering.
115
- def label_style_or_default
116
- label_style || style.foreground(DEFAULT_LABEL_COLOR)
117
- end
118
-
119
- # Interpolates between gradient[0] and gradient[1] at the fractional +position+ (0.0 to 1.0).
120
- # Returns the first gradient color if width is 1; otherwise returns a blended hex string based on position.
121
- def color_at(position)
122
- return gradient.first unless width > 1
123
-
124
- blend(gradient.first, gradient.last, position / (width - 1).to_f)
125
- end
126
-
127
- # Blends two hex colors by interpolating their red/green/blue components at fractional +amount+.
128
- # Accepts strings like "#ff0000" and produces a new "#rrggbb" string.
129
- def blend(start_hex, end_hex, amount)
130
- start_rgb = rgb(start_hex)
131
- end_rgb = rgb(end_hex)
132
- mixed = start_rgb.zip(end_rgb).map { |from, to| (from + ((to - from) * amount)).round }
133
- "#%02x%02x%02x" % mixed
134
- end
135
-
136
- # Decomposes a hex color string ("#rrggbb") into an array of three integers [r, g, b].
137
- def rgb(hex)
138
- value = hex.to_s.delete_prefix("#")
139
- raise ArgumentError, "gradient colors must be #rrggbb" unless value.match?(/\A[0-9a-fA-F]{6}\z/)
140
-
141
- [value[0..1], value[2..3], value[4..5]].map { |part| part.to_i(16) }
142
- end
143
-
144
- # Advances the animation frame counter, wrapping around after +FRAME_COUNT+ (10) steps.
145
- def frame
146
- index % FRAME_COUNT
147
- end
148
-
149
- # Produces a deterministic integer hash from the input string using FNV-1a hashing, ensuring the same
150
- # characters appear at the same positions across multiple renderings of this indicator.
151
- def stable_hash(value)
152
- value.bytes.reduce(FNV_OFFSET) do |hash, byte|
153
- ((hash ^ byte) * FNV_PRIME) & FNV_MASK
154
- end
155
- end
156
- end
157
- end
158
- end