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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0eaad21d88747b525afbae63f84bf610e51efe4fbc56d5e628255efe66e35d8
4
- data.tar.gz: 2e4b4669da9b3975a8e09b6d248e1b74802b216f8036e52e6df8582cebc289c7
3
+ metadata.gz: 739b00d7bbbe867e98ec93bc614157e59650eb5ecd45a48127139fb5ea11adb1
4
+ data.tar.gz: 8c7008f8bcd6eba1464d01e317c23a37d93a47cf272cc617295c5a4f6f50e379
5
5
  SHA512:
6
- metadata.gz: bf4004256b03622484367fa47cdcd5a855f9693f6623e1f3b54fc54c9d952f66477ad3ef6342963f1cdc0fca75eb9b0dd399ca2f9d07f217198c1f02ae139372
7
- data.tar.gz: 4257cdb45bd0fa51866f4ada6db6a279cc92cec5b23248ee1d7bf467fbebc546564df83c57bd93ca797ebc81a5cee9b562a31ac93a6771483b8f2643e7452cf0
6
+ metadata.gz: 69179596724fc972425aa5c47cd449d31278ea51671c6afc25543d5db58ea2b2edcb035843cc76f53c663f65bbda24e9990cc5cab90142036a228bb68e9cf9e9
7
+ data.tar.gz: 55b113fa7a8a0208559716a30d18ec070e6c0b070032f16a9d66daa67273b9c3d81ab5aefe231b36240b783c200d9eff4321f7df9596a845b43f2343f4754f8e
data/README.md CHANGED
@@ -2,405 +2,65 @@
2
2
 
3
3
  A Rails-inspired terminal user interface framework for **Ruby 4+**.
4
4
 
5
- ```ruby
6
- class MyApp < Charming::Application
7
- routes do
8
- root "counter#show"
9
- end
10
- end
5
+ Charming gives terminal apps familiar application structure: routes, controllers, state objects, templates, layouts, reusable components, themes, keyboard bindings, command palettes, timers, background tasks, and testable terminal backends.
11
6
 
12
- class CounterController < Charming::Controller
13
- key "up", :increment
14
- key "down", :decrement
15
- key "q", :quit, scope: :global
7
+ ## Project Status
16
8
 
17
- def show
18
- render "Count: #{counter.count}"
19
- end
9
+ Charming is still in its infancy and is under constant development. APIs, behavior, and generated app structure may change until the project reaches a stable `1.0` release.
20
10
 
21
- def increment
22
- counter.count += 1
23
- show
24
- end
11
+ ## Quick Start
25
12
 
26
- def decrement
27
- counter.count -= 1
28
- show
29
- end
30
-
31
- private
32
-
33
- def counter
34
- model(:counter, CounterModel)
35
- end
36
- end
37
-
38
- class CounterModel < Charming::ApplicationModel
39
- attribute :count, :integer, default: 0
40
- end
41
- ```
42
-
43
- ## Installation
44
-
45
- Add this line to your application's Gemfile:
46
-
47
- ```ruby
48
- gem "charming"
49
- ```
50
-
51
- Then execute:
13
+ Install the Charming CLI gem on your machine:
52
14
 
53
15
  ```bash
54
- bundle install
16
+ gem install charming
55
17
  ```
56
18
 
57
- ## Generating an App
58
-
59
- Create a complete, runnable Charming app with the built-in generator:
19
+ Generate and run an app:
60
20
 
61
21
  ```bash
62
22
  charming new my_app
63
23
  cd my_app
24
+ bundle install
64
25
  bundle exec exe/my_app
65
26
  ```
66
27
 
