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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/chamomile/components/command_palette.rb +114 -0
  3. data/lib/chamomile/components/cursor.rb +96 -0
  4. data/lib/chamomile/components/file_picker/key_map.rb +17 -0
  5. data/lib/chamomile/components/file_picker.rb +278 -0
  6. data/lib/chamomile/components/help.rb +91 -0
  7. data/lib/chamomile/components/key_binding.rb +31 -0
  8. data/lib/chamomile/components/list/key_map.rb +18 -0
  9. data/lib/chamomile/components/list.rb +434 -0
  10. data/lib/chamomile/components/log_view.rb +141 -0
  11. data/lib/chamomile/components/paginator/key_map.rb +10 -0
  12. data/lib/chamomile/components/paginator.rb +99 -0
  13. data/lib/chamomile/components/progress.rb +201 -0
  14. data/lib/chamomile/components/render_cache.rb +15 -0
  15. data/lib/chamomile/components/spinner/types.rb +20 -0
  16. data/lib/chamomile/components/spinner.rb +75 -0
  17. data/lib/chamomile/components/stopwatch.rb +103 -0
  18. data/lib/chamomile/components/table/key_map.rb +14 -0
  19. data/lib/chamomile/components/table.rb +214 -0
  20. data/lib/chamomile/components/text_area/key_map.rb +27 -0
  21. data/lib/chamomile/components/text_area.rb +451 -0
  22. data/lib/chamomile/components/text_input/key_map.rb +20 -0
  23. data/lib/chamomile/components/text_input.rb +293 -0
  24. data/lib/chamomile/components/timer.rb +124 -0
  25. data/lib/chamomile/components/viewport/key_map.rb +18 -0
  26. data/lib/chamomile/components/viewport.rb +270 -0
  27. data/lib/chamomile/layout/horizontal.rb +1 -1
  28. data/lib/chamomile/layout/list.rb +2 -2
  29. data/lib/chamomile/layout/panel.rb +8 -8
  30. data/lib/chamomile/layout/spinner.rb +2 -2
  31. data/lib/chamomile/layout/status_bar.rb +1 -1
  32. data/lib/chamomile/layout/table.rb +2 -2
  33. data/lib/chamomile/layout/text.rb +1 -1
  34. data/lib/chamomile/layout/vertical.rb +1 -1
  35. data/lib/chamomile/styling/align.rb +33 -0
  36. data/lib/chamomile/styling/ansi.rb +103 -0
  37. data/lib/chamomile/styling/border.rb +67 -0
  38. data/lib/chamomile/styling/color.rb +105 -0
  39. data/lib/chamomile/styling/color_profile.rb +136 -0
  40. data/lib/chamomile/styling/join.rb +61 -0
  41. data/lib/chamomile/styling/place.rb +34 -0
  42. data/lib/chamomile/styling/style.rb +617 -0
  43. data/lib/chamomile/styling/wrap.rb +111 -0
  44. data/lib/chamomile/version.rb +1 -1
  45. data/lib/chamomile.rb +136 -6
  46. data/lib/flourish.rb +9 -0
  47. data/lib/petals.rb +9 -0
  48. 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