charming 0.1.2 → 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/lib/charming/application.rb +3 -3
- data/lib/charming/controller/class_methods.rb +2 -2
- data/lib/charming/controller/command_palette.rb +2 -2
- data/lib/charming/controller/rendering.rb +2 -2
- data/lib/charming/controller/session_state.rb +1 -1
- data/lib/charming/generators/component_generator.rb +1 -1
- data/lib/charming/generators/templates/app/application.template +1 -1
- data/lib/charming/generators/templates/app/layout.template +3 -6
- data/lib/charming/generators/templates/app/view.template +1 -1
- data/lib/charming/generators/templates/component/component.rb.template +1 -1
- data/lib/charming/generators/templates/screen/view.rb.template +1 -1
- data/lib/charming/generators/templates/view/view.rb.template +1 -1
- data/lib/charming/internal/renderer/differential.rb +13 -5
- data/lib/charming/internal/terminal/tty_backend.rb +22 -2
- 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 -49
- data/lib/charming/presentation/components/form/builder.rb +52 -54
- data/lib/charming/presentation/components/form/confirm.rb +49 -51
- data/lib/charming/presentation/components/form/field.rb +94 -96
- data/lib/charming/presentation/components/form/input.rb +53 -55
- data/lib/charming/presentation/components/form/note.rb +27 -29
- data/lib/charming/presentation/components/form/select.rb +84 -86
- data/lib/charming/presentation/components/form/textarea.rb +67 -69
- data/lib/charming/presentation/components/form.rb +120 -122
- data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
- data/lib/charming/presentation/components/list.rb +123 -125
- data/lib/charming/presentation/components/markdown.rb +21 -23
- data/lib/charming/presentation/components/modal.rb +46 -48
- data/lib/charming/presentation/components/progressbar.rb +51 -53
- data/lib/charming/presentation/components/spinner.rb +40 -42
- data/lib/charming/presentation/components/table.rb +109 -111
- data/lib/charming/presentation/components/text_area.rb +219 -221
- data/lib/charming/presentation/components/text_input.rb +120 -122
- data/lib/charming/presentation/components/viewport.rb +218 -220
- data/lib/charming/presentation/layout/builder.rb +64 -66
- data/lib/charming/presentation/layout/overlay.rb +48 -50
- data/lib/charming/presentation/layout/pane.rb +122 -118
- data/lib/charming/presentation/layout/rect.rb +14 -16
- data/lib/charming/presentation/layout/screen_layout.rb +40 -42
- data/lib/charming/presentation/layout/split.rb +101 -103
- data/lib/charming/presentation/layout.rb +28 -30
- data/lib/charming/presentation/markdown/block_renderers.rb +94 -96
- data/lib/charming/presentation/markdown/inline_renderers.rb +52 -54
- data/lib/charming/presentation/markdown/render_context.rb +12 -14
- data/lib/charming/presentation/markdown/renderer.rb +84 -86
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +57 -59
- data/lib/charming/presentation/markdown.rb +4 -6
- data/lib/charming/presentation/template_view.rb +22 -24
- data/lib/charming/presentation/templates/erb_handler.rb +4 -6
- data/lib/charming/presentation/templates.rb +47 -49
- data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
- data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
- data/lib/charming/presentation/ui/border.rb +24 -26
- data/lib/charming/presentation/ui/border_painter.rb +37 -39
- data/lib/charming/presentation/ui/canvas.rb +59 -61
- data/lib/charming/presentation/ui/style.rb +173 -175
- data/lib/charming/presentation/ui/theme.rb +133 -135
- data/lib/charming/presentation/ui/width.rb +12 -14
- data/lib/charming/presentation/ui.rb +69 -71
- data/lib/charming/presentation/view.rb +103 -105
- data/lib/charming/runtime.rb +23 -10
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +3 -2
- metadata +2 -1
|
@@ -3,140 +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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
end
|
|
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
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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?
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
end
|
|
45
|
+
case Charming.key_of(event)
|
|
46
|
+
when :enter then [:selected, selected_row]
|
|
47
|
+
else super
|
|
50
48
|
end
|
|
49
|
+
end
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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?
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
clicked = event.y - HEADER_HEIGHT
|
|
58
|
+
return nil if clicked.negative? || clicked >= rows.length
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
@selected_index = clicked
|
|
61
|
+
:handled
|
|
62
|
+
end
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
# Returns the currently selected row, or nil when the table is empty.
|
|
65
|
+
def selected_row
|
|
66
|
+
rows[selected_index]
|
|
67
|
+
end
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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?
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
normalized = rows.map { |row| normalize_row(row) }
|
|
74
|
+
lines = TTY::Table.new(header: header, rows: normalized)
|
|
75
|
+
.render(:unicode)
|
|
76
|
+
.lines(chomp: true)
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
compact_layout(lines)
|
|
79
|
+
end
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
end
|
|
92
|
-
return cells if header.length <= 1 || cells.length <= header.length
|
|
93
|
-
|
|
94
|
-
kept = cells.first(header.length - 1)
|
|
95
|
-
merged = cells[(header.length - 1)..].join(" ")
|
|
96
|
-
kept + [merged]
|
|
81
|
+
private
|
|
82
|
+
|
|
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)
|
|
97
90
|
end
|
|
91
|
+
return cells if header.length <= 1 || cells.length <= header.length
|
|
98
92
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
kept = cells.first(header.length - 1)
|
|
94
|
+
merged = cells[(header.length - 1)..].join(" ")
|
|
95
|
+
kept + [merged]
|
|
96
|
+
end
|
|
102
97
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
106
101
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
top, header_line, _separator, *rest = lines
|
|
103
|
+
body = rest.first(rows.length)
|
|
104
|
+
bottom = rest[rows.length]
|
|
110
105
|
|
|
111
|
-
|
|
106
|
+
highlighted = body.each_with_index.map do |line, index|
|
|
107
|
+
(index == selected_index) ? "\e[7m#{line}\e[m" : line
|
|
112
108
|
end
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@selected_index -= 1 if selected_index.positive?
|
|
117
|
-
end
|
|
110
|
+
[top, header_line, *highlighted, bottom].compact.join("\n")
|
|
111
|
+
end
|
|
118
112
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
# Moves the selection up one row.
|
|
114
|
+
def move_up
|
|
115
|
+
@selected_index -= 1 if selected_index.positive?
|
|
116
|
+
end
|
|
123
117
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
# Moves the selection down one row.
|
|
119
|
+
def move_down
|
|
120
|
+
@selected_index += 1 if selected_index < rows.length - 1
|
|
121
|
+
end
|
|
128
122
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
# Moves the selection to the first row.
|
|
124
|
+
def move_home
|
|
125
|
+
@selected_index = 0
|
|
126
|
+
end
|
|
133
127
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
# Moves the selection to the last row.
|
|
129
|
+
def move_end
|
|
130
|
+
@selected_index = rows.length - 1
|
|
131
|
+
end
|
|
137
132
|
|
|
138
|
-
|
|
139
|
-
|
|
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)
|
|
140
138
|
end
|
|
141
139
|
end
|
|
142
140
|
end
|