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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/charming/application.rb +11 -0
- data/lib/charming/cli.rb +23 -0
- data/lib/charming/controller/class_methods.rb +115 -0
- data/lib/charming/controller/command_palette.rb +135 -0
- data/lib/charming/controller/component_dispatching.rb +81 -0
- data/lib/charming/controller/dispatching.rb +60 -0
- data/lib/charming/controller/focus_management.rb +30 -0
- data/lib/charming/controller/rendering.rb +127 -0
- data/lib/charming/controller/session_state.rb +41 -0
- data/lib/charming/controller/sidebar_navigation.rb +111 -0
- data/lib/charming/controller.rb +35 -559
- data/lib/charming/database_commands.rb +16 -0
- data/lib/charming/database_installer.rb +27 -0
- data/lib/charming/focus.rb +58 -2
- data/lib/charming/generators/app_file_generator.rb +13 -0
- data/lib/charming/generators/app_generator.rb +123 -47
- data/lib/charming/generators/base.rb +26 -0
- data/lib/charming/generators/component_generator.rb +10 -10
- data/lib/charming/generators/controller_generator.rb +22 -11
- data/lib/charming/generators/model_generator.rb +38 -29
- data/lib/charming/generators/name.rb +10 -0
- data/lib/charming/generators/screen_generator.rb +78 -32
- data/lib/charming/generators/templates/app/Gemfile.template +5 -0
- data/lib/charming/generators/templates/app/README.md.template +9 -0
- data/lib/charming/generators/templates/app/Rakefile.template +3 -0
- data/lib/charming/generators/templates/app/application.template +13 -0
- data/lib/charming/generators/templates/app/application_controller.template +19 -0
- data/lib/charming/generators/templates/app/application_record.template +7 -0
- data/lib/charming/generators/templates/app/application_state.template +6 -0
- data/lib/charming/generators/templates/app/database_config.template +12 -0
- data/lib/charming/generators/templates/app/executable.template +7 -0
- data/lib/charming/generators/templates/app/gemspec.template +6 -0
- data/lib/charming/generators/templates/app/home_controller.template +6 -0
- data/lib/charming/generators/templates/app/home_state.template +7 -0
- data/lib/charming/generators/templates/app/keep.template +0 -0
- data/lib/charming/generators/templates/app/layout.template +113 -0
- data/lib/charming/generators/templates/app/root_file.template +20 -0
- data/lib/charming/generators/templates/app/routes.template +5 -0
- data/lib/charming/generators/templates/app/seeds.template +1 -0
- data/lib/charming/generators/templates/app/spec_controller.template +17 -0
- data/lib/charming/generators/templates/app/spec_helper.template +3 -0
- data/lib/charming/generators/templates/app/spec_state.template +17 -0
- data/lib/charming/generators/templates/app/spec_view.template +16 -0
- data/lib/charming/generators/templates/app/version.template +5 -0
- data/lib/charming/generators/templates/app/view.template +21 -0
- data/lib/charming/generators/templates/component/component.rb.template +9 -0
- data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
- data/lib/charming/generators/templates/model/migration.rb.template +9 -0
- data/lib/charming/generators/templates/model/model.rb.template +6 -0
- data/lib/charming/generators/templates/model/spec.rb.template +9 -0
- data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
- data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
- data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
- data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
- data/lib/charming/generators/templates/screen/state.rb.template +7 -0
- data/lib/charming/generators/templates/screen/view.rb.template +11 -0
- data/lib/charming/generators/templates/view/view.rb.template +11 -0
- data/lib/charming/generators/view_generator.rb +19 -3
- data/lib/charming/internal/renderer/differential.rb +15 -0
- data/lib/charming/internal/renderer/full_repaint.rb +6 -0
- data/lib/charming/internal/terminal/adapter.rb +29 -3
- data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
- data/lib/charming/internal/terminal/memory_backend.rb +28 -1
- data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
- data/lib/charming/internal/terminal/tty_backend.rb +43 -113
- data/lib/charming/presentation/components/empty_state.rb +13 -0
- data/lib/charming/presentation/components/form/builder.rb +14 -0
- data/lib/charming/presentation/components/form/confirm.rb +13 -0
- data/lib/charming/presentation/components/form/field.rb +25 -0
- data/lib/charming/presentation/components/form/input.rb +14 -0
- data/lib/charming/presentation/components/form/note.rb +9 -0
- data/lib/charming/presentation/components/form/select.rb +23 -0
- data/lib/charming/presentation/components/form/textarea.rb +16 -0
- data/lib/charming/presentation/components/form.rb +29 -0
- data/lib/charming/presentation/components/list.rb +28 -0
- data/lib/charming/presentation/components/markdown.rb +6 -0
- data/lib/charming/presentation/components/modal.rb +14 -0
- data/lib/charming/presentation/components/progressbar.rb +13 -0
- data/lib/charming/presentation/components/spinner.rb +10 -0
- data/lib/charming/presentation/components/table.rb +25 -0
- data/lib/charming/presentation/components/text_area.rb +48 -0
- data/lib/charming/presentation/components/text_input.rb +24 -0
- data/lib/charming/presentation/components/viewport.rb +52 -0
- data/lib/charming/presentation/layout/builder.rb +86 -0
- data/lib/charming/presentation/layout/overlay.rb +57 -0
- data/lib/charming/presentation/layout/pane.rb +145 -0
- data/lib/charming/presentation/layout/rect.rb +23 -0
- data/lib/charming/presentation/layout/screen_layout.rb +60 -0
- data/lib/charming/presentation/layout/split.rb +134 -0
- data/lib/charming/presentation/markdown/block_renderers.rb +120 -0
- data/lib/charming/presentation/markdown/inline_renderers.rb +68 -0
- data/lib/charming/presentation/markdown/render_context.rb +22 -0
- data/lib/charming/presentation/markdown/renderer.rb +45 -135
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +16 -0
- data/lib/charming/presentation/markdown.rb +3 -0
- data/lib/charming/presentation/template_view.rb +7 -0
- data/lib/charming/presentation/templates.rb +17 -0
- data/lib/charming/presentation/ui/ansi_codes.rb +89 -0
- data/lib/charming/presentation/ui/ansi_slicer.rb +94 -0
- data/lib/charming/presentation/ui/border_painter.rb +58 -0
- data/lib/charming/presentation/ui/canvas.rb +82 -0
- data/lib/charming/presentation/ui/style.rb +62 -95
- data/lib/charming/presentation/ui.rb +15 -156
- data/lib/charming/presentation/view.rb +17 -0
- data/lib/charming/runtime.rb +2 -0
- data/lib/charming/tasks/inline_executor.rb +9 -0
- data/lib/charming/tasks/task.rb +3 -0
- data/lib/charming/tasks/threaded_executor.rb +12 -0
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +13 -0
- metadata +59 -10
- data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -90
- data/lib/charming/generators/app_generator/basic_templates.rb +0 -81
- data/lib/charming/generators/app_generator/component_templates.rb +0 -36
- data/lib/charming/generators/app_generator/controller_template.rb +0 -60
- data/lib/charming/generators/app_generator/database_templates.rb +0 -45
- data/lib/charming/generators/app_generator/layout_template.rb +0 -66
- data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -69
- data/lib/charming/generators/app_generator/state_templates.rb +0 -30
- 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
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
data/lib/charming/runtime.rb
CHANGED
|
@@ -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
|
data/lib/charming/tasks/task.rb
CHANGED
|
@@ -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
|
data/lib/charming/version.rb
CHANGED
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.
|
|
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
|