charming 0.1.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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +421 -0
  4. data/exe/charming +6 -0
  5. data/lib/charming/application.rb +90 -0
  6. data/lib/charming/application_model.rb +13 -0
  7. data/lib/charming/cli.rb +60 -0
  8. data/lib/charming/component.rb +8 -0
  9. data/lib/charming/components/activity_indicator.rb +158 -0
  10. data/lib/charming/components/command_palette.rb +118 -0
  11. data/lib/charming/components/keyboard_handler.rb +22 -0
  12. data/lib/charming/components/list.rb +105 -0
  13. data/lib/charming/components/modal.rb +48 -0
  14. data/lib/charming/components/progressbar.rb +55 -0
  15. data/lib/charming/components/spinner.rb +37 -0
  16. data/lib/charming/components/table.rb +115 -0
  17. data/lib/charming/components/text_input.rb +103 -0
  18. data/lib/charming/components/viewport.rb +191 -0
  19. data/lib/charming/controller.rb +523 -0
  20. data/lib/charming/focus.rb +65 -0
  21. data/lib/charming/generators/app_file_generator.rb +28 -0
  22. data/lib/charming/generators/app_generator/app_spec_templates.rb +86 -0
  23. data/lib/charming/generators/app_generator/basic_templates.rb +69 -0
  24. data/lib/charming/generators/app_generator/component_templates.rb +36 -0
  25. data/lib/charming/generators/app_generator/controller_template.rb +69 -0
  26. data/lib/charming/generators/app_generator/layout_template.rb +160 -0
  27. data/lib/charming/generators/app_generator/model_templates.rb +30 -0
  28. data/lib/charming/generators/app_generator/screen_spec_templates.rb +70 -0
  29. data/lib/charming/generators/app_generator/view_template.rb +90 -0
  30. data/lib/charming/generators/app_generator.rb +76 -0
  31. data/lib/charming/generators/base.rb +29 -0
  32. data/lib/charming/generators/component_generator.rb +30 -0
  33. data/lib/charming/generators/controller_generator.rb +50 -0
  34. data/lib/charming/generators/name.rb +32 -0
  35. data/lib/charming/generators/screen_generator.rb +154 -0
  36. data/lib/charming/generators/view_generator.rb +34 -0
  37. data/lib/charming/generators.rb +7 -0
  38. data/lib/charming/internal/renderer/differential.rb +53 -0
  39. data/lib/charming/internal/renderer/full_repaint.rb +19 -0
  40. data/lib/charming/internal/terminal/adapter.rb +52 -0
  41. data/lib/charming/internal/terminal/memory_backend.rb +91 -0
  42. data/lib/charming/internal/terminal/tty_backend.rb +250 -0
  43. data/lib/charming/key_event.rb +13 -0
  44. data/lib/charming/mouse_event.rb +40 -0
  45. data/lib/charming/resize_event.rb +7 -0
  46. data/lib/charming/response.rb +33 -0
  47. data/lib/charming/router.rb +137 -0
  48. data/lib/charming/runtime.rb +192 -0
  49. data/lib/charming/screen.rb +8 -0
  50. data/lib/charming/task.rb +7 -0
  51. data/lib/charming/task_event.rb +17 -0
  52. data/lib/charming/task_executor.rb +62 -0
  53. data/lib/charming/timer_event.rb +7 -0
  54. data/lib/charming/ui/border.rb +33 -0
  55. data/lib/charming/ui/style.rb +244 -0
  56. data/lib/charming/ui/theme.rb +178 -0
  57. data/lib/charming/ui/themes/phosphor.json +100 -0
  58. data/lib/charming/ui/width.rb +24 -0
  59. data/lib/charming/ui.rb +230 -0
  60. data/lib/charming/version.rb +5 -0
  61. data/lib/charming/view.rb +116 -0
  62. data/lib/charming.rb +24 -0
  63. data/sig/charming.rbs +3 -0
  64. metadata +225 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e0eaad21d88747b525afbae63f84bf610e51efe4fbc56d5e628255efe66e35d8
