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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/charming/application.rb +3 -3
  3. data/lib/charming/controller/class_methods.rb +2 -2
  4. data/lib/charming/controller/command_palette.rb +2 -2
  5. data/lib/charming/controller/rendering.rb +2 -2
  6. data/lib/charming/controller/session_state.rb +1 -1
  7. data/lib/charming/generators/component_generator.rb +1 -1
  8. data/lib/charming/generators/templates/app/application.template +1 -1
  9. data/lib/charming/generators/templates/app/layout.template +3 -6
  10. data/lib/charming/generators/templates/app/view.template +1 -1
  11. data/lib/charming/generators/templates/component/component.rb.template +1 -1
  12. data/lib/charming/generators/templates/screen/view.rb.template +1 -1
  13. data/lib/charming/generators/templates/view/view.rb.template +1 -1
  14. data/lib/charming/internal/renderer/differential.rb +13 -5
  15. data/lib/charming/internal/terminal/tty_backend.rb +22 -2
  16. data/lib/charming/presentation/component.rb +3 -5
  17. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  18. data/lib/charming/presentation/components/command_palette.rb +94 -96
  19. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  20. data/lib/charming/presentation/components/empty_state.rb +47 -49
  21. data/lib/charming/presentation/components/form/builder.rb +52 -54
  22. data/lib/charming/presentation/components/form/confirm.rb +49 -51
  23. data/lib/charming/presentation/components/form/field.rb +94 -96
  24. data/lib/charming/presentation/components/form/input.rb +53 -55
  25. data/lib/charming/presentation/components/form/note.rb +27 -29
  26. data/lib/charming/presentation/components/form/select.rb +84 -86
  27. data/lib/charming/presentation/components/form/textarea.rb +67 -69
  28. data/lib/charming/presentation/components/form.rb +120 -122
  29. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  30. data/lib/charming/presentation/components/list.rb +123 -125
  31. data/lib/charming/presentation/components/markdown.rb +21 -23
  32. data/lib/charming/presentation/components/modal.rb +46 -48
  33. data/lib/charming/presentation/components/progressbar.rb +51 -53
  34. data/lib/charming/presentation/components/spinner.rb +40 -42
  35. data/lib/charming/presentation/components/table.rb +109 -111
  36. data/lib/charming/presentation/components/text_area.rb +219 -221
  37. data/lib/charming/presentation/components/text_input.rb +120 -122
  38. data/lib/charming/presentation/components/viewport.rb +218 -220
  39. data/lib/charming/presentation/layout/builder.rb +64 -66
  40. data/lib/charming/presentation/layout/overlay.rb +48 -50
  41. data/lib/charming/presentation/layout/pane.rb +122 -118
  42. data/lib/charming/presentation/layout/rect.rb +14 -16
  43. data/lib/charming/presentation/layout/screen_layout.rb +40 -42
  44. data/lib/charming/presentation/layout/split.rb +101 -103
  45. data/lib/charming/presentation/layout.rb +28 -30
  46. data/lib/charming/presentation/markdown/block_renderers.rb +94 -96
  47. data/lib/charming/presentation/markdown/inline_renderers.rb +52 -54
  48. data/lib/charming/presentation/markdown/render_context.rb +12 -14
  49. data/lib/charming/presentation/markdown/renderer.rb +84 -86
  50. data/lib/charming/presentation/markdown/syntax_highlighter.rb +57 -59
  51. data/lib/charming/presentation/markdown.rb +4 -6
  52. data/lib/charming/presentation/template_view.rb +22 -24
  53. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  54. data/lib/charming/presentation/templates.rb +47 -49
  55. data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
  56. data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
  57. data/lib/charming/presentation/ui/border.rb +24 -26
  58. data/lib/charming/presentation/ui/border_painter.rb +37 -39
  59. data/lib/charming/presentation/ui/canvas.rb +59 -61
  60. data/lib/charming/presentation/ui/style.rb +173 -175
  61. data/lib/charming/presentation/ui/theme.rb +133 -135
  62. data/lib/charming/presentation/ui/width.rb +12 -14
  63. data/lib/charming/presentation/ui.rb +69 -71
  64. data/lib/charming/presentation/view.rb +103 -105
  65. data/lib/charming/runtime.rb +23 -10
  66. data/lib/charming/version.rb +1 -1
  67. data/lib/charming.rb +3 -2
  68. metadata +2 -1
