charming 0.1.1 → 0.1.3
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 +14 -3
- 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 +110 -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 +25 -2
- 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 +65 -115
- data/lib/charming/presentation/component.rb +3 -5
- data/lib/charming/presentation/components/activity_indicator.rb +173 -134
- data/lib/charming/presentation/components/command_palette.rb +94 -96
- data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
- data/lib/charming/presentation/components/empty_state.rb +47 -36
- data/lib/charming/presentation/components/form/builder.rb +52 -40
- data/lib/charming/presentation/components/form/confirm.rb +50 -39
- data/lib/charming/presentation/components/form/field.rb +94 -71
- data/lib/charming/presentation/components/form/input.rb +61 -49
- data/lib/charming/presentation/components/form/note.rb +27 -20
- data/lib/charming/presentation/components/form/select.rb +84 -63
- data/lib/charming/presentation/components/form/textarea.rb +67 -53
- data/lib/charming/presentation/components/form.rb +120 -93
- data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
- data/lib/charming/presentation/components/list.rb +123 -97
- data/lib/charming/presentation/components/markdown.rb +21 -17
- data/lib/charming/presentation/components/modal.rb +55 -43
- data/lib/charming/presentation/components/progressbar.rb +61 -50
- data/lib/charming/presentation/components/spinner.rb +35 -27
- data/lib/charming/presentation/components/table.rb +108 -85
- data/lib/charming/presentation/components/text_area.rb +219 -173
- data/lib/charming/presentation/components/text_input.rb +120 -98
- data/lib/charming/presentation/components/viewport.rb +218 -168
- data/lib/charming/presentation/layout/builder.rb +84 -0
- data/lib/charming/presentation/layout/overlay.rb +55 -0
- data/lib/charming/presentation/layout/pane.rb +149 -0
- data/lib/charming/presentation/layout/rect.rb +21 -0
- data/lib/charming/presentation/layout/screen_layout.rb +58 -0
- data/lib/charming/presentation/layout/split.rb +132 -0
- data/lib/charming/presentation/layout.rb +28 -30
- data/lib/charming/presentation/markdown/block_renderers.rb +118 -0
- data/lib/charming/presentation/markdown/inline_renderers.rb +66 -0
- data/lib/charming/presentation/markdown/render_context.rb +20 -0
- data/lib/charming/presentation/markdown/renderer.rb +82 -174
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -44
- data/lib/charming/presentation/markdown.rb +4 -3
- data/lib/charming/presentation/template_view.rb +22 -17
- data/lib/charming/presentation/templates/erb_handler.rb +4 -6
- data/lib/charming/presentation/templates.rb +47 -32
- data/lib/charming/presentation/ui/ansi_codes.rb +87 -0
- data/lib/charming/presentation/ui/ansi_slicer.rb +92 -0
- data/lib/charming/presentation/ui/border.rb +24 -26
- data/lib/charming/presentation/ui/border_painter.rb +56 -0
- data/lib/charming/presentation/ui/canvas.rb +80 -0
- data/lib/charming/presentation/ui/style.rb +170 -205
- data/lib/charming/presentation/ui/theme.rb +133 -135
- data/lib/charming/presentation/ui/width.rb +12 -14
- data/lib/charming/presentation/ui.rb +70 -213
- data/lib/charming/presentation/view.rb +107 -92
- data/lib/charming/runtime.rb +25 -10
- 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 +16 -2
- metadata +60 -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
|
@@ -1,38 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
module Components
|
|
5
|
+
# Spinner is a simple rotating-frame indicator. The component cycles through a list of
|
|
6
|
+
# frames on each `tick`; pair it with a controller timer to drive animation. An optional
|
|
7
|
+
# *label* is appended after the current frame on each render.
|
|
8
|
+
class Spinner < Component
|
|
9
|
+
# The default frame set: a 4-frame ASCII spinner.
|
|
10
|
+
DEFAULT_FRAMES = ["-", "\\", "|", "/"].freeze
|
|
11
|
+
|
|
12
|
+
# The current frame list, frame index, and optional label string.
|
|
13
|
+
attr_reader :frames, :index, :label
|
|
14
|
+
|
|
15
|
+
# *frames* defaults to DEFAULT_FRAMES but may be replaced with any array of frame strings.
|
|
16
|
+
# *index* is the starting frame index. *label* is an optional suffix shown after the frame.
|
|
17
|
+
def initialize(frames: DEFAULT_FRAMES, index: 0, label: nil)
|
|
18
|
+
super()
|
|
19
|
+
raise ArgumentError, "frames cannot be empty" if frames.empty?
|
|
20
|
+
|
|
21
|
+
@frames = frames
|
|
22
|
+
@index = index
|
|
23
|
+
@label = label
|
|
24
|
+
end
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
# Advances the frame index by one position, wrapping around. Returns self for chaining.
|
|
27
|
+
def tick
|
|
28
|
+
@index = (index + 1) % frames.length
|
|
29
|
+
self
|
|
30
|
+
end
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
# Renders the current frame, optionally followed by the label and a space.
|
|
33
|
+
def render
|
|
34
|
+
return frame unless label
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
"#{frame} #{label}"
|
|
37
|
+
end
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
private
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
# Returns the current frame string (with index modulo frame count to be safe).
|
|
42
|
+
def frame
|
|
43
|
+
frames.fetch(index % frames.length)
|
|
36
44
|
end
|
|
37
45
|
end
|
|
38
46
|
end
|
|
@@ -3,115 +3,138 @@
|
|
|
3
3
|
require "tty-table"
|
|
4
4
|
|
|
5
5
|
module Charming
|
|
6
|
-
module
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
6
|
+
module Components
|
|
7
|
+
# Table renders tabular data with a header row, a selected row highlight, and keyboard
|
|
8
|
+
# navigation. Mouse clicks within the body area also select rows. The table is rendered
|
|
9
|
+
# via tty-table and the selected row is overlaid with reverse-video ANSI styling.
|
|
10
|
+
class Table < Component
|
|
11
|
+
include KeyboardHandler
|
|
12
|
+
|
|
13
|
+
# Maps navigation keys to the instance methods that move the selection. Shared with
|
|
14
|
+
# List and Viewport via KeyboardHandler.
|
|
15
|
+
KEY_ACTIONS = {
|
|
16
|
+
up: :move_up,
|
|
17
|
+
down: :move_down,
|
|
18
|
+
home: :move_home,
|
|
19
|
+
end: :move_end
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Number of terminal rows occupied by the table's top border and header line. Used by
|
|
23
|
+
# the mouse handler to translate absolute row coordinates to body rows.
|
|
24
|
+
HEADER_HEIGHT = 2
|
|
25
|
+
|
|
26
|
+
# The header row, the body rows, and the currently selected row index, respectively.
|
|
27
|
+
attr_reader :header, :rows, :selected_index
|
|
28
|
+
|
|
29
|
+
# *header* is an array of column labels. *rows* is the array of body rows (each either a
|
|
30
|
+
# String, an Array, or a Hash of column-value pairs). *selected_index* defaults to 0.
|
|
31
|
+
# *keymap* selects the keybinding style (`:vim` enables h/j/k/l → left/down/up/right).
|
|
32
|
+
def initialize(header:, rows: [], selected_index: 0, keymap: :vim)
|
|
33
|
+
super()
|
|
34
|
+
@header = Array(header).map(&:to_s)
|
|
35
|
+
@rows = Array(rows)
|
|
36
|
+
@selected_index = clamp_index(selected_index)
|
|
37
|
+
@keymap = keymap
|
|
38
|
+
end
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
# Handles key events. Returns `[:selected, row]` on Enter; otherwise delegates to the
|
|
41
|
+
# KeyboardHandler for navigation keys.
|
|
42
|
+
def handle_key(event)
|
|
43
|
+
return nil if rows.empty?
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
45
|
+
case Charming.key_of(event)
|
|
46
|
+
when :enter then [:selected, selected_row]
|
|
47
|
+
else super
|
|
37
48
|
end
|
|
49
|
+
end
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return nil if clicked.negative? || clicked >= rows.length
|
|
51
|
+
# Handles mouse events: a click within the body area selects the clicked row.
|
|
52
|
+
# Returns :handled on a successful click.
|
|
53
|
+
def handle_mouse(event)
|
|
54
|
+
return nil if rows.empty?
|
|
55
|
+
return nil unless event.respond_to?(:click?) && event.click?
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
end
|
|
57
|
+
clicked = event.y - HEADER_HEIGHT
|
|
58
|
+
return nil if clicked.negative? || clicked >= rows.length
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
@selected_index = clicked
|
|
61
|
+
:handled
|
|
62
|
+
end
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
# Returns the currently selected row, or nil when the table is empty.
|
|
65
|
+
def selected_row
|
|
66
|
+
rows[selected_index]
|
|
67
|
+
end
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.lines(chomp: true)
|
|
69
|
+
# Renders the table to a string. Returns a placeholder when both header and rows are empty.
|
|
70
|
+
def render
|
|
71
|
+
return "(empty table)" if header.empty? && rows.empty?
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
normalized = rows.map { |row| normalize_row(row) }
|
|
74
|
+
lines = TTY::Table.new(header: header, rows: normalized)
|
|
75
|
+
.render(:unicode)
|
|
76
|
+
.lines(chomp: true)
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
compact_layout(lines)
|
|
79
|
+
end
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
cells = case row
|
|
69
|
-
when Hash then row.values
|
|
70
|
-
when String then [row]
|
|
71
|
-
else Array(row)
|
|
72
|
-
end
|
|
73
|
-
return cells if header.length <= 1 || cells.length <= header.length
|
|
81
|
+
private
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
# Coerces a *row* (Hash / String / Array) into a flat cell array matching the header.
|
|
84
|
+
# Excess cells are merged into the last column with a space separator.
|
|
85
|
+
def normalize_row(row)
|
|
86
|
+
cells = case row
|
|
87
|
+
when Hash then row.values
|
|
88
|
+
when String then [row]
|
|
89
|
+
else Array(row)
|
|
78
90
|
end
|
|
91
|
+
return cells if header.length <= 1 || cells.length <= header.length
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
kept = cells.first(header.length - 1)
|
|
94
|
+
merged = cells[(header.length - 1)..].join(" ")
|
|
95
|
+
kept + [merged]
|
|
96
|
+
end
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
# Applies the selected-row highlight and trims unused body rows below the actual row count.
|
|
99
|
+
def compact_layout(lines)
|
|
100
|
+
return lines.join("\n") if lines.length < 4
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
top, header_line, _separator, *rest = lines
|
|
103
|
+
body = rest.first(rows.length)
|
|
104
|
+
bottom = rest[rows.length]
|
|
90
105
|
|
|
91
|
-
|
|
106
|
+
highlighted = body.each_with_index.map do |line, index|
|
|
107
|
+
(index == selected_index) ? "\e[7m#{line}\e[m" : line
|
|
92
108
|
end
|
|
93
109
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
end
|
|
110
|
+
[top, header_line, *highlighted, bottom].compact.join("\n")
|
|
111
|
+
end
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
# Moves the selection up one row.
|
|
114
|
+
def move_up
|
|
115
|
+
@selected_index -= 1 if selected_index.positive?
|
|
116
|
+
end
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
# Moves the selection down one row.
|
|
119
|
+
def move_down
|
|
120
|
+
@selected_index += 1 if selected_index < rows.length - 1
|
|
121
|
+
end
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
# Moves the selection to the first row.
|
|
124
|
+
def move_home
|
|
125
|
+
@selected_index = 0
|
|
126
|
+
end
|
|
109
127
|
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
# Moves the selection to the last row.
|
|
129
|
+
def move_end
|
|
130
|
+
@selected_index = rows.length - 1
|
|
131
|
+
end
|
|
112
132
|
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
# Clamps *value* to the valid row range, defaulting to 0 when the table is empty.
|
|
134
|
+
def clamp_index(value)
|
|
135
|
+
return 0 if rows.empty?
|
|
136
|
+
|
|
137
|
+
value.to_i.clamp(0, rows.length - 1)
|
|
115
138
|
end
|
|
116
139
|
end
|
|
117
140
|
end
|