charming 0.1.0 → 0.1.2

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 (163) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -378
  3. data/lib/charming/application.rb +14 -3
  4. data/lib/charming/{application_model.rb → application_state.rb} +3 -3
  5. data/lib/charming/cli.rb +62 -3
  6. data/lib/charming/controller/class_methods.rb +115 -0
  7. data/lib/charming/controller/command_palette.rb +135 -0
  8. data/lib/charming/controller/component_dispatching.rb +81 -0
  9. data/lib/charming/controller/dispatching.rb +60 -0
  10. data/lib/charming/controller/focus_management.rb +30 -0
  11. data/lib/charming/controller/rendering.rb +127 -0
  12. data/lib/charming/controller/session_state.rb +41 -0
  13. data/lib/charming/controller/sidebar_navigation.rb +111 -0
  14. data/lib/charming/controller.rb +46 -448
  15. data/lib/charming/database_commands.rb +103 -0
  16. data/lib/charming/database_installer.rb +152 -0
  17. data/lib/charming/events/key_event.rb +15 -0
  18. data/lib/charming/events/mouse_event.rb +42 -0
  19. data/lib/charming/events/resize_event.rb +9 -0
  20. data/lib/charming/events/task_event.rb +19 -0
  21. data/lib/charming/events/timer_event.rb +9 -0
  22. data/lib/charming/focus.rb +58 -2
  23. data/lib/charming/generators/app_file_generator.rb +13 -0
  24. data/lib/charming/generators/app_generator.rb +147 -45
  25. data/lib/charming/generators/base.rb +26 -0
  26. data/lib/charming/generators/component_generator.rb +10 -10
  27. data/lib/charming/generators/controller_generator.rb +22 -14
  28. data/lib/charming/generators/model_generator.rb +128 -0
  29. data/lib/charming/generators/name.rb +10 -4
  30. data/lib/charming/generators/screen_generator.rb +84 -52
  31. data/lib/charming/generators/templates/app/Gemfile.template +5 -0
  32. data/lib/charming/generators/templates/app/README.md.template +9 -0
  33. data/lib/charming/generators/templates/app/Rakefile.template +3 -0
  34. data/lib/charming/generators/templates/app/application.template +13 -0
  35. data/lib/charming/generators/templates/app/application_controller.template +19 -0
  36. data/lib/charming/generators/templates/app/application_record.template +7 -0
  37. data/lib/charming/generators/templates/app/application_state.template +6 -0
  38. data/lib/charming/generators/templates/app/database_config.template +12 -0
  39. data/lib/charming/generators/templates/app/executable.template +7 -0
  40. data/lib/charming/generators/templates/app/gemspec.template +6 -0
  41. data/lib/charming/generators/templates/app/home_controller.template +6 -0
  42. data/lib/charming/generators/templates/app/home_state.template +7 -0
  43. data/lib/charming/generators/templates/app/keep.template +0 -0
  44. data/lib/charming/generators/templates/app/layout.template +113 -0
  45. data/lib/charming/generators/templates/app/root_file.template +20 -0
  46. data/lib/charming/generators/templates/app/routes.template +5 -0
  47. data/lib/charming/generators/templates/app/seeds.template +1 -0
  48. data/lib/charming/generators/templates/app/spec_controller.template +17 -0
  49. data/lib/charming/generators/templates/app/spec_helper.template +3 -0
  50. data/lib/charming/generators/templates/app/spec_state.template +17 -0
  51. data/lib/charming/generators/templates/app/spec_view.template +16 -0
  52. data/lib/charming/generators/templates/app/version.template +5 -0
  53. data/lib/charming/generators/templates/app/view.template +21 -0
  54. data/lib/charming/generators/templates/component/component.rb.template +9 -0
  55. data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
  56. data/lib/charming/generators/templates/model/migration.rb.template +9 -0
  57. data/lib/charming/generators/templates/model/model.rb.template +6 -0
  58. data/lib/charming/generators/templates/model/spec.rb.template +9 -0
  59. data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
  60. data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
  61. data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
  62. data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
  63. data/lib/charming/generators/templates/screen/state.rb.template +7 -0
  64. data/lib/charming/generators/templates/screen/view.rb.template +11 -0
  65. data/lib/charming/generators/templates/view/view.rb.template +11 -0
  66. data/lib/charming/generators/view_generator.rb +26 -13
  67. data/lib/charming/internal/renderer/differential.rb +17 -3
  68. data/lib/charming/internal/renderer/full_repaint.rb +6 -0
  69. data/lib/charming/internal/terminal/adapter.rb +29 -3
  70. data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
  71. data/lib/charming/internal/terminal/memory_backend.rb +28 -1
  72. data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
  73. data/lib/charming/internal/terminal/tty_backend.rb +62 -115
  74. data/lib/charming/presentation/component.rb +10 -0
  75. data/lib/charming/presentation/components/activity_indicator.rb +160 -0
  76. data/lib/charming/presentation/components/command_palette.rb +120 -0
  77. data/lib/charming/presentation/components/empty_state.rb +56 -0
  78. data/lib/charming/presentation/components/form/builder.rb +62 -0
  79. data/lib/charming/presentation/components/form/confirm.rb +69 -0
  80. data/lib/charming/presentation/components/form/field.rb +121 -0
  81. data/lib/charming/presentation/components/form/input.rb +71 -0
  82. data/lib/charming/presentation/components/form/note.rb +41 -0
  83. data/lib/charming/presentation/components/form/select.rb +112 -0
  84. data/lib/charming/presentation/components/form/textarea.rb +86 -0
  85. data/lib/charming/presentation/components/form.rb +156 -0
  86. data/lib/charming/presentation/components/keyboard_handler.rb +58 -0
  87. data/lib/charming/presentation/components/list.rb +132 -0
  88. data/lib/charming/presentation/components/markdown.rb +31 -0
  89. data/lib/charming/presentation/components/modal.rb +64 -0
  90. data/lib/charming/presentation/components/progressbar.rb +70 -0
  91. data/lib/charming/presentation/components/spinner.rb +49 -0
  92. data/lib/charming/presentation/components/table.rb +143 -0
  93. data/lib/charming/presentation/components/text_area.rb +267 -0
  94. data/lib/charming/presentation/components/text_input.rb +129 -0
  95. data/lib/charming/presentation/components/viewport.rb +272 -0
  96. data/lib/charming/presentation/layout/builder.rb +86 -0
  97. data/lib/charming/presentation/layout/overlay.rb +57 -0
  98. data/lib/charming/presentation/layout/pane.rb +145 -0
  99. data/lib/charming/presentation/layout/rect.rb +23 -0
  100. data/lib/charming/presentation/layout/screen_layout.rb +60 -0
  101. data/lib/charming/presentation/layout/split.rb +134 -0
  102. data/lib/charming/presentation/layout.rb +43 -0
  103. data/lib/charming/presentation/markdown/block_renderers.rb +120 -0
  104. data/lib/charming/presentation/markdown/inline_renderers.rb +68 -0
  105. data/lib/charming/presentation/markdown/render_context.rb +22 -0
  106. data/lib/charming/presentation/markdown/renderer.rb +113 -0
  107. data/lib/charming/presentation/markdown/syntax_highlighter.rb +79 -0
  108. data/lib/charming/presentation/markdown.rb +11 -0
  109. data/lib/charming/presentation/template_view.rb +34 -0
  110. data/lib/charming/presentation/templates/erb_handler.rb +15 -0
  111. data/lib/charming/presentation/templates.rb +68 -0
  112. data/lib/charming/presentation/ui/ansi_codes.rb +89 -0
  113. data/lib/charming/presentation/ui/ansi_slicer.rb +94 -0
  114. data/lib/charming/presentation/ui/border.rb +35 -0
  115. data/lib/charming/presentation/ui/border_painter.rb +58 -0
  116. data/lib/charming/presentation/ui/canvas.rb +82 -0
  117. data/lib/charming/presentation/ui/style.rb +213 -0
  118. data/lib/charming/presentation/ui/theme.rb +180 -0
  119. data/lib/charming/{ui → presentation/ui}/themes/phosphor.json +2 -2
  120. data/lib/charming/presentation/ui/width.rb +26 -0
  121. data/lib/charming/presentation/ui.rb +91 -0
  122. data/lib/charming/presentation/view.rb +135 -0
  123. data/lib/charming/runtime.rb +9 -7
  124. data/lib/charming/screen.rb +5 -1
  125. data/lib/charming/tasks/inline_executor.rb +37 -0
  126. data/lib/charming/tasks/task.rb +12 -0
  127. data/lib/charming/tasks/threaded_executor.rb +51 -0
  128. data/lib/charming/version.rb +1 -1
  129. data/lib/charming.rb +17 -0
  130. metadata +170 -36
  131. data/lib/charming/component.rb +0 -8
  132. data/lib/charming/components/activity_indicator.rb +0 -158
  133. data/lib/charming/components/command_palette.rb +0 -118
  134. data/lib/charming/components/keyboard_handler.rb +0 -22
  135. data/lib/charming/components/list.rb +0 -105
  136. data/lib/charming/components/modal.rb +0 -48
  137. data/lib/charming/components/progressbar.rb +0 -55
  138. data/lib/charming/components/spinner.rb +0 -37
  139. data/lib/charming/components/table.rb +0 -115
  140. data/lib/charming/components/text_input.rb +0 -103
  141. data/lib/charming/components/viewport.rb +0 -191
  142. data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -86
  143. data/lib/charming/generators/app_generator/basic_templates.rb +0 -69
  144. data/lib/charming/generators/app_generator/component_templates.rb +0 -36
  145. data/lib/charming/generators/app_generator/controller_template.rb +0 -69
  146. data/lib/charming/generators/app_generator/layout_template.rb +0 -160
  147. data/lib/charming/generators/app_generator/model_templates.rb +0 -30
  148. data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -70
  149. data/lib/charming/generators/app_generator/view_template.rb +0 -90
  150. data/lib/charming/key_event.rb +0 -13
  151. data/lib/charming/mouse_event.rb +0 -40
  152. data/lib/charming/resize_event.rb +0 -7
  153. data/lib/charming/task.rb +0 -7
  154. data/lib/charming/task_event.rb +0 -17
  155. data/lib/charming/task_executor.rb +0 -62
  156. data/lib/charming/timer_event.rb +0 -7
  157. data/lib/charming/ui/border.rb +0 -33
  158. data/lib/charming/ui/style.rb +0 -244
  159. data/lib/charming/ui/theme.rb +0 -178
  160. data/lib/charming/ui/width.rb +0 -24
  161. data/lib/charming/ui.rb +0 -230
  162. data/lib/charming/view.rb +0 -116
  163. /data/lib/charming/{generators.rb → generators/error.rb} +0 -0
