charming 0.2.3 → 0.4.0

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/lib/charming/application.rb +98 -14
  4. data/lib/charming/application_state.rb +23 -0
  5. data/lib/charming/cli.rb +2 -2
  6. data/lib/charming/controller/action_hooks.rb +5 -1
  7. data/lib/charming/controller/class_methods.rb +79 -17
  8. data/lib/charming/controller/component_dispatch.rb +163 -0
  9. data/lib/charming/controller/dispatching.rb +1 -2
  10. data/lib/charming/controller/focus_management.rb +67 -1
  11. data/lib/charming/controller/key_dispatch.rb +7 -7
  12. data/lib/charming/controller/rendering.rb +22 -6
  13. data/lib/charming/controller/session_state.rb +63 -22
  14. data/lib/charming/controller/timers.rb +25 -0
  15. data/lib/charming/controller.rb +253 -60
  16. data/lib/charming/cross_thread_access.rb +9 -0
  17. data/lib/charming/double_render_error.rb +8 -0
  18. data/lib/charming/generators/layout_generator.rb +4 -1
  19. data/lib/charming/generators/migration_generator.rb +1 -1
  20. data/lib/charming/generators/model_generator.rb +2 -2
  21. data/lib/charming/generators/name.rb +1 -1
  22. data/lib/charming/generators/screen_generator.rb +2 -2
  23. data/lib/charming/generators/view_generator.rb +1 -1
  24. data/lib/charming/internal/deep_freeze.rb +23 -0
  25. data/lib/charming/internal/env_inquirer.rb +22 -0
  26. data/lib/charming/internal/event_loop.rb +25 -2
  27. data/lib/charming/internal/inflections.rb +93 -0
  28. data/lib/charming/internal/session_guard.rb +27 -0
  29. data/lib/charming/internal/terminal/cursor.rb +29 -0
  30. data/lib/charming/internal/terminal/size.rb +47 -0
  31. data/lib/charming/internal/terminal/tty_backend.rb +10 -8
  32. data/lib/charming/internal/timer_control.rb +48 -0
  33. data/lib/charming/presentation/components/autocomplete.rb +14 -6
  34. data/lib/charming/presentation/components/command_palette.rb +11 -9
  35. data/lib/charming/presentation/components/filepicker.rb +4 -4
  36. data/lib/charming/presentation/components/form/confirm.rb +2 -1
  37. data/lib/charming/presentation/components/form/field.rb +1 -1
  38. data/lib/charming/presentation/components/form/input.rb +3 -3
  39. data/lib/charming/presentation/components/form/multiselect.rb +4 -5
  40. data/lib/charming/presentation/components/form/select.rb +3 -3
  41. data/lib/charming/presentation/components/form/textarea.rb +3 -3
  42. data/lib/charming/presentation/components/form.rb +6 -6
  43. data/lib/charming/presentation/components/help_overlay.rb +2 -2
  44. data/lib/charming/presentation/components/keyboard_handler.rb +3 -3
  45. data/lib/charming/presentation/components/list.rb +14 -5
  46. data/lib/charming/presentation/components/modal.rb +3 -2
  47. data/lib/charming/presentation/components/multi_select_list.rb +14 -7
  48. data/lib/charming/presentation/components/result.rb +61 -0
  49. data/lib/charming/presentation/components/tab_bar.rb +14 -6
  50. data/lib/charming/presentation/components/table.rb +64 -32
  51. data/lib/charming/presentation/components/text_area.rb +7 -7
  52. data/lib/charming/presentation/components/text_input.rb +7 -7
  53. data/lib/charming/presentation/components/tree.rb +15 -6
  54. data/lib/charming/presentation/components/viewport.rb +3 -3
  55. data/lib/charming/presentation/layout/pane.rb +6 -2
  56. data/lib/charming/presentation/layout/screen_layout.rb +7 -0
  57. data/lib/charming/presentation/view.rb +35 -29
  58. data/lib/charming/projectile.rb +64 -0
  59. data/lib/charming/render_artifacts.rb +24 -0
  60. data/lib/charming/response.rb +19 -8
  61. data/lib/charming/router.rb +50 -68
  62. data/lib/charming/runtime.rb +54 -28
  63. data/lib/charming/{controller/command_palette.rb → shell/palette.rb} +43 -11
  64. data/lib/charming/{controller/sidebar_navigation.rb → shell/sidebar.rb} +11 -11
  65. data/lib/charming/spring.rb +126 -0
  66. data/lib/charming/tasks/context.rb +35 -0
  67. data/lib/charming/test_helper.rb +42 -22
  68. data/lib/charming/unhandled_component_event.rb +9 -0
  69. data/lib/charming/unknown_slot.rb +9 -0
  70. data/lib/charming/version.rb +1 -1
  71. data/lib/charming/welcome.rb +1 -1
  72. data/lib/charming.rb +20 -6
  73. metadata +25 -70
  74. data/lib/charming/controller/component_dispatching.rb +0 -125
