charming 0.1.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +421 -0
- data/exe/charming +6 -0
- data/lib/charming/application.rb +90 -0
- data/lib/charming/application_model.rb +13 -0
- data/lib/charming/cli.rb +60 -0
- data/lib/charming/component.rb +8 -0
- data/lib/charming/components/activity_indicator.rb +158 -0
- data/lib/charming/components/command_palette.rb +118 -0
- data/lib/charming/components/keyboard_handler.rb +22 -0
- data/lib/charming/components/list.rb +105 -0
- data/lib/charming/components/modal.rb +48 -0
- data/lib/charming/components/progressbar.rb +55 -0
- data/lib/charming/components/spinner.rb +37 -0
- data/lib/charming/components/table.rb +115 -0
- data/lib/charming/components/text_input.rb +103 -0
- data/lib/charming/components/viewport.rb +191 -0
- data/lib/charming/controller.rb +523 -0
- data/lib/charming/focus.rb +65 -0
- data/lib/charming/generators/app_file_generator.rb +28 -0
- data/lib/charming/generators/app_generator/app_spec_templates.rb +86 -0
- data/lib/charming/generators/app_generator/basic_templates.rb +69 -0
- data/lib/charming/generators/app_generator/component_templates.rb +36 -0
- data/lib/charming/generators/app_generator/controller_template.rb +69 -0
- data/lib/charming/generators/app_generator/layout_template.rb +160 -0
- data/lib/charming/generators/app_generator/model_templates.rb +30 -0
- data/lib/charming/generators/app_generator/screen_spec_templates.rb +70 -0
- data/lib/charming/generators/app_generator/view_template.rb +90 -0
- data/lib/charming/generators/app_generator.rb +76 -0
- data/lib/charming/generators/base.rb +29 -0
- data/lib/charming/generators/component_generator.rb +30 -0
- data/lib/charming/generators/controller_generator.rb +50 -0
- data/lib/charming/generators/name.rb +32 -0
- data/lib/charming/generators/screen_generator.rb +154 -0
- data/lib/charming/generators/view_generator.rb +34 -0
- data/lib/charming/generators.rb +7 -0
- data/lib/charming/internal/renderer/differential.rb +53 -0
- data/lib/charming/internal/renderer/full_repaint.rb +19 -0
- data/lib/charming/internal/terminal/adapter.rb +52 -0
- data/lib/charming/internal/terminal/memory_backend.rb +91 -0
- data/lib/charming/internal/terminal/tty_backend.rb +250 -0
- data/lib/charming/key_event.rb +13 -0
- data/lib/charming/mouse_event.rb +40 -0
- data/lib/charming/resize_event.rb +7 -0
- data/lib/charming/response.rb +33 -0
- data/lib/charming/router.rb +137 -0
- data/lib/charming/runtime.rb +192 -0
- data/lib/charming/screen.rb +8 -0
- data/lib/charming/task.rb +7 -0
- data/lib/charming/task_event.rb +17 -0
- data/lib/charming/task_executor.rb +62 -0
- data/lib/charming/timer_event.rb +7 -0
- data/lib/charming/ui/border.rb +33 -0
- data/lib/charming/ui/style.rb +244 -0
- data/lib/charming/ui/theme.rb +178 -0
- data/lib/charming/ui/themes/phosphor.json +100 -0
- data/lib/charming/ui/width.rb +24 -0
- data/lib/charming/ui.rb +230 -0
- data/lib/charming/version.rb +5 -0
- data/lib/charming/view.rb +116 -0
- data/lib/charming.rb +24 -0
- data/sig/charming.rbs +3 -0
- metadata +225 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class AppGenerator
|
|
6
|
+
module BasicTemplates
|
|
7
|
+
def gemfile
|
|
8
|
+
%(# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
source "https://rubygems.org"
|
|
11
|
+
|
|
12
|
+
gemspec
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rakefile
|
|
17
|
+
%(# frozen_string_literal: true
|
|
18
|
+
|
|
19
|
+
require "bundler/gem_tasks"
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def readme
|
|
24
|
+
%(# #{name.class_name}
|
|
25
|
+
|
|
26
|
+
A Charming terminal user interface.
|
|
27
|
+
|
|
28
|
+
Run it with:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
bundle exec #{name.snake_name}
|
|
32
|
+
```
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def gemspec
|
|
37
|
+
%(# frozen_string_literal: true
|
|
38
|
+
|
|
39
|
+
require_relative "lib/#{name.snake_name}/version"
|
|
40
|
+
|
|
41
|
+
Gem::Specification.new do |spec|
|
|
42
|
+
#{gemspec_attributes}
|
|
43
|
+
#{gemspec_dependencies}
|
|
44
|
+
end
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def gemspec_attributes
|
|
49
|
+
%( spec.name = "#{name.snake_name}"
|
|
50
|
+
spec.version = #{name.class_name}::VERSION
|
|
51
|
+
spec.summary = "A Charming terminal user interface."
|
|
52
|
+
spec.authors = ["TODO: Your name"]
|
|
53
|
+
spec.email = ["TODO: Your email"]
|
|
54
|
+
spec.files = Dir.glob("{app,config,exe,lib}/**/*") + %w[README.md]
|
|
55
|
+
spec.bindir = "exe"
|
|
56
|
+
spec.executables = ["#{name.snake_name}"]
|
|
57
|
+
spec.require_paths = ["lib"]
|
|
58
|
+
spec.required_ruby_version = ">= 4.0.0"
|
|
59
|
+
spec.metadata["rubygems_mfa_required"] = "true")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def gemspec_dependencies
|
|
63
|
+
%(
|
|
64
|
+
spec.add_dependency "charming")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class AppGenerator
|
|
6
|
+
module ComponentTemplates
|
|
7
|
+
def component
|
|
8
|
+
%(# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
module #{name.class_name}
|
|
11
|
+
class AppFrameComponent < Charming::Component
|
|
12
|
+
def render
|
|
13
|
+
column(title_line, help_line, gap: 1)
|
|
14
|
+
end
|
|
15
|
+
#{component_helpers}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def component_helpers
|
|
22
|
+
%(
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def title_line
|
|
26
|
+
text title, style: theme.title
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def help_line
|
|
30
|
+
text "Press p for commands, q to quit.", style: theme.muted
|
|
31
|
+
end)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class AppGenerator
|
|
6
|
+
module ControllerTemplate
|
|
7
|
+
def application_controller
|
|
8
|
+
%(# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
module #{name.class_name}
|
|
11
|
+
class ApplicationController < Charming::Controller
|
|
12
|
+
layout Layouts::Application
|
|
13
|
+
focus_ring :sidebar, :content
|
|
14
|
+
|
|
15
|
+
key "p", :open_command_palette, scope: :global
|
|
16
|
+
key "q", :quit, scope: :global
|
|
17
|
+
|
|
18
|
+
command "Home" do
|
|
19
|
+
navigate_to "/"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
command "Theme", :open_theme_palette
|
|
23
|
+
command "Close palette", :close_command_palette
|
|
24
|
+
command "Quit app", :quit
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def controller
|
|
31
|
+
%(# frozen_string_literal: true
|
|
32
|
+
|
|
33
|
+
module #{name.class_name}
|
|
34
|
+
class HomeController < ApplicationController
|
|
35
|
+
#{controller_actions}
|
|
36
|
+
#{controller_helpers}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def controller_actions
|
|
43
|
+
%(
|
|
44
|
+
def show
|
|
45
|
+
render_home
|
|
46
|
+
end)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def controller_helpers
|
|
50
|
+
%(
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
#{render_helpers})
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def render_helpers
|
|
57
|
+
%(
|
|
58
|
+
def render_home
|
|
59
|
+
render HomeView.new(home: home, palette: command_palette, screen: screen, theme: theme)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def home
|
|
63
|
+
model(:home, HomeModel)
|
|
64
|
+
end)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class AppGenerator
|
|
6
|
+
module LayoutTemplate
|
|
7
|
+
def layout
|
|
8
|
+
%(# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
module #{name.class_name}
|
|
11
|
+
module Layouts
|
|
12
|
+
class Application < Charming::View
|
|
13
|
+
def render
|
|
14
|
+
body = Charming::UI.place(app_frame, width: screen.width, height: screen.height)
|
|
15
|
+
return body unless palette
|
|
16
|
+
|
|
17
|
+
Charming::UI.overlay(body, command_palette_modal)
|
|
18
|
+
end
|
|
19
|
+
#{layout_helpers}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def layout_helpers
|
|
27
|
+
%(
|
|
28
|
+
private
|
|
29
|
+
#{layout_frame_helpers}
|
|
30
|
+
#{layout_navigation_helpers}
|
|
31
|
+
#{layout_modal_helpers}
|
|
32
|
+
#{layout_style_helpers})
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def layout_frame_helpers
|
|
36
|
+
%(
|
|
37
|
+
def app_frame
|
|
38
|
+
narrow? ? column(sidebar, main_content, gap: 1) : row(sidebar, main_content, gap: 1)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def sidebar
|
|
42
|
+
box(column(app_title, navigation, shortcuts, gap: 1), style: sidebar_style)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def main_content
|
|
46
|
+
box(yield_content, style: main_content_style)
|
|
47
|
+
end)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def layout_navigation_helpers
|
|
51
|
+
%(
|
|
52
|
+
|
|
53
|
+
def app_title
|
|
54
|
+
text "#{name.class_name}", style: theme.header_accent.align(:center).width(sidebar_width)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def navigation
|
|
58
|
+
column(*nav_items)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def nav_items
|
|
62
|
+
controller.application.routes.all.each_with_index.map do |route, index|
|
|
63
|
+
text nav_label(route, index), style: nav_style(route, index)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def nav_label(route, index)
|
|
68
|
+
cursor = sidebar_focused? && index == sidebar_index ? ">" : " "
|
|
69
|
+
active = current_route?(route) ? "●" : " "
|
|
70
|
+
"\#{cursor} \#{active} \#{route.title}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def nav_style(route, index)
|
|
74
|
+
return theme.selected if sidebar_focused? && index == sidebar_index
|
|
75
|
+
return theme.title if current_route?(route)
|
|
76
|
+
|
|
77
|
+
theme.muted
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def shortcuts
|
|
81
|
+
text "tab focus\\np commands\\nq quit", style: theme.muted
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def sidebar_focused?
|
|
85
|
+
controller.sidebar_focused?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def content_focused?
|
|
89
|
+
controller.content_focused?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def sidebar_index
|
|
93
|
+
controller.sidebar_index
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def current_route?(route)
|
|
97
|
+
route.controller_class == controller.class && route.action == :show
|
|
98
|
+
end)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def layout_modal_helpers
|
|
102
|
+
%(
|
|
103
|
+
|
|
104
|
+
def command_palette_modal
|
|
105
|
+
render_component Charming::Components::Modal.new(
|
|
106
|
+
content: palette,
|
|
107
|
+
title: "Command palette",
|
|
108
|
+
help: "Type to filter. Enter selects. Escape closes.",
|
|
109
|
+
width: 52,
|
|
110
|
+
theme: theme
|
|
111
|
+
)
|
|
112
|
+
end)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def layout_style_helpers
|
|
116
|
+
%(
|
|
117
|
+
#{layout_frame_style_helpers}
|
|
118
|
+
#{layout_dimension_helpers})
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def layout_frame_style_helpers
|
|
122
|
+
%(
|
|
123
|
+
def sidebar_style
|
|
124
|
+
base = sidebar_focused? ? theme.title : theme.border
|
|
125
|
+
base = base.border(:rounded).padding(1, 2).width(sidebar_width).height(panel_height)
|
|
126
|
+
palette ? base.faint : base
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def main_content_style
|
|
130
|
+
base = content_focused? ? theme.title : theme.border
|
|
131
|
+
base = base.border(:rounded).padding(1, 2).width(main_content_width).height(panel_height)
|
|
132
|
+
palette ? base.faint : base
|
|
133
|
+
end)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def layout_dimension_helpers
|
|
137
|
+
%(
|
|
138
|
+
|
|
139
|
+
def narrow?
|
|
140
|
+
screen.width < 72 && screen.height >= 20
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def sidebar_width
|
|
144
|
+
narrow? ? [screen.width - 6, 20].max : 22
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def main_content_width
|
|
148
|
+
narrow? ? [screen.width - 6, 20].max : [screen.width - sidebar_width - 13, 20].max
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def panel_height
|
|
152
|
+
return nil if narrow?
|
|
153
|
+
|
|
154
|
+
[screen.height - 4, 5].max
|
|
155
|
+
end)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class AppGenerator
|
|
6
|
+
module ModelTemplates
|
|
7
|
+
def application_model
|
|
8
|
+
%(# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
module #{name.class_name}
|
|
11
|
+
class ApplicationModel < Charming::ApplicationModel
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def home_model
|
|
18
|
+
%(# frozen_string_literal: true
|
|
19
|
+
|
|
20
|
+
module #{name.class_name}
|
|
21
|
+
class HomeModel < ApplicationModel
|
|
22
|
+
attribute :title, :string, default: "#{name.class_name}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class AppGenerator
|
|
6
|
+
module ScreenSpecTemplates
|
|
7
|
+
def spec_model
|
|
8
|
+
%(# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
require "#{app_name.snake_name}"
|
|
11
|
+
|
|
12
|
+
RSpec.describe #{app_name.class_name}::#{name.class_name}Model do
|
|
13
|
+
describe "#title" do
|
|
14
|
+
it "has the correct default string value" do
|
|
15
|
+
instance = described_class.new
|
|
16
|
+
expect(instance.title).to eq("#{name.class_name}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "accepts overridden title values" do
|
|
20
|
+
instance = described_class.new(title: "Alternative")
|
|
21
|
+
expect(instance.title).to eq("Alternative")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def spec_controller
|
|
29
|
+
%(# frozen_string_literal: true
|
|
30
|
+
|
|
31
|
+
require "#{app_name.snake_name}"
|
|
32
|
+
|
|
33
|
+
RSpec.describe #{app_name.class_name}::#{name.controller_class_name} do
|
|
34
|
+
let(:application) { #{app_name.class_name}::Application.new }
|
|
35
|
+
|
|
36
|
+
subject(:controller) { described_class.new(application: application) }
|
|
37
|
+
|
|
38
|
+
describe "#show" do
|
|
39
|
+
it "renders the view with the model" do
|
|
40
|
+
response = controller.dispatch(:show)
|
|
41
|
+
|
|
42
|
+
expect(response).to respond_to(:body)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def spec_view
|
|
50
|
+
%(# frozen_string_literal: true
|
|
51
|
+
|
|
52
|
+
require "#{app_name.snake_name}"
|
|
53
|
+
|
|
54
|
+
RSpec.describe #{app_name.class_name}::#{name.view_class_name} do
|
|
55
|
+
describe "#render" do
|
|
56
|
+
it "renders the model title" do
|
|
57
|
+
view = described_class.new(
|
|
58
|
+
#{name.snake_name}: double(title: "#{name.class_name}")
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
expect(view.render).to eq("#{name.class_name}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class AppGenerator
|
|
6
|
+
module ViewTemplate
|
|
7
|
+
def executable
|
|
8
|
+
%(#!/usr/bin/env ruby
|
|
9
|
+
# frozen_string_literal: true
|
|
10
|
+
|
|
11
|
+
require "bundler/setup"
|
|
12
|
+
require "#{name.snake_name}"
|
|
13
|
+
|
|
14
|
+
Charming.run(#{name.class_name}::Application.new)
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def root_file
|
|
19
|
+
%(# frozen_string_literal: true
|
|
20
|
+
|
|
21
|
+
require "charming"
|
|
22
|
+
require "zeitwerk"
|
|
23
|
+
|
|
24
|
+
module #{name.class_name}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
loader = Zeitwerk::Loader.new
|
|
28
|
+
loader.tag = "#{name.snake_name}"
|
|
29
|
+
loader.inflector.inflect("version" => "VERSION")
|
|
30
|
+
loader.push_dir(File.expand_path("#{name.snake_name}", __dir__), namespace: #{name.class_name})
|
|
31
|
+
loader.push_dir(File.expand_path("../app/models", __dir__), namespace: #{name.class_name})
|
|
32
|
+
loader.push_dir(File.expand_path("../app/components", __dir__), namespace: #{name.class_name})
|
|
33
|
+
loader.push_dir(File.expand_path("../app/views", __dir__), namespace: #{name.class_name})
|
|
34
|
+
loader.push_dir(File.expand_path("../app/controllers", __dir__), namespace: #{name.class_name})
|
|
35
|
+
loader.setup
|
|
36
|
+
|
|
37
|
+
require_relative "../config/routes"
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def application
|
|
42
|
+
%(# frozen_string_literal: true
|
|
43
|
+
|
|
44
|
+
module #{name.class_name}
|
|
45
|
+
class Application < Charming::Application
|
|
46
|
+
Charming::UI::Theme.built_in_names.each do |theme_name|
|
|
47
|
+
theme theme_name.to_sym, built_in: theme_name
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
default_theme :phosphor
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def version
|
|
57
|
+
%(# frozen_string_literal: true
|
|
58
|
+
|
|
59
|
+
module #{name.class_name}
|
|
60
|
+
VERSION = "0.1.0"
|
|
61
|
+
end
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def view
|
|
66
|
+
%(# frozen_string_literal: true
|
|
67
|
+
|
|
68
|
+
module #{name.class_name}
|
|
69
|
+
class HomeView < Charming::View
|
|
70
|
+
def render
|
|
71
|
+
app_frame
|
|
72
|
+
end
|
|
73
|
+
#{view_helpers}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def view_helpers
|
|
80
|
+
%(
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def app_frame
|
|
84
|
+
render_component AppFrameComponent.new(title: home.title, theme: theme)
|
|
85
|
+
end)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class AppGenerator < Base
|
|
6
|
+
include BasicTemplates
|
|
7
|
+
include ComponentTemplates
|
|
8
|
+
include ControllerTemplate
|
|
9
|
+
include LayoutTemplate
|
|
10
|
+
include ModelTemplates
|
|
11
|
+
include ScreenSpecTemplates
|
|
12
|
+
include ViewTemplate
|
|
13
|
+
include AppSpecTemplates
|
|
14
|
+
|
|
15
|
+
FILE_TEMPLATES = [
|
|
16
|
+
["Gemfile", :gemfile],
|
|
17
|
+
["Rakefile", :rakefile],
|
|
18
|
+
["README.md", :readme],
|
|
19
|
+
["%<name>s.gemspec", :gemspec],
|
|
20
|
+
["exe/%<name>s", :executable],
|
|
21
|
+
["lib/%<name>s.rb", :root_file],
|
|
22
|
+
["lib/%<name>s/application.rb", :application],
|
|
23
|
+
["lib/%<name>s/version.rb", :version],
|
|
24
|
+
["config/routes.rb", :routes],
|
|
25
|
+
["app/models/application_model.rb", :application_model],
|
|
26
|
+
["app/models/home_model.rb", :home_model],
|
|
27
|
+
["app/controllers/application_controller.rb", :application_controller],
|
|
28
|
+
["app/controllers/home_controller.rb", :controller],
|
|
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]
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
def initialize(name, out:, destination:, force: false)
|
|
40
|
+
super(out: out, destination: File.join(destination, name), force: force)
|
|
41
|
+
@name = Name.new(name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def generate
|
|
45
|
+
FILE_TEMPLATES.each do |path, template|
|
|
46
|
+
create_file(file_path(path), send(template), executable: template == :executable)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
attr_reader :name
|
|
53
|
+
alias_method :app_name, :name
|
|
54
|
+
|
|
55
|
+
def file_path(path)
|
|
56
|
+
format(path, name: name.snake_name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def routes
|
|
60
|
+
%(# frozen_string_literal: true
|
|
61
|
+
|
|
62
|
+
#{name.class_name}::Application.routes do
|
|
63
|
+
root "home#show"
|
|
64
|
+
end
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def spec_helper
|
|
69
|
+
%(# frozen_string_literal: true
|
|
70
|
+
|
|
71
|
+
require "#{name.snake_name}"
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Charming
|
|
6
|
+
module Generators
|
|
7
|
+
class Base
|
|
8
|
+
def initialize(out:, destination:, force: false)
|
|
9
|
+
@out = out
|
|
10
|
+
@destination = destination
|
|
11
|
+
@force = force
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
attr_reader :out, :destination
|
|
17
|
+
|
|
18
|
+
def create_file(path, content, executable: false)
|
|
19
|
+
absolute_path = File.join(destination, path)
|
|
20
|
+
raise Error, "File already exists: #{path}" if File.exist?(absolute_path) && !@force
|
|
21
|
+
|
|
22
|
+
FileUtils.mkdir_p(File.dirname(absolute_path))
|
|
23
|
+
File.write(absolute_path, content)
|
|
24
|
+
FileUtils.chmod("u+x,go+rx", absolute_path) if executable
|
|
25
|
+
out.puts "create #{path}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class ComponentGenerator < AppFileGenerator
|
|
6
|
+
def generate
|
|
7
|
+
create_file(app_path("app", "components"), component)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def suffix
|
|
13
|
+
"component"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def component
|
|
17
|
+
%(# frozen_string_literal: true
|
|
18
|
+
|
|
19
|
+
module #{app_name.class_name}
|
|
20
|
+
class #{name.component_class_name} < Charming::Component
|
|
21
|
+
def render
|
|
22
|
+
text "#{name.class_name}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class ControllerGenerator < AppFileGenerator
|
|
6
|
+
def initialize(name, args, out:, destination:, force: false)
|
|
7
|
+
super
|
|
8
|
+
@actions = args
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def generate
|
|
12
|
+
create_file(app_path("app", "controllers"), controller)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :actions
|
|
18
|
+
|
|
19
|
+
def suffix
|
|
20
|
+
"controller"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def controller
|
|
24
|
+
%(# frozen_string_literal: true
|
|
25
|
+
|
|
26
|
+
module #{app_name.class_name}
|
|
27
|
+
class #{name.controller_class_name} < ApplicationController
|
|
28
|
+
#{action_methods} end
|
|
29
|
+
end
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def action_methods
|
|
34
|
+
return action_method("show") if actions.empty?
|
|
35
|
+
|
|
36
|
+
actions.map { |action| action_method(action) }.join("\n")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def action_method(action)
|
|
40
|
+
%( def #{action}
|
|
41
|
+
render #{name.view_class_name}.new(
|
|
42
|
+
palette: command_palette,
|
|
43
|
+
screen: screen
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|