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
|
@@ -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
|
|
@@ -1,211 +1,209 @@
|
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
end
|
|
4
|
+
module UI
|
|
5
|
+
# Style is an immutable builder for terminal text styling. Every method returns a new
|
|
6
|
+
# Style instance with the requested attribute added, so styles can be safely chained and
|
|
7
|
+
# shared across views. `render(value)` applies the accumulated style to a string.
|
|
8
|
+
class Style
|
|
9
|
+
ATTRIBUTES = ANSICodes::ATTRIBUTES
|
|
10
|
+
|
|
11
|
+
COLORS = ANSICodes::COLORS
|
|
12
|
+
|
|
13
|
+
# Initializes a new style with an optional options hash. Recognized keys: `:attributes`
|
|
14
|
+
# (array of attribute symbols), `:padding` ([top, right, bottom, left]), `:align`
|
|
15
|
+
# (`:left`/`:right`/`:center`), and any of `:foreground`, `:background`, `:border`,
|
|
16
|
+
# `:border_sides`, `:border_foreground`, `:width`, `:height`.
|
|
17
|
+
def initialize(options = {})
|
|
18
|
+
@options = {
|
|
19
|
+
attributes: [],
|
|
20
|
+
padding: [0, 0, 0, 0],
|
|
21
|
+
align: :left
|
|
22
|
+
}.merge(options)
|
|
23
|
+
end
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
# Returns a new Style with the foreground *color* set. *color* is a color name (":red"),
|
|
26
|
+
# 256-color index (integer), or hex string ("#rrggbb").
|
|
27
|
+
def foreground(color)
|
|
28
|
+
with(foreground: color)
|
|
29
|
+
end
|
|
30
|
+
alias_method :fg, :foreground
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# Attribute methods (bold, italic, underline, …) are defined dynamically by the
|
|
40
|
-
# metaprogramming loop below. Each toggles a single text attribute on the style.
|
|
41
|
-
ATTRIBUTES.each_key do |attribute|
|
|
42
|
-
define_method(attribute) do
|
|
43
|
-
with(attributes: (@options.fetch(:attributes) + [attribute]).uniq)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
32
|
+
# Returns a new Style with the background *color* set.
|
|
33
|
+
def background(color)
|
|
34
|
+
with(background: color)
|
|
35
|
+
end
|
|
36
|
+
alias_method :bg, :background
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
# Attribute methods (bold, italic, underline, …) are defined dynamically by the
|
|
39
|
+
# metaprogramming loop below. Each toggles a single text attribute on the style.
|
|
40
|
+
ATTRIBUTES.each_key do |attribute|
|
|
41
|
+
define_method(attribute) do
|
|
42
|
+
with(attributes: (@options.fetch(:attributes) + [attribute]).uniq)
|
|
51
43
|
end
|
|
44
|
+
end
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
end
|
|
46
|
+
# Returns a new Style with the padding set. Accepts 1, 2, or 4 values following CSS-style
|
|
47
|
+
# shorthand: 1 → all sides, 2 → [vertical, horizontal], 4 → [top, right, bottom, left].
|
|
48
|
+
def padding(*values)
|
|
49
|
+
with(padding: expand_box_values(values))
|
|
50
|
+
end
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
# Returns a new Style with the border set. *style* is a border name (e.g., :normal,
|
|
53
|
+
# :rounded). *sides* optionally restricts the border to specific sides. *foreground*
|
|
54
|
+
# sets the border color.
|
|
55
|
+
def border(style = :normal, sides: nil, foreground: nil)
|
|
56
|
+
with(border: style, border_sides: sides, border_foreground: foreground)
|
|
57
|
+
end
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
# Returns a new Style that fixes the rendered width to *value* (in display columns).
|
|
60
|
+
def width(value)
|
|
61
|
+
with(width: value)
|
|
62
|
+
end
|
|
69
63
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
# Returns a new Style that fixes the rendered height to *value* (in rows).
|
|
65
|
+
def height(value)
|
|
66
|
+
with(height: value)
|
|
67
|
+
end
|
|
74
68
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
lines = apply_dimensions(value.to_s.lines(chomp: true))
|
|
80
|
-
lines = apply_padding(lines)
|
|
81
|
-
lines = apply_border(lines)
|
|
82
|
-
apply_ansi(lines.join("\n"))
|
|
83
|
-
end
|
|
69
|
+
# Returns a new Style with horizontal alignment set (`:left`, `:right`, or `:center`).
|
|
70
|
+
def align(value)
|
|
71
|
+
with(align: value)
|
|
72
|
+
end
|
|
84
73
|
|
|
85
|
-
|
|
74
|
+
# Applies the configured style to *value* and returns the styled string. Steps:
|
|
75
|
+
# 1. wrap to `:width`, 2. align horizontally, 3. expand to `:height`, 4. apply padding,
|
|
76
|
+
# 5. paint border, 6. emit ANSI attribute/foreground/background escapes.
|
|
77
|
+
def render(value)
|
|
78
|
+
lines = apply_dimensions(value.to_s.lines(chomp: true))
|
|
79
|
+
lines = apply_padding(lines)
|
|
80
|
+
lines = apply_border(lines)
|
|
81
|
+
apply_ansi(lines.join("\n"))
|
|
82
|
+
end
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
def with(changes)
|
|
89
|
-
self.class.new(@options.merge(changes))
|
|
90
|
-
end
|
|
84
|
+
private
|
|
91
85
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
dimensioned = lines.map { |line| align_line(fit_line(line, content_width), content_width) }
|
|
97
|
-
apply_height(dimensioned, content_width)
|
|
98
|
-
end
|
|
86
|
+
# Returns a copy of self with *changes* merged into the options hash.
|
|
87
|
+
def with(changes)
|
|
88
|
+
self.class.new(@options.merge(changes))
|
|
89
|
+
end
|
|
99
90
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
# Wraps each line to the target width and applies horizontal alignment, then expands
|
|
92
|
+
# to the target height.
|
|
93
|
+
def apply_dimensions(lines)
|
|
94
|
+
content_width = target_content_width(lines)
|
|
95
|
+
dimensioned = lines.map { |line| align_line(fit_line(line, content_width), content_width) }
|
|
96
|
+
apply_height(dimensioned, content_width)
|
|
97
|
+
end
|
|
107
98
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
# Returns the target content width: the explicit :width if set, otherwise the natural
|
|
100
|
+
# max display width of the lines.
|
|
101
|
+
def target_content_width(lines)
|
|
102
|
+
explicit_width = @options[:width]
|
|
103
|
+
natural_width = lines.map { |line| Width.measure(line) }.max || 0
|
|
104
|
+
explicit_width || natural_width
|
|
105
|
+
end
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
# Clips *line* to *width* display columns, preserving ANSI styling where possible.
|
|
108
|
+
def fit_line(line, width)
|
|
109
|
+
return line if Width.measure(line) <= width
|
|
110
|
+
|
|
111
|
+
UI.visible_slice(line, 0, width)
|
|
112
|
+
end
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
# Truncates or pads the lines array to *height* rows, filling with blank rows.
|
|
115
|
+
def apply_height(lines, width)
|
|
116
|
+
height = @options[:height]
|
|
117
|
+
return lines unless height
|
|
119
118
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
visible = lines.first(height)
|
|
120
|
+
visible + Array.new([height - visible.length, 0].max) { " " * width }
|
|
121
|
+
end
|
|
123
122
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
Array.new(top, empty) + padded + Array.new(bottom, empty)
|
|
123
|
+
# Applies padding by prepending/appending blank rows (vertical) and indenting each
|
|
124
|
+
# line (horizontal).
|
|
125
|
+
def apply_padding(lines)
|
|
126
|
+
top, right, bottom, left = @options.fetch(:padding)
|
|
127
|
+
inner_width = lines.map { |line| Width.measure(line) }.max || 0
|
|
128
|
+
empty = " " * (left + inner_width + right)
|
|
129
|
+
padded = lines.map do |line|
|
|
130
|
+
pad_line(line, inner_width, left, right)
|
|
135
131
|
end
|
|
136
132
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
border_name = @options[:border]
|
|
140
|
-
return lines unless border_name
|
|
133
|
+
Array.new(top, empty) + padded + Array.new(bottom, empty)
|
|
134
|
+
end
|
|
141
135
|
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
# Paints the configured border around the lines, when :border is set.
|
|
137
|
+
def apply_border(lines)
|
|
138
|
+
border_name = @options[:border]
|
|
139
|
+
return lines unless border_name
|
|
144
140
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
(" " * left) + line + (" " * (inner_width - Width.measure(line) + right))
|
|
148
|
-
end
|
|
141
|
+
border_painter(border_name).paint(lines, content_width(lines))
|
|
142
|
+
end
|
|
149
143
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
sides: @options[:border_sides],
|
|
155
|
-
foreground: @options[:border_foreground],
|
|
156
|
-
background: @options[:background]
|
|
157
|
-
)
|
|
158
|
-
end
|
|
144
|
+
# Pads a single line to *inner_width*, with *left* and *right* padding spaces.
|
|
145
|
+
def pad_line(line, inner_width, left, right)
|
|
146
|
+
(" " * left) + line + (" " * (inner_width - Width.measure(line) + right))
|
|
147
|
+
end
|
|
159
148
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
149
|
+
# Builds a BorderPainter configured for the current border options.
|
|
150
|
+
def border_painter(border_name)
|
|
151
|
+
BorderPainter.new(
|
|
152
|
+
border: Border.fetch(border_name),
|
|
153
|
+
sides: @options[:border_sides],
|
|
154
|
+
foreground: @options[:border_foreground],
|
|
155
|
+
background: @options[:background]
|
|
156
|
+
)
|
|
157
|
+
end
|
|
164
158
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
159
|
+
# Returns the natural display width of the longest line in *lines*.
|
|
160
|
+
def content_width(lines)
|
|
161
|
+
lines.map { |line| Width.measure(line) }.max || 0
|
|
162
|
+
end
|
|
169
163
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
# Applies the active ANSI attribute/foreground/background codes to *value*.
|
|
165
|
+
def apply_ansi(value)
|
|
166
|
+
ansi_codes_obj.apply(value)
|
|
167
|
+
end
|
|
174
168
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
169
|
+
# The list of active ANSI escape sequence strings (attribute + foreground + background).
|
|
170
|
+
def ansi_codes
|
|
171
|
+
ansi_codes_obj.codes
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Builds an ANSICodes object from the active attributes, foreground, and background.
|
|
175
|
+
def ansi_codes_obj
|
|
176
|
+
ANSICodes.new(
|
|
177
|
+
attributes: @options.fetch(:attributes),
|
|
178
|
+
foreground: @options[:foreground],
|
|
179
|
+
background: @options[:background]
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Pads *line* on the left or right (or both, for :center) according to :align.
|
|
184
|
+
def align_line(line, width)
|
|
185
|
+
remaining = width - Width.measure(line)
|
|
186
|
+
return line if remaining <= 0
|
|
183
187
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
when :center
|
|
193
|
-
left = remaining / 2
|
|
194
|
-
(" " * left) + line + (" " * (remaining - left))
|
|
195
|
-
else
|
|
196
|
-
line + (" " * remaining)
|
|
197
|
-
end
|
|
188
|
+
case @options.fetch(:align)
|
|
189
|
+
when :right
|
|
190
|
+
(" " * remaining) + line
|
|
191
|
+
when :center
|
|
192
|
+
left = remaining / 2
|
|
193
|
+
(" " * left) + line + (" " * (remaining - left))
|
|
194
|
+
else
|
|
195
|
+
line + (" " * remaining)
|
|
198
196
|
end
|
|
197
|
+
end
|
|
199
198
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
end
|
|
199
|
+
# Normalizes 1/2/4 padding value arguments into a [top, right, bottom, left] array.
|
|
200
|
+
def expand_box_values(values)
|
|
201
|
+
case values.length
|
|
202
|
+
when 1 then [values[0], values[0], values[0], values[0]]
|
|
203
|
+
when 2 then [values[0], values[1], values[0], values[1]]
|
|
204
|
+
when 4 then values
|
|
205
|
+
else
|
|
206
|
+
raise ArgumentError, "padding expects 1, 2, or 4 values"
|
|
209
207
|
end
|
|
210
208
|
end
|
|
211
209
|
end
|