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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -378
  3. data/lib/charming/application.rb +3 -3
  4. data/lib/charming/{application_model.rb → application_state.rb} +3 -3
  5. data/lib/charming/cli.rb +39 -3
  6. data/lib/charming/controller.rb +146 -24
  7. data/lib/charming/database_commands.rb +87 -0
  8. data/lib/charming/database_installer.rb +125 -0
  9. data/lib/charming/events/key_event.rb +15 -0
  10. data/lib/charming/events/mouse_event.rb +42 -0
  11. data/lib/charming/events/resize_event.rb +9 -0
  12. data/lib/charming/events/task_event.rb +19 -0
  13. data/lib/charming/events/timer_event.rb +9 -0
  14. data/lib/charming/generators/app_generator/app_spec_templates.rb +12 -8
  15. data/lib/charming/generators/app_generator/basic_templates.rb +14 -2
  16. data/lib/charming/generators/app_generator/component_templates.rb +1 -1
  17. data/lib/charming/generators/app_generator/controller_template.rb +3 -12
  18. data/lib/charming/generators/app_generator/database_templates.rb +45 -0
  19. data/lib/charming/generators/app_generator/layout_template.rb +51 -145
  20. data/lib/charming/generators/app_generator/screen_spec_templates.rb +7 -8
  21. data/lib/charming/generators/app_generator/{model_templates.rb → state_templates.rb} +5 -5
  22. data/lib/charming/generators/app_generator/view_template.rb +12 -18
  23. data/lib/charming/generators/app_generator.rb +37 -11
  24. data/lib/charming/generators/component_generator.rb +1 -1
  25. data/lib/charming/generators/controller_generator.rb +1 -4
  26. data/lib/charming/generators/model_generator.rb +119 -0
  27. data/lib/charming/generators/name.rb +0 -4
  28. data/lib/charming/generators/screen_generator.rb +14 -28
  29. data/lib/charming/generators/view_generator.rb +11 -14
  30. data/lib/charming/internal/renderer/differential.rb +2 -3
  31. data/lib/charming/internal/terminal/tty_backend.rb +25 -8
  32. data/lib/charming/presentation/component.rb +10 -0
  33. data/lib/charming/presentation/components/activity_indicator.rb +160 -0
  34. data/lib/charming/presentation/components/command_palette.rb +120 -0
  35. data/lib/charming/presentation/components/empty_state.rb +43 -0
  36. data/lib/charming/presentation/components/form/builder.rb +48 -0
  37. data/lib/charming/presentation/components/form/confirm.rb +56 -0
  38. data/lib/charming/presentation/components/form/field.rb +96 -0
  39. data/lib/charming/presentation/components/form/input.rb +57 -0
  40. data/lib/charming/presentation/components/form/note.rb +32 -0
  41. data/lib/charming/presentation/components/form/select.rb +89 -0
  42. data/lib/charming/presentation/components/form/textarea.rb +70 -0
  43. data/lib/charming/presentation/components/form.rb +127 -0
  44. data/lib/charming/presentation/components/keyboard_handler.rb +58 -0
  45. data/lib/charming/presentation/components/list.rb +104 -0
  46. data/lib/charming/presentation/components/markdown.rb +25 -0
  47. data/lib/charming/presentation/components/modal.rb +50 -0
  48. data/lib/charming/presentation/components/progressbar.rb +57 -0
  49. data/lib/charming/presentation/components/spinner.rb +39 -0
  50. data/lib/charming/presentation/components/table.rb +118 -0
  51. data/lib/charming/presentation/components/text_area.rb +219 -0
  52. data/lib/charming/presentation/components/text_input.rb +105 -0
  53. data/lib/charming/presentation/components/viewport.rb +220 -0
  54. data/lib/charming/presentation/layout.rb +43 -0
  55. data/lib/charming/presentation/markdown/renderer.rb +203 -0
  56. data/lib/charming/presentation/markdown/syntax_highlighter.rb +63 -0
  57. data/lib/charming/presentation/markdown.rb +8 -0
  58. data/lib/charming/presentation/template_view.rb +27 -0
  59. data/lib/charming/presentation/templates/erb_handler.rb +15 -0
  60. data/lib/charming/presentation/templates.rb +51 -0
  61. data/lib/charming/presentation/ui/border.rb +35 -0
  62. data/lib/charming/presentation/ui/style.rb +246 -0
  63. data/lib/charming/presentation/ui/theme.rb +180 -0
  64. data/lib/charming/{ui → presentation/ui}/themes/phosphor.json +2 -2
  65. data/lib/charming/presentation/ui/width.rb +26 -0
  66. data/lib/charming/presentation/ui.rb +232 -0
  67. data/lib/charming/presentation/view.rb +118 -0
  68. data/lib/charming/runtime.rb +7 -7
  69. data/lib/charming/screen.rb +5 -1
  70. data/lib/charming/tasks/inline_executor.rb +28 -0
  71. data/lib/charming/tasks/task.rb +9 -0
  72. data/lib/charming/{task_executor.rb → tasks/threaded_executor.rb} +4 -27
  73. data/lib/charming/version.rb +1 -1
  74. data/lib/charming.rb +4 -0
  75. metadata +114 -29
  76. data/lib/charming/component.rb +0 -8
  77. data/lib/charming/components/activity_indicator.rb +0 -158
  78. data/lib/charming/components/command_palette.rb +0 -118
  79. data/lib/charming/components/keyboard_handler.rb +0 -22
  80. data/lib/charming/components/list.rb +0 -105
  81. data/lib/charming/components/modal.rb +0 -48
  82. data/lib/charming/components/progressbar.rb +0 -55
  83. data/lib/charming/components/spinner.rb +0 -37
  84. data/lib/charming/components/table.rb +0 -115
  85. data/lib/charming/components/text_input.rb +0 -103
  86. data/lib/charming/components/viewport.rb +0 -191
  87. data/lib/charming/key_event.rb +0 -13
  88. data/lib/charming/mouse_event.rb +0 -40
  89. data/lib/charming/resize_event.rb +0 -7
  90. data/lib/charming/task.rb +0 -7
  91. data/lib/charming/task_event.rb +0 -17
  92. data/lib/charming/timer_event.rb +0 -7
  93. data/lib/charming/ui/border.rb +0 -33
  94. data/lib/charming/ui/style.rb +0 -244
  95. data/lib/charming/ui/theme.rb +0 -178
  96. data/lib/charming/ui/width.rb +0 -24
  97. data/lib/charming/ui.rb +0 -230
  98. data/lib/charming/view.rb +0 -116
  99. /data/lib/charming/{generators.rb → generators/error.rb} +0 -0