67
- The generator produces a full Bundler gem with conventional Rails-like structure:
68
-
69
- ```text
70
- app/controllers/ # application controllers
71
- app/models/ # persistent state models
72
- app/views/ # screen views
73
- app/components/ # reusable widgets (AppFrame, etc.)
74
- config/routes.rb # route definitions
75
- lib/my_app.rb # namespace loader (Zeitwerk)
76
- exe/my_app # executable entry point
77
- ```
78
-
79
- Inside a generated app, scaffold more code:
80
-
81
- ```bash
82
- charming generate controller users index show
83
- charming generate view details
84
- charming generate component status_badge
85
- charming g controller products # shortcut
86
- ```
87
-
88
- Generated apps ship with a command palette (press `p`) and a sidebar navigation layout with theming and focus management baked in.
89
-
90
- ## Running Without the Generator
91
-
92
- You can also build an app from scratch. Run it with:
93
-
94
- ```ruby
95
- Charming.run(MyApp.new)
96
- # ^ entry point — starts the terminal event loop
97
- ```
98
-
99
- The `Charming::Runtime` manages the terminal lifecycle, reads events from a TTY backend, and dispatches them to controllers. An in-memory backend (`MemoryBackend`) is available for scripting and testing without a real terminal.
100
-
101
- ## Application & Routing
102
-
103
- Define routes with `root` and `screen` — each maps a URL path to a controller and action:
104
-
105
- ```ruby
106
- class MyApp < Charming::Application
107
- routes do
108
- root "home#index"
109
- screen "/cities/:id", to: "cities#show"
110
- end
111
- end
112
- ```
113
-
114
- Dynamic segments (`:id`) are available in controllers through `params`:
28
+ Charming can also be added to an existing Ruby project with Bundler, but the primary workflow is installing the gem globally and using `charming new` to create a complete app.
115
29
 
116
- ```ruby
117
- class CitiesController < Charming::Controller
118
- def show
119
- render "City #{params[:id]}"
120
- end
121
- end
122
- ```
123
-
124
- Exact routes take precedence over dynamic routes. Multiple screens are listed in route order and rendered as sidebar entries (handled automatically by generated app layouts).
125
-
126
- ## Controllers
127
-
128
- The base `Charming::Controller` provides key bindings, command palette entries, timer-driven actions, navigation, and state management:
129
-
130
- **Key bindings** — strings or symbols mapped to action methods, scoped as either content-focused or global:
131
-
132
- ```ruby
133
- class HomeController < Charming::Controller
134
- key "up", :increment
135
- key "j", :navigate_down, scope: :content
136
- key "q", :quit, scope: :global
137
- end
138
- ```
139
-
140
- **Command palette** — entries visible in the fuzzy-search command palette. Accepts a method name or an inline block:
141
-
142
- ```ruby
143
- command "Save changes", :save
144
- command "Clear" do
145
- @model = reset_model
146
- end
147
- ```
148
-
149
- **Timers** — periodic actions that fire at a given interval on the current controller:
150
-
151
- ```ruby
152
- timer :blink, every: 0.5, action: :toggle_spinner
153
- ```
154
-
155
- **Model storage** — models are stored in session and lazily instantiated:
156
-
157
- ```ruby
158
- def counter
159
- model(:counter, CounterModel)
160
- end
161
- # ^ name ^ class
162
- ```
163
-
164
- Subsequent calls with the same name return the cached instance. Use this pattern to define accessors on your controllers.
165
-
166
- **Navigation** — redirect to a new route or quit the application:
167
-
168
- ```ruby
169
- navigate_to "/settings" # redirect
170
- quit # exit the app
171
- open_command_palette # open command palette
172
- close_command_palette # close it
173
- use_theme :phosphor # switch theme (persists in session)
174
- ```
175
-
176
- ## Models
30
+ ## Documentation
177
31
 