@@ -2,19 +2,24 @@
2
2
 
3
3
  module Charming
4
4
  module Generators
5
+ # ScreenGenerator implements `charming generate screen NAME`. Writes a complete vertical
6
+ # slice for a new screen: a state class, a controller with a `show` action, a view,
7
+ # matching spec files, and inserts a route into `config/routes.rb` and a command entry
8
+ # into `ApplicationController` for the command palette.
5
9
  class ScreenGenerator < AppFileGenerator
6
- include AppGenerator::ScreenSpecTemplates
7
-
10
+ # *name* is the resource name. *args* is unused (raises Error when non-empty).
8
11
  def initialize(name, args, out:, destination:, force: false)
9
12
  super
10
13
  raise Error, "Usage: charming generate screen NAME" if args.any?
11
14
  end
12
15
 
16
+ # Writes the state, controller, view, and three spec files, then inserts a route
17
+ # and a command-palette entry.
13
18
  def generate
14
- create_file(model_path, model)
19
+ create_file(state_path, state)
15
20
  create_file(controller_path, controller)
16
21
  create_file(view_path, view)
17
- create_file(spec_model_path, spec_model)
22
+ create_file(spec_state_path, spec_state)
18
23
  create_file(spec_controller_path, spec_controller)
19
24
  create_file(spec_view_path, spec_view)