@@ -8,7 +8,7 @@ module Charming
8
8
  %(# frozen_string_literal: true
9
9
 
10
10
  module #{name.class_name}
11
- class AppFrameComponent < Charming::Component
11
+ class AppFrameComponent < Charming::Presentation::Component
12
12
  def render
13
13
  column(title_line, help_line, gap: 1)
14
14
  end
@@ -9,7 +9,7 @@ module Charming
9
9
 
10
10
  module #{name.class_name}
11
11
  class ApplicationController < Charming::Controller
12
- layout Layouts::Application
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
- render_home
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
- model(:home, HomeModel)
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
- %(# 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
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 spec_model
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}Model do
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 model" do
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 #{app_name.class_name}::#{name.view_class_name} do
54
+ RSpec.describe "#{name.snake_name}/show template" do
55
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
- )
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 ModelTemplates
7
- def application_model
6
+ module StateTemplates
7
+ def application_state
8
8
  %(# frozen_string_literal: true
9
9
 
10
10
  module #{name.class_name}
11
- class ApplicationModel < Charming::ApplicationModel
11
+ class ApplicationState < Charming::ApplicationState
12
12
  end
13
13
  end
14
14
  )
15
15
  end
16
16
 
17
- def home_model
17
+ def home_state
18
18
  %(# frozen_string_literal: true
19
19
 
20
20
  module #{name.class_name}
21
- class HomeModel < ApplicationModel
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/models", __dir__), namespace: #{name.class_name})
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
- Charming::UI::Theme.built_in_names.each do |theme_name|
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
- %(# 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
69
+ %(<%= render_component AppFrameComponent.new(title: home.title, theme: theme) %>
76
70
  )
77
71
  end
78
72
 
79
- def view_helpers
80
- %(
81
- private
73
+ def database_require
74
+ database? ? %(require_relative "../config/database") : ""
75
+ end
82
76
 
83
- def app_frame
84
- render_component AppFrameComponent.new(title: home.title, theme: theme)
85
- end)
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 ModelTemplates
11
+ include StateTemplates
11
12
  include ScreenSpecTemplates
12
13
  include ViewTemplate
13
14
  include AppSpecTemplates
14
15
 
15
- FILE_TEMPLATES = [
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/models/application_model.rb", :application_model],
26
- ["app/models/home_model.rb", :home_model],
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.rb", :layout],
30
- ["app/views/home_view.rb", :view],
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/models/home_model_spec.rb", :spec_model],
34
+ ["spec/state/home_state_spec.rb", :spec_state],
34
35
  ["spec/controllers/home_controller_spec.rb", :spec_controller],
35
- ["spec/views/home_view_spec.rb", :spec_view],
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
- def initialize(name, out:, destination:, force: false)
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
- FILE_TEMPLATES.each do |path, template|
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
@@ -38,10 +38,7 @@ end
38
38
 
39
39
  def action_method(action)
40
40
  %( def #{action}
41
- render #{name.view_class_name}.new(
42
- palette: command_palette,
43
- screen: screen
44
- )
41
+ render :#{action}, palette: command_palette
45
42
  end
46
43
  )
47
44
  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
@@ -20,10 +20,6 @@ module Charming
20
20
  "#{class_name}Controller"
21
21
  end
22
22
 
23
- def view_class_name
24
- "#{class_name}View"
25
- end
26
-
27
23
  def component_class_name
28
24
  "#{class_name}Component"
29
25
  end