178
- Application models inherit from `Charming::ApplicationModel`, which includes `ActiveModel::Model` and `ActiveModel::Attributes`:
32
+ | Guide | Purpose |
33
+ |-------|---------|
34
+ | [Docs Index](docs/README.md) | Suggested reading paths and all documentation links. |
35
+ | [Getting Started](docs/getting_started.md) | Build and run a generated Charming app. |
36
+ | [Core Concepts](docs/core_concepts.md) | App architecture, runtime flow, ephemeral controllers, and state. |
37
+ | [Routing](docs/routing.md) | `root`, `screen`, dynamic params, route titles, and route order. |
38
+ | [Controllers & Templates](docs/controllers_and_templates.md) | Actions, `render :show`, `render_template`, key bindings, commands, timers, and tasks. |
39
+ | [Layouts](docs/layouts.md) | Template layouts, `yield_content`, split panes, overlays, responsive layouts, and styles. |
40
+ | [State](docs/state.md) | `ApplicationState`, typed attributes, validations, and session-backed state. |
41
+ | [Database](docs/database.md) | Optional SQLite persistence with Active Record models and migrations. |
42
+ | [Components](docs/components.md) | Built-in components, custom components, and interaction return values. |
43
+ | [Themes](docs/themes.md) | Theme registration, tokens, and runtime theme switching. |
44
+ | [API Reference](docs/api.md) | Compact public API reference. |
45
+ | [Testing](docs/testing.md) | Controller, template, component, runtime, timer, and task tests. |
179
46
 
180
- ```ruby
181
- class CounterModel < Charming::ApplicationModel
182
- attribute :count, :integer, default: 0
183
-
184
- validate :count_gte_zero do
185
- errors.add(:count, "must be >= 0") if count < 0
186
- end
187
- end
188
- ```
47
+ ## Generated App Structure
189
48
 
190
- Models are the only place persistent state should live. Controllers are created fresh per event — never store state on them.
191
-
192
- ## Views
193
-
194
- Views inherit from `Charming::View` and expose assigns (from `initialize`) as instance-local accessor methods:
195
-
196
- ```ruby
197
- class HomeView < Charming::View
198
- def render
199
- row(title, subtitle, gap: 2)
200
- end
201
-
202
- private
203
-
204
- def title
205
- text "Hello!", style: theme.title
206
- end
207
-
208
- def subtitle
209
- text "World", style: theme.muted
210
- end
211
- end
212
- ```
49
+ The generator produces a Bundler gem with a Rails-like structure:
213
50
 
