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
@@ -32,36 +32,29 @@ module Charming
32
32
  blocks.join("\n" * (gap + 1))
33
33
  end
34
34
 
35
- # Centers a *block* within a canvas of the given *width* and *height*, then returns the result.
36
- def center(block, width:, height:, background: nil)
37
- place(block, width: width, height: height, top: :center, left: :center, background: background)
35
+ # Places *block* onto a blank canvas of *width* × *height* at an offset determined by *top* (row)
36
+ # and *left* (column). Non-:center values are treated as absolute positions. When *background* is
37
+ # given, the assembled frame is wrapped so the theme bg paints the entire canvas — overlay content
38
+ # with its own bg overrides per-cell; resets re-apply the canvas bg.
39
+ def place(block, width:, height:, top: 0, left: 0, background: nil)
40
+ Canvas.new(width, height).place(block, top: top, left: left, background: background)
38
41
  end
39
42
 
40
43
  # Draws *overlay* on top of a base at the specified *top* (row) and *left* (column) coordinates,
41
44
  # defaulting to center in both directions. ANSI styling on the base content is preserved underneath.
42
45
  def overlay(base, overlay, top: :center, left: :center)
43
- base_lines = base.to_s.lines(chomp: true)
44
- overlay_lines = overlay.to_s.lines(chomp: true)
45
- width = block_width(base_lines)
46
- row = offset(top, base_lines.length, overlay_lines.length)
47
- column = offset(left, width, block_width(overlay_lines))
48
-
49
- draw_lines(base_lines, overlay_lines, row: row, column: column, width: width)
46
+ Canvas.parse(base).overlay(overlay, top: top, left: left).to_s
50
47
  end
51
48
 
52
- # Places a *block* onto a blank canvas of *width* × *height* at an offset determined by *top* (row)
53
- # and *left* (column). Non-:center values are treated as absolute positions. When *background* is
54
- # given, the assembled frame is wrapped so the theme bg paints the entire canvas — overlay content
55
- # with its own bg overrides per-cell; resets re-apply the canvas bg.
56
- def place(block, width:, height:, top: 0, left: 0, background: nil)
57
- lines = block.to_s.lines(chomp: true)
58
- row = offset(top, height, lines.length)
59
- column = offset(left, width, block_width(lines))
60
- canvas = Array.new(height) { " " * width }
61
- composed = draw_lines(canvas, lines, row: row, column: column, width: width)
62
- return composed unless background
49
+ # Centers a *block* within a canvas of the given *width* and *height*, then returns the result.
50
+ def center(block, width:, height:, background: nil)
51
+ place(block, width: width, height: height, top: :center, left: :center, background: background)
52
+ end
63
53
 
64
- Style.new.background(background).render(composed)
54
+ # Returns a visible-slice of *line* starting at *start_column* spanning *width* characters, preserving any
55
+ # ANSI escape sequences that were active at the start of the slice. Non-positive widths return `""`.
56
+ def visible_slice(line, start_column, width)
57
+ ANSISlicer.slice(line, start_column, width)
65
58
  end
66
59
 
67
60
  # Normalizes an array of mixed objects into arrays of lines by calling `#to_s` on each element.
@@ -93,140 +86,6 @@ module Charming
93
86
  line + (" " * (widths[block_index] - Width.measure(line)))
94
87
  end
95
88
  end
