charming 0.1.0 → 0.1.1
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 +3 -3
- data/lib/charming/{application_model.rb → application_state.rb} +3 -3
- data/lib/charming/cli.rb +39 -3
- data/lib/charming/controller.rb +146 -24
- data/lib/charming/database_commands.rb +87 -0
- data/lib/charming/database_installer.rb +125 -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/generators/app_generator/app_spec_templates.rb +12 -8
- data/lib/charming/generators/app_generator/basic_templates.rb +14 -2
- data/lib/charming/generators/app_generator/component_templates.rb +1 -1
- data/lib/charming/generators/app_generator/controller_template.rb +3 -12
- data/lib/charming/generators/app_generator/database_templates.rb +45 -0
- data/lib/charming/generators/app_generator/layout_template.rb +51 -145
- data/lib/charming/generators/app_generator/screen_spec_templates.rb +7 -8
- data/lib/charming/generators/app_generator/{model_templates.rb → state_templates.rb} +5 -5
- data/lib/charming/generators/app_generator/view_template.rb +12 -18
- data/lib/charming/generators/app_generator.rb +37 -11
- data/lib/charming/generators/component_generator.rb +1 -1
- data/lib/charming/generators/controller_generator.rb +1 -4
- data/lib/charming/generators/model_generator.rb +119 -0
- data/lib/charming/generators/name.rb +0 -4
- data/lib/charming/generators/screen_generator.rb +14 -28
- data/lib/charming/generators/view_generator.rb +11 -14
- data/lib/charming/internal/renderer/differential.rb +2 -3
- data/lib/charming/internal/terminal/tty_backend.rb +25 -8
- 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 +43 -0
- data/lib/charming/presentation/components/form/builder.rb +48 -0
- data/lib/charming/presentation/components/form/confirm.rb +56 -0
- data/lib/charming/presentation/components/form/field.rb +96 -0
- data/lib/charming/presentation/components/form/input.rb +57 -0
- data/lib/charming/presentation/components/form/note.rb +32 -0
- data/lib/charming/presentation/components/form/select.rb +89 -0
- data/lib/charming/presentation/components/form/textarea.rb +70 -0
- data/lib/charming/presentation/components/form.rb +127 -0
- data/lib/charming/presentation/components/keyboard_handler.rb +58 -0
- data/lib/charming/presentation/components/list.rb +104 -0
- data/lib/charming/presentation/components/markdown.rb +25 -0
- data/lib/charming/presentation/components/modal.rb +50 -0
- data/lib/charming/presentation/components/progressbar.rb +57 -0
- data/lib/charming/presentation/components/spinner.rb +39 -0
- data/lib/charming/presentation/components/table.rb +118 -0
- data/lib/charming/presentation/components/text_area.rb +219 -0
- data/lib/charming/presentation/components/text_input.rb +105 -0
- data/lib/charming/presentation/components/viewport.rb +220 -0
- data/lib/charming/presentation/layout.rb +43 -0
- data/lib/charming/presentation/markdown/renderer.rb +203 -0
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +63 -0
- data/lib/charming/presentation/markdown.rb +8 -0
- data/lib/charming/presentation/template_view.rb +27 -0
- data/lib/charming/presentation/templates/erb_handler.rb +15 -0
- data/lib/charming/presentation/templates.rb +51 -0
- data/lib/charming/presentation/ui/border.rb +35 -0
- data/lib/charming/presentation/ui/style.rb +246 -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 +232 -0
- data/lib/charming/presentation/view.rb +118 -0
- data/lib/charming/runtime.rb +7 -7
- data/lib/charming/screen.rb +5 -1
- data/lib/charming/tasks/inline_executor.rb +28 -0
- data/lib/charming/tasks/task.rb +9 -0
- data/lib/charming/{task_executor.rb → tasks/threaded_executor.rb} +4 -27
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +4 -0
- metadata +114 -29
- 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/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/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
|
@@ -9,7 +9,7 @@ module Charming
|
|
|
9
9
|
|
|
10
10
|
module #{name.class_name}
|
|
11
11
|
class ApplicationController < Charming::Controller
|
|
12
|
-
layout
|
|
12
|
+
layout "layouts/application"
|
|
13
13
|
focus_ring :sidebar, :content
|
|
14
14
|
|
|
15
15
|
key "p", :open_command_palette, scope: :global
|
|
@@ -42,7 +42,7 @@ end
|
|
|
42
42
|
def controller_actions
|
|
43
43
|
%(
|
|
44
44
|
def show
|
|
45
|
-
|
|
45
|
+
render :show, home: home, palette: command_palette
|
|
46
46
|
end)
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -50,17 +50,8 @@ end
|
|
|
50
50
|
%(
|
|
51
51
|
|
|
52
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
53
|
def home
|
|
63
|
-
|
|
54
|
+
state(:home, HomeState)
|
|
64
55
|
end)
|
|
65
56
|
end
|
|
66
57
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class AppGenerator
|
|
6
|
+
module DatabaseTemplates
|
|
7
|
+
def database_config
|
|
8
|
+
%(# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
require "active_record"
|
|
11
|
+
require "fileutils"
|
|
12
|
+
|
|
13
|
+
database_path = File.expand_path("../db/development.sqlite3", __dir__)
|
|
14
|
+
FileUtils.mkdir_p(File.dirname(database_path))
|
|
15
|
+
|
|
16
|
+
ActiveRecord::Base.establish_connection(
|
|
17
|
+
adapter: "sqlite3",
|
|
18
|
+
database: database_path
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def application_record
|
|
24
|
+
%(# frozen_string_literal: true
|
|
25
|
+
|
|
26
|
+
module #{name.class_name}
|
|
27
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
28
|
+
self.abstract_class = true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def keep
|
|
35
|
+
""
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def seeds
|
|
39
|
+
%(# frozen_string_literal: true
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -5,155 +5,61 @@ module Charming
|
|
|
5
5
|
class AppGenerator
|
|
6
6
|
module LayoutTemplate
|
|
7
7
|
def layout
|
|
8
|
-
%(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
#{
|
|
20
|
-
|
|
8
|
+
%(<%
|
|
9
|
+
palette_component = assigns.fetch(:palette, nil)
|
|
10
|
+
sidebar_focused = controller.sidebar_focused?
|
|
11
|
+
content_focused = controller.content_focused?
|
|
12
|
+
sidebar_index = controller.sidebar_index
|
|
13
|
+
narrow = screen.narrow?(below: 72, min_height: 20)
|
|
14
|
+
layout = Charming::Presentation::Layout
|
|
15
|
+
sidebar_width = narrow ? layout.available_width(screen, reserved: 6, min: 20) : 22
|
|
16
|
+
main_content_width = narrow ? layout.available_width(screen, reserved: 6, min: 20) : layout.clamp_size(screen.width - sidebar_width - 13, min: 20)
|
|
17
|
+
panel_height = narrow ? nil : layout.available_height(screen, reserved: 4, min: 5)
|
|
18
|
+
|
|
19
|
+
app_title = text "#{name.class_name}", style: theme.header_accent.align(:center).width(sidebar_width)
|
|
20
|
+
nav_items = controller.sidebar_routes.each_with_index.map do |route, index|
|
|
21
|
+
current_route = controller.current_route?(route)
|
|
22
|
+
cursor = sidebar_focused && index == sidebar_index ? ">" : " "
|
|
23
|
+
active = current_route ? "●" : " "
|
|
24
|
+
item_style = if sidebar_focused && index == sidebar_index
|
|
25
|
+
theme.selected
|
|
26
|
+
elsif current_route
|
|
27
|
+
theme.title
|
|
28
|
+
else
|
|
29
|
+
theme.muted
|
|
21
30
|
end
|
|
31
|
+
|
|
32
|
+
text "\#{cursor} \#{active} \#{route.title}", style: item_style
|
|
33
|
+
end
|
|
34
|
+
navigation = column(*nav_items)
|
|
35
|
+
shortcuts = text "tab focus\np commands\nq quit", style: theme.muted
|
|
36
|
+
|
|
37
|
+
sidebar_style = sidebar_focused ? theme.title : theme.border
|
|
38
|
+
sidebar_style = sidebar_style.border(:rounded).padding(1, 2).width(sidebar_width).height(panel_height)
|
|
39
|
+
sidebar_style = sidebar_style.faint if palette_component
|
|
40
|
+
|
|
41
|
+
main_content_style = content_focused ? theme.title : theme.border
|
|
42
|
+
main_content_style = main_content_style.border(:rounded).padding(1, 2).width(main_content_width).height(panel_height)
|
|
43
|
+
main_content_style = main_content_style.faint if palette_component
|
|
44
|
+
|
|
45
|
+
sidebar = box(column(app_title, navigation, shortcuts, gap: 1), style: sidebar_style)
|
|
46
|
+
main_content = box(yield_content, style: main_content_style)
|
|
47
|
+
app_frame = layout.stack_or_row(sidebar, main_content, narrow: narrow, gap: 1)
|
|
48
|
+
body = Charming::Presentation::UI.place(app_frame, width: screen.width, height: screen.height)
|
|
49
|
+
|
|
50
|
+
if palette_component
|
|
51
|
+
command_palette_modal = render_component Charming::Presentation::Components::Modal.new(
|
|
52
|
+
content: palette_component,
|
|
53
|
+
title: "Command palette",
|
|
54
|
+
help: "Type to filter. Enter selects. Escape closes.",
|
|
55
|
+
width: 52,
|
|
56
|
+
theme: theme
|
|
57
|
+
)
|
|
58
|
+
body = Charming::Presentation::UI.overlay(body, command_palette_modal)
|
|
22
59
|
end
|
|
60
|
+
%><%= body %>
|
|
23
61
|
)
|
|
24
62
|
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
63
|
end
|
|
158
64
|
end
|
|
159
65
|
end
|
|
@@ -4,12 +4,12 @@ module Charming
|
|
|
4
4
|
module Generators
|
|
5
5
|
class AppGenerator
|
|
6
6
|
module ScreenSpecTemplates
|
|
7
|
-
def
|
|
7
|
+
def spec_state
|
|
8
8
|
%(# frozen_string_literal: true
|
|
9
9
|
|
|
10
10
|
require "#{app_name.snake_name}"
|
|
11
11
|
|
|
12
|
-
RSpec.describe #{app_name.class_name}::#{name.class_name}
|
|
12
|
+
RSpec.describe #{app_name.class_name}::#{name.class_name}State do
|
|
13
13
|
describe "#title" do
|
|
14
14
|
it "has the correct default string value" do
|
|
15
15
|
instance = described_class.new
|
|
@@ -36,7 +36,7 @@ RSpec.describe #{app_name.class_name}::#{name.controller_class_name} do
|
|
|
36
36
|
subject(:controller) { described_class.new(application: application) }
|
|
37
37
|
|
|
38
38
|
describe "#show" do
|
|
39
|
-
it "renders the view with the
|
|
39
|
+
it "renders the view with the state" do
|
|
40
40
|
response = controller.dispatch(:show)
|
|
41
41
|
|
|
42
42
|
expect(response).to respond_to(:body)
|
|
@@ -51,12 +51,11 @@ end
|
|
|
51
51
|
|
|
52
52
|
require "#{app_name.snake_name}"
|
|
53
53
|
|
|
54
|
-
RSpec.describe #{
|
|
54
|
+
RSpec.describe "#{name.snake_name}/show template" do
|
|
55
55
|
describe "#render" do
|
|
56
|
-
it "renders the
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
56
|
+
it "renders the state title" do
|
|
57
|
+
template = Charming::Presentation::Templates.resolve("#{name.snake_name}/show", root: #{app_name.class_name}::Application.root)
|
|
58
|
+
view = Charming::Presentation::TemplateView.new(template: template, #{name.snake_name}: double(title: "#{name.class_name}"))
|
|
60
59
|
|
|
61
60
|
expect(view.render).to eq("#{name.class_name}")
|
|
62
61
|
end
|
|
@@ -3,22 +3,22 @@
|
|
|
3
3
|
module Charming
|
|
4
4
|
module Generators
|
|
5
5
|
class AppGenerator
|
|
6
|
-
module
|
|
7
|
-
def
|
|
6
|
+
module StateTemplates
|
|
7
|
+
def application_state
|
|
8
8
|
%(# frozen_string_literal: true
|
|
9
9
|
|
|
10
10
|
module #{name.class_name}
|
|
11
|
-
class
|
|
11
|
+
class ApplicationState < Charming::ApplicationState
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def home_state
|
|
18
18
|
%(# frozen_string_literal: true
|
|
19
19
|
|
|
20
20
|
module #{name.class_name}
|
|
21
|
-
class
|
|
21
|
+
class HomeState < ApplicationState
|
|
22
22
|
attribute :title, :string, default: "#{name.class_name}"
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -20,6 +20,7 @@ Charming.run(#{name.class_name}::Application.new)
|
|
|
20
20
|
|
|
21
21
|
require "charming"
|
|
22
22
|
require "zeitwerk"
|
|
23
|
+
#{database_require}
|
|
23
24
|
|
|
24
25
|
module #{name.class_name}
|
|
25
26
|
end
|
|
@@ -28,7 +29,7 @@ loader = Zeitwerk::Loader.new
|
|
|
28
29
|
loader.tag = "#{name.snake_name}"
|
|
29
30
|
loader.inflector.inflect("version" => "VERSION")
|
|
30
31
|
loader.push_dir(File.expand_path("#{name.snake_name}", __dir__), namespace: #{name.class_name})
|
|
31
|
-
loader.push_dir(File.expand_path("../app/
|
|
32
|
+
#{model_loader}loader.push_dir(File.expand_path("../app/state", __dir__), namespace: #{name.class_name})
|
|
32
33
|
loader.push_dir(File.expand_path("../app/components", __dir__), namespace: #{name.class_name})
|
|
33
34
|
loader.push_dir(File.expand_path("../app/views", __dir__), namespace: #{name.class_name})
|
|
34
35
|
loader.push_dir(File.expand_path("../app/controllers", __dir__), namespace: #{name.class_name})
|
|
@@ -43,7 +44,9 @@ require_relative "../config/routes"
|
|
|
43
44
|
|
|
44
45
|
module #{name.class_name}
|
|
45
46
|
class Application < Charming::Application
|
|
46
|
-
|
|
47
|
+
root File.expand_path("../..", __dir__)
|
|
48
|
+
|
|
49
|
+
Charming::Presentation::UI::Theme.built_in_names.each do |theme_name|
|
|
47
50
|
theme theme_name.to_sym, built_in: theme_name
|
|
48
51
|
end
|
|
49
52
|
|
|
@@ -63,26 +66,17 @@ end
|
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
def view
|
|
66
|
-
%(
|
|
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
|
|
69
|
+
%(<%= render_component AppFrameComponent.new(title: home.title, theme: theme) %>
|
|
76
70
|
)
|
|
77
71
|
end
|
|
78
72
|
|
|
79
|
-
def
|
|
80
|
-
%(
|
|
81
|
-
|
|
73
|
+
def database_require
|
|
74
|
+
database? ? %(require_relative "../config/database") : ""
|
|
75
|
+
end
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
def model_loader
|
|
78
|
+
database? ? %(loader.push_dir(File.expand_path("../app/models", __dir__), namespace: #{name.class_name})
|
|
79
|
+
) : ""
|
|
86
80
|
end
|
|
87
81
|
end
|
|
88
82
|
end
|
|
@@ -6,13 +6,14 @@ module Charming
|
|
|
6
6
|
include BasicTemplates
|
|
7
7
|
include ComponentTemplates
|
|
8
8
|
include ControllerTemplate
|
|
9
|
+
include DatabaseTemplates
|
|
9
10
|
include LayoutTemplate
|
|
10
|
-
include
|
|
11
|
+
include StateTemplates
|
|
11
12
|
include ScreenSpecTemplates
|
|
12
13
|
include ViewTemplate
|
|
13
14
|
include AppSpecTemplates
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
BASE_FILE_TEMPLATES = [
|
|
16
17
|
["Gemfile", :gemfile],
|
|
17
18
|
["Rakefile", :rakefile],
|
|
18
19
|
["README.md", :readme],
|
|
@@ -22,36 +23,53 @@ module Charming
|
|
|
22
23
|
["lib/%<name>s/application.rb", :application],
|
|
23
24
|
["lib/%<name>s/version.rb", :version],
|
|
24
25
|
["config/routes.rb", :routes],
|
|
25
|
-
["app/
|
|
26
|
-
["app/
|
|
26
|
+
["app/state/application_state.rb", :application_state],
|
|
27
|
+
["app/state/home_state.rb", :home_state],
|
|
27
28
|
["app/controllers/application_controller.rb", :application_controller],
|
|
28
29
|
["app/controllers/home_controller.rb", :controller],
|
|
29
|
-
["app/views/layouts/application.
|
|
30
|
-
["app/views/
|
|
30
|
+
["app/views/layouts/application.tui.erb", :layout],
|
|
31
|
+
["app/views/home/show.tui.erb", :view],
|
|
31
32
|
["app/components/app_frame_component.rb", :component],
|
|
32
33
|
["spec/spec_helper.rb", :spec_helper],
|
|
33
|
-
["spec/
|
|
34
|
+
["spec/state/home_state_spec.rb", :spec_state],
|
|
34
35
|
["spec/controllers/home_controller_spec.rb", :spec_controller],
|
|
35
|
-
["spec/views/
|
|
36
|
+
["spec/views/home/show_template_spec.rb", :spec_view],
|
|
36
37
|
["spec/components/app_frame_component_spec.rb", :spec_component]
|
|
37
38
|
].freeze
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
DATABASE_FILE_TEMPLATES = [
|
|
41
|
+
["config/database.rb", :database_config],
|
|
42
|
+
["app/models/application_record.rb", :application_record],
|
|
43
|
+
["db/migrate/.keep", :keep],
|
|
44
|
+
["db/seeds.rb", :seeds]
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
def initialize(name, out:, destination:, force: false, database: nil)
|
|
40
48
|
super(out: out, destination: File.join(destination, name), force: force)
|
|
41
49
|
@name = Name.new(name)
|
|
50
|
+
@database = database
|
|
42
51
|
end
|
|
43
52
|
|
|
44
53
|
def generate
|
|
45
|
-
|
|
54
|
+
file_templates.each do |path, template|
|
|
46
55
|
create_file(file_path(path), send(template), executable: template == :executable)
|
|
47
56
|
end
|
|
57
|
+
initialize_git_repository
|
|
48
58
|
end
|
|
49
59
|
|
|
50
60
|
private
|
|
51
61
|
|
|
52
|
-
attr_reader :name
|
|
62
|
+
attr_reader :name, :database
|
|
53
63
|
alias_method :app_name, :name
|
|
54
64
|
|
|
65
|
+
def database?
|
|
66
|
+
!!database
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def file_templates
|
|
70
|
+
database? ? BASE_FILE_TEMPLATES + DATABASE_FILE_TEMPLATES : BASE_FILE_TEMPLATES
|
|
71
|
+
end
|
|
72
|
+
|
|
55
73
|
def file_path(path)
|
|
56
74
|
format(path, name: name.snake_name)
|
|
57
75
|
end
|
|
@@ -71,6 +89,14 @@ end
|
|
|
71
89
|
require "#{name.snake_name}"
|
|
72
90
|
)
|
|
73
91
|
end
|
|
92
|
+
|
|
93
|
+
def initialize_git_repository
|
|
94
|
+
unless system("git", "init", chdir: destination, out: File::NULL, err: File::NULL)
|
|
95
|
+
raise Error, "Could not initialize git repository"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
out.puts "init git"
|
|
99
|
+
end
|
|
74
100
|
end
|
|
75
101
|
end
|
|
76
102
|
end
|
|
@@ -17,7 +17,7 @@ module Charming
|
|
|
17
17
|
%(# frozen_string_literal: true
|
|
18
18
|
|
|
19
19
|
module #{app_name.class_name}
|
|
20
|
-
class #{name.component_class_name} < Charming::Component
|
|
20
|
+
class #{name.component_class_name} < Charming::Presentation::Component
|
|
21
21
|
def render
|
|
22
22
|
text "#{name.class_name}"
|
|
23
23
|
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Generators
|
|
5
|
+
class ModelGenerator < AppFileGenerator
|
|
6
|
+
Field = Data.define(:name, :type)
|
|
7
|
+
VALID_TYPES = %w[string text integer float decimal boolean date datetime time].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(name, args, out:, destination:, force: false)
|
|
10
|
+
super
|
|
11
|
+
@fields = args.map { |arg| parse_field(arg) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate
|
|
15
|
+
raise Error, "Database support is not configured. Generate the app with --database sqlite3 first." unless database_configured?
|
|
16
|
+
|
|
17
|
+
create_file(model_path, model)
|
|
18
|
+
create_file(migration_path, migration)
|
|
19
|
+
create_file(spec_path, spec)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :fields
|
|
25
|
+
|
|
26
|
+
def suffix
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def model_path
|
|
31
|
+
File.join("app", "models", "#{name.snake_name}.rb")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def migration_path
|
|
35
|
+
File.join("db", "migrate", "#{timestamp}_create_#{table_name}.rb")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def spec_path
|
|
39
|
+
File.join("spec", "models", "#{name.snake_name}_spec.rb")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def model
|
|
43
|
+
%(# frozen_string_literal: true
|
|
44
|
+
|
|
45
|
+
module #{app_name.class_name}
|
|
46
|
+
class #{name.class_name} < ApplicationRecord
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def migration
|
|
53
|
+
%(# frozen_string_literal: true
|
|
54
|
+
|
|
55
|
+
class Create#{table_class_name} < ActiveRecord::Migration[8.1]
|
|
56
|
+
def change
|
|
57
|
+
create_table :#{table_name} do |t|
|
|
58
|
+
#{field_lines} t.timestamps
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def spec
|
|
66
|
+
%(# frozen_string_literal: true
|
|
67
|
+
|
|
68
|
+
require "#{app_name.snake_name}"
|
|
69
|
+
|
|
70
|
+
RSpec.describe #{app_name.class_name}::#{name.class_name} do
|
|
71
|
+
it "inherits from ApplicationRecord" do
|
|
72
|
+
expect(described_class.superclass).to eq(#{app_name.class_name}::ApplicationRecord)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def field_lines
|
|
79
|
+
fields.map { |field|
|
|
80
|
+
%( t.#{field.type} :#{field.name}
|
|
81
|
+
)
|
|
82
|
+
}.join
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parse_field(value)
|
|
86
|
+
field_name, type = value.split(":", 2)
|
|
87
|
+
raise Error, "Invalid field: #{value.inspect}" unless field_name && type
|
|
88
|
+
raise Error, "Invalid field name: #{field_name.inspect}" unless Name::VALID_NAME.match?(field_name)
|
|
89
|
+
raise Error, "Unsupported field type: #{type.inspect}" unless VALID_TYPES.include?(type)
|
|
90
|
+
|
|
91
|
+
Field.new(name: field_name, type: type)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def database_configured?
|
|
95
|
+
File.exist?(File.join(destination, "config", "database.rb")) &&
|
|
96
|
+
File.exist?(File.join(destination, "app", "models", "application_record.rb"))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def table_name
|
|
100
|
+
pluralize(name.snake_name)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def table_class_name
|
|
104
|
+
table_name.split("_").map(&:capitalize).join
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def pluralize(value)
|
|
108
|
+
return value.sub(/y\z/, "ies") if value.end_with?("y")
|
|
109
|
+
return "#{value}es" if value.match?(/(?:s|x|z|ch|sh)\z/)
|
|
110
|
+
|
|
111
|
+
"#{value}s"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def timestamp
|
|
115
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|