214
- Views use `row`, `column` for layout, `box` for bordered containers, and `text` for styled output. Assigns passed to `initialize` become reader methods automatically:
215
-
216
- ```ruby
217
- class HomeView < Charming::View
218
- # Pass assigns via initialize; they are accessible as regular methods inside render()
219
- end
220
-
221
- view = HomeView.new(title: "Hello", count: 42)
222
- # view.title "Hello"
223
- # view.count 42
224
- ```
225
-
226
- Defining your own method with the same name will override the auto-generated accessor.
227
-
228
- ### Layouts
229
-
230
- Layouts are views that wrap the current screen with a wrapper (sidebar, header, etc.):
231
-
232
- ```ruby
233
- class ApplicationController < Charming::Controller
234
- layout Layouts::Application
235
- end
236
- ```
237
-
238
- Subclasses inherit their parent's layout. Override with `layout false` to disable wrapping.
239
-
240
- Layouts use `yield_content` to render the primary screen and receive `screen`, `controller`, and `theme` as assigns:
241
-
242
- ```ruby
243
- module MyProject
244
- module Layouts
245
- class Application < Charming::View
246
- def render
247
- body = Charming::UI.place(content, width: screen.width, height: screen.height)
248
- return body unless command_palette_open?
249
-
250
- Charming::UI.overlay(body, command_palette.render)
251
- end
252
- end
253
- end
254
- end
255
- ```
256
-
257
- ### Partials
258
-
259
- Render class-based partial views from other views:
260
-
261
- ```ruby
262
- render_component HeaderView.new(title: "Dashboard")
263
- # ^ component is a View subclass or Component
264
- ```
265
-
266
- Components are just `Charming::View` subclasses — they gain the same assigns, helpers, and rendering behavior.
267
-
268
- ## Themes
269
-
270
- Applications register named themes from bundled JSON files or custom locations:
271
-
272
- ```ruby
273
- class MyApp < Charming::Application
274
- Charming::UI::Theme.built_in_names.each do |theme_name|
275
- theme theme_name.to_sym, built_in: theme_name
276
- end
277
-
278
- default_theme :phosphor
279
-
280
- theme :custom, from: "config/themes/custom.json"
281
- end
282
- ```
283
-
284
- Charming ships with the Phosphor theme by default. Views use semantic tokens — not hardcoded colors — for all styling:
285
-
286
- ```ruby
287
- text "Welcome", style: theme.title # bright cyan + bold
288
- text "Status", style: theme.muted # dim gray
289
- text "Alert", style: theme.info # cyan
290
- ```
291
-
292
- ## Components
293
-
294
- Charming ships with interactive terminal widgets that inherit from `Charming::View` (and thus gain all View helpers):
295
-
296
- | Component | Description |
297
- |-----------|-------------|
298
- | `TextInput` | Editable text field with cursor movement, selection, and character insertion |
299
- | `List` | Selectable list with keyboard navigation (up/down/home/end/enter) and mouse support |
300
- | `Modal` | Overlay dialog with title, content, and help text |
301
- | `CommandPalette` | Fuzzy-search command input used internally by the framework |
302
- | `Viewport` | Scrollable container for tall content lists |
303
- | `Spinner` | Animated progress indicator |
304
- | `ActivityIndicator` | Spinner variant (same underlying widget) |
305
- | `Progressbar` | A text-based progress bar |
306
- | `Table` | Unicode-rendered data table with keyboard navigation and mouse selection |
307
- | `KeyboardHandler` | Key-mapping mixin for custom components |
308
-
309
- All components accept a `theme:` parameter and are rendered from views via `render_component`:
310
-
311
- ```ruby
312
- render_component List.new(
313
- items: ["Alpha", "Beta", "Gamma"],
314
- selected_index: 0,
315
- theme: theme
316
- )
317
- ```
318
-
319
- Components return specific values from their `handle_key(event)` methods — the framework recognizes conventions like `[:selected, item]` and `:cancelled`. They also provide a `handle_mouse(event)` method for mouse-driven interaction.
320
-
321
- ## Async Tasks
322
-
323
- Dispatch background work via `run_task` on controllers. Results arrive as `TaskEvent`s that trigger controller actions:
324
-
325
- ```ruby
326
- class HomeController < Charming::Controller
327
- on_task :fetch_data, action: :data_loaded
328
-
329
- def load_data
330
- run_task :fetch_data do
331
- # runs in a background thread
332
- sleep 2
333
- "done"
334
- end
335
- end
336
-
337
- def data_loaded
338
- render "Task complete!"
339
- end
340
- end
341
- ```
342
-
343
- Register handlers with `on_task` on the controller and dispatch work with `run_task`.
344
-
345
- ## Focus Management
346
-
347
- Multi-screen layouts can define focusable areas using `focus_ring`:
348
-
349
- ```ruby
350
- class ApplicationController < Charming::Controller
351
- focus_ring :sidebar, :content
352
- end
353
- ```
354
-
355
- This enables keyboard-driven focus traversal — `Tab` cycles forward, `Shift+Tab` backward. Use `focused?(slot)`, `focus_sidebar`, and `focus_content` to programmatically control focus:
356
-
357
- ```ruby
358
- def show
359
- focus_sidebar if params[:sidebar]
360
- render HomeView.new(...)
361
- end
362
- ```
363
-
364
- ## Layout Primitives
365
-
366
- The `Charming::UI` module provides layout primitives for building custom screen layouts. These work independently of the runtime and backends:
367
-
368
- | Method | Description |
369
- |--------|-------------|
370
- | `Style.new` | Create a new style for chaining colors, padding, borders, alignment |
371
- | `UI.join_horizontal(*blocks, gap: 0)` | Place blocks side-by-side |
372
- | `UI.join_vertical(*blocks, gap: 0)` | Stack blocks vertically |
373
- | `UI.center(block, width:, height:)` | Center a block in a fixed canvas |
374
- | `UI.place(block, width:, height:, top:, left:, background:)` | Place anywhere on a canvas |
375
- | `UI.overlay(base, overlay, top:, left:)` | Overlay content atop another |
376
-
377
- All methods work with ANSI-styled strings and correctly handle Unicode display widths:
378
-
379
- ```ruby
380
- body = UI.join_horizontal(sidebar, main_content, gap: 1)
381
- canvas = UI.place(body, width: screen.width, height: screen.height)
382
- Charming::UI.overlay(canvas, modal_view.render)
383
- ```
384
-
385
- ## Testing
386
-
387
- Charming uses an in-memory backend (`MemoryBackend`) for testing, so specs run without a real terminal. Pass `backend: MemoryBackend.new(...)` to `Charming::Runtime`:
388
-
389
- ```ruby
390
- backend = Charming::Internal::Terminal::MemoryBackend.new(
391
- events: [
392
- Charming::KeyEvent.new(key: :up),
393
- Charming::KeyEvent.new(key: :q)
394
- ]
395
- )
396
- runtime = described_class.new(app, backend: backend)
397
- runtime.run
398
-
399
- expect(backend.frames).to eq(["Count: 0", "Count: 1"])
400
- # ^ captured terminal output, one frame per render
51
+ ```text
52
+ app/controllers/ # controller actions and input bindings
53
+ app/state/ # session-backed TUI state
54
+ app/models/ # optional Active Record models
55
+ app/views/home/show.tui.erb # screen templates
56
+ app/views/layouts/application.tui.erb # layout template
57
+ app/components/ # reusable components
58
+ config/routes.rb # route definitions
59
+ lib/my_app.rb # namespace loader (Zeitwerk)
60
+ exe/my_app # executable entry point
401
61
  ```
402
62
 
403
- The `MemoryBackend` constructor accepts `events:` (a series of events to feed the loop), and `width:` / `height:` for screen dimensions. After running, assertions go against `backend.frames` an array capturing each rendered terminal frame passed through `write_frame`. This pattern is used throughout the test suite.
63
+ Generated apps include a sidebar/content layout, command palette, focus management, theme switching, and default key bindings for commands (`p`) and quit (`q`).
404
64
 
405
65
  ## Development
406
66
 
@@ -408,7 +68,7 @@ After checking out the repo, run:
408
68
 
409
69
  ```bash
