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.
- checksums.yaml +4 -4
- data/README.md +38 -378
- data/lib/charming/application.rb +14 -3
- data/lib/charming/{application_model.rb → application_state.rb} +3 -3
- data/lib/charming/cli.rb +62 -3
- data/lib/charming/controller/class_methods.rb +115 -0
- data/lib/charming/controller/command_palette.rb +135 -0
- data/lib/charming/controller/component_dispatching.rb +81 -0
- data/lib/charming/controller/dispatching.rb +60 -0
- data/lib/charming/controller/focus_management.rb +30 -0
- data/lib/charming/controller/rendering.rb +127 -0
- data/lib/charming/controller/session_state.rb +41 -0
- data/lib/charming/controller/sidebar_navigation.rb +111 -0
- data/lib/charming/controller.rb +46 -448
- data/lib/charming/database_commands.rb +103 -0
- data/lib/charming/database_installer.rb +152 -0
- data/lib/charming/events/key_event.rb +15 -0
- data/lib/charming/events/mouse_event.rb +42 -0
- data/lib/charming/events/resize_event.rb +9 -0
- data/lib/charming/events/task_event.rb +19 -0
- data/lib/charming/events/timer_event.rb +9 -0
- data/lib/charming/focus.rb +58 -2
- data/lib/charming/generators/app_file_generator.rb +13 -0
- data/lib/charming/generators/app_generator.rb +147 -45
- data/lib/charming/generators/base.rb +26 -0
- data/lib/charming/generators/component_generator.rb +10 -10
- data/lib/charming/generators/controller_generator.rb +22 -14
- data/lib/charming/generators/model_generator.rb +128 -0
- data/lib/charming/generators/name.rb +10 -4
- data/lib/charming/generators/screen_generator.rb +84 -52
- data/lib/charming/generators/templates/app/Gemfile.template +5 -0
- data/lib/charming/generators/templates/app/README.md.template +9 -0
- data/lib/charming/generators/templates/app/Rakefile.template +3 -0
- data/lib/charming/generators/templates/app/application.template +13 -0
- data/lib/charming/generators/templates/app/application_controller.template +19 -0
- data/lib/charming/generators/templates/app/application_record.template +7 -0
- data/lib/charming/generators/templates/app/application_state.template +6 -0
- data/lib/charming/generators/templates/app/database_config.template +12 -0
- data/lib/charming/generators/templates/app/executable.template +7 -0
- data/lib/charming/generators/templates/app/gemspec.template +6 -0
- data/lib/charming/generators/templates/app/home_controller.template +6 -0
- data/lib/charming/generators/templates/app/home_state.template +7 -0
- data/lib/charming/generators/templates/app/keep.template +0 -0
- data/lib/charming/generators/templates/app/layout.template +113 -0
- data/lib/charming/generators/templates/app/root_file.template +20 -0
- data/lib/charming/generators/templates/app/routes.template +5 -0
- data/lib/charming/generators/templates/app/seeds.template +1 -0
- data/lib/charming/generators/templates/app/spec_controller.template +17 -0
- data/lib/charming/generators/templates/app/spec_helper.template +3 -0
- data/lib/charming/generators/templates/app/spec_state.template +17 -0
- data/lib/charming/generators/templates/app/spec_view.template +16 -0
- data/lib/charming/generators/templates/app/version.template +5 -0
- data/lib/charming/generators/templates/app/view.template +21 -0
- data/lib/charming/generators/templates/component/component.rb.template +9 -0
- data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
- data/lib/charming/generators/templates/model/migration.rb.template +9 -0
- data/lib/charming/generators/templates/model/model.rb.template +6 -0
- data/lib/charming/generators/templates/model/spec.rb.template +9 -0
- data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
- data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
- data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
- data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
- data/lib/charming/generators/templates/screen/state.rb.template +7 -0
- data/lib/charming/generators/templates/screen/view.rb.template +11 -0
- data/lib/charming/generators/templates/view/view.rb.template +11 -0
- data/lib/charming/generators/view_generator.rb +26 -13
- data/lib/charming/internal/renderer/differential.rb +17 -3
- data/lib/charming/internal/renderer/full_repaint.rb +6 -0
- data/lib/charming/internal/terminal/adapter.rb +29 -3
- data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
- data/lib/charming/internal/terminal/memory_backend.rb +28 -1
- data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
- data/lib/charming/internal/terminal/tty_backend.rb +62 -115
- data/lib/charming/presentation/component.rb +10 -0
- data/lib/charming/presentation/components/activity_indicator.rb +160 -0
- data/lib/charming/presentation/components/command_palette.rb +120 -0
- data/lib/charming/presentation/components/empty_state.rb +56 -0
- data/lib/charming/presentation/components/form/builder.rb +62 -0
- data/lib/charming/presentation/components/form/confirm.rb +69 -0
- data/lib/charming/presentation/components/form/field.rb +121 -0
- data/lib/charming/presentation/components/form/input.rb +71 -0
- data/lib/charming/presentation/components/form/note.rb +41 -0
- data/lib/charming/presentation/components/form/select.rb +112 -0
- data/lib/charming/presentation/components/form/textarea.rb +86 -0
- data/lib/charming/presentation/components/form.rb +156 -0
- data/lib/charming/presentation/components/keyboard_handler.rb +58 -0
- data/lib/charming/presentation/components/list.rb +132 -0
- data/lib/charming/presentation/components/markdown.rb +31 -0
- data/lib/charming/presentation/components/modal.rb +64 -0
- data/lib/charming/presentation/components/progressbar.rb +70 -0
- data/lib/charming/presentation/components/spinner.rb +49 -0
- data/lib/charming/presentation/components/table.rb +143 -0
- data/lib/charming/presentation/components/text_area.rb +267 -0
- data/lib/charming/presentation/components/text_input.rb +129 -0
- data/lib/charming/presentation/components/viewport.rb +272 -0
- data/lib/charming/presentation/layout/builder.rb +86 -0
- data/lib/charming/presentation/layout/overlay.rb +57 -0
- data/lib/charming/presentation/layout/pane.rb +145 -0
- data/lib/charming/presentation/layout/rect.rb +23 -0
- data/lib/charming/presentation/layout/screen_layout.rb +60 -0
- data/lib/charming/presentation/layout/split.rb +134 -0
- data/lib/charming/presentation/layout.rb +43 -0
- data/lib/charming/presentation/markdown/block_renderers.rb +120 -0
- data/lib/charming/presentation/markdown/inline_renderers.rb +68 -0
- data/lib/charming/presentation/markdown/render_context.rb +22 -0
- data/lib/charming/presentation/markdown/renderer.rb +113 -0
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +79 -0
- data/lib/charming/presentation/markdown.rb +11 -0
- data/lib/charming/presentation/template_view.rb +34 -0
- data/lib/charming/presentation/templates/erb_handler.rb +15 -0
- data/lib/charming/presentation/templates.rb +68 -0
- data/lib/charming/presentation/ui/ansi_codes.rb +89 -0
- data/lib/charming/presentation/ui/ansi_slicer.rb +94 -0
- data/lib/charming/presentation/ui/border.rb +35 -0
- data/lib/charming/presentation/ui/border_painter.rb +58 -0
- data/lib/charming/presentation/ui/canvas.rb +82 -0
- data/lib/charming/presentation/ui/style.rb +213 -0
- data/lib/charming/presentation/ui/theme.rb +180 -0
- data/lib/charming/{ui → presentation/ui}/themes/phosphor.json +2 -2
- data/lib/charming/presentation/ui/width.rb +26 -0
- data/lib/charming/presentation/ui.rb +91 -0
- data/lib/charming/presentation/view.rb +135 -0
- data/lib/charming/runtime.rb +9 -7
- data/lib/charming/screen.rb +5 -1
- data/lib/charming/tasks/inline_executor.rb +37 -0
- data/lib/charming/tasks/task.rb +12 -0
- data/lib/charming/tasks/threaded_executor.rb +51 -0
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +17 -0
- metadata +170 -36
- data/lib/charming/component.rb +0 -8
- data/lib/charming/components/activity_indicator.rb +0 -158
- data/lib/charming/components/command_palette.rb +0 -118
- data/lib/charming/components/keyboard_handler.rb +0 -22
- data/lib/charming/components/list.rb +0 -105
- data/lib/charming/components/modal.rb +0 -48
- data/lib/charming/components/progressbar.rb +0 -55
- data/lib/charming/components/spinner.rb +0 -37
- data/lib/charming/components/table.rb +0 -115
- data/lib/charming/components/text_input.rb +0 -103
- data/lib/charming/components/viewport.rb +0 -191
- data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -86
- data/lib/charming/generators/app_generator/basic_templates.rb +0 -69
- data/lib/charming/generators/app_generator/component_templates.rb +0 -36
- data/lib/charming/generators/app_generator/controller_template.rb +0 -69
- data/lib/charming/generators/app_generator/layout_template.rb +0 -160
- data/lib/charming/generators/app_generator/model_templates.rb +0 -30
- data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -70
- data/lib/charming/generators/app_generator/view_template.rb +0 -90
- data/lib/charming/key_event.rb +0 -13
- data/lib/charming/mouse_event.rb +0 -40
- data/lib/charming/resize_event.rb +0 -7
- data/lib/charming/task.rb +0 -7
- data/lib/charming/task_event.rb +0 -17
- data/lib/charming/task_executor.rb +0 -62
- data/lib/charming/timer_event.rb +0 -7
- data/lib/charming/ui/border.rb +0 -33
- data/lib/charming/ui/style.rb +0 -244
- data/lib/charming/ui/theme.rb +0 -178
- data/lib/charming/ui/width.rb +0 -24
- data/lib/charming/ui.rb +0 -230
- data/lib/charming/view.rb +0 -116
- /data/lib/charming/{generators.rb → generators/error.rb} +0 -0
|
@@ -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
|
-
|
|
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(
|
|
19
|
+
create_file(state_path, state)
|
|
15
20
|
create_file(controller_path, controller)
|
|
16
21
|
create_file(view_path, view)
|
|
17
|
-
create_file(
|
|
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
|
-
|
|
31
|
-
|
|
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",
|
|
48
|
+
File.join("app", "views", name.snake_name, "show_view.rb")
|
|
40
49
|
end
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
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",
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
86
|
-
render
|
|
87
|
-
#{name.snake_name}: #{name.snake_name}
|
|
88
|
-
palette: command_palette
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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,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,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
|
+
)
|
|
File without changes
|
|
@@ -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 @@
|
|
|
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,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,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,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
|