charming 0.1.1 → 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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/charming/application.rb +11 -0
  4. data/lib/charming/cli.rb +23 -0
  5. data/lib/charming/controller/class_methods.rb +115 -0
  6. data/lib/charming/controller/command_palette.rb +135 -0
  7. data/lib/charming/controller/component_dispatching.rb +81 -0
  8. data/lib/charming/controller/dispatching.rb +60 -0
  9. data/lib/charming/controller/focus_management.rb +30 -0
  10. data/lib/charming/controller/rendering.rb +127 -0
  11. data/lib/charming/controller/session_state.rb +41 -0
  12. data/lib/charming/controller/sidebar_navigation.rb +111 -0
  13. data/lib/charming/controller.rb +35 -559
  14. data/lib/charming/database_commands.rb +16 -0
  15. data/lib/charming/database_installer.rb +27 -0
  16. data/lib/charming/focus.rb +58 -2
  17. data/lib/charming/generators/app_file_generator.rb +13 -0
  18. data/lib/charming/generators/app_generator.rb +123 -47
  19. data/lib/charming/generators/base.rb +26 -0
  20. data/lib/charming/generators/component_generator.rb +10 -10
  21. data/lib/charming/generators/controller_generator.rb +22 -11
  22. data/lib/charming/generators/model_generator.rb +38 -29
  23. data/lib/charming/generators/name.rb +10 -0
  24. data/lib/charming/generators/screen_generator.rb +78 -32
  25. data/lib/charming/generators/templates/app/Gemfile.template +5 -0
  26. data/lib/charming/generators/templates/app/README.md.template +9 -0
  27. data/lib/charming/generators/templates/app/Rakefile.template +3 -0
  28. data/lib/charming/generators/templates/app/application.template +13 -0
  29. data/lib/charming/generators/templates/app/application_controller.template +19 -0
  30. data/lib/charming/generators/templates/app/application_record.template +7 -0
  31. data/lib/charming/generators/templates/app/application_state.template +6 -0
  32. data/lib/charming/generators/templates/app/database_config.template +12 -0
  33. data/lib/charming/generators/templates/app/executable.template +7 -0
  34. data/lib/charming/generators/templates/app/gemspec.template +6 -0
  35. data/lib/charming/generators/templates/app/home_controller.template +6 -0
  36. data/lib/charming/generators/templates/app/home_state.template +7 -0
  37. data/lib/charming/generators/templates/app/keep.template +0 -0
  38. data/lib/charming/generators/templates/app/layout.template +113 -0
  39. data/lib/charming/generators/templates/app/root_file.template +20 -0
  40. data/lib/charming/generators/templates/app/routes.template +5 -0
  41. data/lib/charming/generators/templates/app/seeds.template +1 -0
  42. data/lib/charming/generators/templates/app/spec_controller.template +17 -0
  43. data/lib/charming/generators/templates/app/spec_helper.template +3 -0
  44. data/lib/charming/generators/templates/app/spec_state.template +17 -0
  45. data/lib/charming/generators/templates/app/spec_view.template +16 -0
  46. data/lib/charming/generators/templates/app/version.template +5 -0
  47. data/lib/charming/generators/templates/app/view.template +21 -0
  48. data/lib/charming/generators/templates/component/component.rb.template +9 -0
  49. data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
  50. data/lib/charming/generators/templates/model/migration.rb.template +9 -0
  51. data/lib/charming/generators/templates/model/model.rb.template +6 -0
  52. data/lib/charming/generators/templates/model/spec.rb.template +9 -0
  53. data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
  54. data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
  55. data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
  56. data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
  57. data/lib/charming/generators/templates/screen/state.rb.template +7 -0
  58. data/lib/charming/generators/templates/screen/view.rb.template +11 -0
  59. data/lib/charming/generators/templates/view/view.rb.template +11 -0
  60. data/lib/charming/generators/view_generator.rb +19 -3
  61. data/lib/charming/internal/renderer/differential.rb +15 -0
  62. data/lib/charming/internal/renderer/full_repaint.rb +6 -0
  63. data/lib/charming/internal/terminal/adapter.rb +29 -3
  64. data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
  65. data/lib/charming/internal/terminal/memory_backend.rb +28 -1
  66. data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
  67. data/lib/charming/internal/terminal/tty_backend.rb +43 -113
  68. data/lib/charming/presentation/components/empty_state.rb +13 -0
  69. data/lib/charming/presentation/components/form/builder.rb +14 -0
  70. data/lib/charming/presentation/components/form/confirm.rb +13 -0
  71. data/lib/charming/presentation/components/form/field.rb +25 -0
  72. data/lib/charming/presentation/components/form/input.rb +14 -0
  73. data/lib/charming/presentation/components/form/note.rb +9 -0
  74. data/lib/charming/presentation/components/form/select.rb +23 -0
  75. data/lib/charming/presentation/components/form/textarea.rb +16 -0
  76. data/lib/charming/presentation/components/form.rb +29 -0
  77. data/lib/charming/presentation/components/list.rb +28 -0
  78. data/lib/charming/presentation/components/markdown.rb +6 -0
  79. data/lib/charming/presentation/components/modal.rb +14 -0
  80. data/lib/charming/presentation/components/progressbar.rb +13 -0
  81. data/lib/charming/presentation/components/spinner.rb +10 -0
  82. data/lib/charming/presentation/components/table.rb +25 -0
  83. data/lib/charming/presentation/components/text_area.rb +48 -0
  84. data/lib/charming/presentation/components/text_input.rb +24 -0
  85. data/lib/charming/presentation/components/viewport.rb +52 -0
  86. data/lib/charming/presentation/layout/builder.rb +86 -0
  87. data/lib/charming/presentation/layout/overlay.rb +57 -0
  88. data/lib/charming/presentation/layout/pane.rb +145 -0
  89. data/lib/charming/presentation/layout/rect.rb +23 -0
  90. data/lib/charming/presentation/layout/screen_layout.rb +60 -0
  91. data/lib/charming/presentation/layout/split.rb +134 -0
  92. data/lib/charming/presentation/markdown/block_renderers.rb +120 -0
  93. data/lib/charming/presentation/markdown/inline_renderers.rb +68 -0
  94. data/lib/charming/presentation/markdown/render_context.rb +22 -0
  95. data/lib/charming/presentation/markdown/renderer.rb +45 -135
  96. data/lib/charming/presentation/markdown/syntax_highlighter.rb +16 -0
  97. data/lib/charming/presentation/markdown.rb +3 -0
  98. data/lib/charming/presentation/template_view.rb +7 -0
  99. data/lib/charming/presentation/templates.rb +17 -0
  100. data/lib/charming/presentation/ui/ansi_codes.rb +89 -0
  101. data/lib/charming/presentation/ui/ansi_slicer.rb +94 -0
  102. data/lib/charming/presentation/ui/border_painter.rb +58 -0
  103. data/lib/charming/presentation/ui/canvas.rb +82 -0
  104. data/lib/charming/presentation/ui/style.rb +62 -95
  105. data/lib/charming/presentation/ui.rb +15 -156
  106. data/lib/charming/presentation/view.rb +17 -0
  107. data/lib/charming/runtime.rb +2 -0
  108. data/lib/charming/tasks/inline_executor.rb +9 -0
  109. data/lib/charming/tasks/task.rb +3 -0
  110. data/lib/charming/tasks/threaded_executor.rb +12 -0
  111. data/lib/charming/version.rb +1 -1
  112. data/lib/charming.rb +13 -0
  113. metadata +59 -10
  114. data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -90
  115. data/lib/charming/generators/app_generator/basic_templates.rb +0 -81
  116. data/lib/charming/generators/app_generator/component_templates.rb +0 -36
  117. data/lib/charming/generators/app_generator/controller_template.rb +0 -60
  118. data/lib/charming/generators/app_generator/database_templates.rb +0 -45
  119. data/lib/charming/generators/app_generator/layout_template.rb +0 -66
  120. data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -69
  121. data/lib/charming/generators/app_generator/state_templates.rb +0 -30
  122. data/lib/charming/generators/app_generator/view_template.rb +0 -84