20
25
  insert_route
@@ -23,102 +28,124 @@ module Charming
23
28
 
24
29
  private
25
30
 
31
+ # The file-name suffix used by `app_path` ("screen" — only used by the parent class).
26
32
  def suffix
27
33
  "screen"
28
34
  end
29
35
 
30
- def model_path
31
- File.join("app", "models", "#{name.snake_name}_model.rb")
36
+ # Path to the generated state class.
37
+ def state_path
38
+ File.join("app", "state", "#{name.snake_name}_state.rb")
32
39
  end
33
40
 
41
+ # Path to the generated controller class.
34
42
  def controller_path
35
43
  File.join("app", "controllers", "#{name.snake_name}_controller.rb")
36
44
  end
37
45
 
46
+ # Path to the generated `show` view.
38
47
  def view_path
39
- File.join("app", "views", "#{name.snake_name}_view.rb")
48
+ File.join("app", "views", name.snake_name, "show_view.rb")
40
49
  end
41
50
 
42
- def spec_model_path
43
- File.join("spec", "models", "#{name.snake_name}_model_spec.rb")
51
+ # Path to the generated state spec.
52
+ def spec_state_path
53
+ File.join("spec", "state", "#{name.snake_name}_state_spec.rb")
44
54
  end
45
55
 
