charming 0.2.1 → 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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -3
  3. data/lib/charming/application.rb +48 -0
  4. data/lib/charming/cli.rb +16 -4
  5. data/lib/charming/controller/key_dispatch.rb +113 -0
  6. data/lib/charming/controller/session_state.rb +11 -0
  7. data/lib/charming/controller/terminal.rb +33 -0
  8. data/lib/charming/controller.rb +13 -40
  9. data/lib/charming/escape.rb +81 -0
  10. data/lib/charming/events/mouse_event.rb +22 -9
  11. data/lib/charming/generators/app_file_generator.rb +29 -0
  12. data/lib/charming/generators/app_generator.rb +13 -25
  13. data/lib/charming/generators/base.rb +5 -0
  14. data/lib/charming/generators/layout_generator.rb +155 -0
  15. data/lib/charming/generators/screen_generator.rb +0 -29
  16. data/lib/charming/generators/templates/app/Gemfile.template +6 -0
  17. data/lib/charming/generators/templates/app/README.md.template +15 -1
  18. data/lib/charming/generators/templates/app/application.template +0 -6
  19. data/lib/charming/generators/templates/app/application_controller.template +0 -10
  20. data/lib/charming/generators/templates/app/dot_rspec.template +2 -0
  21. data/lib/charming/generators/templates/app/layout.template +4 -93
  22. data/lib/charming/generators/templates/app/routes.template +3 -1
  23. data/lib/charming/generators/templates/layout/sidebar/application_layout.rb.template +110 -0
  24. data/lib/charming/image/protocol/kitty.rb +133 -0
  25. data/lib/charming/image/protocol.rb +18 -0
  26. data/lib/charming/image/source.rb +95 -0
  27. data/lib/charming/image/terminal.rb +52 -0
  28. data/lib/charming/image/transmit.rb +11 -0
  29. data/lib/charming/image.rb +21 -0
  30. data/lib/charming/internal/event_loop.rb +155 -0
  31. data/lib/charming/internal/terminal/adapter.rb +14 -0
  32. data/lib/charming/internal/terminal/key_normalizer.rb +20 -0
  33. data/lib/charming/internal/terminal/memory_backend.rb +21 -3
  34. data/lib/charming/internal/terminal/modified_key_parser.rb +63 -0
  35. data/lib/charming/internal/terminal/mouse_parser.rb +32 -27
  36. data/lib/charming/internal/terminal/tty_backend.rb +79 -2
  37. data/lib/charming/presentation/components/activity_indicator.rb +2 -19
  38. data/lib/charming/presentation/components/autocomplete.rb +7 -0
  39. data/lib/charming/presentation/components/chart.rb +80 -0
  40. data/lib/charming/presentation/components/error_screen.rb +1 -1
  41. data/lib/charming/presentation/components/filepicker.rb +101 -0
  42. data/lib/charming/presentation/components/form/builder.rb +5 -0
  43. data/lib/charming/presentation/components/form/field.rb +5 -0
  44. data/lib/charming/presentation/components/form/input.rb +14 -4
  45. data/lib/charming/presentation/components/form/multiselect.rb +105 -0
  46. data/lib/charming/presentation/components/form/textarea.rb +18 -8
  47. data/lib/charming/presentation/components/form.rb +6 -0
  48. data/lib/charming/presentation/components/image.rb +38 -0
  49. data/lib/charming/presentation/components/list.rb +22 -4
  50. data/lib/charming/presentation/components/modal.rb +4 -4
  51. data/lib/charming/presentation/components/paginator.rb +54 -0
  52. data/lib/charming/presentation/components/progressbar.rb +26 -4
  53. data/lib/charming/presentation/components/sparkline.rb +38 -0
  54. data/lib/charming/presentation/components/spinner.rb +22 -3
  55. data/lib/charming/presentation/components/stopwatch.rb +55 -0
  56. data/lib/charming/presentation/components/table.rb +42 -1
  57. data/lib/charming/presentation/components/text_area.rb +3 -4
  58. data/lib/charming/presentation/components/text_input.rb +11 -6
  59. data/lib/charming/presentation/components/time_display.rb +20 -0
  60. data/lib/charming/presentation/components/timer.rb +43 -0
  61. data/lib/charming/presentation/components/toast.rb +4 -3
  62. data/lib/charming/presentation/components/viewport/content_lines.rb +1 -1
  63. data/lib/charming/presentation/components/viewport/line_window.rb +1 -1
  64. data/lib/charming/presentation/components/viewport.rb +11 -1
  65. data/lib/charming/presentation/markdown/renderer.rb +1 -1
  66. data/lib/charming/presentation/markdown/style_config.rb +1 -0
  67. data/lib/charming/presentation/markdown/table_renderer.rb +2 -3
  68. data/lib/charming/presentation/ui/adaptive_color.rb +20 -0
  69. data/lib/charming/presentation/ui/ansi_codes.rb +5 -2
  70. data/lib/charming/presentation/ui/background.rb +58 -0
  71. data/lib/charming/presentation/ui/border.rb +14 -1
  72. data/lib/charming/presentation/ui/border_painter.rb +23 -11
  73. data/lib/charming/presentation/ui/braille_canvas.rb +80 -0
  74. data/lib/charming/presentation/ui/canvas.rb +1 -0
  75. data/lib/charming/presentation/ui/gradient.rb +47 -0
  76. data/lib/charming/presentation/ui/style.rb +119 -19
  77. data/lib/charming/presentation/{markdown → ui}/text_wrapper.rb +4 -4
  78. data/lib/charming/presentation/ui/truncate.rb +29 -0
  79. data/lib/charming/presentation/ui/width.rb +11 -0
  80. data/lib/charming/presentation/ui.rb +52 -11
  81. data/lib/charming/presentation/view.rb +8 -6
  82. data/lib/charming/response.rb +11 -6
  83. data/lib/charming/runtime.rb +167 -90
  84. data/lib/charming/version.rb +1 -1
  85. data/lib/charming/welcome/controller.rb +23 -0
  86. data/lib/charming/welcome/show_view.rb +113 -0
  87. data/lib/charming/welcome.rb +13 -0
  88. metadata +34 -9
  89. data/lib/charming/generators/templates/app/home_controller.template +0 -6
  90. data/lib/charming/generators/templates/app/home_state.template +0 -7
  91. data/lib/charming/generators/templates/app/spec_controller.template +0 -17
  92. data/lib/charming/generators/templates/app/spec_state.template +0 -17
  93. data/lib/charming/generators/templates/app/spec_view.template +0 -16
  94. data/lib/charming/generators/templates/app/view.template +0 -21