data/lib/charming.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/inflector"
4
- require "active_support/string_inquirer"
5
3
  require "logger"
6
4
  require "zeitwerk"
7
5
 
@@ -26,15 +24,15 @@ module Charming
26
24
  # Base error class for all Charming-specific exceptions (used by templates, generators, runtime, etc.).
27
25
  class Error < StandardError; end
28
26
 
29
- # The current environment, read from CHARMING_ENV (default "development"). Returns a
30
- # StringInquirer so callers can write `Charming.env.test?` / `Charming.env.production?`.
27
+ # The current environment, read from CHARMING_ENV (default "development"). Returns an
28
+ # EnvInquirer so callers can write `Charming.env.test?` / `Charming.env.production?`.
31
29
  def self.env
32
- @env ||= ActiveSupport::StringInquirer.new(ENV["CHARMING_ENV"] || "development")
30
+ @env ||= Internal::EnvInquirer.new(ENV["CHARMING_ENV"] || "development")
33
31
  end
34
32
 
35
33
  # Overrides the environment (used by tests and the CLI).
36
34
  def self.env=(value)
37
- @env = value.nil? ? nil : ActiveSupport::StringInquirer.new(value.to_s)
35
+ @env = value.nil? ? nil : Internal::EnvInquirer.new(value.to_s)
38
36
  end
39
37
 
40
38
  # Entry point for running a Charming application. Instantiates a Runtime for *application* and starts
@@ -43,6 +41,12 @@ module Charming
43
41
  Runtime.new(application, backend: backend).run
44
42
  end
45
43
 
44
+ # Returns the seconds-per-frame time delta for a frame rate, for initializing
45
+ # physics primitives: `Charming::Spring.new(delta_time: Charming.fps(60))`.
46
+ def self.fps(frames_per_second)
47
+ 1.0 / frames_per_second
48
+ end
49
+
46
50
  # Returns the normalized key symbol for an event-like object — `event.key` when the object responds
47
51
  # to it, otherwise `event.to_sym`. Lets components treat raw strings and KeyEvent objects uniformly.
48
52
  def self.key_of(event)
@@ -78,6 +82,16 @@ module Charming
78
82
 
79
83
  :"#{(ordered_modifiers + [key]).join("+")}"
80
84
  end
85
+
86
+ # Emits a deprecation warning once per process per *category*, prefixed
87
+ # "[charming deprecation]". Warnings go to stderr.
88
+ def self.deprecate(message, category:)
89
+ @deprecation_emitted ||= {}
90
+ return if @deprecation_emitted[category]
91
+
92
+ @deprecation_emitted[category] = true
93
+ warn("[charming deprecation] #{message}")
94
+ end
81
95
  end
82
96
 
83
97
  Charming::Templates.register ".tui.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.2.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - pando
@@ -29,26 +29,6 @@ dependencies:
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
31
  version: 8.1.2
32
- - !ruby/object:Gem::Dependency
33
- name: activesupport
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
32
  - !ruby/object:Gem::Dependency
