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,74 +2,176 @@
|
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
4
|
module Generators
|
|
5
|
+
# AppGenerator implements `charming new NAME`. Writes a complete Bundler-gem-style
|
|
6
|
+
# Charming app skeleton: Gemfile, Rakefile, gemspec, exe, lib root + application +
|
|
7
|
+
# version, config/routes.rb, app/state, app/controllers, app/views/layouts + home view,
|
|
8
|
+
# and a baseline spec/ tree. Optionally also creates the database files when
|
|
9
|
+
# `database:` is set.
|
|
5
10
|
class AppGenerator < Base
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
["
|
|
17
|
-
["
|
|
18
|
-
["
|
|
19
|
-
["
|
|
20
|
-
["
|
|
21
|
-
["
|
|
22
|
-
["
|
|
23
|
-
["
|
|
24
|
-
["
|
|
25
|
-
["
|
|
26
|
-
["
|
|
27
|
-
["
|
|
28
|
-
["
|
|
29
|
-
["app/views/layouts/application.rb", :layout],
|
|
30
|
-
["app/views/home_view.rb", :view],
|
|
31
|
-
["app/components/app_frame_component.rb", :component],
|
|
32
|
-
["spec/spec_helper.rb", :spec_helper],
|
|
33
|
-
["spec/models/home_model_spec.rb", :spec_model],
|
|
34
|
-
["spec/controllers/home_controller_spec.rb", :spec_controller],
|
|
35
|
-
["spec/views/home_view_spec.rb", :spec_view],
|
|
36
|
-
["spec/components/app_frame_component_spec.rb", :spec_component]
|
|
11
|
+
# The list of [relative-path, template-path, executable-flag] triples to render
|
|
12
|
+
# for a non-database app.
|
|
13
|
+
BASE_FILE_TEMPLATES = [
|
|
14
|
+
["Gemfile", "app/Gemfile.template", false],
|
|
15
|
+
["Rakefile", "app/Rakefile.template", false],
|
|
16
|
+
["README.md", "app/README.md.template", false],
|
|
17
|
+
["%<name>s.gemspec", "app/gemspec.template", false],
|
|
18
|
+
["exe/%<name>s", "app/executable.template", true],
|
|
19
|
+
["lib/%<name>s.rb", "app/root_file.template", false],
|
|
20
|
+
["lib/%<name>s/application.rb", "app/application.template", false],
|
|
21
|
+
["lib/%<name>s/version.rb", "app/version.template", false],
|
|
22
|
+
["config/routes.rb", "app/routes.template", false],
|
|
23
|
+
["app/state/application_state.rb", "app/application_state.template", false],
|
|
24
|
+
["app/state/home_state.rb", "app/home_state.template", false],
|
|
25
|
+
["app/controllers/application_controller.rb", "app/application_controller.template", false],
|
|
26
|
+
["app/controllers/home_controller.rb", "app/home_controller.template", false],
|
|
27
|
+
["app/views/layouts/application_layout.rb", "app/layout.template", false],
|
|
28
|
+
["app/views/home/show_view.rb", "app/view.template", false],
|
|
29
|
+
["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]
|
|
37
34
|
].freeze
|
|
38
35
|
|
|
39
|
-
|
|
36
|
+
# The list of [relative-path, template-path, executable-flag] triples to render in
|
|
37
|
+
# addition to the base list when `database:` is set on the generator.
|
|
38
|
+
DATABASE_FILE_TEMPLATES = [
|
|
39
|
+
["config/database.rb", "app/database_config.template", false],
|
|
40
|
+
["app/models/application_record.rb", "app/application_record.template", false],
|
|
41
|
+
["db/migrate/.keep", "app/keep.template", false],
|
|
42
|
+
["db/seeds.rb", "app/seeds.template", false]
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
# *name* is the new app's name. *out* is the status stream. *destination* is the
|
|
46
|
+
# parent directory under which `<name>/` will be created. *force* allows overwriting
|
|
47
|
+
# existing files. *database* optionally enables the database template set.
|
|
48
|
+
def initialize(name, out:, destination:, force: false, database: nil)
|
|
40
49
|
super(out: out, destination: File.join(destination, name), force: force)
|
|
41
50
|
@name = Name.new(name)
|
|
51
|
+
@database = database
|
|
42
52
|
end
|
|
43
53
|
|
|
54
|
+
# Renders every template in the chosen template list (base + optional database)
|
|
55
|
+
# and writes the files, then initializes a git repository in the new app directory.
|
|
44
56
|
def generate
|
|
45
|
-
|
|
46
|
-
create_file(file_path(path),
|
|
57
|
+
file_templates.each do |path, template_path, executable|
|
|
58
|
+
create_file(file_path(path), render_app_template(template_path), executable: executable)
|
|
47
59
|
end
|
|
60
|
+
initialize_git_repository
|
|
48
61
|
end
|
|
49
62
|
|
|
50
63
|
private
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
# The resource name and the database adapter name (or nil).
|
|
66
|
+
attr_reader :name, :database
|
|
53
67
|
alias_method :app_name, :name
|
|
54
68
|
|
|
69
|
+
# True when the database template set should be rendered.
|
|
70
|
+
def database?
|
|
71
|
+
!!database
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns the template list: base only, or base + database extras.
|
|
75
|
+
def file_templates
|
|
76
|
+
database? ? BASE_FILE_TEMPLATES + DATABASE_FILE_TEMPLATES : BASE_FILE_TEMPLATES
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Substitutes `name.snake_name` into a relative-path template (paths use `%<name>s`).
|
|
55
80
|
def file_path(path)
|
|
56
81
|
format(path, name: name.snake_name)
|
|
57
82
|
end
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
# Renders an app template file by replacing `__TOKEN__` placeholders with the
|
|
85
|
+
# appropriate values derived from the current *name* and *database* setting.
|
|
86
|
+
def render_app_template(relative_path)
|
|
87
|
+
render_template(relative_path, **app_template_tokens)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns the token map used to render every app template.
|
|
91
|
+
def app_template_tokens
|
|
92
|
+
{
|
|
93
|
+
app_name: name.class_name,
|
|
94
|
+
app_snake: name.snake_name,
|
|
95
|
+
app_class: name.class_name,
|
|
96
|
+
gemspec_attributes: gemspec_attributes,
|
|
97
|
+
gemspec_dependencies: gemspec_dependencies,
|
|
98
|
+
controller_actions: controller_actions,
|
|
99
|
+
controller_helpers: controller_helpers,
|
|
100
|
+
database_require: database_require,
|
|
101
|
+
model_loader: model_loader
|
|
102
|
+
}
|
|
103
|
+
end
|
|
61
104
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
105
|
+
# The `Gem::Specification` attributes block (indented two spaces to match the wrapping
|
|
106
|
+
# `Gem::Specification.new do |spec|`).
|
|
107
|
+
def gemspec_attributes
|
|
108
|
+
" spec.name = \"#{name.snake_name}\"\n" \
|
|
109
|
+
" spec.version = #{name.class_name}::VERSION\n" \
|
|
110
|
+
" spec.summary = \"A Charming terminal user interface.\"\n" \
|
|
111
|
+
" spec.authors = [\"TODO: Your name\"]\n" \
|
|
112
|
+
" spec.email = [\"TODO: Your email\"]\n" \
|
|
113
|
+
" spec.files = Dir.glob(\"#{gemspec_file_glob}/**/*\") + %w[README.md]\n" \
|
|
114
|
+
" spec.bindir = \"exe\"\n" \
|
|
115
|
+
" spec.executables = [\"#{name.snake_name}\"]\n" \
|
|
116
|
+
" spec.require_paths = [\"lib\"]\n" \
|
|
117
|
+
" spec.required_ruby_version = \">= 4.0.0\"\n" \
|
|
118
|
+
" spec.metadata[\"rubygems_mfa_required\"] = \"true\""
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The `Gem::Specification` `add_dependency` lines (trailing newline).
|
|
122
|
+
def gemspec_dependencies
|
|
123
|
+
"\n spec.add_dependency \"charming\"#{database_dependencies}\n"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The file glob used by the gemspec to enumerate packaged files.
|
|
127
|
+
def gemspec_file_glob
|
|
128
|
+
database? ? "{app,config,db,exe,lib}" : "{app,config,exe,lib}"
|
|
66
129
|
end
|
|
67
130
|
|
|
68
|
-
|
|
69
|
-
|
|
131
|
+
# The optional `activerecord`/`sqlite3` dependency lines (with leading newlines and
|
|
132
|
+
# trailing newline) when the app is database-configured; otherwise an empty string.
|
|
133
|
+
def database_dependencies
|
|
134
|
+
return "" unless database?
|
|
135
|
+
|
|
136
|
+
"\n spec.add_dependency \"activerecord\", \"~> 8.1\"\n" \
|
|
137
|
+
" spec.add_dependency \"sqlite3\", \"~> 2.0\""
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The body of the home controller's `show` action.
|
|
141
|
+
def controller_actions
|
|
142
|
+
"\n def show\n" \
|
|
143
|
+
" render :show, home: home, palette: command_palette\n" \
|
|
144
|
+
" end"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The body of the home controller's private `home` helper, prefixed by a blank line.
|
|
148
|
+
def controller_helpers
|
|
149
|
+
"\n\n private\n" \
|
|
150
|
+
" def home\n" \
|
|
151
|
+
" state(:home, HomeState)\n" \
|
|
152
|
+
" end"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The `require_relative "../config/database"` line when the app is database-configured.
|
|
156
|
+
def database_require
|
|
157
|
+
database? ? "require_relative \"../config/database\"" : ""
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# The model loader `push_dir` line (with trailing newline) when the app is
|
|
161
|
+
# database-configured; otherwise an empty string.
|
|
162
|
+
def model_loader
|
|
163
|
+
return "" unless database?
|
|
164
|
+
|
|
165
|
+
"loader.push_dir(File.expand_path(\"../app/models\", __dir__), namespace: #{name.class_name})\n"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Initializes a git repository in the new app's directory. Raises Error on failure.
|
|
169
|
+
def initialize_git_repository
|
|
170
|
+
unless system("git", "init", chdir: destination, out: File::NULL, err: File::NULL)
|
|
171
|
+
raise Error, "Could not initialize git repository"
|
|
172
|
+
end
|
|
70
173
|
|
|
71
|
-
|
|
72
|
-
)
|
|
174
|
+
out.puts "init git"
|
|
73
175
|
end
|
|
74
176
|
end
|
|
75
177
|
end
|
|
@@ -4,7 +4,13 @@ require "fileutils"
|
|
|
4
4
|
|
|
5
5
|
module Charming
|
|
6
6
|
module Generators
|
|
7
|
+
# Base is the parent class for all Charming file generators. Subclasses implement
|
|
8
|
+
# `generate` to write the appropriate files. The base class provides `create_file`,
|
|
9
|
+
# which writes content to a path under the configured *destination* and refuses to
|
|
10
|
+
# overwrite existing files unless *force* was set.
|
|
7
11
|
class Base
|
|
12
|
+
# *out* is the status-output stream. *destination* is the app root for generated files.
|
|
13
|
+
# *force* (default false) allows overwriting existing files.
|
|
8
14
|
def initialize(out:, destination:, force: false)
|
|
9
15
|
@out = out
|
|
10
16
|
@destination = destination
|
|
@@ -13,8 +19,12 @@ module Charming
|
|
|
13
19
|
|
|
14
20
|
private
|
|
15
21
|
|
|
22
|
+
# Status output stream and destination directory accessor (subclasses use these).
|
|
16
23
|
attr_reader :out, :destination
|
|
17
24
|
|
|
25
|
+
# Writes *content* to *path* (relative to the destination), creating intermediate
|
|
26
|
+
# directories as needed. Raises Generators::Error when the file already exists and
|
|
27
|
+
# *force* is false. Marks the file as executable when *executable:* is true.
|
|
18
28
|
def create_file(path, content, executable: false)
|
|
19
29
|
absolute_path = File.join(destination, path)
|
|
20
30
|
raise Error, "File already exists: #{path}" if File.exist?(absolute_path) && !@force
|
|
@@ -24,6 +34,22 @@ module Charming
|
|
|
24
34
|
FileUtils.chmod("u+x,go+rx", absolute_path) if executable
|
|
25
35
|
out.puts "create #{path}"
|
|
26
36
|
end
|
|
37
|
+
|
|
38
|
+
# Renders a template file by replacing `__TOKEN__` placeholders with values from
|
|
39
|
+
# *tokens*. *template_path* is relative to the generators templates directory.
|
|
40
|
+
def render_template(template_path, **tokens)
|
|
41
|
+
body = File.read(template_file(template_path))
|
|
42
|
+
tokens.each do |key, value|
|
|
43
|
+
body = body.gsub("__#{key.to_s.upcase}__", value.to_s)
|
|
44
|
+
end
|
|
45
|
+
body
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Absolute path to a generator template file. *relative_path* is relative to the
|
|
49
|
+
# generators templates directory.
|
|
50
|
+
def template_file(relative_path)
|
|
51
|
+
File.join(__dir__, "templates", relative_path)
|
|
52
|
+
end
|
|
27
53
|
end
|
|
28
54
|
end
|
|
29
55
|
end
|
|
@@ -2,28 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
4
|
module Generators
|
|
5
|
+
# ComponentGenerator implements `charming generate component NAME`. Writes a
|
|
6
|
+
# `Charming::Presentation::Component` subclass to `app/components/<name>_component.rb`.
|
|
5
7
|
class ComponentGenerator < AppFileGenerator
|
|
8
|
+
# Writes the component file to the standard `app/components` path.
|
|
6
9
|
def generate
|
|
7
10
|
create_file(app_path("app", "components"), component)
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
private
|
|
11
14
|
|
|
15
|
+
# The file-name suffix used by `app_path` (sets "component" so the file is
|
|
16
|
+
# `<name>_component.rb`).
|
|
12
17
|
def suffix
|
|
13
18
|
"component"
|
|
14
19
|
end
|
|
15
20
|
|
|
21
|
+
# The full source of the generated component class.
|
|
16
22
|
def component
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def render
|
|
22
|
-
text "#{name.class_name}"
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
)
|
|
23
|
+
render_template("component/component.rb.template",
|
|
24
|
+
app_class: app_name.class_name,
|
|
25
|
+
component_class: name.component_class_name,
|
|
26
|
+
resource_name: name.class_name)
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -2,48 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
4
|
module Generators
|
|
5
|
+
# ControllerGenerator implements `charming generate controller NAME [ACTION ...]`.
|
|
6
|
+
# Writes `app/controllers/<name>_controller.rb` containing a class that inherits
|
|
7
|
+
# from the app's `ApplicationController` and a `show` (or named) action that renders
|
|
8
|
+
# the conventional view with the command palette passed as an assign.
|
|
5
9
|
class ControllerGenerator < AppFileGenerator
|
|
10
|
+
# *name* is the resource name. *args* is the list of action names (defaults to `show`).
|
|
11
|
+
# *out*, *destination*, and *force* are forwarded to the parent.
|
|
6
12
|
def initialize(name, args, out:, destination:, force: false)
|
|
7
13
|
super
|
|
8
14
|
@actions = args
|
|
9
15
|
end
|
|
10
16
|
|
|
17
|
+
# Writes the controller file to the standard app/controllers path.
|
|
11
18
|
def generate
|
|
12
19
|
create_file(app_path("app", "controllers"), controller)
|
|
13
20
|
end
|
|
14
21
|
|
|
15
22
|
private
|
|
16
23
|
|
|
24
|
+
# The list of action names supplied on the command line.
|
|
17
25
|
attr_reader :actions
|
|
18
26
|
|
|
27
|
+
# The file-name suffix used by `app_path` (sets "controller" so the file is
|
|
28
|
+
# `<name>_controller.rb`).
|
|
19
29
|
def suffix
|
|
20
30
|
"controller"
|
|
21
31
|
end
|
|
22
32
|
|
|
33
|
+
# The full source of the generated controller file.
|
|
23
34
|
def controller
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#{action_methods} end
|
|
29
|
-
end
|
|
30
|
-
)
|
|
35
|
+
render_template("controller/controller.rb.template",
|
|
36
|
+
app_class: app_name.class_name,
|
|
37
|
+
controller_class: name.controller_class_name,
|
|
38
|
+
action_methods: action_methods)
|
|
31
39
|
end
|
|
32
40
|
|
|
41
|
+
# Renders one action method per action name; falls back to a single `show` action
|
|
42
|
+
# when no actions were specified.
|
|
33
43
|
def action_methods
|
|
34
44
|
return action_method("show") if actions.empty?
|
|
35
45
|
|
|
36
46
|
actions.map { |action| action_method(action) }.join("\n")
|
|
37
47
|
end
|
|
38
48
|
|
|
49
|
+
# Source for a single action method that renders the matching conventional view and
|
|
50
|
+
# passes the command palette as an assign.
|
|
39
51
|
def action_method(action)
|
|
40
|
-
|
|
41
|
-
render
|
|
42
|
-
|
|
43
|
-
screen: screen
|
|
44
|
-
)
|
|
45
|
-
end
|
|
46
|
-
)
|
|
52
|
+
" def #{action}\n" \
|
|
53
|
+
" render :#{action}, palette: command_palette\n" \
|
|
54
|
+
" end\n"
|
|
47
55
|
end
|
|
48
56
|
end
|
|
49
57
|
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
# ModelGenerator implements `charming generate model NAME [name:type ...]`. Writes an
|
|
6
|
+
# ActiveRecord model class, a `Create<Table>` migration (with one column per supplied
|
|
7
|
+
# field), and a baseline spec. Requires the app to have been generated with
|
|
8
|
+
# `--database sqlite3`.
|
|
9
|
+
class ModelGenerator < AppFileGenerator
|
|
10
|
+
# A single model field: column *name* and ActiveRecord *type* (e.g., "string").
|
|
11
|
+
Field = Data.define(:name, :type)
|
|
12
|
+
|
|
13
|
+
# The set of ActiveRecord column types accepted on the command line.
|
|
14
|
+
VALID_TYPES = %w[string text integer float decimal boolean date datetime time].freeze
|
|
15
|
+
|
|
16
|
+
# *name* is the resource name. *args* is the list of `name:type` field specifications.
|
|
17
|
+
def initialize(name, args, out:, destination:, force: false)
|
|
18
|
+
super
|
|
19
|
+
@fields = args.map { |arg| parse_field(arg) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Validates that the app is database-configured, then writes the model, migration,
|
|
23
|
+
# and spec files.
|
|
24
|
+
def generate
|
|
25
|
+
raise Error, "Database support is not configured. Generate the app with --database sqlite3 first." unless database_configured?
|
|
26
|
+
|
|
27
|
+
create_file(model_path, model)
|
|
28
|
+
create_file(migration_path, migration)
|
|
29
|
+
create_file(spec_path, spec)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# The list of parsed Field entries supplied on the command line.
|
|
35
|
+
attr_reader :fields
|
|
36
|
+
|
|
37
|
+
# No file-name suffix; ModelGenerator writes files to explicit paths.
|
|
38
|
+
def suffix
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Path to the generated `app/models/<name>.rb` file.
|
|
43
|
+
def model_path
|
|
44
|
+
File.join("app", "models", "#{name.snake_name}.rb")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Path to the generated `db/migrate/<timestamp>_create_<table>.rb` file.
|
|
48
|
+
def migration_path
|
|
49
|
+
File.join("db", "migrate", "#{timestamp}_create_#{table_name}.rb")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Path to the generated `spec/models/<name>_spec.rb` file.
|
|
53
|
+
def spec_path
|
|
54
|
+
File.join("spec", "models", "#{name.snake_name}_spec.rb")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The full source of the generated ActiveRecord model class.
|
|
58
|
+
def model
|
|
59
|
+
render_template("model/model.rb.template",
|
|
60
|
+
app_class: app_name.class_name,
|
|
61
|
+
model_class: name.class_name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The full source of the generated migration, with one `t.<type> :<name>` line per field.
|
|
65
|
+
def migration
|
|
66
|
+
render_template("model/migration.rb.template",
|
|
67
|
+
table_class: table_class_name,
|
|
68
|
+
table_name: table_name,
|
|
69
|
+
field_lines: field_lines)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The full source of the generated model spec (asserts the model inherits from
|
|
73
|
+
# `ApplicationRecord`).
|
|
74
|
+
def spec
|
|
75
|
+
render_template("model/spec.rb.template",
|
|
76
|
+
app_snake: app_name.snake_name,
|
|
77
|
+
app_class: app_name.class_name,
|
|
78
|
+
model_class: name.class_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Renders one `t.<type> :<name>` line per field, joined together.
|
|
82
|
+
def field_lines
|
|
83
|
+
fields.map { |field|
|
|
84
|
+
" t.#{field.type} :#{field.name}\n"
|
|
85
|
+
}.join
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Parses a single `name:type` argument. Raises Error on invalid names or unsupported types.
|
|
89
|
+
def parse_field(value)
|
|
90
|
+
field_name, type = value.split(":", 2)
|
|
91
|
+
raise Error, "Invalid field: #{value.inspect}" unless field_name && type
|
|
92
|
+
raise Error, "Invalid field name: #{field_name.inspect}" unless Name::VALID_NAME.match?(field_name)
|
|
93
|
+
raise Error, "Unsupported field type: #{type.inspect}" unless VALID_TYPES.include?(type)
|
|
94
|
+
|
|
95
|
+
Field.new(name: field_name, type: type)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# True when `config/database.rb` and `app/models/application_record.rb` both exist.
|
|
99
|
+
def database_configured?
|
|
100
|
+
File.exist?(File.join(destination, "config", "database.rb")) &&
|
|
101
|
+
File.exist?(File.join(destination, "app", "models", "application_record.rb"))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The pluralized table name (e.g., "user" → "users", "category" → "categories").
|
|
105
|
+
def table_name
|
|
106
|
+
pluralize(name.snake_name)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The CamelCase migration class name (e.g., "users" → "Users").
|
|
110
|
+
def table_class_name
|
|
111
|
+
table_name.split("_").map(&:capitalize).join
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Minimal English pluralization for the model name (covers the common -y, -s/x/z/ch/sh cases).
|
|
115
|
+
def pluralize(value)
|
|
116
|
+
return value.sub(/y\z/, "ies") if value.end_with?("y")
|
|
117
|
+
return "#{value}es" if value.match?(/(?:s|x|z|ch|sh)\z/)
|
|
118
|
+
|
|
119
|
+
"#{value}s"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The current UTC timestamp in the format ActiveRecord uses for migration filenames.
|
|
123
|
+
def timestamp
|
|
124
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -2,28 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
4
|
module Generators
|
|
5
|
+
# Name validates a generator resource name and exposes the conventional Ruby class-name
|
|
6
|
+
# variants (singular class, controller, component) derived from it. The original input
|
|
7
|
+
# must match `VALID_NAME` (lowercase, snake_case, must start with a letter).
|
|
5
8
|
class Name
|
|
9
|
+
# Regex matching a valid snake_case resource name: lowercase letter, then any
|
|
10
|
+
# combination of lowercase letters, digits, and underscores.
|
|
6
11
|
VALID_NAME = /\A[a-z][a-z0-9_]*\z/
|
|
7
12
|
|
|
13
|
+
# The original snake_case name as supplied.
|
|
8
14
|
attr_reader :snake_name
|
|
9
15
|
|
|
16
|
+
# Raises Error when *value* doesn't match `VALID_NAME`.
|
|
10
17
|
def initialize(value)
|
|
11
18
|
@snake_name = value.to_s
|
|
12
19
|
raise Error, "Invalid name: #{value}" unless VALID_NAME.match?(@snake_name)
|
|
13
20
|
end
|
|
14
21
|
|
|
22
|
+
# The CamelCase class name (e.g., "user" → "User").
|
|
15
23
|
def class_name
|
|
16
24
|
snake_name.split("_").map(&:capitalize).join
|
|
17
25
|
end
|
|
18
26
|
|
|
27
|
+
# The controller class name (e.g., "user" → "UserController").
|
|
19
28
|
def controller_class_name
|
|
20
29
|
"#{class_name}Controller"
|
|
21
30
|
end
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
"#{class_name}View"
|
|
25
|
-
end
|
|
26
|
-
|
|
32
|
+
# The component class name (e.g., "user" → "UserComponent").
|
|
27
33
|
def component_class_name
|
|
28
34
|
"#{class_name}Component"
|
|
29
35
|
end
|