@@ -13,6 +13,7 @@ module Charming
13
13
  BASE_FILE_TEMPLATES = [
14
14
  ["Gemfile", "app/Gemfile.template", false],
15
15
  ["Rakefile", "app/Rakefile.template", false],
16
+ [".rspec", "app/dot_rspec.template", false],
16
17
  ["README.md", "app/README.md.template", false],
17
18
  ["%<name>s.gemspec", "app/gemspec.template", false],
18
19
  ["exe/%<name>s", "app/executable.template", true],
@@ -21,16 +22,10 @@ module Charming
21
22
  ["lib/%<name>s/version.rb", "app/version.template", false],
22
23
  ["config/routes.rb", "app/routes.template", false],
23
24
  ["app/state/application_state.rb", "app/application_state.template", false],
24
- ["app/state/home_state.rb", "app/home_state.template", false],
25
25
  ["app/controllers/application_controller.rb", "app/application_controller.template", false],
26
- ["app/controllers/home_controller.rb", "app/home_controller.template", false],
27
26
  ["app/views/layouts/application_layout.rb", "app/layout.template", false],
28
- ["app/views/home/show_view.rb", "app/view.template", false],
29
27
  ["app/components/.keep", "app/keep.template", false],
30
- ["spec/spec_helper.rb", "app/spec_helper.template", false],
31
- ["spec/state/home_state_spec.rb", "app/spec_state.template", false],
32
- ["spec/controllers/home_controller_spec.rb", "app/spec_controller.template", false],
33
- ["spec/views/home/show_view_spec.rb", "app/spec_view.template", false]
28
+ ["spec/spec_helper.rb", "app/spec_helper.template", false]
34
29
  ].freeze
35
30
 
36
31
  # The list of [relative-path, template-path, executable-flag] triples to render in
@@ -95,15 +90,23 @@ module Charming
95
90
  app_class: name.class_name,
96
91
  gemspec_attributes: gemspec_attributes,
97
92
  gemspec_dependencies: gemspec_dependencies,
98
- controller_actions: controller_actions,
99
- controller_helpers: controller_helpers,
100
93
  database_require: database_require,
101
94
  model_loader: model_loader,
102
95
  env_setup: env_setup,
103
- database_spec_setup: database_spec_setup
96
+ database_spec_setup: database_spec_setup,
97
+ database_setup: database_setup
104
98
  }
105
99
  end
106
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
+
107
110
  # Pins CHARMING_ENV to "test" before the app (and its database config) loads, so
108
111
  # specs hit db/test.sqlite3. Empty for non-database apps.
109
112
  def env_setup
@@ -174,21 +177,6 @@ module Charming
174
177
  " spec.add_dependency \"sqlite3\", \"~> 2.0\""
175
178
  end
176
179
 
177
- # The body of the home controller's `show` action.
178
- def controller_actions
179
- "\n def show\n" \
180
- " render :show, home: home, palette: command_palette\n" \
181
- " end"
182
- end
183
-
184
- # The body of the home controller's private `home` helper, prefixed by a blank line.
185
- def controller_helpers
186
- "\n\n private\n" \
187
- " def home\n" \
188
- " state(:home, HomeState)\n" \
189
- " end"
190
- end
191
-
192
180
  # The `require_relative "../config/database"` line when the app is database-configured.
193
181
  def database_require
194
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
@@ -3,3 +3,9 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
+
7
+ group :development, :test do
8
+ gem "irb" # `charming console` (not a default gem on Ruby 4.0+)
9
+ gem "rake", "~> 13.0"
10
+ gem "rspec", "~> 3.13"
11
+ 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
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format documentation
@@ -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
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ module Layouts
5
+ class ApplicationLayout < Charming::View
6
+ def render
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
16
+ end
17
+
18
+ overlay command_palette_modal if command_palette_modal
19
+ end
20
+ end
21
+
22
+ private
23
+
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)
107
+ end
108
+ end
109
+ end
110
+ end