53
33
  name: commonmarker
54
34
  requirement: !ruby/object:Gem::Requirement
@@ -91,48 +71,6 @@ dependencies:
91
71
  - - "~>"
92
72
  - !ruby/object:Gem::Version
93
73
  version: '5.0'
94
- - !ruby/object:Gem::Dependency
95
- name: tty-cursor
96
- requirement: !ruby/object:Gem::Requirement
97
- requirements:
98
- - - "~>"
99
- - !ruby/object:Gem::Version
100
- version: '0.7'
101
- type: :runtime
102
- prerelease: false
103
- version_requirements: !ruby/object:Gem::Requirement
104
- requirements:
105
- - - "~>"
106
- - !ruby/object:Gem::Version
107
- version: '0.7'
108
- - !ruby/object:Gem::Dependency
109
- name: zeitwerk
110
- requirement: !ruby/object:Gem::Requirement
111
- requirements:
112
- - - "~>"
113
- - !ruby/object:Gem::Version
114
- version: '2.6'
115
- type: :runtime
116
- prerelease: false
117
- version_requirements: !ruby/object:Gem::Requirement
118
- requirements:
119
- - - "~>"
120
- - !ruby/object:Gem::Version
121
- version: '2.6'
122
- - !ruby/object:Gem::Dependency
123
- name: tty-table
124
- requirement: !ruby/object:Gem::Requirement
125
- requirements:
126
- - - "~>"
127
- - !ruby/object:Gem::Version
128
- version: '0.12'
129
- type: :runtime
130
- prerelease: false
131
- version_requirements: !ruby/object:Gem::Requirement
132
- requirements:
133
- - - "~>"
134
- - !ruby/object:Gem::Version
135
- version: '0.12'
136
74
  - !ruby/object:Gem::Dependency
137
75
  name: tty-reader
138
76
  requirement: !ruby/object:Gem::Requirement
@@ -148,21 +86,21 @@ dependencies:
148
86
  - !ruby/object:Gem::Version
149
87
  version: '0.9'
150
88
  - !ruby/object:Gem::Dependency
151
- name: tty-screen
89
+ name: unicode-display_width
152
90
  requirement: !ruby/object:Gem::Requirement
153
91
  requirements:
154
92
  - - "~>"
155
93
  - !ruby/object:Gem::Version
156
- version: '0.8'
94
+ version: '2.6'
157
95
  type: :runtime
158
96
  prerelease: false
159
97
  version_requirements: !ruby/object:Gem::Requirement
160
98
  requirements:
161
99
  - - "~>"
162
100
  - !ruby/object:Gem::Version
163
- version: '0.8'
101
+ version: '2.6'
164
102
  - !ruby/object:Gem::Dependency
165
- name: unicode-display_width
103
+ name: zeitwerk
166
104
  requirement: !ruby/object:Gem::Requirement
167
105
  requirements:
168
106
  - - "~>"
@@ -196,17 +134,18 @@ files:
196
134
  - lib/charming/controller.rb
197
135
  - lib/charming/controller/action_hooks.rb
198
136
  - lib/charming/controller/class_methods.rb
199
- - lib/charming/controller/command_palette.rb
200
- - lib/charming/controller/component_dispatching.rb
137
+ - lib/charming/controller/component_dispatch.rb
201
138
  - lib/charming/controller/dispatching.rb
202
139
  - lib/charming/controller/focus.rb
203
140
  - lib/charming/controller/focus_management.rb
204
141
  - lib/charming/controller/key_dispatch.rb
205
142
  - lib/charming/controller/rendering.rb
206
143
  - lib/charming/controller/session_state.rb
207
- - lib/charming/controller/sidebar_navigation.rb
208
144
  - lib/charming/controller/terminal.rb
145
+ - lib/charming/controller/timers.rb
146
+ - lib/charming/cross_thread_access.rb
209
147
  - lib/charming/database/commands.rb