56
+ # Path to the generated controller spec.
46
57
  def spec_controller_path
47
58
  File.join("spec", "controllers", "#{name.snake_name}_controller_spec.rb")
48
59
  end
49
60
 
61
+ # Path to the generated view spec.
50
62
  def spec_view_path
51
- File.join("spec", "views", "#{name.snake_name}_view_spec.rb")
63
+ File.join("spec", "views", name.snake_name, "show_view_spec.rb")
52
64
  end
53
65
 
66
+ # Absolute path to the app's `config/routes.rb`.
54
67
  def route_path
55
68
  File.join(destination, "config", "routes.rb")
56
69
  end
57
70
 
71
+ # Absolute path to the app's `ApplicationController`.
58
72
  def application_controller_path
59
73
  File.join(destination, "app", "controllers", "application_controller.rb")
60
74
  end
61
75
 
62
- def model
63
- %(# frozen_string_literal: true
64
-
65
- module #{app_name.class_name}
66
- class #{name.class_name}Model < ApplicationModel
67
- attribute :title, :string, default: "#{name.class_name}"
68
- end
69
- end
70
- )
76
+ # The source of the generated state class.
77
+ def state
78
+ render_template("screen/state.rb.template",
79
+ app_class: app_name.class_name,
80
+ state_class: "#{name.class_name}State",
81
+ title: name.class_name)
71
82
  end
72
83
 
84
+ # The source of the generated controller class.
73
85
  def controller
74
- %(# frozen_string_literal: true
75
-
76
- module #{app_name.class_name}
77
- class #{name.controller_class_name} < ApplicationController
78
- #{controller_body}
79
- end
80
- end
81
- )
86
+ render_template("screen/controller.rb.template",
87
+ app_class: app_name.class_name,
88
+ controller_class: name.controller_class_name,
89
+ controller_body: controller_body)
82
90
  end
83
91
 
92
+ # The body of the controller: a `show` action and a private accessor for the state.
84
93
  def controller_body
85
- %( def show
86
- render #{name.view_class_name}.new(
87
- #{name.snake_name}: #{name.snake_name},
88
- palette: command_palette,
89
- screen: screen
90
- )
91
- end
92
-
93
- private
94
-
95
- def #{name.snake_name}
96
- model(:#{name.snake_name}, #{name.class_name}Model)
97
- end)
94
+ " def show\n" \
95
+ " render :show,\n" \
96
+ " #{name.snake_name}: #{name.snake_name},\n" \
97
+ " palette: command_palette\n" \
98
+ " end\n" \
99
+ "\n" \
100
+ " private\n" \
101
+ "\n" \
102
+ " def #{name.snake_name}\n" \
103
+ " state(:#{name.snake_name}, #{name.class_name}State)\n" \
104
+ " end"
105
+ end
106
+
107
+ # The source of the generated view class.
108
+ def view
109
+ render_template("screen/view.rb.template",
110
+ app_class: app_name.class_name,
111
+ resource_module: name.class_name,
112
+ screen_name: name.snake_name)
98
113
  end
99
114
 
100
- def view
101
- %(# frozen_string_literal: true
115
+ # The source of the generated state spec.
116
+ def spec_state
117
+ render_template("screen/spec_state.rb.template",
118
+ app_snake: app_name.snake_name,
119
+ app_class: app_name.class_name,
120
+ state_class: "#{name.class_name}State",
121
+ title: name.class_name)
122
+ end
102
123
 
103
- module #{app_name.class_name}
104
- class #{name.view_class_name} < Charming::View
105
- #{view_body}
106
- end
107
- end
108
- )
124
+ # The source of the generated controller spec.
125
+ def spec_controller
126
+ render_template("screen/spec_controller.rb.template",
127
+ app_snake: app_name.snake_name,
128
+ app_class: app_name.class_name,
129
+ controller_class: name.controller_class_name)
109
130
  end