96
-
97
- # Computes a placement coordinate: if *value* is `:center` the result centres the *size* within *available*;
98
- # otherwise *value* is returned verbatim as an absolute integer position.
99
- def offset(value, available, size)
100
- return [(available - size) / 2, 0].max if value == :center
101
-
102
- value
103
- end
104
-
105
- # Merges an *overlay_line* into a *base_line* at the given *column*, returning the combined string. The
106
- # overlay replaces (covers) underlying characters; anything to the right that exceeds *width* is truncated.
107
- def composed_overlay_line(base_line, overlay_line, column, width)
108
- return visible_slice(base_line, 0, width) if column >= width
109
- return visible_slice(base_line, 0, width) if column + Width.measure(overlay_line) <= 0
110
-
111
- target_column = [column, 0].max
112
- overlay_start = [0 - column, 0].max
113
- overlay = visible_slice(overlay_line, overlay_start, width - target_column)
114
- overlay_width = Width.measure(overlay)
115
- return visible_slice(base_line, 0, width) if overlay_width.zero?
116
-
117
- right_column = target_column + overlay_width
118
-
119
- visible_slice(base_line, 0, target_column) +
120
- overlay +
121
- visible_slice(base_line, right_column, [width - right_column, 0].max)
122
- end
123
-
124
- # Returns a visible-slice of *line* starting at *start_column* spanning *width* characters, preserving any
125
- # ANSI escape sequences that were active at the start of the slice. Non-positive widths return `""`.
126
- def visible_slice(line, start_column, width)
127
- return "" unless width.positive?
128
-
129
- slice_visible_text(line.to_s, start_column, start_column + width)
130
- end
131
-
132
- # Slices a string by visible terminal columns while preserving ANSI style state.
133
- def slice_visible_text(line, start_column, end_column)
134
- state = {column: 0, output: +"", active: [], started: false, styled: false}
135
-
136
- each_ansi_or_char(line) do |token, ansi|
137
- if ansi
138
- slice_ansi(token, state, start_column, end_column)
139
- else
140
- slice_char(token, state, start_column, end_column)
141
- end
142
- end
143
-
144
- terminate_slice(state)
145
- end
146
-
147
- # Splits a *line* into token-range pieces bounded by *start_column* and *end_column*, preserving ANSI escapes
148
- # that fall within the visible range. Yields each character or escape sequence along with whether it is ANSI.
149
- def each_ansi_or_char(line)
150
- index = 0
151
- while index < line.length
152
- match = line.match(Width::ANSI_PATTERN, index)
153
- if match&.begin(0) == index
154
- yield match[0], true
155
- index = match.end(0)
156
- else
157
- char = line[index]
158
- yield char, false
159
- index += 1
160
- end
161
- end
162
- end
163
-
164
- # Slices an ANSI *token* (escape sequence) into *state*, writing active markers to the output if the current
165
- # *column* falls within the [start_column, end_column) range. Resets styles on `[0m` sequences.
166
- def slice_ansi(token, state, start_column, end_column)
167
- started = state[:started]
168
- update_active_styles(state[:active], token)
169
- return unless state[:column].between?(start_column, end_column - 1)
170
-
171
- start_slice(state)
172
- if started
173
- state[:output] << token
174
- state[:styled] = !token.include?("[0m")
175
- end
176
- end
177
-
178
- # Slices a plain *char* into *state*, advancing the column tracker by the character's visual width. If the
179
- # character overlaps with the [start_column, end_column) range it is appended to the output.
180
- def slice_char(char, state, start_column, end_column)
181
- char_width = Width.measure(char)
182
- char_start = state[:column]
183
- char_end = char_start + char_width
184
- state[:column] = char_end
185
- return unless char_end > start_column && char_start < end_column
186
-
187
- start_slice(state)
188
- state[:output] << char
189
- end
190
-
191
- # Starts writing to the output buffer, flushing any active ANSI markers if this is the first character placed.
192
- def start_slice(state)
193
- return if state[:started]
194
-
195
- state[:output] << state[:active].join
196
- state[:styled] = true unless state[:active].empty?
197
- state[:started] = true
198
- end
199
-
200
- # Closes the slice by appending a final `[0m` reset escape to the output unless no active styling exists or
201
- # nothing was written. Returns the fully constructed output string with trailing reset applied.
202
- def terminate_slice(state)
203
- return state[:output] if !state[:styled] || state[:output].empty?
204
-
205
- "#{state[:output]}\e[0m"
206
- end
207
-
208
- # Updates *state*[:active] with an ANSI *token*: resets all active styles on `[0m` or appends the token as a
209
- # new active marker otherwise. Called during each_ansi_or_char iteration.
210
- def update_active_styles(active, token)
211
- if token.include?("[0m")
212
- active.clear
213
- else
214
- active << token
215
- end
216
- end
217
-
218
- # Overlays *lines* onto a *canvas* starting at (*row*, *column*), writing each overlaid line into the canvas
219
- # via `composed_overlay_line`. Returns the final canvas joined by newlines.
220
- def draw_lines(canvas, lines, row:, column:, width:)
221
- lines.each_with_index do |line, index|
222
- line_index = row + index
223
- next if line_index.negative? || line_index >= canvas.length
224
-
225
- canvas[line_index] = composed_overlay_line(canvas[line_index], line, column, width)
226
- end
227
-
228
- canvas.join("\n")
229
- end
230
89
  end
