chamomile 0.2.0 → 1.0.0
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/chamomile/components/command_palette.rb +114 -0
- data/lib/chamomile/components/cursor.rb +96 -0
- data/lib/chamomile/components/file_picker/key_map.rb +17 -0
- data/lib/chamomile/components/file_picker.rb +278 -0
- data/lib/chamomile/components/help.rb +91 -0
- data/lib/chamomile/components/key_binding.rb +31 -0
- data/lib/chamomile/components/list/key_map.rb +18 -0
- data/lib/chamomile/components/list.rb +434 -0
- data/lib/chamomile/components/log_view.rb +141 -0
- data/lib/chamomile/components/paginator/key_map.rb +10 -0
- data/lib/chamomile/components/paginator.rb +99 -0
- data/lib/chamomile/components/progress.rb +201 -0
- data/lib/chamomile/components/render_cache.rb +15 -0
- data/lib/chamomile/components/spinner/types.rb +20 -0
- data/lib/chamomile/components/spinner.rb +75 -0
- data/lib/chamomile/components/stopwatch.rb +103 -0
- data/lib/chamomile/components/table/key_map.rb +14 -0
- data/lib/chamomile/components/table.rb +214 -0
- data/lib/chamomile/components/text_area/key_map.rb +27 -0
- data/lib/chamomile/components/text_area.rb +451 -0
- data/lib/chamomile/components/text_input/key_map.rb +20 -0
- data/lib/chamomile/components/text_input.rb +293 -0
- data/lib/chamomile/components/timer.rb +124 -0
- data/lib/chamomile/components/viewport/key_map.rb +18 -0
- data/lib/chamomile/components/viewport.rb +270 -0
- data/lib/chamomile/layout/horizontal.rb +1 -1
- data/lib/chamomile/layout/list.rb +2 -2
- data/lib/chamomile/layout/panel.rb +8 -8
- data/lib/chamomile/layout/spinner.rb +2 -2
- data/lib/chamomile/layout/status_bar.rb +1 -1
- data/lib/chamomile/layout/table.rb +2 -2
- data/lib/chamomile/layout/text.rb +1 -1
- data/lib/chamomile/layout/vertical.rb +1 -1
- data/lib/chamomile/styling/align.rb +33 -0
- data/lib/chamomile/styling/ansi.rb +103 -0
- data/lib/chamomile/styling/border.rb +67 -0
- data/lib/chamomile/styling/color.rb +105 -0
- data/lib/chamomile/styling/color_profile.rb +136 -0
- data/lib/chamomile/styling/join.rb +61 -0
- data/lib/chamomile/styling/place.rb +34 -0
- data/lib/chamomile/styling/style.rb +617 -0
- data/lib/chamomile/styling/wrap.rb +111 -0
- data/lib/chamomile/version.rb +1 -1
- data/lib/chamomile.rb +136 -6
- data/lib/flourish.rb +9 -0
- data/lib/petals.rb +9 -0
- metadata +42 -5
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chamomile
|
|
4
|
+
class Style
|
|
5
|
+
# Property symbols
|
|
6
|
+
BOLD = :bold
|
|
7
|
+
ITALIC = :italic
|
|
8
|
+
FAINT = :faint
|
|
9
|
+
BLINK = :blink
|
|
10
|
+
STRIKETHROUGH = :strikethrough
|
|
11
|
+
UNDERLINE = :underline
|
|
12
|
+
REVERSE = :reverse
|
|
13
|
+
FOREGROUND = :foreground
|
|
14
|
+
BACKGROUND = :background
|
|
15
|
+
TAB_WIDTH = :tab_width
|
|
16
|
+
INLINE = :inline
|
|
17
|
+
TRANSFORM = :transform
|
|
18
|
+
WIDTH = :width
|
|
19
|
+
HEIGHT = :height
|
|
20
|
+
MAX_WIDTH = :max_width
|
|
21
|
+
MAX_HEIGHT = :max_height
|
|
22
|
+
PADDING_TOP = :padding_top
|
|
23
|
+
PADDING_RIGHT = :padding_right
|
|
24
|
+
PADDING_BOTTOM = :padding_bottom
|
|
25
|
+
PADDING_LEFT = :padding_left
|
|
26
|
+
MARGIN_TOP = :margin_top
|
|
27
|
+
MARGIN_RIGHT = :margin_right
|
|
28
|
+
MARGIN_BOTTOM = :margin_bottom
|
|
29
|
+
MARGIN_LEFT = :margin_left
|
|
30
|
+
BORDER_STYLE = :border_style
|
|
31
|
+
BORDER_TOP = :border_top
|
|
32
|
+
BORDER_RIGHT = :border_right
|
|
33
|
+
BORDER_BOTTOM = :border_bottom
|
|
34
|
+
BORDER_LEFT = :border_left
|
|
35
|
+
BORDER_TOP_FG = :border_top_fg
|
|
36
|
+
BORDER_RIGHT_FG = :border_right_fg
|
|
37
|
+
BORDER_BOTTOM_FG = :border_bottom_fg
|
|
38
|
+
BORDER_LEFT_FG = :border_left_fg
|
|
39
|
+
BORDER_TOP_BG = :border_top_bg
|
|
40
|
+
BORDER_RIGHT_BG = :border_right_bg
|
|
41
|
+
BORDER_BOTTOM_BG = :border_bottom_bg
|
|
42
|
+
BORDER_LEFT_BG = :border_left_bg
|
|
43
|
+
ALIGN_HORIZONTAL = :align_horizontal
|
|
44
|
+
ALIGN_VERTICAL = :align_vertical
|
|
45
|
+
UNDERLINE_SPACES = :underline_spaces
|
|
46
|
+
STRIKETHROUGH_SPACES = :strikethrough_spaces
|
|
47
|
+
COLOR_WHITESPACE = :color_whitespace
|
|
48
|
+
|
|
49
|
+
SGR_CODES = {
|
|
50
|
+
BOLD => "1",
|
|
51
|
+
FAINT => "2",
|
|
52
|
+
ITALIC => "3",
|
|
53
|
+
UNDERLINE => "4",
|
|
54
|
+
BLINK => "5",
|
|
55
|
+
REVERSE => "7",
|
|
56
|
+
STRIKETHROUGH => "9",
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
DEFAULT_TAB_WIDTH = 4
|
|
60
|
+
|
|
61
|
+
def initialize
|
|
62
|
+
@set_props = Set.new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# --- Text attributes ---
|
|
66
|
+
|
|
67
|
+
def bold(v = true) = assign_prop(:bold, v)
|
|
68
|
+
def italic(v = true) = assign_prop(:italic, v)
|
|
69
|
+
def faint(v = true) = assign_prop(:faint, v)
|
|
70
|
+
def blink(v = true) = assign_prop(:blink, v)
|
|
71
|
+
def strikethrough(v = true) = assign_prop(:strikethrough, v)
|
|
72
|
+
def underline(v = true) = assign_prop(:underline, v)
|
|
73
|
+
def reverse(v = true) = assign_prop(:reverse, v)
|
|
74
|
+
|
|
75
|
+
# --- Colors ---
|
|
76
|
+
|
|
77
|
+
def foreground(color) = assign_prop(:foreground, Color.parse(color))
|
|
78
|
+
def background(color) = assign_prop(:background, Color.parse(color))
|
|
79
|
+
|
|
80
|
+
# --- Utility ---
|
|
81
|
+
|
|
82
|
+
def tab_width(n) = assign_prop(:tab_width, n)
|
|
83
|
+
def inline(v = true) = assign_prop(:inline, v)
|
|
84
|
+
|
|
85
|
+
def transform(&block)
|
|
86
|
+
assign_prop(:transform, block)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# --- Dimensions ---
|
|
90
|
+
|
|
91
|
+
def width(n) = assign_prop(:width, n)
|
|
92
|
+
def height(n) = assign_prop(:height, n)
|
|
93
|
+
def max_width(n) = assign_prop(:max_width, n)
|
|
94
|
+
def max_height(n) = assign_prop(:max_height, n)
|
|
95
|
+
|
|
96
|
+
# --- Padding (CSS shorthand) ---
|
|
97
|
+
|
|
98
|
+
def padding(*args)
|
|
99
|
+
top, right, bottom, left = expand_shorthand(args)
|
|
100
|
+
copy = dup
|
|
101
|
+
copy.set_prop!(:padding_top, top)
|
|
102
|
+
copy.set_prop!(:padding_right, right)
|
|
103
|
+
copy.set_prop!(:padding_bottom, bottom)
|
|
104
|
+
copy.set_prop!(:padding_left, left)
|
|
105
|
+
copy
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def padding_top(n) = assign_prop(:padding_top, n)
|
|
109
|
+
def padding_right(n) = assign_prop(:padding_right, n)
|
|
110
|
+
def padding_bottom(n) = assign_prop(:padding_bottom, n)
|
|
111
|
+
def padding_left(n) = assign_prop(:padding_left, n)
|
|
112
|
+
|
|
113
|
+
# --- Margin (CSS shorthand) ---
|
|
114
|
+
|
|
115
|
+
def margin(*args)
|
|
116
|
+
top, right, bottom, left = expand_shorthand(args)
|
|
117
|
+
copy = dup
|
|
118
|
+
copy.set_prop!(:margin_top, top)
|
|
119
|
+
copy.set_prop!(:margin_right, right)
|
|
120
|
+
copy.set_prop!(:margin_bottom, bottom)
|
|
121
|
+
copy.set_prop!(:margin_left, left)
|
|
122
|
+
copy
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def margin_top(n) = assign_prop(:margin_top, n)
|
|
126
|
+
def margin_right(n) = assign_prop(:margin_right, n)
|
|
127
|
+
def margin_bottom(n) = assign_prop(:margin_bottom, n)
|
|
128
|
+
def margin_left(n) = assign_prop(:margin_left, n)
|
|
129
|
+
|
|
130
|
+
# --- Border ---
|
|
131
|
+
|
|
132
|
+
def border(style, *sides)
|
|
133
|
+
copy = dup
|
|
134
|
+
copy.set_prop!(:border_style, style)
|
|
135
|
+
copy.send(:apply_border_sides, sides)
|
|
136
|
+
copy
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def border_style(style)
|
|
140
|
+
assign_prop(:border_style, style)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def border_top(v = true) = assign_prop(:border_top, v)
|
|
144
|
+
def border_right(v = true) = assign_prop(:border_right, v)
|
|
145
|
+
def border_bottom(v = true) = assign_prop(:border_bottom, v)
|
|
146
|
+
def border_left(v = true) = assign_prop(:border_left, v)
|
|
147
|
+
|
|
148
|
+
def border_foreground(*colors)
|
|
149
|
+
copy = dup
|
|
150
|
+
copy.send(:apply_border_colors, colors, :fg)
|
|
151
|
+
copy
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def border_background(*colors)
|
|
155
|
+
copy = dup
|
|
156
|
+
copy.send(:apply_border_colors, colors, :bg)
|
|
157
|
+
copy
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def border_top_foreground(color) = assign_prop(:border_top_fg, Color.parse(color))
|
|
161
|
+
def border_right_foreground(color) = assign_prop(:border_right_fg, Color.parse(color))
|
|
162
|
+
def border_bottom_foreground(color) = assign_prop(:border_bottom_fg, Color.parse(color))
|
|
163
|
+
def border_left_foreground(color) = assign_prop(:border_left_fg, Color.parse(color))
|
|
164
|
+
def border_top_background(color) = assign_prop(:border_top_bg, Color.parse(color))
|
|
165
|
+
def border_right_background(color) = assign_prop(:border_right_bg, Color.parse(color))
|
|
166
|
+
def border_bottom_background(color) = assign_prop(:border_bottom_bg, Color.parse(color))
|
|
167
|
+
def border_left_background(color) = assign_prop(:border_left_bg, Color.parse(color))
|
|
168
|
+
|
|
169
|
+
# --- Alignment ---
|
|
170
|
+
|
|
171
|
+
def align(*positions)
|
|
172
|
+
copy = dup
|
|
173
|
+
copy.set_prop!(:align_horizontal, Chamomile.resolve_position(positions[0])) if positions.length >= 1
|
|
174
|
+
copy.set_prop!(:align_vertical, Chamomile.resolve_position(positions[1])) if positions.length >= 2
|
|
175
|
+
copy
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def align_horizontal(pos) = assign_prop(:align_horizontal, Chamomile.resolve_position(pos))
|
|
179
|
+
def align_vertical(pos) = assign_prop(:align_vertical, Chamomile.resolve_position(pos))
|
|
180
|
+
|
|
181
|
+
# --- Whitespace options ---
|
|
182
|
+
|
|
183
|
+
def underline_spaces(v = true) = assign_prop(:underline_spaces, v)
|
|
184
|
+
def strikethrough_spaces(v = true) = assign_prop(:strikethrough_spaces, v)
|
|
185
|
+
def color_whitespace(v = true) = assign_prop(:color_whitespace, v)
|
|
186
|
+
|
|
187
|
+
# --- Public query methods ---
|
|
188
|
+
|
|
189
|
+
def bold? = !!@bold
|
|
190
|
+
def italic? = !!@italic
|
|
191
|
+
def faint? = !!@faint
|
|
192
|
+
def blink? = !!@blink
|
|
193
|
+
def strikethrough? = !!@strikethrough
|
|
194
|
+
def underline? = !!@underline
|
|
195
|
+
def reverse? = !!@reverse
|
|
196
|
+
def inline? = !!@inline
|
|
197
|
+
def foreground_color = @foreground
|
|
198
|
+
def background_color = @background
|
|
199
|
+
def border_top? = !!@border_top
|
|
200
|
+
def border_right? = !!@border_right
|
|
201
|
+
def border_bottom? = !!@border_bottom
|
|
202
|
+
def border_left? = !!@border_left
|
|
203
|
+
def underline_spaces? = !!@underline_spaces
|
|
204
|
+
def strikethrough_spaces? = !!@strikethrough_spaces
|
|
205
|
+
def color_whitespace? = !!@color_whitespace
|
|
206
|
+
|
|
207
|
+
# --- Inheritance ---
|
|
208
|
+
|
|
209
|
+
def inherit(other)
|
|
210
|
+
copy = dup
|
|
211
|
+
other.set_props.each do |prop|
|
|
212
|
+
next if copy.set_props.include?(prop)
|
|
213
|
+
|
|
214
|
+
copy.instance_variable_set(:"@#{prop}", other.instance_variable_get(:"@#{prop}"))
|
|
215
|
+
copy.set_props.add(prop)
|
|
216
|
+
end
|
|
217
|
+
copy
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def unset(*props)
|
|
221
|
+
copy = dup
|
|
222
|
+
props.each do |prop|
|
|
223
|
+
copy.set_props.delete(prop)
|
|
224
|
+
copy.instance_variable_set(:"@#{prop}", nil)
|
|
225
|
+
end
|
|
226
|
+
copy
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def set?(prop)
|
|
230
|
+
@set_props.include?(prop)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def copy
|
|
234
|
+
dup
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Merge another style on top of this one. The other style's set properties win.
|
|
238
|
+
def merge(other)
|
|
239
|
+
return dup if other.nil?
|
|
240
|
+
|
|
241
|
+
copy = dup
|
|
242
|
+
other.set_props.each do |prop|
|
|
243
|
+
copy.set_prop!(prop, other.instance_variable_get(:"@#{prop}"))
|
|
244
|
+
end
|
|
245
|
+
copy
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Empty style singleton
|
|
249
|
+
EMPTY = new.freeze
|
|
250
|
+
|
|
251
|
+
# --- Render ---
|
|
252
|
+
|
|
253
|
+
def render(*strs)
|
|
254
|
+
text = strs.join(" ")
|
|
255
|
+
|
|
256
|
+
# Apply transform
|
|
257
|
+
text = @transform.call(text) if @transform
|
|
258
|
+
|
|
259
|
+
# Convert tabs
|
|
260
|
+
tw = effective_tab_width
|
|
261
|
+
text = text.gsub("\t", " " * tw) if tw.positive?
|
|
262
|
+
|
|
263
|
+
# Calculate dimensions
|
|
264
|
+
content_width = compute_content_width
|
|
265
|
+
target_height = effective_height
|
|
266
|
+
|
|
267
|
+
# Word wrap if width is set
|
|
268
|
+
text = Wrap.word_wrap(text, content_width) if content_width.positive?
|
|
269
|
+
|
|
270
|
+
lines = text.split("\n", -1)
|
|
271
|
+
lines = [""] if lines.empty?
|
|
272
|
+
|
|
273
|
+
# Apply text SGR per line
|
|
274
|
+
sgr = build_sgr
|
|
275
|
+
lines = apply_sgr_to_lines(lines, sgr)
|
|
276
|
+
|
|
277
|
+
# Alignment (inside border, after SGR)
|
|
278
|
+
lines = apply_alignment(lines, content_width) if content_width.positive?
|
|
279
|
+
|
|
280
|
+
# Enforce inner content height (before padding/border)
|
|
281
|
+
if target_height.positive?
|
|
282
|
+
inner_height = compute_inner_height(target_height)
|
|
283
|
+
lines = enforce_height(lines, inner_height) if inner_height.positive?
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Padding
|
|
287
|
+
lines = apply_padding(lines)
|
|
288
|
+
|
|
289
|
+
# Border
|
|
290
|
+
lines = apply_border(lines) if effective_border_style
|
|
291
|
+
|
|
292
|
+
# Margin
|
|
293
|
+
lines = apply_margin(lines)
|
|
294
|
+
|
|
295
|
+
# Enforce max constraints
|
|
296
|
+
lines = enforce_max_width(lines)
|
|
297
|
+
lines = enforce_max_height(lines)
|
|
298
|
+
|
|
299
|
+
lines.join("\n")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
protected
|
|
303
|
+
|
|
304
|
+
# Accessible from other Style instances for inheritance
|
|
305
|
+
attr_reader :set_props
|
|
306
|
+
|
|
307
|
+
# Mutate a prop in place — used by multi-prop methods after dup
|
|
308
|
+
def set_prop!(prop, value)
|
|
309
|
+
instance_variable_set(:"@#{prop}", value)
|
|
310
|
+
@set_props.add(prop)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def initialize_dup(other)
|
|
314
|
+
super
|
|
315
|
+
@set_props = @set_props.dup
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
private
|
|
319
|
+
|
|
320
|
+
# --- Property assignment (immutable: returns a new Style) ---
|
|
321
|
+
|
|
322
|
+
def assign_prop(prop, value)
|
|
323
|
+
copy = dup
|
|
324
|
+
copy.instance_variable_set(:"@#{prop}", value)
|
|
325
|
+
copy.set_props.add(prop)
|
|
326
|
+
copy
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# --- CSS shorthand expansion ---
|
|
330
|
+
|
|
331
|
+
def expand_shorthand(args)
|
|
332
|
+
case args.size
|
|
333
|
+
when 1 then [args[0]] * 4
|
|
334
|
+
when 2 then [args[0], args[1], args[0], args[1]]
|
|
335
|
+
when 3 then [args[0], args[1], args[2], args[1]]
|
|
336
|
+
when 4 then args
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# --- Whitespace helper ---
|
|
341
|
+
|
|
342
|
+
def spaces(n)
|
|
343
|
+
n <= 0 ? "" : " " * n
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# --- Internal getters ---
|
|
347
|
+
|
|
348
|
+
def effective_tab_width = @tab_width || DEFAULT_TAB_WIDTH
|
|
349
|
+
def effective_width = @width || 0
|
|
350
|
+
def effective_height = @height || 0
|
|
351
|
+
def effective_max_width = @max_width || 0
|
|
352
|
+
def effective_max_height = @max_height || 0
|
|
353
|
+
def effective_padding_top = @padding_top || 0
|
|
354
|
+
def effective_padding_right = @padding_right || 0
|
|
355
|
+
def effective_padding_bottom = @padding_bottom || 0
|
|
356
|
+
def effective_padding_left = @padding_left || 0
|
|
357
|
+
def effective_margin_top = @margin_top || 0
|
|
358
|
+
def effective_margin_right = @margin_right || 0
|
|
359
|
+
def effective_margin_bottom = @margin_bottom || 0
|
|
360
|
+
def effective_margin_left = @margin_left || 0
|
|
361
|
+
def effective_border_style = @border_style
|
|
362
|
+
def effective_align_horizontal = @align_horizontal || 0.0
|
|
363
|
+
def effective_align_vertical = @align_vertical || 0.0
|
|
364
|
+
|
|
365
|
+
def compute_content_width
|
|
366
|
+
w = effective_width
|
|
367
|
+
return 0 if w <= 0
|
|
368
|
+
|
|
369
|
+
w -= effective_padding_left + effective_padding_right
|
|
370
|
+
w -= 1 if border_left?
|
|
371
|
+
w -= 1 if border_right?
|
|
372
|
+
[w, 0].max
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def compute_inner_height(total_height)
|
|
376
|
+
h = total_height
|
|
377
|
+
h -= effective_padding_top + effective_padding_bottom
|
|
378
|
+
h -= 1 if border_top?
|
|
379
|
+
h -= 1 if border_bottom?
|
|
380
|
+
[h, 0].max
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def build_sgr
|
|
384
|
+
parts = []
|
|
385
|
+
|
|
386
|
+
parts << "1" if @bold
|
|
387
|
+
parts << "2" if @faint
|
|
388
|
+
parts << "3" if @italic
|
|
389
|
+
parts << "4" if @underline
|
|
390
|
+
parts << "5" if @blink
|
|
391
|
+
parts << "7" if @reverse
|
|
392
|
+
parts << "9" if @strikethrough
|
|
393
|
+
|
|
394
|
+
parts << @foreground.fg_sequence if @foreground && !@foreground.no_color?
|
|
395
|
+
parts << @background.bg_sequence if @background && !@background.no_color?
|
|
396
|
+
|
|
397
|
+
parts.join(";")
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def apply_sgr_to_lines(lines, sgr)
|
|
401
|
+
return lines if sgr.empty?
|
|
402
|
+
|
|
403
|
+
lines.map do |line|
|
|
404
|
+
"\e[#{sgr}m#{line}\e[0m"
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def apply_alignment(lines, content_width)
|
|
409
|
+
h_pos = effective_align_horizontal
|
|
410
|
+
Align.horizontal(lines, content_width, h_pos)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def apply_padding(lines)
|
|
414
|
+
pt = effective_padding_top
|
|
415
|
+
pr = effective_padding_right
|
|
416
|
+
pb = effective_padding_bottom
|
|
417
|
+
pl = effective_padding_left
|
|
418
|
+
|
|
419
|
+
return lines if pt.zero? && pr.zero? && pb.zero? && pl.zero?
|
|
420
|
+
|
|
421
|
+
left_fill = spaces(pl)
|
|
422
|
+
right_fill = spaces(pr)
|
|
423
|
+
|
|
424
|
+
ws_sgr = build_whitespace_sgr
|
|
425
|
+
unless ws_sgr.empty?
|
|
426
|
+
left_fill = "\e[#{ws_sgr}m#{left_fill}\e[0m" unless left_fill.empty?
|
|
427
|
+
right_fill = "\e[#{ws_sgr}m#{right_fill}\e[0m" unless right_fill.empty?
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
result = []
|
|
431
|
+
blank_line = build_blank_padding_line(left_fill, right_fill, lines)
|
|
432
|
+
pt.times { result << blank_line }
|
|
433
|
+
lines.each { |line| result << "#{left_fill}#{line}#{right_fill}" }
|
|
434
|
+
pb.times { result << blank_line }
|
|
435
|
+
result
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def build_blank_padding_line(left_fill, right_fill, lines)
|
|
439
|
+
# Calculate the inner content width from existing lines
|
|
440
|
+
inner_width = lines.map { |l| ANSI.printable_width(l) }.max || 0
|
|
441
|
+
inner_fill = spaces(inner_width)
|
|
442
|
+
ws_sgr = build_whitespace_sgr
|
|
443
|
+
inner_fill = "\e[#{ws_sgr}m#{inner_fill}\e[0m" if !ws_sgr.empty? && !inner_fill.empty?
|
|
444
|
+
"#{left_fill}#{inner_fill}#{right_fill}"
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def build_whitespace_sgr
|
|
448
|
+
return "" unless color_whitespace?
|
|
449
|
+
|
|
450
|
+
parts = []
|
|
451
|
+
parts << @background.bg_sequence if @background && !@background.no_color?
|
|
452
|
+
parts.join(";")
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def apply_border(lines)
|
|
456
|
+
bs = effective_border_style
|
|
457
|
+
return lines unless bs
|
|
458
|
+
|
|
459
|
+
has_top = border_top?
|
|
460
|
+
has_bottom = border_bottom?
|
|
461
|
+
has_left = border_left?
|
|
462
|
+
has_right = border_right?
|
|
463
|
+
|
|
464
|
+
return lines unless has_top || has_bottom || has_left || has_right
|
|
465
|
+
|
|
466
|
+
content_width = lines.map { |l| ANSI.printable_width(l) }.max || 0
|
|
467
|
+
|
|
468
|
+
result = []
|
|
469
|
+
|
|
470
|
+
if has_top
|
|
471
|
+
top_line = build_border_top(bs, content_width, has_left, has_right)
|
|
472
|
+
result << top_line
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
lines.each do |line|
|
|
476
|
+
bordered = +""
|
|
477
|
+
bordered << style_border_char(bs.left, :left) if has_left
|
|
478
|
+
line_width = ANSI.printable_width(line)
|
|
479
|
+
pad = content_width - line_width
|
|
480
|
+
bordered << line
|
|
481
|
+
bordered << (" " * pad) if pad.positive?
|
|
482
|
+
bordered << style_border_char(bs.right, :right) if has_right
|
|
483
|
+
result << bordered
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
if has_bottom
|
|
487
|
+
bottom_line = build_border_bottom(bs, content_width, has_left, has_right)
|
|
488
|
+
result << bottom_line
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
result
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def build_border_top(bs, width, has_left, has_right)
|
|
495
|
+
line = +""
|
|
496
|
+
line << style_border_char(bs.top_left, :top) if has_left
|
|
497
|
+
line << style_border_char(bs.top * width, :top)
|
|
498
|
+
line << style_border_char(bs.top_right, :top) if has_right
|
|
499
|
+
line
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def build_border_bottom(bs, width, has_left, has_right)
|
|
503
|
+
line = +""
|
|
504
|
+
line << style_border_char(bs.bottom_left, :bottom) if has_left
|
|
505
|
+
line << style_border_char(bs.bottom * width, :bottom)
|
|
506
|
+
line << style_border_char(bs.bottom_right, :bottom) if has_right
|
|
507
|
+
line
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def style_border_char(char, side)
|
|
511
|
+
fg = instance_variable_get(:"@border_#{side}_fg")
|
|
512
|
+
bg = instance_variable_get(:"@border_#{side}_bg")
|
|
513
|
+
|
|
514
|
+
parts = []
|
|
515
|
+
parts << fg.fg_sequence if fg && !fg.no_color?
|
|
516
|
+
parts << bg.bg_sequence if bg && !bg.no_color?
|
|
517
|
+
|
|
518
|
+
return char if parts.empty?
|
|
519
|
+
|
|
520
|
+
"\e[#{parts.join(";")}m#{char}\e[0m"
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def enforce_height(lines, target_height)
|
|
524
|
+
if lines.length < target_height
|
|
525
|
+
v_pos = effective_align_vertical
|
|
526
|
+
Align.vertical(lines, target_height, v_pos)
|
|
527
|
+
elsif lines.length > target_height
|
|
528
|
+
lines[0, target_height]
|
|
529
|
+
else
|
|
530
|
+
lines
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def apply_margin(lines)
|
|
535
|
+
mt = effective_margin_top
|
|
536
|
+
mr = effective_margin_right
|
|
537
|
+
mb = effective_margin_bottom
|
|
538
|
+
ml = effective_margin_left
|
|
539
|
+
|
|
540
|
+
return lines if mt.zero? && mr.zero? && mb.zero? && ml.zero?
|
|
541
|
+
|
|
542
|
+
left_fill = spaces(ml)
|
|
543
|
+
right_fill = spaces(mr)
|
|
544
|
+
|
|
545
|
+
result = []
|
|
546
|
+
mt.times { result << "" }
|
|
547
|
+
lines.each { |line| result << "#{left_fill}#{line}#{right_fill}" }
|
|
548
|
+
mb.times { result << "" }
|
|
549
|
+
result
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def enforce_max_width(lines)
|
|
553
|
+
mw = effective_max_width
|
|
554
|
+
return lines if mw <= 0
|
|
555
|
+
|
|
556
|
+
lines.map do |line|
|
|
557
|
+
truncate_line(line, mw)
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def enforce_max_height(lines)
|
|
562
|
+
mh = effective_max_height
|
|
563
|
+
return lines if mh <= 0
|
|
564
|
+
return lines if lines.length <= mh
|
|
565
|
+
|
|
566
|
+
lines[0, mh]
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def truncate_line(line, max_width)
|
|
570
|
+
width = 0
|
|
571
|
+
result = +""
|
|
572
|
+
i = 0
|
|
573
|
+
chars = line.chars
|
|
574
|
+
has_open_sgr = false
|
|
575
|
+
|
|
576
|
+
while i < chars.length
|
|
577
|
+
seq = chars[i] == "\e" ? ANSI.extract_escape(chars, i) : nil
|
|
578
|
+
if seq
|
|
579
|
+
result << seq
|
|
580
|
+
has_open_sgr = ANSI.sgr_open_after?(has_open_sgr, seq)
|
|
581
|
+
i += seq.length
|
|
582
|
+
next
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
ch_w = ANSI.printable_width(chars[i])
|
|
586
|
+
break if width + ch_w > max_width
|
|
587
|
+
|
|
588
|
+
result << chars[i]
|
|
589
|
+
width += ch_w
|
|
590
|
+
i += 1
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Close any open SGR sequences to prevent escape code leaking
|
|
594
|
+
result << "\e[0m" if has_open_sgr
|
|
595
|
+
|
|
596
|
+
result
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def apply_border_sides(sides)
|
|
600
|
+
values = sides.empty? ? [true, true, true, true] : expand_shorthand(sides)
|
|
601
|
+
set_prop!(:border_top, values[0])
|
|
602
|
+
set_prop!(:border_right, values[1])
|
|
603
|
+
set_prop!(:border_bottom, values[2])
|
|
604
|
+
set_prop!(:border_left, values[3])
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def apply_border_colors(colors, type)
|
|
608
|
+
suffix = type == :fg ? "_fg" : "_bg"
|
|
609
|
+
parsed = colors.map { |c| Color.parse(c) }
|
|
610
|
+
top, right, bottom, left = expand_shorthand(parsed)
|
|
611
|
+
set_prop!(:"border_top#{suffix}", top)
|
|
612
|
+
set_prop!(:"border_right#{suffix}", right)
|
|
613
|
+
set_prop!(:"border_bottom#{suffix}", bottom)
|
|
614
|
+
set_prop!(:"border_left#{suffix}", left)
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
end
|