148
+ - lib/charming/double_render_error.rb
210
149
  - lib/charming/escape.rb
211
150
  - lib/charming/events/focus_event.rb
212
151
  - lib/charming/events/key_event.rb
@@ -267,15 +206,22 @@ files:
267
206
  - lib/charming/image/source.rb
268
207
  - lib/charming/image/terminal.rb
269
208
  - lib/charming/image/transmit.rb
209
+ - lib/charming/internal/deep_freeze.rb
210
+ - lib/charming/internal/env_inquirer.rb
270
211
  - lib/charming/internal/event_loop.rb
212
+ - lib/charming/internal/inflections.rb
271
213
  - lib/charming/internal/renderer/differential.rb
272
214
  - lib/charming/internal/renderer/full_repaint.rb
215
+ - lib/charming/internal/session_guard.rb
273
216
  - lib/charming/internal/terminal/adapter.rb
217
+ - lib/charming/internal/terminal/cursor.rb
274
218
  - lib/charming/internal/terminal/key_normalizer.rb
275
219
  - lib/charming/internal/terminal/memory_backend.rb
276
220
  - lib/charming/internal/terminal/modified_key_parser.rb
277
221
  - lib/charming/internal/terminal/mouse_parser.rb
222
+ - lib/charming/internal/terminal/size.rb
278
223
  - lib/charming/internal/terminal/tty_backend.rb
224
+ - lib/charming/internal/timer_control.rb
279
225
  - lib/charming/presentation/component.rb
280
226
  - lib/charming/presentation/components/activity_indicator.rb
281
227
  - lib/charming/presentation/components/audio.rb
@@ -307,6 +253,7 @@ files:
307
253
  - lib/charming/presentation/components/multi_select_list.rb
308
254
  - lib/charming/presentation/components/paginator.rb
309
255
  - lib/charming/presentation/components/progressbar.rb
256
+ - lib/charming/presentation/components/result.rb
310
257
  - lib/charming/presentation/components/sparkline.rb
311
258
  - lib/charming/presentation/components/spinner.rb
312
259
  - lib/charming/presentation/components/status_bar.rb
@@ -366,16 +313,24 @@ files:
366
313
  - lib/charming/presentation/ui/truncate.rb
367
314
  - lib/charming/presentation/ui/width.rb
368
315
  - lib/charming/presentation/view.rb
316
+ - lib/charming/projectile.rb
317
+ - lib/charming/render_artifacts.rb
369
318
  - lib/charming/response.rb
370
319
  - lib/charming/router.rb
371
320
  - lib/charming/runtime.rb
372
321
  - lib/charming/screen.rb
322
+ - lib/charming/shell/palette.rb
323
+ - lib/charming/shell/sidebar.rb
324
+ - lib/charming/spring.rb
373
325
  - lib/charming/tasks/cancelled.rb
326
+ - lib/charming/tasks/context.rb
374
327
  - lib/charming/tasks/inline_executor.rb
375
328
  - lib/charming/tasks/progress.rb
376
329
  - lib/charming/tasks/task.rb
377
330
  - lib/charming/tasks/threaded_executor.rb
378
331
  - lib/charming/test_helper.rb
332
+ - lib/charming/unhandled_component_event.rb
333
+ - lib/charming/unknown_slot.rb
379
334
  - lib/charming/version.rb
380
335
  - lib/charming/welcome.rb
381
336
  - lib/charming/welcome/controller.rb
