charming 0.2.2 → 0.2.3
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 +15 -2
- data/lib/charming/cli.rb +16 -4
- data/lib/charming/controller.rb +1 -1
- data/lib/charming/generators/app_file_generator.rb +29 -0
- data/lib/charming/generators/app_generator.rb +12 -25
- data/lib/charming/generators/base.rb +5 -0
- data/lib/charming/generators/layout_generator.rb +155 -0
- data/lib/charming/generators/screen_generator.rb +0 -29
- data/lib/charming/generators/templates/app/README.md.template +15 -1
- data/lib/charming/generators/templates/app/application.template +0 -6
- data/lib/charming/generators/templates/app/application_controller.template +0 -10
- data/lib/charming/generators/templates/app/layout.template +4 -93
- data/lib/charming/generators/templates/app/routes.template +3 -1
- data/lib/charming/generators/templates/layout/sidebar/application_layout.rb.template +110 -0
- data/lib/charming/image/protocol/kitty.rb +7 -0
- data/lib/charming/image/source.rb +10 -0
- data/lib/charming/presentation/components/autocomplete.rb +7 -0
- data/lib/charming/presentation/components/form/field.rb +5 -0
- data/lib/charming/presentation/components/form/input.rb +14 -4
- data/lib/charming/presentation/components/form/textarea.rb +18 -8
- data/lib/charming/presentation/components/form.rb +6 -0
- data/lib/charming/presentation/components/modal.rb +4 -4
- data/lib/charming/presentation/components/stopwatch.rb +1 -1
- data/lib/charming/presentation/components/text_area.rb +2 -2
- data/lib/charming/presentation/components/text_input.rb +2 -2
- data/lib/charming/presentation/components/timer.rb +1 -1
- data/lib/charming/presentation/components/toast.rb +4 -3
- data/lib/charming/presentation/components/viewport.rb +11 -1
- data/lib/charming/runtime.rb +13 -2
- data/lib/charming/version.rb +1 -1
- data/lib/charming/welcome/controller.rb +23 -0
- data/lib/charming/welcome/show_view.rb +113 -0
- data/lib/charming/welcome.rb +13 -0
- metadata +6 -7
- data/lib/charming/generators/templates/app/home_controller.template +0 -6
- data/lib/charming/generators/templates/app/home_state.template +0 -7
- data/lib/charming/generators/templates/app/spec_controller.template +0 -17
- data/lib/charming/generators/templates/app/spec_state.template +0 -17
- data/lib/charming/generators/templates/app/spec_view.template +0 -16
- data/lib/charming/generators/templates/app/view.template +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 651009e81e5bb2eb02b2df432b5e399773150691413f2519a6888160b60bd1ca
|
|
4
|
+
data.tar.gz: 8aa3bf4aebc1abab7890eec9b68791d4ed0fd354cffb982cc40f2cf4f261f70c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4276dd6ef588077877b4d4d44758a9c176161fccddd22a5d74161f8615ee38f3c8078872a0ccbca6c8d73e83c994f7e0221f45f75348b9b81bef507a35ff491
|
|
7
|
+
data.tar.gz: 595008aad5caa6c524e872593b8554b588c8018592aca1ead16820200c31dec67cb1189e338658e902abafa9033612780441ed71bb57bcdb8f296b8f03bbfecc
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Charming
|
|
2
2
|
|
|
3
|
+
[](https://github.com/pandorocks/charming/actions/workflows/main.yml)
|
|
4
|
+
|
|
3
5
|
A Rails-inspired terminal user interface framework for **Ruby 4+**.
|
|
4
6
|
|
|
5
7
|
Charming gives terminal apps familiar application structure: routes, controllers, state objects, templates, layouts, reusable components, themes, keyboard bindings, command palettes, timers, background tasks, cross-platform audio playback, inline image display (Kitty graphics protocol), system clipboard / desktop notifications / window title, braille charts and sparklines, and testable terminal backends.
|
|
@@ -39,7 +41,7 @@ The generator produces a Bundler gem with a Rails-like structure:
|
|
|
39
41
|
app/controllers/ # controller actions and input bindings
|
|
40
42
|
app/state/ # session-backed TUI state
|
|
41
43
|
app/models/ # optional Active Record models
|
|
42
|
-
app/views/
|
|
44
|
+
app/views/ # screen view classes
|
|
43
45
|
app/views/layouts/application_layout.rb # layout view class
|
|
44
46
|
app/components/ # reusable components
|
|
45
47
|
config/routes.rb # route definitions
|
|
@@ -47,7 +49,17 @@ lib/my_app.rb # namespace loader (Zeitwerk)
|
|
|
47
49
|
exe/my_app # executable entry point
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
Generated apps
|
|
52
|
+
Generated apps start minimal. Until the app defines a route, it boots to a built-in welcome screen served from the gem itself (like Rails' welcome page) — generate your first screen and it disappears, with nothing to delete:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bundle exec charming generate screen home
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Restore the full app chrome — sidebar/content layout, command palette (`ctrl+p`), focus management, and theme switching — with one command:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bundle exec charming generate layout --style sidebar
|
|
62
|
+
```
|
|
51
63
|
|
|
52
64
|
## Development
|
|
53
65
|
|
|
@@ -65,4 +77,5 @@ bin/rspec # run specs only
|
|
|
65
77
|
bin/format # auto-format with Standard Ruby
|
|
66
78
|
bin/lint # style checks with Standard Ruby
|
|
67
79
|
bin/check # run everything
|
|
80
|
+
bin/ci # what CI runs: specs, lint, eager-load check, gem build
|
|
68
81
|
```
|
data/lib/charming/cli.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Charming
|
|
|
5
5
|
# or database commands. Subcommands:
|
|
6
6
|
# - `charming new NAME [--database sqlite3] [--force]` — scaffolds a new app
|
|
7
7
|
# - `charming generate TYPE NAME [args]` — runs a sub-generator (controller, model, screen, view, component)
|
|
8
|
+
# - `charming generate layout [--style sidebar]` — restores the sidebar/theme/palette app chrome
|
|
8
9
|
# - `charming db:COMMAND` — runs a database command (db:create, db:migrate, db:rollback, db:drop, db:seed, db:install)
|
|
9
10
|
#
|
|
10
11
|
# Generator errors are caught and printed to stderr; the process exits with status 1.
|
|
@@ -58,14 +59,24 @@ module Charming
|
|
|
58
59
|
0
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
# Generator types that take no positional NAME argument.
|
|
63
|
+
NAMELESS_GENERATORS = %w[layout].freeze
|
|
64
|
+
|
|
61
65
|
# Builds the generator instance for the given *type*, popping the name from *args*.
|
|
62
66
|
def generator(type, args, force)
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
generator_class(type).new(generator_name(type, args), args, out: out, destination: pwd, force: force)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Pops the NAME argument for a generator *type*, or supplies a fixed name for
|
|
71
|
+
# nameless generators (e.g., layout targets the application layout).
|
|
72
|
+
def generator_name(type, args)
|
|
73
|
+
return "application" if NAMELESS_GENERATORS.include?(type)
|
|
74
|
+
|
|
75
|
+
args.shift || raise(Generators::Error, "Usage: charming generate #{type} NAME")
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
# Returns the generator class for a *type* string (controller, model, screen, view,
|
|
68
|
-
# component, migration).
|
|
79
|
+
# component, migration, layout).
|
|
69
80
|
def generator_class(type)
|
|
70
81
|
{
|
|
71
82
|
"controller" => Generators::ControllerGenerator,
|
|
@@ -73,7 +84,8 @@ module Charming
|
|
|
73
84
|
"screen" => Generators::ScreenGenerator,
|
|
74
85
|
"view" => Generators::ViewGenerator,
|
|
75
86
|
"component" => Generators::ComponentGenerator,
|
|
76
|
-
"migration" => Generators::MigrationGenerator
|
|
87
|
+
"migration" => Generators::MigrationGenerator,
|
|
88
|
+
"layout" => Generators::LayoutGenerator
|
|
77
89
|
}.fetch(type) { raise Generators::Error, "Unknown generator: #{type}" }
|
|
78
90
|
end
|
|
79
91
|
|
data/lib/charming/controller.rb
CHANGED
|
@@ -71,7 +71,7 @@ module Charming
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Paste event dispatcher: forwards pasted text to the focused component's
|
|
74
|
-
# `handle_paste` (TextInput, TextArea,
|
|
74
|
+
# `handle_paste` (TextInput, TextArea, Form text fields, and Autocomplete support it).
|
|
75
75
|
def dispatch_paste
|
|
76
76
|
slot = focus.current
|
|
77
77
|
return nil unless slot && respond_to?(slot, true)
|
|
@@ -36,6 +36,35 @@ module Charming
|
|
|
36
36
|
|
|
37
37
|
File.basename(gemspec, ".gemspec")
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
# Inserts *content* into *path* just before the line matching *end_line*. No-ops when
|
|
41
|
+
# the content is already present. Raises Error when the file or end-line is missing.
|
|
42
|
+
def insert_before_end(path, content, label, end_line)
|
|
43
|
+
raise Error, "Missing file: #{relative_path(path)}" unless File.exist?(path)
|
|
44
|
+
|
|
45
|
+
current = File.read(path)
|
|
46
|
+
return if current.include?(content)
|
|
47
|
+
|
|
48
|
+
lines = current.lines
|
|
49
|
+
index = insertion_index(lines, path, end_line)
|
|
50
|
+
lines.insert(index, "#{content}\n")
|
|
51
|
+
File.write(path, lines.join)
|
|
52
|
+
out.puts "insert #{label} #{relative_path(path)}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns the index of the last line in *lines* that matches *end_line* (the line
|
|
56
|
+
# just before which new content will be inserted). Raises Error when not found.
|
|
57
|
+
def insertion_index(lines, path, end_line)
|
|
58
|
+
index = lines.rindex { |line| line.chomp == end_line }
|
|
59
|
+
raise Error, "Could not update #{relative_path(path)}" unless index
|
|
60
|
+
|
|
61
|
+
index
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Strips the destination prefix from *path* for human-friendly status output.
|
|
65
|
+
def relative_path(path)
|
|
66
|
+
path.delete_prefix("#{destination}/")
|
|
67
|
+
end
|
|
39
68
|
end
|
|
40
69
|
end
|
|
41
70
|
end
|
|
@@ -22,16 +22,10 @@ module Charming
|
|
|
22
22
|
["lib/%<name>s/version.rb", "app/version.template", false],
|
|
23
23
|
["config/routes.rb", "app/routes.template", false],
|
|
24
24
|
["app/state/application_state.rb", "app/application_state.template", false],
|
|
25
|
-
["app/state/home_state.rb", "app/home_state.template", false],
|
|
26
25
|
["app/controllers/application_controller.rb", "app/application_controller.template", false],
|
|
27
|
-
["app/controllers/home_controller.rb", "app/home_controller.template", false],
|
|
28
26
|
["app/views/layouts/application_layout.rb", "app/layout.template", false],
|
|
29
|
-
["app/views/home/show_view.rb", "app/view.template", false],
|
|
30
27
|
["app/components/.keep", "app/keep.template", false],
|
|
31
|
-
["spec/spec_helper.rb", "app/spec_helper.template", false]
|
|
32
|
-
["spec/state/home_state_spec.rb", "app/spec_state.template", false],
|
|
33
|
-
["spec/controllers/home_controller_spec.rb", "app/spec_controller.template", false],
|
|
34
|
-
["spec/views/home/show_view_spec.rb", "app/spec_view.template", false]
|
|
28
|
+
["spec/spec_helper.rb", "app/spec_helper.template", false]
|
|
35
29
|
].freeze
|
|
36
30
|
|
|
37
31
|
# The list of [relative-path, template-path, executable-flag] triples to render in
|
|
@@ -96,15 +90,23 @@ module Charming
|
|
|
96
90
|
app_class: name.class_name,
|
|
97
91
|
gemspec_attributes: gemspec_attributes,
|
|
98
92
|
gemspec_dependencies: gemspec_dependencies,
|
|
99
|
-
controller_actions: controller_actions,
|
|
100
|
-
controller_helpers: controller_helpers,
|
|
101
93
|
database_require: database_require,
|
|
102
94
|
model_loader: model_loader,
|
|
103
95
|
env_setup: env_setup,
|
|
104
|
-
database_spec_setup: database_spec_setup
|
|
96
|
+
database_spec_setup: database_spec_setup,
|
|
97
|
+
database_setup: database_setup
|
|
105
98
|
}
|
|
106
99
|
end
|
|
107
100
|
|
|
101
|
+
# README instructions for preparing the database before first run. Empty for
|
|
102
|
+
# non-database apps.
|
|
103
|
+
def database_setup
|
|
104
|
+
return "" unless database?
|
|
105
|
+
|
|
106
|
+
"Set up the database (create + load schema + seed):\n\n" \
|
|
107
|
+
"```sh\nbundle exec charming db:setup\n```\n\n"
|
|
108
|
+
end
|
|
109
|
+
|
|
108
110
|
# Pins CHARMING_ENV to "test" before the app (and its database config) loads, so
|
|
109
111
|
# specs hit db/test.sqlite3. Empty for non-database apps.
|
|
110
112
|
def env_setup
|
|
@@ -175,21 +177,6 @@ module Charming
|
|
|
175
177
|
" spec.add_dependency \"sqlite3\", \"~> 2.0\""
|
|
176
178
|
end
|
|
177
179
|
|
|
178
|
-
# The body of the home controller's `show` action.
|
|
179
|
-
def controller_actions
|
|
180
|
-
"\n def show\n" \
|
|
181
|
-
" render :show, home: home, palette: command_palette\n" \
|
|
182
|
-
" end"
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# The body of the home controller's private `home` helper, prefixed by a blank line.
|
|
186
|
-
def controller_helpers
|
|
187
|
-
"\n\n private\n\n" \
|
|
188
|
-
" def home\n" \
|
|
189
|
-
" state(:home, HomeState)\n" \
|
|
190
|
-
" end\n"
|
|
191
|
-
end
|
|
192
|
-
|
|
193
180
|
# The `require_relative "../config/database"` line when the app is database-configured.
|
|
194
181
|
def database_require
|
|
195
182
|
database? ? "require_relative \"../config/database\"" : ""
|
|
@@ -22,6 +22,11 @@ module Charming
|
|
|
22
22
|
# Status output stream and destination directory accessor (subclasses use these).
|
|
23
23
|
attr_reader :out, :destination
|
|
24
24
|
|
|
25
|
+
# True when overwriting existing files was requested (`--force`).
|
|
26
|
+
def force?
|
|
27
|
+
@force
|
|
28
|
+
end
|
|
29
|
+
|
|
25
30
|
# Writes *content* to *path* (relative to the destination), creating intermediate
|
|
26
31
|
# directories as needed. Raises Generators::Error when the file already exists and
|
|
27
32
|
# *force* is false. Marks the file as executable when *executable:* is true.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
# LayoutGenerator implements `charming generate layout [--style sidebar]`. It restores
|
|
6
|
+
# the full app chrome that `charming new` no longer ships by default: the sidebar
|
|
7
|
+
# layout, the sidebar/content focus ring, the command palette bindings, and the
|
|
8
|
+
# built-in theme registration. Safe to re-run: existing chrome is left untouched.
|
|
9
|
+
class LayoutGenerator < AppFileGenerator
|
|
10
|
+
STYLES = %w[sidebar].freeze
|
|
11
|
+
USAGE = "Usage: charming generate layout [--style sidebar]"
|
|
12
|
+
|
|
13
|
+
# *name* is unused (the CLI passes "application"); *args* may carry `--style STYLE`.
|
|
14
|
+
def initialize(name, args, out:, destination:, force: false)
|
|
15
|
+
@style = extract_style(args)
|
|
16
|
+
super
|
|
17
|
+
raise Error, USAGE if args.any?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Writes the styled application layout, then inserts the controller chrome and the
|
|
21
|
+
# theme registration into the existing app files.
|
|
22
|
+
def generate
|
|
23
|
+
write_layout
|
|
24
|
+
insert_controller_chrome
|
|
25
|
+
insert_theme_registration
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# The layout style name (currently always "sidebar").
|
|
31
|
+
attr_reader :style
|
|
32
|
+
|
|
33
|
+
# The file-name suffix used by `app_path` (only used by the parent class).
|
|
34
|
+
def suffix
|
|
35
|
+
"layout"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Extracts the optional `--style` argument from *args*, removing it in place.
|
|
39
|
+
# Defaults to the first supported style when absent.
|
|
40
|
+
def extract_style(args)
|
|
41
|
+
inline = args.find { |arg| arg.start_with?("--style=") }
|
|
42
|
+
return validate_style(args.delete(inline).split("=", 2).last) if inline
|
|
43
|
+
|
|
44
|
+
index = args.index("--style")
|
|
45
|
+
return STYLES.first unless index
|
|
46
|
+
|
|
47
|
+
args.delete_at(index)
|
|
48
|
+
validate_style(args.delete_at(index) || raise(Error, USAGE))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Validates that *style* is a supported layout style.
|
|
52
|
+
def validate_style(style)
|
|
53
|
+
return style if STYLES.include?(style)
|
|
54
|
+
|
|
55
|
+
raise Error, "Unknown layout style: #{style.inspect} (available: #{STYLES.join(", ")})"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Replaces the application layout with the styled one. Creates it when missing,
|
|
59
|
+
# no-ops when already styled, and refuses to clobber local edits unless forced.
|
|
60
|
+
def write_layout
|
|
61
|
+
return create_file(layout_relative_path, styled_layout) unless File.exist?(layout_path)
|
|
62
|
+
return out.puts("identical #{layout_relative_path}") if current_layout == styled_layout
|
|
63
|
+
raise Error, "#{layout_relative_path} has local changes; re-run with --force to overwrite" unless replaceable_layout?
|
|
64
|
+
|
|
65
|
+
overwrite_layout
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# True when the current layout may be replaced: forced, or still the generated bare layout.
|
|
69
|
+
def replaceable_layout?
|
|
70
|
+
force? || current_layout == bare_layout
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def overwrite_layout
|
|
74
|
+
File.write(layout_path, styled_layout)
|
|
75
|
+
out.puts "overwrite #{layout_relative_path}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def current_layout
|
|
79
|
+
File.read(layout_path)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The rendered layout for the requested style.
|
|
83
|
+
def styled_layout
|
|
84
|
+
@styled_layout ||= render_template("layout/#{style}/application_layout.rb.template",
|
|
85
|
+
app_class: app_name.class_name,
|
|
86
|
+
app_name: app_name.class_name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The bare layout `charming new` generates, used to recognize unmodified apps.
|
|
90
|
+
def bare_layout
|
|
91
|
+
render_template("app/layout.template", app_class: app_name.class_name)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Inserts the focus ring, palette key binding, and palette commands into
|
|
95
|
+
# `ApplicationController`, idempotently.
|
|
96
|
+
def insert_controller_chrome
|
|
97
|
+
insert_before_end(application_controller_path, controller_chrome, "chrome", " end")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def controller_chrome
|
|
101
|
+
class_body_block(<<~RUBY)
|
|
102
|
+
focus_ring :sidebar, :content
|
|
103
|
+
|
|
104
|
+
key "ctrl+p", :open_command_palette, scope: :global
|
|
105
|
+
|
|
106
|
+
command "Home" do
|
|
107
|
+
navigate_to "/"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
command "Theme", :open_theme_palette
|
|
111
|
+
command "Close palette", :close_command_palette
|
|
112
|
+
command "Quit app", :quit
|
|
113
|
+
RUBY
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Inserts the built-in theme registration into the app's Application class, idempotently.
|
|
117
|
+
def insert_theme_registration
|
|
118
|
+
insert_before_end(application_config_path, theme_registration, "themes", " end")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def theme_registration
|
|
122
|
+
class_body_block(<<~RUBY)
|
|
123
|
+
Charming::UI::Theme.built_in_names.each do |theme_name|
|
|
124
|
+
theme theme_name.to_sym, built_in: theme_name
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
default_theme :phosphor
|
|
128
|
+
RUBY
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Indents *source* to class-body depth and prefixes a blank separator line, so the
|
|
132
|
+
# inserted block reads naturally after the existing declarations.
|
|
133
|
+
def class_body_block(source)
|
|
134
|
+
body = source.gsub(/^/, " ").gsub(/^ +$/, "").chomp
|
|
135
|
+
"\n#{body}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def layout_relative_path
|
|
139
|
+
File.join("app", "views", "layouts", "application_layout.rb")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def layout_path
|
|
143
|
+
File.join(destination, layout_relative_path)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def application_controller_path
|
|
147
|
+
File.join(destination, "app", "controllers", "application_controller.rb")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def application_config_path
|
|
151
|
+
File.join(destination, "lib", app_name.snake_name, "application.rb")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -152,35 +152,6 @@ module Charming
|
|
|
152
152
|
end)
|
|
153
153
|
insert_before_end(application_controller_path, command, "command", " end")
|
|
154
154
|
end
|
|
155
|
-
|
|
156
|
-
# Inserts *content* into *path* just before the line matching *end_line*. No-ops when
|
|
157
|
-
# the content is already present. Raises Error when the file or end-line is missing.
|
|
158
|
-
def insert_before_end(path, content, label, end_line)
|
|
159
|
-
raise Error, "Missing file: #{relative_path(path)}" unless File.exist?(path)
|
|
160
|
-
|
|
161
|
-
current = File.read(path)
|
|
162
|
-
return if current.include?(content)
|
|
163
|
-
|
|
164
|
-
lines = current.lines
|
|
165
|
-
index = insertion_index(lines, path, end_line)
|
|
166
|
-
lines.insert(index, "#{content}\n")
|
|
167
|
-
File.write(path, lines.join)
|
|
168
|
-
out.puts "insert #{label} #{relative_path(path)}"
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Returns the index of the last line in *lines* that matches *end_line* (the line
|
|
172
|
-
# just before which new content will be inserted). Raises Error when not found.
|
|
173
|
-
def insertion_index(lines, path, end_line)
|
|
174
|
-
index = lines.rindex { |line| line.chomp == end_line }
|
|
175
|
-
raise Error, "Could not update #{relative_path(path)}" unless index
|
|
176
|
-
|
|
177
|
-
index
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Strips the destination prefix from *path* for human-friendly status output.
|
|
181
|
-
def relative_path(path)
|
|
182
|
-
path.delete_prefix("#{destination}/")
|
|
183
|
-
end
|
|
184
155
|
end
|
|
185
156
|
end
|
|
186
157
|
end
|
|
@@ -2,8 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
A Charming terminal user interface.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
__DATABASE_SETUP__Run it with:
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
bundle exec __APP_SNAKE__
|
|
9
9
|
```
|
|
10
|
+
|
|
11
|
+
Until a route is defined the app boots to Charming's built-in welcome screen. Generate
|
|
12
|
+
your first screen (and make it home with `root` in `config/routes.rb`):
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bundle exec charming generate screen home
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Add the sidebar layout, command palette (`ctrl+p`), and theme switching (screens generated
|
|
19
|
+
with `charming generate screen` are reached through the command palette):
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
bundle exec charming generate layout --style sidebar
|
|
23
|
+
```
|
|
@@ -3,11 +3,5 @@
|
|
|
3
3
|
module __APP_CLASS__
|
|
4
4
|
class Application < Charming::Application
|
|
5
5
|
root File.expand_path("../..", __dir__)
|
|
6
|
-
|
|
7
|
-
Charming::UI::Theme.built_in_names.each do |theme_name|
|
|
8
|
-
theme theme_name.to_sym, built_in: theme_name
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
default_theme :phosphor
|
|
12
6
|
end
|
|
13
7
|
end
|
|
@@ -3,17 +3,7 @@
|
|
|
3
3
|
module __APP_CLASS__
|
|
4
4
|
class ApplicationController < Charming::Controller
|
|
5
5
|
layout Layouts::ApplicationLayout
|
|
6
|
-
focus_ring :sidebar, :content
|
|
7
6
|
|
|
8
|
-
key "ctrl+p", :open_command_palette, scope: :global
|
|
9
7
|
key "q", :quit, scope: :global
|
|
10
|
-
|
|
11
|
-
command "Home" do
|
|
12
|
-
navigate_to "/"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
command "Theme", :open_theme_palette
|
|
16
|
-
command "Close palette", :close_command_palette
|
|
17
|
-
command "Quit app", :quit
|
|
18
8
|
end
|
|
19
9
|
end
|
|
@@ -5,105 +5,16 @@ module __APP_CLASS__
|
|
|
5
5
|
class ApplicationLayout < Charming::View
|
|
6
6
|
def render
|
|
7
7
|
screen_layout(background: theme.background) do
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
column(app_title, navigation, shortcuts, gap: 1)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
pane(:content, grow: 1, border: :rounded, padding: [1, 2], style: content_style) do
|
|
14
|
-
yield_content
|
|
15
|
-
end
|
|
8
|
+
pane(style: centered) do
|
|
9
|
+
yield_content
|
|
16
10
|
end
|
|
17
|
-
|
|
18
|
-
overlay command_palette_modal if command_palette_modal
|
|
19
11
|
end
|
|
20
12
|
end
|
|
21
13
|
|
|
22
14
|
private
|
|
23
15
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def narrow?
|
|
29
|
-
screen.narrow?(below: 72, min_height: 20)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def sidebar_options
|
|
33
|
-
narrow? ? {height: [screen.height / 3, 5].max} : {width: 22}
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def sidebar_inner_width
|
|
37
|
-
narrow? ? [screen.width - 6, 20].max : 16
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def app_title
|
|
41
|
-
text "__APP_NAME__", style: theme.header_accent.align(:center).width(sidebar_inner_width)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def navigation
|
|
45
|
-
column(*nav_items)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def nav_items
|
|
49
|
-
controller.sidebar_routes.each_with_index.map do |route, index|
|
|
50
|
-
text nav_item_label(route, index), style: nav_item_style(route, index)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def nav_item_label(route, index)
|
|
55
|
-
cursor = (sidebar_focused? && index == sidebar_index) ? ">" : " "
|
|
56
|
-
active = current_route?(route) ? "\u{25cf}" : " "
|
|
57
|
-
"#{cursor} #{active} #{route.title}"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def nav_item_style(route, index)
|
|
61
|
-
if sidebar_focused? && index == sidebar_index
|
|
62
|
-
theme.selected
|
|
63
|
-
elsif current_route?(route)
|
|
64
|
-
theme.title
|
|
65
|
-
else
|
|
66
|
-
theme.muted
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def shortcuts
|
|
71
|
-
text "tab focus\nctrl+p commands\nq quit", style: theme.muted
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def sidebar_style
|
|
75
|
-
focused_style = sidebar_focused? ? theme.title : theme.border
|
|
76
|
-
palette_component ? focused_style.faint : focused_style
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def content_style
|
|
80
|
-
focused_style = content_focused? ? theme.title : theme.border
|
|
81
|
-
palette_component ? focused_style.faint : focused_style
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def command_palette_modal
|
|
85
|
-
return unless palette_component
|
|
86
|
-
|
|
87
|
-
render_component Charming::Components::CommandPaletteModal.new(
|
|
88
|
-
content: palette_component,
|
|
89
|
-
theme: theme
|
|
90
|
-
)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def sidebar_focused?
|
|
94
|
-
controller.sidebar_focused?
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def content_focused?
|
|
98
|
-
controller.content_focused?
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def sidebar_index
|
|
102
|
-
controller.sidebar_index
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def current_route?(route)
|
|
106
|
-
controller.current_route?(route)
|
|
16
|
+
def centered
|
|
17
|
+
style.align(:center).align_vertical(:middle)
|
|
107
18
|
end
|
|
108
19
|
end
|
|
109
20
|
end
|