231
90
  end
232
91
  end
@@ -78,6 +78,13 @@ module Charming
78
78
  render_component(partial)
79
79
  end
80
80
 
81
+ # Builds a declarative layout tree for the current terminal screen and renders it.
82
+ def screen_layout(background: nil, &)
83
+ layout = Layout::Builder.build(screen: layout_screen, view: self, background: background, &)
84
+ register_layout_focus(layout)
85
+ layout.render
86
+ end
87
+
81
88
  # Yields the layout's `content` slot — used by view templates to inject their body into a layout wrapper (e.g., sidebar).
82
89
  def yield_content
83
90
  assigns.fetch(:content, "")
@@ -113,6 +120,16 @@ module Charming
113
120
  define_singleton_method(name) { assigns.fetch(name) }
114
121
  end
115
122
  end
123
+
124
+ def layout_screen
125
+ assigns[:screen] || assigns[:controller]&.screen || Charming::Screen.new(width: 80, height: 24)
126
+ end
127
+
128
+ def register_layout_focus(layout)
129
+ return unless assigns[:controller]
130
+
131
+ assigns[:controller].focus.define_layout(layout.focusable_names)
132
+ end
116
133
  end
117
134
  end
118
135
  end
@@ -75,6 +75,8 @@ module Charming
75
75
  controller(event: event).dispatch_mouse
76
76
  end
77
77
 
78
+ # Instantiates a fresh controller for the active route, passing the application, current *event*,
79
+ # route params, screen dimensions, and route object. Called by every dispatch path.
78
80
  def controller(event: nil)
79
81
  @route.controller_class.new(application: @application, event: event, params: @route.params, screen: screen, route: @route)
80
82
  end
@@ -2,22 +2,31 @@
2
2
 
3
3
  module Charming
4
4
  module Tasks
5
+ # InlineExecutor runs submitted tasks synchronously on the calling thread, pushing
6
+ # the resulting TaskEvent directly into the runtime's *queue*. Used for testing and
7
+ # for environments where spawning background threads is undesirable.
5
8
  class InlineExecutor
9
+ # *queue* is the thread-safe Queue (typically `runtime.@task_queue`) into which
10
+ # completed TaskEvents are pushed.
6
11
  def initialize(queue)
7
12
  @queue = queue
8
13
  end
9
14
 
15
+ # Wraps *block* in a Task, invokes it immediately, and pushes the resulting
16
+ # TaskEvent (value or error) onto the queue. Returns nil.
10
17
  def submit(name, &block)
11
18
  task = Task.new(name: name.to_sym, block: block)
12
19
  @queue << run(task)
13
20
  nil
14
21
  end
15
22
 
23
+ # No-op stub for the shutdown contract; nothing to join since tasks run on the caller.
16
24
  def shutdown(timeout: 0.0)
17
25
  end
18
26
 
19
27
  private
20
28
 
29
+ # Invokes the task's block and wraps the result (or raised exception) in a TaskEvent.
21
30
  def run(task)
22
31
  Events::TaskEvent.new(name: task.name, value: task.call)
23
32
  rescue StandardError, ScriptError => e
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Charming
4
4
  module Tasks
5
+ # Task is the unit of work submitted to a task executor. It pairs a *name* (used by
6
+ # `on_task` handlers to route the result) with a *block* to invoke on the executor.
5
7
  Task = Data.define(:name, :block) do
8
+ # Invokes the task's block in the executor's thread and returns its value (or raises).
6
9
  def call = block.call
7
10
  end
8
11
  end
@@ -2,13 +2,22 @@
2
2
 
3
3
  module Charming