@@ -2,45 +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
- %(# 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
- )
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
- %( def #{action}
41
- render :#{action}, palette: command_palette
42
- end
43
- )
52
+ " def #{action}\n" \
53
+ " render :#{action}, palette: command_palette\n" \
54
+ " end\n"
44
55
  end
45
56
  end
46
57
  end
@@ -2,15 +2,25 @@
2
2
 
3
3
  module Charming
4
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`.
5
9
  class ModelGenerator < AppFileGenerator
10
+ # A single model field: column *name* and ActiveRecord *type* (e.g., "string").
6
11
  Field = Data.define(:name, :type)
12
+
13
+ # The set of ActiveRecord column types accepted on the command line.
7
14
  VALID_TYPES = %w[string text integer float decimal boolean date datetime time].freeze
8
15
 
16
+ # *name* is the resource name. *args* is the list of `name:type` field specifications.
9
17
  def initialize(name, args, out:, destination:, force: false)
10
18
  super
11
19
  @fields = args.map { |arg| parse_field(arg) }
12
20
  end
13
21
 
22
+ # Validates that the app is database-configured, then writes the model, migration,
23
+ # and spec files.
14
24
  def generate
15
25
  raise Error, "Database support is not configured. Generate the app with --database sqlite3 first." unless database_configured?
16
26
 
@@ -21,67 +31,61 @@ module Charming
21
31
 
22
32
  private
23
33
 
34
+ # The list of parsed Field entries supplied on the command line.
24
35
  attr_reader :fields
25
36
 
37
+ # No file-name suffix; ModelGenerator writes files to explicit paths.
26
38
  def suffix
27
39
  nil
28
40
  end
29
41
 
42
+ # Path to the generated `app/models/<name>.rb` file.
30
43
  def model_path
31
44
  File.join("app", "models", "#{name.snake_name}.rb")
32
45
  end
33
46
 
47
+ # Path to the generated `db/migrate/<timestamp>_create_<table>.rb` file.
34
48
  def migration_path
35
49
  File.join("db", "migrate", "#{timestamp}_create_#{table_name}.rb")
36
50
  end
37
51
 
52
+ # Path to the generated `spec/models/<name>_spec.rb` file.
38
53
  def spec_path
39
54
  File.join("spec", "models", "#{name.snake_name}_spec.rb")
40
55
  end
41
56
 
57
+ # The full source of the generated ActiveRecord model class.
42
58
  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
- )
59
+ render_template("model/model.rb.template",
60
+ app_class: app_name.class_name,
61
+ model_class: name.class_name)
50
62
  end
51
63
 
64
+ # The full source of the generated migration, with one `t.<type> :<name>` line per field.
52
65
  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
- )
66
+ render_template("model/migration.rb.template",
67
+ table_class: table_class_name,
68
+ table_name: table_name,
69
+ field_lines: field_lines)
63
70
  end
64
71
 
72
+ # The full source of the generated model spec (asserts the model inherits from
73
+ # `ApplicationRecord`).
65
74
  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
- )
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)
76
79
  end
77
80
 
81
+ # Renders one `t.<type> :<name>` line per field, joined together.
78
82
  def field_lines
79
83
  fields.map { |field|
80
- %( t.#{field.type} :#{field.name}
81
- )
84
+ " t.#{field.type} :#{field.name}\n"
82
85
  }.join
83
86
  end
84
87
 
88
+ # Parses a single `name:type` argument. Raises Error on invalid names or unsupported types.
85
89
  def parse_field(value)
86
90
  field_name, type = value.split(":", 2)
87
91
  raise Error, "Invalid field: #{value.inspect}" unless field_name && type
@@ -91,19 +95,23 @@ end
91
95
  Field.new(name: field_name, type: type)
92
96
  end
93
97
 
98
+ # True when `config/database.rb` and `app/models/application_record.rb` both exist.
94
99
  def database_configured?
95
100
  File.exist?(File.join(destination, "config", "database.rb")) &&
96
101
  File.exist?(File.join(destination, "app", "models", "application_record.rb"))
97
102
  end
98
103
 
104
+ # The pluralized table name (e.g., "user" → "users", "category" → "categories").
99
105
  def table_name
100
106
  pluralize(name.snake_name)
101
107
  end
102
108
 
109
+ # The CamelCase migration class name (e.g., "users" → "Users").
103
110
  def table_class_name
104
111
  table_name.split("_").map(&:capitalize).join
105
112
  end
106
113
 
114
+ # Minimal English pluralization for the model name (covers the common -y, -s/x/z/ch/sh cases).
107
115
  def pluralize(value)
108
116
  return value.sub(/y\z/, "ies") if value.end_with?("y")
109
117
  return "#{value}es" if value.match?(/(?:s|x|z|ch|sh)\z/)
@@ -111,6 +119,7 @@ end
111
119
  "#{value}s"
112
120
  end
113
121
 
122
+ # The current UTC timestamp in the format ActiveRecord uses for migration filenames.
114
123
  def timestamp
115
124
  Time.now.utc.strftime("%Y%m%d%H%M%S")
116
125
  end
@@ -2,24 +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
 
32
+ # The component class name (e.g., "user" → "UserComponent").
23
33
  def component_class_name
24
34
  "#{class_name}Component"
25
35
  end
@@ -2,14 +2,19 @@
2
2
 
3
3
  module Charming
4
4
  module Generators
5
+ # ScreenGenerator implements `charming generate screen NAME`. Writes a complete vertical
6
+ # slice for a new screen: a state class, a controller with a `show` action, a view,
7
+ # matching spec files, and inserts a route into `config/routes.rb` and a command entry
8
+ # into `ApplicationController` for the command palette.
5
9
  class ScreenGenerator < AppFileGenerator
6
- include AppGenerator::ScreenSpecTemplates
7
-
10
+ # *name* is the resource name. *args* is unused (raises Error when non-empty).
8
11
  def initialize(name, args, out:, destination:, force: false)
9
12
  super
10
13
  raise Error, "Usage: charming generate screen NAME" if args.any?
11
14
  end
12
15
 
16
+ # Writes the state, controller, view, and three spec files, then inserts a route
17
+ # and a command-palette entry.
13
18
  def generate
14
19
  create_file(state_path, state)
15
20
  create_file(controller_path, controller)
@@ -23,88 +28,124 @@ module Charming
23
28
 
24
29
  private
25
30
 
31
+ # The file-name suffix used by `app_path` ("screen" — only used by the parent class).
26
32
  def suffix
27
33
  "screen"
28
34
  end
29
35
 
36
+ # Path to the generated state class.
30
37
  def state_path
31
38
  File.join("app", "state", "#{name.snake_name}_state.rb")
32
39
  end
33
40
 
41
+ # Path to the generated controller class.
34
42
  def controller_path
35
43
  File.join("app", "controllers", "#{name.snake_name}_controller.rb")
36
44
  end
37
45
 
46
+ # Path to the generated `show` view.
38
47
  def view_path
39
- File.join("app", "views", name.snake_name, "show.tui.erb")
48
+ File.join("app", "views", name.snake_name, "show_view.rb")
40
49
  end
41
50
 
51
+ # Path to the generated state spec.
42
52
  def spec_state_path
43
53
  File.join("spec", "state", "#{name.snake_name}_state_spec.rb")
44
54
  end
45
55
 
56
+ # Path to the generated controller spec.
46
57
  def spec_controller_path
47
58
  File.join("spec", "controllers", "#{name.snake_name}_controller_spec.rb")
48
59
  end
49
60
 
61
+ # Path to the generated view spec.
50
62
  def spec_view_path
51
- File.join("spec", "views", name.snake_name, "show_template_spec.rb")
63
+ File.join("spec", "views", name.snake_name, "show_view_spec.rb")
52
64
  end
53
65
 
66
+ # Absolute path to the app's `config/routes.rb`.
54
67
  def route_path
55
68
  File.join(destination, "config", "routes.rb")
56
69
  end
57
70
 
71
+ # Absolute path to the app's `ApplicationController`.
58
72
  def application_controller_path
59
73
  File.join(destination, "app", "controllers", "application_controller.rb")
60
74
  end
61
75
 
76
+ # The source of the generated state class.
62
77
  def state
63
- %(# frozen_string_literal: true
64
-
65
- module #{app_name.class_name}
66
- class #{name.class_name}State < ApplicationState
67
- attribute :title, :string, default: "#{name.class_name}"
68
- end
69
- end
70
- )
78
+ render_template("screen/state.rb.template",
79
+ app_class: app_name.class_name,
80
+ state_class: "#{name.class_name}State",
81
+ title: name.class_name)
71
82
  end
72
83
 
84
+ # The source of the generated controller class.
73
85
  def controller
74
- %(# frozen_string_literal: true
75
-
76
- module #{app_name.class_name}
77
- class #{name.controller_class_name} < ApplicationController
78
- #{controller_body}
79
- end
80
- end
81
- )
86
+ render_template("screen/controller.rb.template",
87
+ app_class: app_name.class_name,
88
+ controller_class: name.controller_class_name,
89
+ controller_body: controller_body)
82
90
  end
83
91
 
92
+ # The body of the controller: a `show` action and a private accessor for the state.
84
93
  def controller_body
85
- %( def show
86
- render :show,
87
- #{name.snake_name}: #{name.snake_name},
88
- palette: command_palette
89
- end
94
+ " def show\n" \
95
+ " render :show,\n" \
96
+ " #{name.snake_name}: #{name.snake_name},\n" \
97
+ " palette: command_palette\n" \
98
+ " end\n" \
99
+ "\n" \
100
+ " private\n" \
101
+ "\n" \
102
+ " def #{name.snake_name}\n" \
103
+ " state(:#{name.snake_name}, #{name.class_name}State)\n" \
104
+ " end"
105
+ end
106
+
107
+ # The source of the generated view class.
108
+ def view
109
+ render_template("screen/view.rb.template",
110
+ app_class: app_name.class_name,
111
+ resource_module: name.class_name,
112
+ screen_name: name.snake_name)
113
+ end
90
114
 
91
- private
115
+ # The source of the generated state spec.
116
+ def spec_state
117
+ render_template("screen/spec_state.rb.template",
118
+ app_snake: app_name.snake_name,
119
+ app_class: app_name.class_name,
120
+ state_class: "#{name.class_name}State",
121
+ title: name.class_name)
122
+ end
92
123
 
93
- def #{name.snake_name}
94
- state(:#{name.snake_name}, #{name.class_name}State)
95
- end)
124
+ # The source of the generated controller spec.
125
+ def spec_controller
126
+ render_template("screen/spec_controller.rb.template",
127
+ app_snake: app_name.snake_name,
128
+ app_class: app_name.class_name,
129
+ controller_class: name.controller_class_name)
96
130
  end
97
131
 
98
- def view
99
- %(<%= #{name.snake_name}.title %>
100
- )
132
+ # The source of the generated view spec.
133
+ def spec_view
134
+ render_template("screen/spec_view.rb.template",
135
+ app_snake: app_name.snake_name,
136
+ app_class: app_name.class_name,
137
+ resource_module: name.class_name,
138
+ screen_name: name.snake_name,
139
+ title: name.class_name)
101
140
  end
102
141
 
142
+ # Inserts a `screen` route into `config/routes.rb`, idempotently.
103
143
  def insert_route
104
144
  route = %( screen "/#{name.snake_name}", to: "#{name.snake_name}#show", title: "#{name.class_name}")
105
145
  insert_before_end(route_path, route, "route", "end")
106
146
  end
107
147
 
148
+ # Inserts a `command` block into `ApplicationController`, idempotently.
108
149
  def insert_command
109
150
  command = %( command "#{name.class_name}" do
110
151
  navigate_to "/#{name.snake_name}"
@@ -112,6 +153,8 @@ end
112
153
  insert_before_end(application_controller_path, command, "command", " end")
113
154
  end
114
155
 
156
+ # Inserts *content* into *path* just before the line matching *end_line*. No-ops when
157
+ # the content is already present. Raises Error when the file or end-line is missing.
115
158
  def insert_before_end(path, content, label, end_line)
116
159
  raise Error, "Missing file: #{relative_path(path)}" unless File.exist?(path)
117
160
 
@@ -125,6 +168,8 @@ end
125
168
  out.puts "insert #{label} #{relative_path(path)}"
126
169
  end
127
170
 
171
+ # Returns the index of the last line in *lines* that matches *end_line* (the line
172
+ # just before which new content will be inserted). Raises Error when not found.
128
173
  def insertion_index(lines, path, end_line)
129
174
  index = lines.rindex { |line| line.chomp == end_line }
130
175
  raise Error, "Could not update #{relative_path(path)}" unless index
@@ -132,6 +177,7 @@ end
132
177
  index
133
178
  end
134
179
 
180
+ # Strips the destination prefix from *path* for human-friendly status output.
135
181
  def relative_path(path)
136
182
  path.delete_prefix("#{destination}/")
137
183
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
@@ -0,0 +1,9 @@
1
+ # __APP_NAME__
2
+
3
+ A Charming terminal user interface.
4
+
5
+ Run it with:
6
+
7
+ ```sh
8
+ bundle exec __APP_SNAKE__
9
+ ```
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class Application < Charming::Application
5
+ root File.expand_path("../..", __dir__)
6
+
7
+ Charming::Presentation::UI::Theme.built_in_names.each do |theme_name|
8
+ theme theme_name.to_sym, built_in: theme_name
9
+ end
10
+
11
+ default_theme :phosphor
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class ApplicationController < Charming::Controller
5
+ layout Layouts::ApplicationLayout
6
+ focus_ring :sidebar, :content
7
+
8
+ key "p", :open_command_palette, scope: :global
9
+ key "q", :quit, scope: :global
10
+
11
+ command "Home" do
12
+ navigate_to "/"
13
+ end
14
+
15
+ command "Theme", :open_theme_palette
16
+ command "Close palette", :close_command_palette
17
+ command "Quit app", :quit
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class ApplicationState < Charming::ApplicationState
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "fileutils"
5
+
6
+ database_path = File.expand_path("../db/development.sqlite3", __dir__)
7
+ FileUtils.mkdir_p(File.dirname(database_path))
8
+
9
+ ActiveRecord::Base.establish_connection(
10
+ adapter: "sqlite3",
11
+ database: database_path
12
+ )
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "__APP_SNAKE__"
6
+
7
+ Charming.run(__APP_CLASS__::Application.new)
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/__APP_SNAKE__/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ __GEMSPEC_ATTRIBUTES____GEMSPEC_DEPENDENCIES__end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class HomeController < ApplicationController
5
+ __CONTROLLER_ACTIONS____CONTROLLER_HELPERS__ end
6
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module __APP_CLASS__
4
+ class HomeState < ApplicationState
5
+ attribute :title, :string, default: "__APP_NAME__"
6
+ end
7
+ end