@@ -1,81 +1,79 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module UI
6
- # Canvas is a 2D character grid of fixed width and height that supports
7
- # placing content at (row, column) coordinates and overlaying one block
8
- # on top of another. Construct via .new(width, height) for a blank grid
9
- # or .parse(string) to reconstruct from rendered output.
10
- class Canvas
11
- def initialize(width, height)
12
- @width = width
13
- @height = height
14
- @grid = Array.new(height) { " " * width }
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
- def self.parse(string)
18
- lines = string.to_s.lines(chomp: true)
19
- width = UI.block_width(lines)
20
- canvas = new(width, lines.length)
21
- lines.each_with_index { |line, i| canvas.instance_variable_get(:@grid)[i] = line }
22
- canvas
23
- end
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
- def to_s
26
- @grid.join("\n")
27
- end
24
+ def to_s
25
+ @grid.join("\n")
26
+ end
28
27
 
29
- def place(block, top: 0, left: 0, background: nil)
30
- lines = block.to_s.lines(chomp: true)
31
- row = Canvas.offset(top, @height, lines.length)
32
- column = Canvas.offset(left, @width, UI.block_width(lines))
33
- draw_lines(lines, row: row, column: column, onto: @grid)
34
- rendered = to_s
35
- background ? UI::Style.new.background(background).render(rendered) : rendered
36
- end
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
- def overlay(other, top: :center, left: :center)
39
- overlay_lines = other.to_s.lines(chomp: true)
40
- row = Canvas.offset(top, @grid.length, overlay_lines.length)
41
- column = Canvas.offset(left, @width, UI.block_width(overlay_lines))
42
- draw_lines(overlay_lines, row: row, column: column, onto: @grid)
43
- self
44
- end
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
- def self.offset(value, available, size)
47
- return [(available - size) / 2, 0].max if value == :center
45
+ def self.offset(value, available, size)
46
+ return [(available - size) / 2, 0].max if value == :center
48
47
 
49
- value
50
- end
48
+ value
49
+ end
51
50
 
52
- private
51
+ private
53
52
 
54
- def draw_lines(lines, row:, column:, onto:)
55
- lines.each_with_index do |line, index|
56
- line_index = row + index
57
- next if line_index.negative? || line_index >= onto.length
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
- onto[line_index] = compose_line(onto[line_index], line, column)
60
- end
58
+ onto[line_index] = compose_line(onto[line_index], line, column)
61
59
  end
60
+ end
62
61
 
63
- def compose_line(base_line, overlay_line, column)
64
- return ANSISlicer.slice(base_line, 0, @width) if column >= @width
65
- return ANSISlicer.slice(base_line, 0, @width) if column + Width.measure(overlay_line) <= 0
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
- target_column = [column, 0].max
68
- overlay_start = [0 - column, 0].max
69
- overlay = ANSISlicer.slice(overlay_line, overlay_start, @width - target_column)
70
- overlay_width = Width.measure(overlay)
71
- return ANSISlicer.slice(base_line, 0, @width) if overlay_width.zero?
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
- right_column = target_column + overlay_width
72
+ right_column = target_column + overlay_width
74
73
 
75
- ANSISlicer.slice(base_line, 0, target_column) +
76
- overlay +
77
- ANSISlicer.slice(base_line, right_column, [@width - right_column, 0].max)
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 Presentation
5
- module UI
6
- # Style is an immutable builder for terminal text styling. Every method returns a new
7
- # Style instance with the requested attribute added, so styles can be safely chained and
8
- # shared across views. `render(value)` applies the accumulated style to a string.
9
- class Style
10
- ATTRIBUTES = ANSICodes::ATTRIBUTES
11
-
12
- COLORS = ANSICodes::COLORS
13
-
14
- # Initializes a new style with an optional options hash. Recognized keys: `:attributes`
15
- # (array of attribute symbols), `:padding` ([top, right, bottom, left]), `:align`
16
- # (`:left`/`:right`/`:center`), and any of `:foreground`, `:background`, `:border`,
17
- # `:border_sides`, `:border_foreground`, `:width`, `:height`.
18
- def initialize(options = {})
19
- @options = {
20
- attributes: [],
21
- padding: [0, 0, 0, 0],
22
- align: :left
23
- }.merge(options)
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
- # Returns a new Style with the foreground *color* set. *color* is a color name (":red"),
27
- # 256-color index (integer), or hex string ("#rrggbb").
28
- def foreground(color)
29
- with(foreground: color)
30
- end
31
- alias_method :fg, :foreground
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
- # Returns a new Style with the background *color* set.
34
- def background(color)
35
- with(background: color)
36
- end
37
- alias_method :bg, :background
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
- # Returns a new Style with the padding set. Accepts 1, 2, or 4 values following CSS-style
48
- # shorthand: 1 all sides, 2 [vertical, horizontal], 4 [top, right, bottom, left].
49
- def padding(*values)
50
- with(padding: expand_box_values(values))
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
- # Returns a new Style with the border set. *style* is a border name (e.g., :normal,
54
- # :rounded). *sides* optionally restricts the border to specific sides. *foreground*
55
- # sets the border color.
56
- def border(style = :normal, sides: nil, foreground: nil)
57
- with(border: style, border_sides: sides, border_foreground: foreground)
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
- # Returns a new Style that fixes the rendered width to *value* (in display columns).
61
- def width(value)
62
- with(width: value)
63
- end
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
- # Returns a new Style that fixes the rendered height to *value* (in rows).
66
- def height(value)
67
- with(height: value)
68
- end
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
- # Returns a new Style with horizontal alignment set (`:left`, `:right`, or `:center`).
71
- def align(value)
72
- with(align: value)
73
- end
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
- # Applies the configured style to *value* and returns the styled string. Steps:
76
- # 1. wrap to `:width`, 2. align horizontally, 3. expand to `:height`, 4. apply padding,
77
- # 5. paint border, 6. emit ANSI attribute/foreground/background escapes.
78
- def render(value)
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
- private
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
- # Returns a copy of self with *changes* merged into the options hash.
88
- def with(changes)
89
- self.class.new(@options.merge(changes))
90
- end
84
+ private
91
85
 