110
131
 
111
- def view_body
112
- %( def render
113
- #{name.snake_name}.title
114
- end)
132
+ # The source of the generated view spec.
133
+ def spec_view
134
+ render_template("screen/spec_view.rb.template",
135
+ app_snake: app_name.snake_name,
136
+ app_class: app_name.class_name,
137
+ resource_module: name.class_name,
138
+ screen_name: name.snake_name,
139
+ title: name.class_name)
115
140
  end
116
141
 
142
+ # Inserts a `screen` route into `config/routes.rb`, idempotently.
117
143
  def insert_route
118
144
  route = %( screen "/#{name.snake_name}", to: "#{name.snake_name}#show", title: "#{name.class_name}")
119
145
  insert_before_end(route_path, route, "route", "end")
120
146
  end
121
147
 
148
+ # Inserts a `command` block into `ApplicationController`, idempotently.
122
149
  def insert_command
123
150
  command = %( command "#{name.class_name}" do
124
151
  navigate_to "/#{name.snake_name}"
@@ -126,6 +153,8 @@ end
126
153
  insert_before_end(application_controller_path, command, "command", " end")
127
154
  end
128
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.
129
158
  def insert_before_end(path, content, label, end_line)
130
159
  raise Error, "Missing file: #{relative_path(path)}" unless File.exist?(path)
131
160
 
@@ -139,6 +168,8 @@ end
139
168
  out.puts "insert #{label} #{relative_path(path)}"
140
169
  end
141
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.
142
173
  def insertion_index(lines, path, end_line)
143
174
  index = lines.rindex { |line| line.chomp == end_line }
144
175
  raise Error, "Could not update #{relative_path(path)}" unless index
@@ -146,6 +177,7 @@ end
146
177
  index
147
178
  end
148
179
 
180
+ # Strips the destination prefix from *path* for human-friendly status output.
149
181
  def relative_path(path)
150
182
  path.delete_prefix("#{destination}/")
151
183
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
@@ -0,0 +1,9 @@
1
+ # __APP_NAME__
2
+
3
+ A Charming terminal user interface.
4
+
5
+ Run it with:
6
+
7
+ ```sh
8
+ bundle exec __APP_SNAKE__
9
+ ```
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class Application < Charming::Application
5
+ root File.expand_path("../..", __dir__)
6
+
7
+ Charming::Presentation::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
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class ApplicationController < Charming::Controller
5
+ layout Layouts::ApplicationLayout
6
+ focus_ring :sidebar, :content
7
+
8
+ key "p", :open_command_palette, scope: :global
9
+ 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
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class ApplicationState < Charming::ApplicationState
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "fileutils"
5
+
6
+ database_path = File.expand_path("../db/development.sqlite3", __dir__)
7
+ FileUtils.mkdir_p(File.dirname(database_path))
8
+
9
+ ActiveRecord::Base.establish_connection(
10
+ adapter: "sqlite3",
11
+ database: database_path
12
+ )
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "__APP_SNAKE__"
6
+
7
+ Charming.run(__APP_CLASS__::Application.new)
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/__APP_SNAKE__/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ __GEMSPEC_ATTRIBUTES____GEMSPEC_DEPENDENCIES__end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class HomeController < ApplicationController
5
+ __CONTROLLER_ACTIONS____CONTROLLER_HELPERS__ end
6
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class HomeState < ApplicationState
5
+ attribute :title, :string, default: "__APP_NAME__"
6
+ end
7
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ module Layouts
5
+ class ApplicationLayout < Charming::Presentation::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\np 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::Presentation::Components::Modal.new(
88
+ content: palette_component,
89
+ title: "Command palette",
90
+ help: "Type to filter. Enter selects. Escape closes.",
91
+ width: 52,
92
+ theme: theme
93
+ )
94
+ end
95
+
96
+ def sidebar_focused?
97
+ controller.sidebar_focused?
98
+ end
99
+
100
+ def content_focused?
101
+ controller.content_focused?
102
+ end
103
+
104
+ def sidebar_index
105
+ controller.sidebar_index
106
+ end
107
+
108
+ def current_route?(route)
109
+ controller.current_route?(route)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "charming"
4
+ require "zeitwerk"
5
+ __DATABASE_REQUIRE__
6
+
7
+ module __APP_CLASS__
8
+ end
9
+
10
+ loader = Zeitwerk::Loader.new
11
+ loader.tag = "__APP_SNAKE__"
12
+ loader.inflector.inflect("version" => "VERSION")
13
+ loader.push_dir(File.expand_path("__APP_SNAKE__", __dir__), namespace: __APP_CLASS__)
14
+ __MODEL_LOADER__loader.push_dir(File.expand_path("../app/state", __dir__), namespace: __APP_CLASS__)
15
+ loader.push_dir(File.expand_path("../app/components", __dir__), namespace: __APP_CLASS__)
16
+ loader.push_dir(File.expand_path("../app/views", __dir__), namespace: __APP_CLASS__)
17
+ loader.push_dir(File.expand_path("../app/controllers", __dir__), namespace: __APP_CLASS__)
18
+ loader.setup
19
+
20
+ require_relative "../config/routes"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ __APP_CLASS__::Application.routes do
4
+ root "home#show"
5
+ end
@@ -0,0 +1 @@
1
+ # frozen_string_literal: true
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "__APP_SNAKE__"
4
+
5
+ RSpec.describe __APP_CLASS__::HomeController do
6
+ let(:application) { __APP_CLASS__::Application.new }
7
+
8
+ subject(:controller) { described_class.new(application: application) }
9
+
10
+ describe "#show" do
11
+ it "renders the view with the state" do
12
+ response = controller.dispatch(:show)
13
+
14
+ expect(response).to respond_to(:body)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "__APP_SNAKE__"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "__APP_SNAKE__"
4
+
5
+ RSpec.describe __APP_CLASS__::HomeState do
6
+ describe "#title" do
7
+ it "has the correct default string value" do
8
+ instance = described_class.new
9
+ expect(instance.title).to eq("__APP_NAME__")
10
+ end
11
+
12
+ it "accepts overridden title values" do
13
+ instance = described_class.new(title: "Alternative")
14
+ expect(instance.title).to eq("Alternative")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "__APP_SNAKE__"
4
+
5
+ RSpec.describe __APP_CLASS__::Home::ShowView do
6
+ describe "#render" do
7
+ it "renders the state title" do
8
+ view = described_class.new(
9
+ home: double(title: "__APP_NAME__"),
10
+ theme: __APP_CLASS__::Application.new.theme
11
+ )
12
+
13
+ expect(view.render).to include("__APP_NAME__")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ module Home
5
+ class ShowView < Charming::Presentation::View
6
+ def render
7
+ column(title_line, help_line, gap: 1)
8
+ end
9
+
10
+ private
11
+
12
+ def title_line
13
+ text home.title, style: theme.title
14
+ end
15
+
16
+ def help_line
17
+ text "Press p for commands, q to quit.", style: theme.muted
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class __COMPONENT_CLASS__ < Charming::Presentation::Component
5
+ def render
6
+ text "__RESOURCE_NAME__"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class __CONTROLLER_CLASS__ < ApplicationController
5
+ __ACTION_METHODS__ end
6
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Create__TABLE_CLASS__ < ActiveRecord::Migration[8.1]
4
+ def change
5
+ create_table :__TABLE_NAME__ do |t|
6
+ __FIELD_LINES__ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class __MODEL_CLASS__ < ApplicationRecord
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "__APP_SNAKE__"
4
+
5
+ RSpec.describe __APP_CLASS__::__MODEL_CLASS__ do
6
+ it "inherits from ApplicationRecord" do
7
+ expect(described_class.superclass).to eq(__APP_CLASS__::ApplicationRecord)
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class __CONTROLLER_CLASS__ < ApplicationController
5
+ __CONTROLLER_BODY__
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "__APP_SNAKE__"
4
+
5
+ RSpec.describe __APP_CLASS__::__CONTROLLER_CLASS__ do
6
+ let(:application) { __APP_CLASS__::Application.new }
7
+
8
+ subject(:controller) { described_class.new(application: application) }
9
+
10
+ describe "#show" do
11
+ it "renders the view with the state" do
12
+ response = controller.dispatch(:show)
13
+
14
+ expect(response).to respond_to(:body)
15
+ end
16
+ end
17
+ end