4
4
  module Tasks
5
+ # ThreadedExecutor runs submitted tasks on background Ruby threads. Each submission
6
+ # creates a new thread that invokes the block and pushes the resulting TaskEvent
7
+ # onto the shared *queue*. Threads are tracked so `shutdown` can wait (or kill)
8
+ # in-flight work.
5
9
  class ThreadedExecutor
10
+ # *queue* is the thread-safe Queue (typically `runtime.@task_queue`) into which
11
+ # completed TaskEvents are pushed.
6
12
  def initialize(queue)
7
13
  @queue = queue
8
14
  @threads = []
9
15
  @mutex = Mutex.new
10
16
  end
11
17
 
18
+ # Wraps *block* in a Task and spawns a new thread to invoke it. The thread's
19
+ # return value (or rescued exception) is pushed onto the queue as a TaskEvent.
20
+ # Returns nil immediately.
12
21
  def submit(name, &block)
13
22
  task = Task.new(name: name.to_sym, block: block)
14
23
  thread = Thread.new(task) { |t| @queue << run(t) }
@@ -16,6 +25,8 @@ module Charming
16
25
  nil
17
26
  end
18
27
 
28
+ # Waits up to *timeout* seconds for in-flight threads to finish, then kills any
29
+ # remaining live threads. Used by Runtime during teardown.
19
30
  def shutdown(timeout: 0.0)
20
31
  threads = @mutex.synchronize { @threads.dup }
21
32
  threads.each { |thread| thread.join(timeout) }
@@ -29,6 +40,7 @@ module Charming
29
40
 
30
41
  private
31
42
 
43
+ # Invokes the task's block and wraps the result (or rescued exception) in a TaskEvent.
32
44
  def run(task)
33
45
  Events::TaskEvent.new(name: task.name, value: task.call)
34
46
  rescue StandardError, ScriptError => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/charming.rb CHANGED
@@ -6,18 +6,31 @@ loader = Zeitwerk::Loader.for_gem
6
6
  loader.inflector.inflect(
7
7
  "cli" => "CLI",
8
8
  "ui" => "UI",
9
+ "ansi_codes" => "ANSICodes",
10
+ "ansi_slicer" => "ANSISlicer",
11
+ "border_painter" => "BorderPainter",
12
+ "block_renderers" => "BlockRenderer",
13
+ "inline_renderers" => "InlineRenderer",
14
+ "render_context" => "RenderContext",
9
15
  "erb_handler" => "ErbHandler",
16
+ "key_normalizer" => "KeyNormalizer",
17
+ "mouse_parser" => "MouseParser",
10
18
  "tty_backend" => "TTYBackend"
11
19
  )
12
20
  loader.setup
13
21
 
14
22
  module Charming
23
+ # Base error class for all Charming-specific exceptions (used by templates, generators, runtime, etc.).
15
24
  class Error < StandardError; end
16
25
 
26
+ # Entry point for running a Charming application. Instantiates a Runtime for *application* and starts
27
+ # the event loop. *backend* defaults to TTYBackend; tests pass MemoryBackend directly via `Charming::Runtime.new`.
17
28
  def self.run(application, backend: nil)
18
29
  Runtime.new(application, backend: backend).run
19
30
  end
20
31
 
32
+ # Returns the normalized key symbol for an event-like object — `event.key` when the object responds
33
+ # to it, otherwise `event.to_sym`. Lets components treat raw strings and KeyEvent objects uniformly.
21
34
  def self.key_of(event)
22
35
  key = event.respond_to?(:key) ? event.key : event
23
36
  key.to_sym
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: charming
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - pando
@@ -206,6 +206,14 @@ files:
206
206
  - lib/charming/application_state.rb
207
207
  - lib/charming/cli.rb
208
208
  - lib/charming/controller.rb
209
+ - lib/charming/controller/class_methods.rb
210
+ - lib/charming/controller/command_palette.rb
211
+ - lib/charming/controller/component_dispatching.rb
212
+ - lib/charming/controller/dispatching.rb
213
+ - lib/charming/controller/focus_management.rb
214
+ - lib/charming/controller/rendering.rb
215
+ - lib/charming/controller/session_state.rb
216
+ - lib/charming/controller/sidebar_navigation.rb
209
217
  - lib/charming/database_commands.rb