4
+ data.tar.gz: 2e4b4669da9b3975a8e09b6d248e1b74802b216f8036e52e6df8582cebc289c7
5
+ SHA512:
6
+ metadata.gz: bf4004256b03622484367fa47cdcd5a855f9693f6623e1f3b54fc54c9d952f66477ad3ef6342963f1cdc0fca75eb9b0dd399ca2f9d07f217198c1f02ae139372
7
+ data.tar.gz: 4257cdb45bd0fa51866f4ada6db6a279cc92cec5b23248ee1d7bf467fbebc546564df83c57bd93ca797ebc81a5cee9b562a31ac93a6771483b8f2643e7452cf0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 pandorocks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,421 @@
1
+ # Charming
2
+
3
+ A Rails-inspired terminal user interface framework for **Ruby 4+**.
4
+
5
+ ```ruby
6
+ class MyApp < Charming::Application
7
+ routes do
8
+ root "counter#show"
9
+ end
10
+ end
11
+
12
+ class CounterController < Charming::Controller
13
+ key "up", :increment
14
+ key "down", :decrement
15
+ key "q", :quit, scope: :global
16
+
17
+ def show
18
+ render "Count: #{counter.count}"
19
+ end
20
+
21
+ def increment
22
+ counter.count += 1
23
+ show
24
+ end
25
+
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:
52
+
53
+ ```bash
54
+ bundle install
55
+ ```
56
+
57
+ ## Generating an App
58
+
59
+ Create a complete, runnable Charming app with the built-in generator:
60
+
61
+ ```bash
62
+ charming new my_app
63
+ cd my_app
64
+ bundle exec exe/my_app
65
+ ```
66
+
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`:
115
+
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
177
+
178
+ Application models inherit from `Charming::ApplicationModel`, which includes `ActiveModel::Model` and `ActiveModel::Attributes`:
179
+
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
+ ```
189
+
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
+ ```
213
+
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
401
+ ```
402
+
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.
404
+
405
+ ## Development
406
+
407
+ After checking out the repo, run:
408
+
409
+ ```bash
410
+ bundle install
411
+ bin/check # run everything — RSpec + Standard Ruby
412
+ ```
413
+
414
+ Common binstubs:
415
+
416
+ ```bash
417
+ bin/rspec # run specs only
418
+ bin/format # auto-format with Standard Ruby
419
+ bin/lint # style checks with Standard Ruby
420
+ bin/check # run everything
421
+ ```
data/exe/charming ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "charming"
5
+
6
+ exit Charming::CLI.new.call(ARGV)
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Charming
4
+ # Application is a lightweight, Rails-inspired application base for building
5
+ # terminal-based apps. It provides routing (via a DSL), session storage, and
6
+ # task execution for managing async operations.
7
+ class Application
8
+ THEME_READER = Object.new.freeze
9
+
10
+ class << self
11
+ # Registers or returns the app's Router. Accepts an optional block to define
12
+ # routes via DSL (screen, root). Lazily initializes a new Router per namespace.
13
+ def routes(&block)
14
+ @routes ||= Router.new(namespace: namespace)
15
+ @routes.draw(&block) if block
16
+ @routes
17
+ end
18
+
19
+ # Derives the module namespace from the class name — e.g., Admin::HomeController
20
+ # yields "Admin". Mirrors Rails' engine-style namespacing.
21
+ def namespace
22
+ name&.split("::")&.then { |parts| parts[0...-1].join("::") }
23
+ end
24
+
25
+ def root(path = THEME_READER)
26
+ return @root if path == THEME_READER
27
+
28
+ @root = File.expand_path(path)
29
+ end
30
+
31
+ def theme(name, from: nil, built_in: nil)
32
+ raise ArgumentError, "theme expects from: or built_in:" unless from || built_in
33
+ raise ArgumentError, "theme expects either from: or built_in:, not both" if from && built_in
34
+
35
+ themes[name.to_sym] = if built_in
36
+ UI::Theme.load_builtin(built_in)
37
+ else
38
+ UI::Theme.load_file(resolve_theme_path(from))
39
+ end
40
+ end
41
+
42
+ def themes
43
+ @themes ||= superclass.respond_to?(:themes) ? superclass.themes.dup : {}
44
+ end
45
+
46
+ def default_theme(name = THEME_READER)
47
+ return @default_theme || themes.keys.first if name == THEME_READER
48
+
49
+ @default_theme = name.to_sym
50
+ end
51
+
52
+ def theme_for(name = nil)
53
+ theme_name = name || default_theme
54
+ return UI::Theme.default unless theme_name
55
+
56
+ themes.fetch(theme_name.to_sym)
57
+ end
58
+
59
+ private
60
+
61
+ def resolve_theme_path(path)
62
+ return path if File.absolute_path?(path)
63
+
64
+ File.expand_path(path, root || Dir.pwd)
65
+ end
66
+ end
67
+
68
+ attr_accessor :task_executor
69
+ attr_reader :session
70
+
71
+ # Initializes an empty session hash for per-request state storage.
72
+ def initialize
73
+ @session = {}
74
+ end
75
+
76
+ # Delegates to the class-level Router, providing instance access to route definitions.
77
+ def routes
78
+ self.class.routes
79
+ end
80
+
81
+ def theme
82
+ self.class.theme_for(session[:theme])
83
+ end
84
+
85
+ def use_theme(name)
86
+ self.class.theme_for(name)
87
+ session[:theme] = name.to_sym
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module Charming
6
+ # ApplicationModel is the persistent state base for application data models. It includes
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
10
+ include ActiveModel::Model
11
+ include ActiveModel::Attributes
12
+ end
13
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Charming
4
+ class CLI
5
+ def initialize(out: $stdout, err: $stderr, pwd: Dir.pwd)
6
+ @out = out
7
+ @err = err
8
+ @pwd = pwd
9
+ end
10
+
11
+ def call(argv)
12
+ command, *args = argv
13
+ case command
14
+ when "new" then new_app(args)
15
+ when "generate", "g" then generate(args)
16
+ else usage(1)
17
+ end
18
+ rescue Generators::Error => e
19
+ err.puts e.message
20
+ 1
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :out, :err, :pwd
26
+
27
+ def new_app(args)
28
+ 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
31
+ 0
32
+ end
33
+
34
+ def generate(args)
35
+ force = args.delete("--force")
36
+ type = args.shift || raise(Generators::Error, "Usage: charming generate TYPE NAME [actions]")
37
+ generator(type, args, force).generate
38
+ 0
39
+ end
40
+
41
+ def generator(type, args, force)
42
+ name = args.shift || raise(Generators::Error, "Usage: charming generate #{type} NAME")
43
+ generator_class(type).new(name, args, out: out, destination: pwd, force: force)
44
+ end
45
+
46
+ def generator_class(type)
47
+ {
48
+ "controller" => Generators::ControllerGenerator,
49
+ "screen" => Generators::ScreenGenerator,
50
+ "view" => Generators::ViewGenerator,
51
+ "component" => Generators::ComponentGenerator
52
+ }.fetch(type) { raise Generators::Error, "Unknown generator: #{type}" }
53
+ end
54
+
55
+ def usage(status)
56
+ err.puts "Usage: charming new NAME | charming generate TYPE NAME [actions]"
57
+ status
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,8 @@
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