92
- # Wraps each line to the target width and applies horizontal alignment, then expands
93
- # to the target height.
94
- def apply_dimensions(lines)
95
- content_width = target_content_width(lines)
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
- # Returns the target content width: the explicit :width if set, otherwise the natural
101
- # max display width of the lines.
102
- def target_content_width(lines)
103
- explicit_width = @options[:width]
104
- natural_width = lines.map { |line| Width.measure(line) }.max || 0
105
- explicit_width || natural_width
106
- end
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
- # Clips *line* to *width* display columns, preserving ANSI styling where possible.
109
- def fit_line(line, width)
110
- return line if Width.measure(line) <= width
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
- UI.visible_slice(line, 0, width)
113
- end
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
- # Truncates or pads the lines array to *height* rows, filling with blank rows.
116
- def apply_height(lines, width)
117
- height = @options[:height]
118
- return lines unless height
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
- visible = lines.first(height)
121
- visible + Array.new([height - visible.length, 0].max) { " " * width }
122
- end
119
+ visible = lines.first(height)
120
+ visible + Array.new([height - visible.length, 0].max) { " " * width }
121
+ end
123
122
 
124
- # Applies padding by prepending/appending blank rows (vertical) and indenting each
125
- # line (horizontal).
126
- def apply_padding(lines)
127
- top, right, bottom, left = @options.fetch(:padding)
128
- inner_width = lines.map { |line| Width.measure(line) }.max || 0
129
- empty = " " * (left + inner_width + right)
130
- padded = lines.map do |line|
131
- pad_line(line, inner_width, left, right)
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
- # Paints the configured border around the lines, when :border is set.
138
- def apply_border(lines)
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
- border_painter(border_name).paint(lines, content_width(lines))
143
- end
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
- # Pads a single line to *inner_width*, with *left* and *right* padding spaces.
146
- def pad_line(line, inner_width, left, right)
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
- # Builds a BorderPainter configured for the current border options.
151
- def border_painter(border_name)
152
- BorderPainter.new(
153
- border: Border.fetch(border_name),
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
- # Returns the natural display width of the longest line in *lines*.
161
- def content_width(lines)
162
- lines.map { |line| Width.measure(line) }.max || 0
163
- end
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
- # Applies the active ANSI attribute/foreground/background codes to *value*.
166
- def apply_ansi(value)
167
- ansi_codes_obj.apply(value)
168
- end
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
- # The list of active ANSI escape sequence strings (attribute + foreground + background).
171
- def ansi_codes
172
- ansi_codes_obj.codes
173
- end
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
- # Builds an ANSICodes object from the active attributes, foreground, and background.
176
- def ansi_codes_obj
177
- ANSICodes.new(
178
- attributes: @options.fetch(:attributes),
179
- foreground: @options[:foreground],
180
- background: @options[:background]
181
- )
182
- end
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
- # Pads *line* on the left or right (or both, for :center) according to :align.
185
- def align_line(line, width)
186
- remaining = width - Width.measure(line)
187
- return line if remaining <= 0
188
-
189
- case @options.fetch(:align)
190
- when :right
191
- (" " * remaining) + line
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
- # Normalizes 1/2/4 padding value arguments into a [top, right, bottom, left] array.
201
- def expand_box_values(values)
202
- case values.length
203
- when 1 then [values[0], values[0], values[0], values[0]]
204
- when 2 then [values[0], values[1], values[0], values[1]]
205
- when 4 then values
206
- else
207
- raise ArgumentError, "padding expects 1, 2, or 4 values"
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