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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -2
  3. data/lib/charming/cli.rb +16 -4
  4. data/lib/charming/controller.rb +1 -1
  5. data/lib/charming/generators/app_file_generator.rb +29 -0
  6. data/lib/charming/generators/app_generator.rb +12 -25
  7. data/lib/charming/generators/base.rb +5 -0
  8. data/lib/charming/generators/layout_generator.rb +155 -0
  9. data/lib/charming/generators/screen_generator.rb +0 -29
  10. data/lib/charming/generators/templates/app/README.md.template +15 -1
  11. data/lib/charming/generators/templates/app/application.template +0 -6
  12. data/lib/charming/generators/templates/app/application_controller.template +0 -10
  13. data/lib/charming/generators/templates/app/layout.template +4 -93
  14. data/lib/charming/generators/templates/app/routes.template +3 -1
  15. data/lib/charming/generators/templates/layout/sidebar/application_layout.rb.template +110 -0
  16. data/lib/charming/image/protocol/kitty.rb +7 -0
  17. data/lib/charming/image/source.rb +10 -0
  18. data/lib/charming/presentation/components/autocomplete.rb +7 -0
  19. data/lib/charming/presentation/components/form/field.rb +5 -0
  20. data/lib/charming/presentation/components/form/input.rb +14 -4
  21. data/lib/charming/presentation/components/form/textarea.rb +18 -8
  22. data/lib/charming/presentation/components/form.rb +6 -0
  23. data/lib/charming/presentation/components/modal.rb +4 -4
  24. data/lib/charming/presentation/components/stopwatch.rb +1 -1
  25. data/lib/charming/presentation/components/text_area.rb +2 -2
  26. data/lib/charming/presentation/components/text_input.rb +2 -2
  27. data/lib/charming/presentation/components/timer.rb +1 -1
  28. data/lib/charming/presentation/components/toast.rb +4 -3
  29. data/lib/charming/presentation/components/viewport.rb +11 -1
  30. data/lib/charming/runtime.rb +13 -2
  31. data/lib/charming/version.rb +1 -1
  32. data/lib/charming/welcome/controller.rb +23 -0
  33. data/lib/charming/welcome/show_view.rb +113 -0
  34. data/lib/charming/welcome.rb +13 -0
  35. metadata +6 -7
  36. data/lib/charming/generators/templates/app/home_controller.template +0 -6
  37. data/lib/charming/generators/templates/app/home_state.template +0 -7
  38. data/lib/charming/generators/templates/app/spec_controller.template +0 -17
  39. data/lib/charming/generators/templates/app/spec_state.template +0 -17
  40. data/lib/charming/generators/templates/app/spec_view.template +0 -16
  41. 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: 71b370eac77c67126921d7347a0d23f834f23774626c111445e9abdef73555d9
4
- data.tar.gz: c8dd983f46183c2fbdb22f9c30753b4f1443d8de2989a4e7f90016f397aa12e4
3
+ metadata.gz: 651009e81e5bb2eb02b2df432b5e399773150691413f2519a6888160b60bd1ca
4
+ data.tar.gz: 8aa3bf4aebc1abab7890eec9b68791d4ed0fd354cffb982cc40f2cf4f261f70c
5
5
  SHA512:
6
- metadata.gz: 2ee80f1c214001c8f91b36ba308f0e2f67aecbc258e7ff3ce63de2eb04093a405a0a23c3174cac6c3a1c6a98746b5353e66dd8179aebab4fe60ad35132603f19
7
- data.tar.gz: 5242fbc9ad41ca1c0dbebbb070bb33cbdd1279955f81aa2f067954a1b11ffdd1b9cb7b90321285fa17e795c31d3fff32c7e052e95dee7e7c90318e19d252fba1
6
+ metadata.gz: d4276dd6ef588077877b4d4d44758a9c176161fccddd22a5d74161f8615ee38f3c8078872a0ccbca6c8d73e83c994f7e0221f45f75348b9b81bef507a35ff491
7
+ data.tar.gz: 595008aad5caa6c524e872593b8554b588c8018592aca1ead16820200c31dec67cb1189e338658e902abafa9033612780441ed71bb57bcdb8f296b8f03bbfecc
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Charming
2
2
 
3
+ [![CI](https://github.com/pandorocks/charming/actions/workflows/main.yml/badge.svg)](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/home/show_view.rb # screen view classes
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 include a sidebar/content layout, command palette, focus management, theme switching, and default key bindings for commands (`ctrl+p`) and quit (`q`).
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
- name = args.shift || raise(Generators::Error, "Usage: charming generate #{type} NAME")
64
- generator_class(type).new(name, args, out: out, destination: pwd, force: force)
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
 
@@ -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, and form fields support it).
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
- Run it with:
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
- split(narrow? ? :vertical : :horizontal, gap: 1) do
9
- pane(:sidebar, **sidebar_options, border: :rounded, padding: [1, 2], style: sidebar_style) do
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 palette_component
25
- assigns.fetch(:palette, nil)
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  __APP_CLASS__::Application.routes do
4
- root "home#show"
4
+ # Generate your first screen with `charming generate screen NAME`, then make it home:
5
+ #
6
+ # root "home#show"
5
7
  end