charming 0.1.2 → 0.1.4
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 +22 -5
- data/lib/charming/cli.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/component_dispatching.rb +47 -3
- data/lib/charming/controller/focus.rb +123 -0
- data/lib/charming/controller/focus_management.rb +1 -1
- data/lib/charming/controller/rendering.rb +6 -17
- data/lib/charming/controller/session_state.rb +12 -1
- data/lib/charming/controller.rb +11 -2
- data/lib/charming/database/commands.rb +106 -0
- data/lib/charming/generators/component_generator.rb +1 -1
- data/lib/charming/generators/database_installer.rb +154 -0
- data/lib/charming/generators/model_generator.rb +2 -10
- data/lib/charming/generators/name.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/generators/view_generator.rb +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 +129 -118
- data/lib/charming/presentation/layout/rect.rb +19 -16
- data/lib/charming/presentation/layout/screen_layout.rb +47 -42
- data/lib/charming/presentation/layout/split.rb +107 -102
- data/lib/charming/presentation/layout.rb +28 -30
- data/lib/charming/presentation/markdown/render_context.rb +30 -14
- data/lib/charming/presentation/markdown/renderer.rb +302 -79
- data/lib/charming/presentation/markdown/style_config.rb +215 -0
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -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 +110 -105
- data/lib/charming/router.rb +3 -8
- data/lib/charming/runtime.rb +25 -10
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +5 -4
- metadata +43 -9
- data/lib/charming/database_commands.rb +0 -103
- data/lib/charming/database_installer.rb +0 -152
- data/lib/charming/focus.rb +0 -121
- data/lib/charming/presentation/markdown/block_renderers.rb +0 -120
- data/lib/charming/presentation/markdown/inline_renderers.rb +0 -68
|
@@ -1,94 +1,92 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
end
|
|
4
|
+
module UI
|
|
5
|
+
# ANSISlicer extracts a visible substring from a string that may contain ANSI
|
|
6
|
+
# escape sequences, preserving the styling that is active at the start of
|
|
7
|
+
# the slice and emitting a trailing reset if any styled content was copied.
|
|
8
|
+
class ANSISlicer
|
|
9
|
+
def self.slice(line, start_column, width)
|
|
10
|
+
return "" unless width.positive?
|
|
11
|
+
|
|
12
|
+
slice_range(line.to_s, start_column, start_column + width)
|
|
13
|
+
end
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
def self.slice_range(line, start_column, end_column)
|
|
16
|
+
state = {column: 0, output: +"", active: [], started: false, styled: false}
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
end
|
|
18
|
+
each_ansi_or_char(line) do |token, ansi|
|
|
19
|
+
if ansi
|
|
20
|
+
slice_ansi_token(token, state, start_column, end_column)
|
|
21
|
+
else
|
|
22
|
+
slice_char(token, state, start_column, end_column)
|
|
25
23
|
end
|
|
26
|
-
|
|
27
|
-
terminate_slice(state)
|
|
28
24
|
end
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
terminate_slice(state)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.each_ansi_or_char(line)
|
|
30
|
+
index = 0
|
|
31
|
+
while index < line.length
|
|
32
|
+
match = line.match(Width::ANSI_PATTERN, index)
|
|
33
|
+
if match&.begin(0) == index
|
|
34
|
+
yield match[0], true
|
|
35
|
+
index = match.end(0)
|
|
36
|
+
else
|
|
37
|
+
yield line[index], false
|
|
38
|
+
index += 1
|
|
41
39
|
end
|
|
42
40
|
end
|
|
41
|
+
end
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
def self.slice_ansi_token(token, state, start_column, end_column)
|
|
44
|
+
started = state[:started]
|
|
45
|
+
update_active_styles(state[:active], token)
|
|
46
|
+
return unless state[:column].between?(start_column, end_column - 1)
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
end
|
|
48
|
+
start_slice(state)
|
|
49
|
+
if started
|
|
50
|
+
state[:output] << token
|
|
51
|
+
state[:styled] = !token.include?("[0m")
|
|
54
52
|
end
|
|
53
|
+
end
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
def self.slice_char(char, state, start_column, end_column)
|
|
56
|
+
char_width = Width.measure(char)
|
|
57
|
+
char_start = state[:column]
|
|
58
|
+
char_end = char_start + char_width
|
|
59
|
+
state[:column] = char_end
|
|
60
|
+
return unless char_end > start_column && char_start < end_column
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
start_slice(state)
|
|
63
|
+
state[:output] << char
|
|
64
|
+
end
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
def self.start_slice(state)
|
|
67
|
+
return if state[:started]
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
state[:output] << state[:active].join
|
|
70
|
+
state[:styled] = true unless state[:active].empty?
|
|
71
|
+
state[:started] = true
|
|
72
|
+
end
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
def self.terminate_slice(state)
|
|
75
|
+
return state[:output] if !state[:styled] || state[:output].empty?
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
"#{state[:output]}\e[0m"
|
|
78
|
+
end
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
end
|
|
80
|
+
def self.update_active_styles(active, token)
|
|
81
|
+
if token.include?("[0m")
|
|
82
|
+
active.clear
|
|
83
|
+
else
|
|
84
|
+
active << token
|
|
87
85
|
end
|
|
88
|
-
|
|
89
|
-
private_class_method :each_ansi_or_char, :slice_ansi_token, :slice_char,
|
|
90
|
-
:start_slice, :terminate_slice, :update_active_styles
|
|
91
86
|
end
|
|
87
|
+
|
|
88
|
+
private_class_method :each_ansi_or_char, :slice_ansi_token, :slice_char,
|
|
89
|
+
:start_slice, :terminate_slice, :update_active_styles
|
|
92
90
|
end
|
|
93
91
|
end
|
|
94
92
|
end
|
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
attr_reader :top_left, :top_right, :bottom_left, :bottom_right, :horizontal, :vertical
|
|
4
|
+
module UI
|
|
5
|
+
class Border
|
|
6
|
+
attr_reader :top_left, :top_right, :bottom_left, :bottom_right, :horizontal, :vertical
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def self.fetch(name)
|
|
15
|
-
STYLES.fetch(name.to_sym)
|
|
16
|
-
end
|
|
8
|
+
def initialize(corners:, edges:)
|
|
9
|
+
@top_left, @top_right, @bottom_left, @bottom_right = corners
|
|
10
|
+
@horizontal, @vertical = edges
|
|
17
11
|
end
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
),
|
|
23
|
-
rounded: Border.new(
|
|
24
|
-
corners: ["╭", "╮", "╰", "╯"], edges: ["─", "│"]
|
|
25
|
-
),
|
|
26
|
-
thick: Border.new(
|
|
27
|
-
corners: ["┏", "┓", "┗", "┛"], edges: ["━", "┃"]
|
|
28
|
-
),
|
|
29
|
-
double: Border.new(
|
|
30
|
-
corners: ["╔", "╗", "╚", "╝"], edges: ["═", "║"]
|
|
31
|
-
)
|
|
32
|
-
}.freeze
|
|
13
|
+
def self.fetch(name)
|
|
14
|
+
STYLES.fetch(name.to_sym)
|
|
15
|
+
end
|
|
33
16
|
end
|
|
17
|
+
|
|
18
|
+
Border::STYLES = {
|
|
19
|
+
normal: Border.new(
|
|
20
|
+
corners: ["+", "+", "+", "+"], edges: ["-", "|"]
|
|
21
|
+
),
|
|
22
|
+
rounded: Border.new(
|
|
23
|
+
corners: ["╭", "╮", "╰", "╯"], edges: ["─", "│"]
|
|
24
|
+
),
|
|
25
|
+
thick: Border.new(
|
|
26
|
+
corners: ["┏", "┓", "┗", "┛"], edges: ["━", "┃"]
|
|
27
|
+
),
|
|
28
|
+
double: Border.new(
|
|
29
|
+
corners: ["╔", "╗", "╚", "╝"], edges: ["═", "║"]
|
|
30
|
+
)
|
|
31
|
+
}.freeze
|
|
34
32
|
end
|
|
35
33
|
end
|
|
@@ -1,57 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
end
|
|
4
|
+
module UI
|
|
5
|
+
class BorderPainter
|
|
6
|
+
DEFAULT_SIDES = %i[top right bottom left].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(border:, sides: nil, foreground: nil, background: nil)
|
|
9
|
+
@border = border
|
|
10
|
+
@sides = Array(sides || DEFAULT_SIDES).map(&:to_sym)
|
|
11
|
+
@foreground = foreground
|
|
12
|
+
@background = background
|
|
13
|
+
end
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
def paint(lines, inner_width)
|
|
16
|
+
horizontal = @border.horizontal * inner_width
|
|
17
|
+
body = lines.map { |line| border_line(line, inner_width) }
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
[top_border(horizontal), *body, bottom_border(horizontal)].compact
|
|
20
|
+
end
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
private
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
def border_line(line, width)
|
|
25
|
+
left = @sides.include?(:left) ? render_border(@border.vertical) : ""
|
|
26
|
+
right = @sides.include?(:right) ? render_border(@border.vertical) : ""
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
"#{left}#{line}#{" " * (width - Width.measure(line))}#{right}"
|
|
29
|
+
end
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
def top_border(horizontal)
|
|
32
|
+
return unless @sides.include?(:top)
|
|
33
|
+
return render_border(horizontal) unless full_horizontal?
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
render_border("#{@border.top_left}#{horizontal}#{@border.top_right}")
|
|
36
|
+
end
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
def bottom_border(horizontal)
|
|
39
|
+
return unless @sides.include?(:bottom)
|
|
40
|
+
return render_border(horizontal) unless full_horizontal?
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
render_border("#{@border.bottom_left}#{horizontal}#{@border.bottom_right}")
|
|
43
|
+
end
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
def full_horizontal?
|
|
46
|
+
@sides.include?(:left) && @sides.include?(:right)
|
|
47
|
+
end
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
def render_border(value)
|
|
50
|
+
return value unless @foreground
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
end
|
|
52
|
+
Style.new(foreground: @foreground, background: @background).render(value)
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
end
|
|
@@ -1,81 +1,79 @@
|
|
|
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
|
-
end
|
|
4
|
+
module UI
|
|
5
|
+
# Canvas is a 2D character grid of fixed width and height that supports
|
|
6
|
+
# placing content at (row, column) coordinates and overlaying one block
|
|
7
|
+
# on top of another. Construct via .new(width, height) for a blank grid
|
|
8
|
+
# or .parse(string) to reconstruct from rendered output.
|
|
9
|
+
class Canvas
|
|
10
|
+
def initialize(width, height)
|
|
11
|
+
@width = width
|
|
12
|
+
@height = height
|
|
13
|
+
@grid = Array.new(height) { " " * width }
|
|
14
|
+
end
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
def self.parse(string)
|
|
17
|
+
lines = string.to_s.lines(chomp: true)
|
|
18
|
+
width = UI.block_width(lines)
|
|
19
|
+
canvas = new(width, lines.length)
|
|
20
|
+
lines.each_with_index { |line, i| canvas.instance_variable_get(:@grid)[i] = line }
|
|
21
|
+
canvas
|
|
22
|
+
end
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
def to_s
|
|
25
|
+
@grid.join("\n")
|
|
26
|
+
end
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
def place(block, top: 0, left: 0, background: nil)
|
|
29
|
+
lines = block.to_s.lines(chomp: true)
|
|
30
|
+
row = Canvas.offset(top, @height, lines.length)
|
|
31
|
+
column = Canvas.offset(left, @width, UI.block_width(lines))
|
|
32
|
+
draw_lines(lines, row: row, column: column, onto: @grid)
|
|
33
|
+
rendered = to_s
|
|
34
|
+
background ? UI::Style.new.background(background).render(rendered) : rendered
|
|
35
|
+
end
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
def overlay(other, top: :center, left: :center)
|
|
38
|
+
overlay_lines = other.to_s.lines(chomp: true)
|
|
39
|
+
row = Canvas.offset(top, @grid.length, overlay_lines.length)
|
|
40
|
+
column = Canvas.offset(left, @width, UI.block_width(overlay_lines))
|
|
41
|
+
draw_lines(overlay_lines, row: row, column: column, onto: @grid)
|
|
42
|
+
self
|
|
43
|
+
end
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
def self.offset(value, available, size)
|
|
46
|
+
return [(available - size) / 2, 0].max if value == :center
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
value
|
|
49
|
+
end
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
private
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
def draw_lines(lines, row:, column:, onto:)
|
|
54
|
+
lines.each_with_index do |line, index|
|
|
55
|
+
line_index = row + index
|
|
56
|
+
next if line_index.negative? || line_index >= onto.length
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
end
|
|
58
|
+
onto[line_index] = compose_line(onto[line_index], line, column)
|
|
61
59
|
end
|
|
60
|
+
end
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
def compose_line(base_line, overlay_line, column)
|
|
63
|
+
return ANSISlicer.slice(base_line, 0, @width) if column >= @width
|
|
64
|
+
return ANSISlicer.slice(base_line, 0, @width) if column + Width.measure(overlay_line) <= 0
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
target_column = [column, 0].max
|
|
67
|
+
overlay_start = [0 - column, 0].max
|
|
68
|
+
overlay = ANSISlicer.slice(overlay_line, overlay_start, @width - target_column)
|
|
69
|
+
overlay_width = Width.measure(overlay)
|
|
70
|
+
return ANSISlicer.slice(base_line, 0, @width) if overlay_width.zero?
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
right_column = target_column + overlay_width
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end
|
|
74
|
+
ANSISlicer.slice(base_line, 0, target_column) +
|
|
75
|
+
overlay +
|
|
76
|
+
ANSISlicer.slice(base_line, right_column, [@width - right_column, 0].max)
|
|
79
77
|
end
|
|
80
78
|
end
|
|
81
79
|
end
|