@@ -1,125 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- class Controller
5
- # Component-dispatch helpers mixed into Controller. Forwards key events to the currently
6
- # focused component (the slot returned by `focus.current`) and translates component return
7
- # values into controller hook calls (e.g., `slot_submitted`, `slot_selected`, `slot_cancelled`).
8
- module ComponentDispatching
9
- private
10
-
11
- # Sends the current key event to the focused component (if it responds to `handle_key`).
12
- # Returns `:handled` after dispatching, or nil when no component is focused.
13
- def dispatch_to_focused_component
14
- slot = focus.current
15
- return nil unless slot && respond_to?(slot, true)
16
-
17
- component = send(slot)
18
- return nil unless component.respond_to?(:handle_key)
19
-
20
- result = component.handle_key(event)
21
- return nil if result.nil?
22
-
23
- dispatch_component_result(slot, result)
24
- :handled
25
- end
26
-
27
- # Translates a component `handle_key` *result* into a controller hook call. `:cancelled`
28
- # triggers `<slot>_cancelled`, `[:submitted, value]` triggers `<slot>_submitted(value)`,
29
- # `[:selected, value]` triggers `<slot>_selected(value)`. Falls back to a default render
30
- # when no matching hook exists.
31
- def dispatch_component_result(slot, result)
32
- action, arguments = component_result_action(slot, result)
33
- action ? send(action, *arguments) : render_default_action
34
- render_default_action unless response
35
- end
36
-
37
- # Resolves which controller hook (if any) corresponds to the *result* from a component.
38
- def component_result_action(slot, result)
39
- case result
40
- when :cancelled
41
- component_action(slot, :cancelled)
42
- when Array
43
- component_array_action(slot, result)
44
- end
45
- end
46
-
47
- # Handles array-shaped component results, currently `[:submitted, value]` and `[:selected, value]`.
48
- def component_array_action(slot, result)
49
- event_name, value = result
50
- return component_action(slot, :submitted, value) if event_name == :submitted
51
- return component_action(slot, :selected, value) if event_name == :selected
52
-
53
- nil
54
- end
55
-
56
- # Returns `[action, arguments]` for the `<slot>_<suffix>` controller hook if defined, or nil.
57
- def component_action(slot, suffix, *arguments)
58
- action = :"#{slot}_#{suffix}"
59
- return unless respond_to?(action, true)
60
-
61
- [action, arguments]
62
- end
63
-
64
- # Handles Tab/Shift+Tab by cycling through the focus ring. Returns :handled after rendering.
65
- def dispatch_tab_traversal
66
- return nil unless key_name == :tab
67
- return nil if focus.ring.empty?
68
-
69
- focus.cycle(event.shift ? -1 : +1)
70
- render_default_action
71
- :handled
72
- end
73
-
74
- # Hit-tests the current mouse event against named layout panes from the latest render.
75
- # Clicks move focus to matching slots; components in clicked panes receive local coordinates.
76
- def dispatch_component_mouse
77
- target = mouse_target_for_event
78
- return nil unless target
79
-
80
- slot = target.fetch(:name)
81
- previous_focus = focus.current
82
- focus.focus(slot) if focusable_click?(slot)
83
-
84
- result = dispatch_mouse_to_target_component(slot, target)
85
- return response if result.nil? && previous_focus == focus.current
86
-
87
- result ? dispatch_component_result(slot, result) : render_default_action
88
- response
89
- end
90
-
91
- def mouse_target_for_event
92
- mouse_targets.rfind { |target| target.fetch(:rect).cover?(event.x, event.y) }
93
- end
94
-
95
- def focusable_click?(slot)
96
- event.respond_to?(:click?) && event.click? && focus.ring.include?(slot)
97
- end
98
-
99
- def dispatch_mouse_to_target_component(slot, target)
100
- return nil unless respond_to?(slot, true)
101
-
102
- component = send(slot)
103
- return nil unless component.respond_to?(:handle_mouse)
104
-
105
- local_event = local_mouse_event(target.fetch(:inner_rect))
106
- return nil unless local_event
107
-
108
- component.handle_mouse(local_event)
109
- end
110
-
111
- def local_mouse_event(rect)
112
- return nil unless rect.cover?(event.x, event.y)
113
-
114
- Events::MouseEvent.new(
115
- button: event.button,
116
- x: event.x - rect.x,
117
- y: event.y - rect.y,
118
- ctrl: event.ctrl,
119
- alt: event.alt,
120
- shift: event.shift
121
- )
122
- end
123
- end
124
- end
125
- end