410
70
  bundle install
411
- bin/check # run everything — RSpec + Standard Ruby
71
+ bin/check
412
72
  ```
413
73
 
414
74
  Common binstubs:
@@ -33,9 +33,9 @@ module Charming
33
33
  raise ArgumentError, "theme expects either from: or built_in:, not both" if from && built_in
34
34
 
35
35
  themes[name.to_sym] = if built_in
36
- UI::Theme.load_builtin(built_in)
36
+ Presentation::UI::Theme.load_builtin(built_in)
37
37
  else
38
- UI::Theme.load_file(resolve_theme_path(from))
38
+ Presentation::UI::Theme.load_file(resolve_theme_path(from))
39
39
  end
40
40
  end
41
41
 
@@ -51,7 +51,7 @@ module Charming
51
51
 
52
52
  def theme_for(name = nil)
53
53
  theme_name = name || default_theme
54
- return UI::Theme.default unless theme_name
54
+ return Presentation::UI::Theme.default unless theme_name
55
55
 
56
56
  themes.fetch(theme_name.to_sym)
57
57
  end
@@ -3,10 +3,10 @@
3
3
  require "active_model"
4
4
 
5
5
  module Charming
6
- # ApplicationModel is the persistent state base for application data models. It includes
6
+ # ApplicationState is the base for session-backed TUI state. It includes
7
7
  # `ActiveModel::Model` (validation, initialisation) and `ActiveModel::Attributes` (typed attributes
8
- # with defaults via `attribute :name, :type, default: ...`), making it suitable as session-stored root objects.
9
- class ApplicationModel
8
+ # with defaults via `attribute :name, :type, default: ...`), making it suitable for screen/form state.
9
+ class ApplicationState
10
10
  include ActiveModel::Model
11
11
  include ActiveModel::Attributes
12
12
  end
data/lib/charming/cli.rb CHANGED
@@ -13,6 +13,7 @@ module Charming
13
13
  case command
14
14
  when "new" then new_app(args)
15
15
  when "generate", "g" then generate(args)
16
+ when /^db:/ then database(command, args)
16
17
  else usage(1)
17
18
  end
18
19
  rescue Generators::Error => e
@@ -26,8 +27,11 @@ module Charming
26
27
 
27
28
  def new_app(args)
28
29
  force = args.delete("--force")
29
- name = args.fetch(0) { raise Generators::Error, "Usage: charming new NAME [--force]" }
30
- Generators::AppGenerator.new(name, out: out, destination: pwd, force: force).generate
30
+ database = extract_database(args)
31
+ name = args.fetch(0) { raise Generators::Error, "Usage: charming new NAME [--database sqlite3] [--force]" }
32
+ raise Generators::Error, "Usage: charming new NAME [--database sqlite3] [--force]" if args.length > 1
33
+
34
+ Generators::AppGenerator.new(name, out: out, destination: pwd, force: force, database: database).generate
31
35
  0
32
36
  end
33
37
 
@@ -46,14 +50,46 @@ module Charming
46
50
  def generator_class(type)
47
51
  {
48
52
  "controller" => Generators::ControllerGenerator,
53
+ "model" => Generators::ModelGenerator,
49
54
  "screen" => Generators::ScreenGenerator,
50
55
  "view" => Generators::ViewGenerator,
51
56
  "component" => Generators::ComponentGenerator
52
57
  }.fetch(type) { raise Generators::Error, "Unknown generator: #{type}" }
53
58
  end
54
59
 
60
+ def database(command, args)
61
+ if command == "db:install"
62
+ database = args.shift || raise(Generators::Error, "Usage: charming db:install sqlite3")
63
+ raise Generators::Error, "Usage: charming db:install sqlite3" if args.any?
64
+
65
+ DatabaseInstaller.new(database, out: out, destination: pwd).install
66
+ else
67
+ raise Generators::Error, "Usage: charming #{command}" if args.any?
68
+
69
+ DatabaseCommands.new(command, out: out, destination: pwd).run
70
+ end
71
+ 0
72
+ end
73
+
74
+ def extract_database(args)
75
+ inline = args.find { |arg| arg.start_with?("--database=") }
76
+ return validate_database(args.delete(inline).split("=", 2).last) if inline
77
+
78
+ index = args.index("--database")
79
+ return nil unless index
80
+
81
+ args.delete_at(index)
82
+ validate_database(args.delete_at(index) || raise(Generators::Error, "Usage: charming new NAME [--database sqlite3] [--force]"))
83
+ end
84
+
85
+ def validate_database(database)
86
+ return database if database == "sqlite3"
87
+
88
+ raise Generators::Error, "Unsupported database: #{database.inspect}"
89
+ end
90
+
55
91
  def usage(status)
56
- err.puts "Usage: charming new NAME | charming generate TYPE NAME [actions]"
92
+ err.puts "Usage: charming new NAME | charming generate TYPE NAME [args] | charming db:COMMAND"
57
93
  status
58
94
  end
59
95
  end