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.
- checksums.yaml +4 -4
- data/README.md +38 -378
- data/lib/charming/application.rb +3 -3
- data/lib/charming/{application_model.rb → application_state.rb} +3 -3
- data/lib/charming/cli.rb +39 -3
- data/lib/charming/controller.rb +146 -24
- data/lib/charming/database_commands.rb +87 -0
- data/lib/charming/database_installer.rb +125 -0
- data/lib/charming/events/key_event.rb +15 -0
- data/lib/charming/events/mouse_event.rb +42 -0
- data/lib/charming/events/resize_event.rb +9 -0
- data/lib/charming/events/task_event.rb +19 -0
- data/lib/charming/events/timer_event.rb +9 -0
- data/lib/charming/generators/app_generator/app_spec_templates.rb +12 -8
- data/lib/charming/generators/app_generator/basic_templates.rb +14 -2
- data/lib/charming/generators/app_generator/component_templates.rb +1 -1
- data/lib/charming/generators/app_generator/controller_template.rb +3 -12
- data/lib/charming/generators/app_generator/database_templates.rb +45 -0
- data/lib/charming/generators/app_generator/layout_template.rb +51 -145
- data/lib/charming/generators/app_generator/screen_spec_templates.rb +7 -8
- data/lib/charming/generators/app_generator/{model_templates.rb → state_templates.rb} +5 -5
- data/lib/charming/generators/app_generator/view_template.rb +12 -18
- data/lib/charming/generators/app_generator.rb +37 -11
- data/lib/charming/generators/component_generator.rb +1 -1
- data/lib/charming/generators/controller_generator.rb +1 -4
- data/lib/charming/generators/model_generator.rb +119 -0
- data/lib/charming/generators/name.rb +0 -4
- data/lib/charming/generators/screen_generator.rb +14 -28
- data/lib/charming/generators/view_generator.rb +11 -14
- data/lib/charming/internal/renderer/differential.rb +2 -3
- data/lib/charming/internal/terminal/tty_backend.rb +25 -8
- data/lib/charming/presentation/component.rb +10 -0
- data/lib/charming/presentation/components/activity_indicator.rb +160 -0
- data/lib/charming/presentation/components/command_palette.rb +120 -0
- data/lib/charming/presentation/components/empty_state.rb +43 -0
- data/lib/charming/presentation/components/form/builder.rb +48 -0
- data/lib/charming/presentation/components/form/confirm.rb +56 -0
- data/lib/charming/presentation/components/form/field.rb +96 -0
- data/lib/charming/presentation/components/form/input.rb +57 -0
- data/lib/charming/presentation/components/form/note.rb +32 -0
- data/lib/charming/presentation/components/form/select.rb +89 -0
- data/lib/charming/presentation/components/form/textarea.rb +70 -0
- data/lib/charming/presentation/components/form.rb +127 -0
- data/lib/charming/presentation/components/keyboard_handler.rb +58 -0
- data/lib/charming/presentation/components/list.rb +104 -0
- data/lib/charming/presentation/components/markdown.rb +25 -0
- data/lib/charming/presentation/components/modal.rb +50 -0
- data/lib/charming/presentation/components/progressbar.rb +57 -0
- data/lib/charming/presentation/components/spinner.rb +39 -0
- data/lib/charming/presentation/components/table.rb +118 -0
- data/lib/charming/presentation/components/text_area.rb +219 -0
- data/lib/charming/presentation/components/text_input.rb +105 -0
- data/lib/charming/presentation/components/viewport.rb +220 -0
- data/lib/charming/presentation/layout.rb +43 -0
- data/lib/charming/presentation/markdown/renderer.rb +203 -0
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +63 -0
- data/lib/charming/presentation/markdown.rb +8 -0
- data/lib/charming/presentation/template_view.rb +27 -0
- data/lib/charming/presentation/templates/erb_handler.rb +15 -0
- data/lib/charming/presentation/templates.rb +51 -0
- data/lib/charming/presentation/ui/border.rb +35 -0
- data/lib/charming/presentation/ui/style.rb +246 -0
- data/lib/charming/presentation/ui/theme.rb +180 -0
- data/lib/charming/{ui → presentation/ui}/themes/phosphor.json +2 -2
- data/lib/charming/presentation/ui/width.rb +26 -0
- data/lib/charming/presentation/ui.rb +232 -0
- data/lib/charming/presentation/view.rb +118 -0
- data/lib/charming/runtime.rb +7 -7
- data/lib/charming/screen.rb +5 -1
- data/lib/charming/tasks/inline_executor.rb +28 -0
- data/lib/charming/tasks/task.rb +9 -0
- data/lib/charming/{task_executor.rb → tasks/threaded_executor.rb} +4 -27
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +4 -0
- metadata +114 -29
- data/lib/charming/component.rb +0 -8
- data/lib/charming/components/activity_indicator.rb +0 -158
- data/lib/charming/components/command_palette.rb +0 -118
- data/lib/charming/components/keyboard_handler.rb +0 -22
- data/lib/charming/components/list.rb +0 -105
- data/lib/charming/components/modal.rb +0 -48
- data/lib/charming/components/progressbar.rb +0 -55
- data/lib/charming/components/spinner.rb +0 -37
- data/lib/charming/components/table.rb +0 -115
- data/lib/charming/components/text_input.rb +0 -103
- data/lib/charming/components/viewport.rb +0 -191
- data/lib/charming/key_event.rb +0 -13
- data/lib/charming/mouse_event.rb +0 -40
- data/lib/charming/resize_event.rb +0 -7
- data/lib/charming/task.rb +0 -7
- data/lib/charming/task_event.rb +0 -17
- data/lib/charming/timer_event.rb +0 -7
- data/lib/charming/ui/border.rb +0 -33
- data/lib/charming/ui/style.rb +0 -244
- data/lib/charming/ui/theme.rb +0 -178
- data/lib/charming/ui/width.rb +0 -24
- data/lib/charming/ui.rb +0 -230
- data/lib/charming/view.rb +0 -116
- /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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 739b00d7bbbe867e98ec93bc614157e59650eb5ecd45a48127139fb5ea11adb1
|
|
4
|
+
data.tar.gz: 8c7008f8bcd6eba1464d01e317c23a37d93a47cf272cc617295c5a4f6f50e379
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
key "up", :increment
|
|
14
|
-
key "down", :decrement
|
|
15
|
-
key "q", :quit, scope: :global
|
|
7
|
+
## Project Status
|
|
16
8
|
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
counter.count += 1
|
|
23
|
-
show
|
|
24
|
-
end
|
|
11
|
+
## Quick Start
|
|
25
12
|
|
|
26
|
-
|
|
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
|
-
|
|
16
|
+
gem install charming
|
|
55
17
|
```
|
|
56
18
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
#
|
|
223
|
-
#
|
|
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
|
-
|
|
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
|
|
71
|
+
bin/check
|
|
412
72
|
```
|
|
413
73
|
|
|
414
74
|
Common binstubs:
|
data/lib/charming/application.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
9
|
-
class
|
|
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
|
-
|
|
30
|
-
|
|
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 [
|
|
92
|
+
err.puts "Usage: charming new NAME | charming generate TYPE NAME [args] | charming db:COMMAND"
|
|
57
93
|
status
|
|
58
94
|
end
|
|
59
95
|
end
|