210
218
  - lib/charming/database_installer.rb
211
219
  - lib/charming/events/key_event.rb
@@ -216,15 +224,6 @@ files:
216
224
  - lib/charming/focus.rb
217
225
  - lib/charming/generators/app_file_generator.rb
218
226
  - lib/charming/generators/app_generator.rb
219
- - lib/charming/generators/app_generator/app_spec_templates.rb
220
- - lib/charming/generators/app_generator/basic_templates.rb
221
- - lib/charming/generators/app_generator/component_templates.rb
222
- - lib/charming/generators/app_generator/controller_template.rb
223
- - lib/charming/generators/app_generator/database_templates.rb
224
- - lib/charming/generators/app_generator/layout_template.rb
225
- - lib/charming/generators/app_generator/screen_spec_templates.rb
226
- - lib/charming/generators/app_generator/state_templates.rb
227
- - lib/charming/generators/app_generator/view_template.rb
228
227
  - lib/charming/generators/base.rb
229
228
  - lib/charming/generators/component_generator.rb
230
229
  - lib/charming/generators/controller_generator.rb
@@ -232,11 +231,48 @@ files:
232
231
  - lib/charming/generators/model_generator.rb
233
232
  - lib/charming/generators/name.rb
234
233
  - lib/charming/generators/screen_generator.rb
234
+ - lib/charming/generators/templates/app/Gemfile.template
235
+ - lib/charming/generators/templates/app/README.md.template
236
+ - lib/charming/generators/templates/app/Rakefile.template
237
+ - lib/charming/generators/templates/app/application.template
238
+ - lib/charming/generators/templates/app/application_controller.template
239
+ - lib/charming/generators/templates/app/application_record.template
240
+ - lib/charming/generators/templates/app/application_state.template
241
+ - lib/charming/generators/templates/app/database_config.template
242
+ - lib/charming/generators/templates/app/executable.template
243
+ - lib/charming/generators/templates/app/gemspec.template
244
+ - lib/charming/generators/templates/app/home_controller.template
245
+ - lib/charming/generators/templates/app/home_state.template
246
+ - lib/charming/generators/templates/app/keep.template
247
+ - lib/charming/generators/templates/app/layout.template
248
+ - lib/charming/generators/templates/app/root_file.template
249
+ - lib/charming/generators/templates/app/routes.template
250
+ - lib/charming/generators/templates/app/seeds.template
251
+ - lib/charming/generators/templates/app/spec_controller.template
252
+ - lib/charming/generators/templates/app/spec_helper.template
253
+ - lib/charming/generators/templates/app/spec_state.template
254
+ - lib/charming/generators/templates/app/spec_view.template
255
+ - lib/charming/generators/templates/app/version.template
256
+ - lib/charming/generators/templates/app/view.template
257
+ - lib/charming/generators/templates/component/component.rb.template
258
+ - lib/charming/generators/templates/controller/controller.rb.template
259
+ - lib/charming/generators/templates/model/migration.rb.template
260
+ - lib/charming/generators/templates/model/model.rb.template
261
+ - lib/charming/generators/templates/model/spec.rb.template
262
+ - lib/charming/generators/templates/screen/controller.rb.template
263
+ - lib/charming/generators/templates/screen/spec_controller.rb.template
264
+ - lib/charming/generators/templates/screen/spec_state.rb.template
265
+ - lib/charming/generators/templates/screen/spec_view.rb.template
266
+ - lib/charming/generators/templates/screen/state.rb.template
267
+ - lib/charming/generators/templates/screen/view.rb.template
268
+ - lib/charming/generators/templates/view/view.rb.template
235
269
  - lib/charming/generators/view_generator.rb
236
270
  - lib/charming/internal/renderer/differential.rb
237
271
  - lib/charming/internal/renderer/full_repaint.rb
238
272
  - lib/charming/internal/terminal/adapter.rb
273
+ - lib/charming/internal/terminal/key_normalizer.rb
239
274
  - lib/charming/internal/terminal/memory_backend.rb
275
+ - lib/charming/internal/terminal/mouse_parser.rb
240
276
  - lib/charming/internal/terminal/tty_backend.rb
241
277
  - lib/charming/presentation/component.rb
242
278
  - lib/charming/presentation/components/activity_indicator.rb
@@ -261,14 +297,27 @@ files:
261
297
  - lib/charming/presentation/components/text_input.rb
262
298
  - lib/charming/presentation/components/viewport.rb
263
299
  - lib/charming/presentation/layout.rb
300
+ - lib/charming/presentation/layout/builder.rb
301
+ - lib/charming/presentation/layout/overlay.rb
302
+ - lib/charming/presentation/layout/pane.rb
303
+ - lib/charming/presentation/layout/rect.rb
304
+ - lib/charming/presentation/layout/screen_layout.rb
305
+ - lib/charming/presentation/layout/split.rb
264
306
  - lib/charming/presentation/markdown.rb
307
+ - lib/charming/presentation/markdown/block_renderers.rb
308
+ - lib/charming/presentation/markdown/inline_renderers.rb
309
+ - lib/charming/presentation/markdown/render_context.rb
265
310
  - lib/charming/presentation/markdown/renderer.rb
266
311
  - lib/charming/presentation/markdown/syntax_highlighter.rb
267
312
  - lib/charming/presentation/template_view.rb
268
313
  - lib/charming/presentation/templates.rb
269
314
  - lib/charming/presentation/templates/erb_handler.rb
270
315
  - lib/charming/presentation/ui.rb
316
+ - lib/charming/presentation/ui/ansi_codes.rb
317
+ - lib/charming/presentation/ui/ansi_slicer.rb
271
318
  - lib/charming/presentation/ui/border.rb
319
+ - lib/charming/presentation/ui/border_painter.rb
320
+ - lib/charming/presentation/ui/canvas.rb
272
321
  - lib/charming/presentation/ui/style.rb
273
322
  - lib/charming/presentation/ui/theme.rb
274
323
  - lib/charming/presentation/ui/themes/phosphor.json
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Charming
4
- module Generators
5
- class AppGenerator
6
- module AppSpecTemplates
7
- def spec_state
8
- %(# frozen_string_literal: true
9
-
10
- require "#{app_name.snake_name}"
11
-
12
- RSpec.describe #{app_name.class_name}::HomeState 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("#{app_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}::HomeController 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 state" 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 "home/show template" do
55
- describe "#render" do
56
- it "renders the state title" do
57
- template = Charming::Presentation::Templates.resolve("home/show", root: #{app_name.class_name}::Application.root)
58
- view = Charming::Presentation::TemplateView.new(
59
- template: template,
60
- namespace: #{app_name.class_name},
61
- home: double(title: "#{app_name.class_name}"),
62
- theme: #{app_name.class_name}::Application.new.theme
63
- )
64
-
65
- expect(view.render).to include("#{app_name.class_name}")
66
- end
67
- end
68
- end
69
- )
70
- end
71
-
72
- def spec_component
73
- %(# frozen_string_literal: true
74
-
75
- require "#{app_name.snake_name}"
76
-
77
- RSpec.describe #{app_name.class_name}::AppFrameComponent do
78
- describe "#render" do
79
- it "returns a string" do
80
- component = described_class.new(title: "#{app_name.class_name}")
81
- expect(component.render).to be_a(String)
82
- end
83
- end
84
- end
85
- )
86
- end
87
- end
88
- end
89
- end
90
- end
@@ -1,81 +0,0 @@
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("#{gemspec_file_glob}/**/*") + %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"#{database_dependencies})
65
- end
66
-
67
- def gemspec_file_glob
68
- database? ? "{app,config,db,exe,lib}" : "{app,config,exe,lib}"
69
- end
70
-
71
- def database_dependencies
72
- return "" unless database?
73
-
74
- %(
75
- spec.add_dependency "activerecord", "~> 8.1"
76
- spec.add_dependency "sqlite3", "~> 2.0")
77
- end
78
- end
79
- end
80
- end
81
- end