tuile 0.1.0 → 0.3.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/CHANGELOG.md +28 -0
- data/README.md +10 -10
- data/examples/file_commander.rb +0 -14
- data/examples/sampler.rb +320 -0
- data/lib/tuile/ansi.rb +14 -0
- data/lib/tuile/component/button.rb +86 -0
- data/lib/tuile/component/label.rb +64 -26
- data/lib/tuile/component/layout.rb +29 -12
- data/lib/tuile/component/list.rb +192 -63
- data/lib/tuile/component/text_area.rb +376 -0
- data/lib/tuile/component/text_field.rb +46 -4
- data/lib/tuile/component/text_view.rb +351 -0
- data/lib/tuile/component/window.rb +13 -5
- data/lib/tuile/component.rb +53 -5
- data/lib/tuile/event_queue.rb +14 -1
- data/lib/tuile/keys.rb +24 -4
- data/lib/tuile/screen.rb +127 -39
- data/lib/tuile/screen_pane.rb +29 -7
- data/lib/tuile/styled_string.rb +761 -0
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile.rb +1 -1
- data/sig/tuile.rbs +958 -53
- metadata +9 -17
data/sig/tuile.rbs
CHANGED
|
@@ -16,6 +16,14 @@ module Tuile
|
|
|
16
16
|
class Error < StandardError
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# ANSI escape sequence constants. Tuile emits colors and text attributes
|
|
20
|
+
# via Rainbow, which produces **SGR** sequences ("Select Graphic
|
|
21
|
+
# Rendition", `ESC [ <params> m` — e.g. `\e[31m` red, `\e[1m` bold,
|
|
22
|
+
# `\e[0m` reset).
|
|
23
|
+
module Ansi
|
|
24
|
+
RESET: String
|
|
25
|
+
end
|
|
26
|
+
|
|
19
27
|
# Constants for keys returned by {.getkey} and helpers for reading them from
|
|
20
28
|
# stdin. The constants are the raw escape sequences emitted by the terminal;
|
|
21
29
|
# see https://en.wikipedia.org/wiki/ANSI_escape_code for the encoding.
|
|
@@ -26,9 +34,13 @@ module Tuile
|
|
|
26
34
|
UP_ARROWS: ::Array[String]
|
|
27
35
|
LEFT_ARROW: String
|
|
28
36
|
RIGHT_ARROW: String
|
|
37
|
+
CTRL_LEFT_ARROW: String
|
|
38
|
+
CTRL_RIGHT_ARROW: String
|
|
29
39
|
ESC: String
|
|
30
40
|
HOME: String
|
|
31
41
|
END_: String
|
|
42
|
+
HOMES: ::Array[String]
|
|
43
|
+
ENDS_: ::Array[String]
|
|
32
44
|
PAGE_UP: String
|
|
33
45
|
PAGE_DOWN: String
|
|
34
46
|
BACKSPACE: String
|
|
@@ -38,6 +50,8 @@ module Tuile
|
|
|
38
50
|
CTRL_U: String
|
|
39
51
|
CTRL_D: String
|
|
40
52
|
ENTER: String
|
|
53
|
+
TAB: String
|
|
54
|
+
SHIFT_TAB: String
|
|
41
55
|
|
|
42
56
|
# Grabs a key from stdin and returns it. Blocks until the key is obtained.
|
|
43
57
|
# Reads a full ESC key sequence; see constants above for some values returned
|
|
@@ -212,6 +226,19 @@ module Tuile
|
|
|
212
226
|
# function exits when the 'ESC' or 'q' key is pressed.
|
|
213
227
|
def run_event_loop: () -> void
|
|
214
228
|
|
|
229
|
+
# Advances focus to the next {Component#tab_stop?} in tree order, wrapping
|
|
230
|
+
# around. Scope is the topmost popup if one is open, otherwise {#content}
|
|
231
|
+
# — this keeps Tab confined inside a modal popup. No-op (returns false) if
|
|
232
|
+
# the modal scope has no tab stops or no content at all.
|
|
233
|
+
#
|
|
234
|
+
# _@return_ — true if focus moved.
|
|
235
|
+
def focus_next: () -> bool
|
|
236
|
+
|
|
237
|
+
# Mirror of {#focus_next} that walks backwards through the tab order.
|
|
238
|
+
#
|
|
239
|
+
# _@return_ — true if focus moved.
|
|
240
|
+
def focus_previous: () -> bool
|
|
241
|
+
|
|
215
242
|
# _@return_ — current active tiled component.
|
|
216
243
|
def active_window: () -> Component?
|
|
217
244
|
|
|
@@ -240,7 +267,23 @@ module Tuile
|
|
|
240
267
|
|
|
241
268
|
def self.close: () -> void
|
|
242
269
|
|
|
243
|
-
# Prints given strings.
|
|
270
|
+
# Prints given strings. While {#repaint} is running, writes are
|
|
271
|
+
# accumulated into a frame buffer and flushed to the terminal as a
|
|
272
|
+
# single `$stdout.write` at the end of the cycle. This stops the
|
|
273
|
+
# emulator from rendering half-finished frames (e.g. a layout's
|
|
274
|
+
# clear-background pass before its children have re-painted), which
|
|
275
|
+
# was visible as a brief flicker when the auto-clear path triggers.
|
|
276
|
+
#
|
|
277
|
+
# Outside repaint, writes go straight to stdout. We deliberately
|
|
278
|
+
# don't raise on a "print outside repaint" — that would be a useful
|
|
279
|
+
# guardrail against components painting outside the repaint loop,
|
|
280
|
+
# but it'd force terminal-housekeeping writes (`Screen#clear`,
|
|
281
|
+
# mouse-tracking start/stop, cursor-show on teardown) to bypass
|
|
282
|
+
# this method entirely and write directly to `$stdout`. {FakeScreen}
|
|
283
|
+
# overrides `print` to capture every byte into its `@prints` array,
|
|
284
|
+
# and tests that exercise `run_event_loop` against a real {Screen}
|
|
285
|
+
# would otherwise leak escape sequences to the test runner's stdout.
|
|
286
|
+
# Keeping `print` as the single sink preserves that override seam.
|
|
244
287
|
#
|
|
245
288
|
# _@param_ `args` — stuff to print.
|
|
246
289
|
def print: (*String args) -> void
|
|
@@ -255,6 +298,17 @@ module Tuile
|
|
|
255
298
|
# but only one focused.
|
|
256
299
|
def cursor_position: () -> Point?
|
|
257
300
|
|
|
301
|
+
# Walks the current modal scope in pre-order, collects tab stops, and
|
|
302
|
+
# advances focus by one (wrapping). When the focused component isn't in
|
|
303
|
+
# the tab order (e.g. focus is parked on a popup/window chrome with no
|
|
304
|
+
# interactable widgets), Tab goes to the first stop and Shift+Tab to the
|
|
305
|
+
# last.
|
|
306
|
+
#
|
|
307
|
+
# _@param_ `forward`
|
|
308
|
+
#
|
|
309
|
+
# _@return_ — true if focus moved.
|
|
310
|
+
def cycle_focus: (forward: bool) -> bool
|
|
311
|
+
|
|
258
312
|
# Collects a component and all its descendants in tree order
|
|
259
313
|
# (parent before children).
|
|
260
314
|
#
|
|
@@ -276,6 +330,11 @@ module Tuile
|
|
|
276
330
|
# A key has been pressed on the keyboard. Handle it, or forward to active
|
|
277
331
|
# window.
|
|
278
332
|
#
|
|
333
|
+
# Tab / Shift+Tab are reserved navigation keys: intercepted here before
|
|
334
|
+
# the pane sees them, so a focused {Component::TextField} (which would
|
|
335
|
+
# otherwise swallow printable keys via the standard cursor-owner
|
|
336
|
+
# suppression) doesn't trap them.
|
|
337
|
+
#
|
|
279
338
|
# _@param_ `key`
|
|
280
339
|
#
|
|
281
340
|
# _@return_ — true if the key was handled by some window.
|
|
@@ -336,12 +395,28 @@ module Tuile
|
|
|
336
395
|
# Focuses this component. Equivalent to `screen.focused = self`.
|
|
337
396
|
def focus: () -> void
|
|
338
397
|
|
|
339
|
-
# Repaints the component.
|
|
398
|
+
# Repaints the component.
|
|
340
399
|
#
|
|
341
|
-
# The
|
|
342
|
-
#
|
|
400
|
+
# The default does the bookkeeping that almost every component would
|
|
401
|
+
# otherwise have to remember: it clears the background and re-invalidates
|
|
402
|
+
# any direct children whose rects leave gaps in {#rect}. Concretely:
|
|
343
403
|
#
|
|
344
|
-
#
|
|
404
|
+
# - Leaf (no children): always clears, so subclasses can paint their
|
|
405
|
+
# content directly without an explicit `clear_background` call.
|
|
406
|
+
# - Container with children that fully tile {#rect}: skipped — the
|
|
407
|
+
# children themselves will repaint and cover everything.
|
|
408
|
+
# - Container with gappy children (e.g. a form layout where widgets
|
|
409
|
+
# don't tile): clears, then invalidates the children so they re-paint
|
|
410
|
+
# on top of the cleared background. This is what makes mixed
|
|
411
|
+
# field/button forms safe without each container learning a custom
|
|
412
|
+
# damage-tracking pass.
|
|
413
|
+
#
|
|
414
|
+
# Subclasses that paint their entire rect themselves (e.g. {Window}'s
|
|
415
|
+
# border draws over the area the default would clear; {Component::List}
|
|
416
|
+
# explicitly paints every row) may skip super and take full
|
|
417
|
+
# responsibility for {#rect}. Everything else should call super.
|
|
418
|
+
#
|
|
419
|
+
# A component must not draw outside of {#rect}.
|
|
345
420
|
def repaint: () -> void
|
|
346
421
|
|
|
347
422
|
# Called when a character is pressed on the keyboard.
|
|
@@ -386,9 +461,23 @@ module Tuile
|
|
|
386
461
|
# only focusable ones can become a focus target that puts themselves and
|
|
387
462
|
# their ancestors on the active chain.
|
|
388
463
|
#
|
|
464
|
+
# See also {#tab_stop?}: focusable controls _can_ receive focus (via click
|
|
465
|
+
# or programmatic assignment), but only tab stops participate in Tab /
|
|
466
|
+
# Shift+Tab cycling. Containers like {Window} and {Popup} are focusable
|
|
467
|
+
# (so a click on chrome lands focus) but are not tab stops.
|
|
468
|
+
#
|
|
389
469
|
# _@return_ — true if this component can be focused.
|
|
390
470
|
def focusable?: () -> bool
|
|
391
471
|
|
|
472
|
+
# Whether this component participates in Tab / Shift+Tab focus cycling.
|
|
473
|
+
# `false` by default. Only true on components that accept direct user
|
|
474
|
+
# input (e.g. {TextField}, {List}, {Component::Button}). Implies
|
|
475
|
+
# {#focusable?} — Screen will skip non-focusable tab stops, but in
|
|
476
|
+
# practice every override should keep the two consistent.
|
|
477
|
+
#
|
|
478
|
+
# _@return_ — true if Tab / Shift+Tab should land on this component.
|
|
479
|
+
def tab_stop?: () -> bool
|
|
480
|
+
|
|
392
481
|
# _@return_ — the distance from the root component; 0 if {#parent}
|
|
393
482
|
# is nil.
|
|
394
483
|
def depth: () -> Integer
|
|
@@ -450,6 +539,15 @@ module Tuile
|
|
|
450
539
|
# needs-repaint and once all events are processed, will call {#repaint}.
|
|
451
540
|
def invalidate: () -> void
|
|
452
541
|
|
|
542
|
+
# Whether direct children fully tile {#rect}. Used by the default
|
|
543
|
+
# {#repaint} to decide whether the framework needs to wipe gaps.
|
|
544
|
+
#
|
|
545
|
+
# Approximated by area: sum of (non-empty) child areas vs the parent's
|
|
546
|
+
# area. Cheap, and correct as long as siblings don't overlap each other
|
|
547
|
+
# — which Tuile already requires (no clipping in the tiled tree).
|
|
548
|
+
# Children with empty rects contribute zero, since they paint nothing.
|
|
549
|
+
def children_tile_rect?: () -> bool
|
|
550
|
+
|
|
453
551
|
# Clears the background: prints spaces into all characters occupied by the
|
|
454
552
|
# component's rect.
|
|
455
553
|
def clear_background: () -> void
|
|
@@ -466,24 +564,34 @@ module Tuile
|
|
|
466
564
|
# no parent.
|
|
467
565
|
attr_accessor parent: Component?
|
|
468
566
|
|
|
469
|
-
# A scrollable list of
|
|
567
|
+
# A scrollable list of items with cursor support.
|
|
470
568
|
#
|
|
471
|
-
# Items are
|
|
472
|
-
#
|
|
473
|
-
# {#
|
|
474
|
-
#
|
|
569
|
+
# Items are modeled as {StyledString}s and painted directly into the
|
|
570
|
+
# component's {#rect}. Lines wider than the viewport are ellipsized via
|
|
571
|
+
# {StyledString#ellipsize} (span styles are preserved across the cut —
|
|
572
|
+
# unlike the older ANSI-as-bytes truncation, color does *not* get
|
|
573
|
+
# dropped on the surviving characters). Vertical scrolling is supported
|
|
574
|
+
# via {#top_line}; the list can also automatically scroll to the bottom
|
|
575
|
+
# if {#auto_scroll} is enabled.
|
|
475
576
|
#
|
|
476
577
|
# Cursor is supported; call {#cursor=} to change cursor behavior. The
|
|
477
|
-
# cursor responds to arrows, `jk`, Home/End, Ctrl+U/D and scrolls the
|
|
478
|
-
# automatically.
|
|
578
|
+
# cursor responds to arrows, `jk`, Home/End, Ctrl+U/D and scrolls the
|
|
579
|
+
# list automatically. The cursor highlight overlays a dark background
|
|
580
|
+
# while preserving each span's foreground color.
|
|
479
581
|
class List < Component
|
|
582
|
+
CURSOR_BG: Integer
|
|
583
|
+
|
|
480
584
|
def initialize: () -> void
|
|
481
585
|
|
|
482
|
-
# Sets new lines. Each entry is coerced
|
|
483
|
-
#
|
|
484
|
-
# {
|
|
586
|
+
# Sets new lines. Each entry is coerced into a {StyledString} (a
|
|
587
|
+
# `String` is parsed via {StyledString.parse}, so embedded ANSI is
|
|
588
|
+
# honored; a {StyledString} is used as-is; anything else is stringified
|
|
589
|
+
# via `#to_s` first), then split on `\n` into separate lines via
|
|
590
|
+
# {StyledString#lines}, with trailing empty pieces dropped and trailing
|
|
591
|
+
# ASCII whitespace stripped — symmetric with {#add_lines}, so the
|
|
592
|
+
# stored `@lines` is always `Array<StyledString>`.
|
|
485
593
|
#
|
|
486
|
-
# _@param_ `lines` —
|
|
594
|
+
# _@param_ `lines` — entries are `String`, `StyledString`, or anything that responds to `#to_s`.
|
|
487
595
|
def lines=: (::Array[untyped] lines) -> void
|
|
488
596
|
|
|
489
597
|
# Without a block, returns the current lines. With a block, fully
|
|
@@ -494,25 +602,29 @@ module Tuile
|
|
|
494
602
|
# end
|
|
495
603
|
# ```
|
|
496
604
|
#
|
|
497
|
-
# _@return_ — current lines (when called without a
|
|
498
|
-
|
|
605
|
+
# _@return_ — current lines (when called without a
|
|
606
|
+
# block).
|
|
607
|
+
def lines: () ?{ (::Array[untyped] buffer) -> void } -> ::Array[StyledString]
|
|
499
608
|
|
|
609
|
+
# sord duck - #to_s looks like a duck type with an equivalent RBS interface, replacing with _ToS
|
|
500
610
|
# Adds a line.
|
|
501
611
|
#
|
|
502
612
|
# _@param_ `line`
|
|
503
|
-
def add_line: (String line) -> void
|
|
613
|
+
def add_line: ((String | StyledString | _ToS) line) -> void
|
|
504
614
|
|
|
505
|
-
# Appends given lines. Each entry is
|
|
506
|
-
#
|
|
507
|
-
#
|
|
615
|
+
# Appends given lines. Each entry is parsed the same way as in
|
|
616
|
+
# {#lines=}: coerced to a {StyledString}, split on `\n`, with trailing
|
|
617
|
+
# empty pieces dropped and trailing ASCII whitespace stripped.
|
|
508
618
|
#
|
|
509
|
-
# _@param_ `lines` — entries
|
|
619
|
+
# _@param_ `lines` — entries are `String`, `StyledString`, or anything that responds to `#to_s`.
|
|
510
620
|
def add_lines: (::Array[untyped] lines) -> void
|
|
511
621
|
|
|
512
622
|
def content_size: () -> Size
|
|
513
623
|
|
|
514
624
|
def focusable?: () -> bool
|
|
515
625
|
|
|
626
|
+
def tab_stop?: () -> bool
|
|
627
|
+
|
|
516
628
|
# _@param_ `key` — a key.
|
|
517
629
|
#
|
|
518
630
|
# _@return_ — true if the key was handled.
|
|
@@ -521,6 +633,8 @@ module Tuile
|
|
|
521
633
|
# Moves the cursor to the next line whose text contains `query`
|
|
522
634
|
# (case-insensitive substring match). Search wraps around the end of the
|
|
523
635
|
# list. Only lines reachable by the current {#cursor} are considered.
|
|
636
|
+
# Matching uses the line's plain text — span styles do not affect the
|
|
637
|
+
# match.
|
|
524
638
|
#
|
|
525
639
|
# _@param_ `query` — substring to match. Empty query never matches.
|
|
526
640
|
#
|
|
@@ -542,8 +656,40 @@ module Tuile
|
|
|
542
656
|
def handle_mouse: (MouseEvent event) -> void
|
|
543
657
|
|
|
544
658
|
# Paints the list items into {#rect}.
|
|
659
|
+
#
|
|
660
|
+
# Skips the {Component#repaint} default's auto-clear: every row of
|
|
661
|
+
# {#rect} is painted below (with blank padding past the last item),
|
|
662
|
+
# so the parent contract — "fully draw over your rect" — is met
|
|
663
|
+
# without an upfront wipe.
|
|
545
664
|
def repaint: () -> void
|
|
546
665
|
|
|
666
|
+
# Rebuilds pre-padded lines when the wrap width changes. The wrap width
|
|
667
|
+
# depends on {#rect}`.width` and the scrollbar gutter, both of which
|
|
668
|
+
# trigger this hook.
|
|
669
|
+
def on_width_changed: () -> void
|
|
670
|
+
|
|
671
|
+
# Coerces and flattens a list of input entries into trimmed
|
|
672
|
+
# {StyledString} lines. Each entry becomes a {StyledString} (String
|
|
673
|
+
# via {StyledString.parse}, StyledString passed through, anything else
|
|
674
|
+
# via `#to_s`), then split on `\n` via {StyledString#lines} — with
|
|
675
|
+
# trailing empty pieces dropped (matching `String#split("\n")`'s
|
|
676
|
+
# default behavior, so `add_line ""` is a no-op) — and trailing ASCII
|
|
677
|
+
# whitespace stripped on each resulting line.
|
|
678
|
+
#
|
|
679
|
+
# _@param_ `entries`
|
|
680
|
+
def parse_input_lines: (::Array[untyped] entries) -> ::Array[StyledString]
|
|
681
|
+
|
|
682
|
+
# _@param_ `entry`
|
|
683
|
+
def split_to_lines: (Object entry) -> ::Array[StyledString]
|
|
684
|
+
|
|
685
|
+
# Returns `line` with trailing ASCII whitespace (space/tab) dropped,
|
|
686
|
+
# preserving span styles on the surviving prefix. Whitespace chars are
|
|
687
|
+
# all single-column ASCII, so byte-count delta equals column-count
|
|
688
|
+
# delta and {StyledString#slice} can do the cut.
|
|
689
|
+
#
|
|
690
|
+
# _@param_ `line`
|
|
691
|
+
def rstrip_styled: (StyledString line) -> StyledString
|
|
692
|
+
|
|
547
693
|
# _@return_ — true if the cursor sits on a real content line.
|
|
548
694
|
def cursor_on_item?: () -> bool
|
|
549
695
|
|
|
@@ -551,6 +697,14 @@ module Tuile
|
|
|
551
697
|
# Caller must ensure {#cursor_on_item?}.
|
|
552
698
|
def fire_item_chosen: () -> void
|
|
553
699
|
|
|
700
|
+
# _@return_ — `[position, line_at_position]`, with `line` nil when the cursor is
|
|
701
|
+
# off-content.
|
|
702
|
+
def cursor_state: () -> [[Integer, StyledString, NilClass]]
|
|
703
|
+
|
|
704
|
+
# Fires {#on_cursor_changed} if {#cursor_state} differs from the last
|
|
705
|
+
# fired state. Idempotent — safe to call after any mutation.
|
|
706
|
+
def notify_cursor_changed: () -> void
|
|
707
|
+
|
|
554
708
|
# _@param_ `query`
|
|
555
709
|
#
|
|
556
710
|
# _@param_ `include_current`
|
|
@@ -596,37 +750,55 @@ module Tuile
|
|
|
596
750
|
# _@return_ — whether the scrollbar should be drawn right now.
|
|
597
751
|
def scrollbar_visible?: () -> bool
|
|
598
752
|
|
|
599
|
-
#
|
|
753
|
+
# _@return_ — column width available for line content (rect width
|
|
754
|
+
# minus the scrollbar gutter, when visible). `0` when {#rect}'s width
|
|
755
|
+
# is non-positive.
|
|
756
|
+
def content_width: () -> Integer
|
|
757
|
+
|
|
758
|
+
# Recomputes {@padded_lines} for the current rect width and scrollbar
|
|
759
|
+
# visibility. Each line is ellipsized to fit and pre-padded with
|
|
760
|
+
# single-space gutters on each side, so {#paintable_line} only has to
|
|
761
|
+
# apply the cursor highlight (if any) and append the scrollbar glyph.
|
|
762
|
+
def rebuild_padded_lines: () -> void
|
|
763
|
+
|
|
764
|
+
# Pads `line` to one full row of the viewport (scrollbar gutter
|
|
765
|
+
# excluded). Lines wider than the content area are ellipsized via
|
|
766
|
+
# {StyledString#ellipsize} (span styles survive the cut); shorter
|
|
767
|
+
# lines are padded with default-styled spaces.
|
|
600
768
|
#
|
|
601
|
-
# _@param_ `
|
|
769
|
+
# _@param_ `line`
|
|
602
770
|
#
|
|
603
|
-
# _@
|
|
604
|
-
|
|
771
|
+
# _@return_ — exactly {#content_width} display columns wide
|
|
772
|
+
# (or {StyledString::EMPTY} when content_width is non-positive).
|
|
773
|
+
def pad_to_row: (StyledString line) -> StyledString
|
|
605
774
|
|
|
606
775
|
# _@param_ `index` — 0-based index into {#lines}.
|
|
607
776
|
#
|
|
608
777
|
# _@param_ `row_in_viewport` — 0-based row within the viewport.
|
|
609
778
|
#
|
|
610
|
-
# _@param_ `width` — number of columns the line should occupy.
|
|
611
|
-
#
|
|
612
779
|
# _@param_ `scrollbar` — scrollbar instance, or nil if not shown.
|
|
613
780
|
#
|
|
614
|
-
# _@return_ — paintable line exactly `width`
|
|
615
|
-
# highlighted if cursor is here.
|
|
616
|
-
def paintable_line: (
|
|
617
|
-
Integer index,
|
|
618
|
-
Integer row_in_viewport,
|
|
619
|
-
Integer width,
|
|
620
|
-
VerticalScrollBar? scrollbar
|
|
621
|
-
) -> String
|
|
781
|
+
# _@return_ — paintable ANSI-encoded line exactly `rect.width`
|
|
782
|
+
# columns wide; highlighted if cursor is here.
|
|
783
|
+
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> String
|
|
622
784
|
|
|
623
785
|
# _@return_ — callback fired when an item is chosen — by pressing
|
|
624
786
|
# Enter on the cursor's item, or by left-clicking an item. Called as
|
|
625
|
-
# `proc.call(index, line)` with the chosen 0-based index and its
|
|
626
|
-
# Never fires when the cursor's position is
|
|
627
|
-
# {Cursor::None}, or empty content).
|
|
787
|
+
# `proc.call(index, line)` with the chosen 0-based index and its
|
|
788
|
+
# {StyledString} line. Never fires when the cursor's position is
|
|
789
|
+
# outside the content (e.g. {Cursor::None}, or empty content).
|
|
628
790
|
attr_accessor on_item_chosen: Proc?
|
|
629
791
|
|
|
792
|
+
# _@return_ — callback fired when the `(index, line)` tuple under
|
|
793
|
+
# the cursor changes. Called as `proc.call(index, line)` where `line`
|
|
794
|
+
# is the {StyledString} at the cursor, or `nil` when the cursor is
|
|
795
|
+
# off-content ({Cursor::None}, empty list, or `index` past the last
|
|
796
|
+
# line). Fires on cursor moves (key, mouse, search), on {#cursor=},
|
|
797
|
+
# and on {#lines=}/{#add_lines} when the line at the cursor's index
|
|
798
|
+
# changes (or its in-range/out-of-range status flips). Useful for
|
|
799
|
+
# keeping a details pane in sync with the highlighted row.
|
|
800
|
+
attr_accessor on_cursor_changed: Proc?
|
|
801
|
+
|
|
630
802
|
# _@return_ — if true and a line is added or new content is set,
|
|
631
803
|
# auto-scrolls to the bottom.
|
|
632
804
|
attr_accessor auto_scroll: bool
|
|
@@ -752,20 +924,44 @@ module Tuile
|
|
|
752
924
|
end
|
|
753
925
|
end
|
|
754
926
|
|
|
755
|
-
# A label which shows static text. No word-wrapping;
|
|
927
|
+
# A label which shows static text. No word-wrapping; long lines are
|
|
928
|
+
# truncated with an ellipsis. Text is modeled as a {StyledString};
|
|
929
|
+
# {#text=} accepts a {String} (parsed via {StyledString.parse}, so
|
|
930
|
+
# embedded ANSI is honored) or a {StyledString} directly. {#text}
|
|
931
|
+
# always returns the {StyledString}.
|
|
756
932
|
class Label < Component
|
|
757
933
|
def initialize: () -> void
|
|
758
934
|
|
|
759
|
-
# _@
|
|
760
|
-
|
|
761
|
-
|
|
935
|
+
# _@return_ — longest hard-line's display width × number of hard
|
|
936
|
+
# lines. Reported on the *unclipped* text — sizing is intrinsic to
|
|
937
|
+
# the content, not the viewport. Empty text returns `Size.new(0, 0)`.
|
|
762
938
|
def content_size: () -> Size
|
|
763
939
|
|
|
940
|
+
# Paints the text into {#rect}.
|
|
941
|
+
#
|
|
942
|
+
# Skips the {Component#repaint} default's auto-clear: every row is
|
|
943
|
+
# painted explicitly (with pre-padded blanks past the last line), so
|
|
944
|
+
# the "fully draw over your rect" contract is met without an upfront
|
|
945
|
+
# wipe.
|
|
764
946
|
def repaint: () -> void
|
|
765
947
|
|
|
766
948
|
def on_width_changed: () -> void
|
|
767
949
|
|
|
768
|
-
|
|
950
|
+
# Recomputes {@clipped_lines} for the current text and rect width.
|
|
951
|
+
# Each line is ellipsized to fit, padded with trailing spaces out to
|
|
952
|
+
# the full width, and pre-rendered to ANSI so {#repaint} is just a
|
|
953
|
+
# lookup + screen.print per row. {@blank_line} covers rows past the
|
|
954
|
+
# last text line.
|
|
955
|
+
def update_clipped_lines: () -> void
|
|
956
|
+
|
|
957
|
+
# _@param_ `line`
|
|
958
|
+
#
|
|
959
|
+
# _@param_ `width`
|
|
960
|
+
def pad_to: (StyledString line, Integer width) -> StyledString
|
|
961
|
+
|
|
962
|
+
# _@return_ — the current text. Defaults to an empty
|
|
963
|
+
# {StyledString}.
|
|
964
|
+
attr_accessor text: (StyledString | String)?
|
|
769
965
|
end
|
|
770
966
|
|
|
771
967
|
# A modal overlay that wraps any {Component} as its content. Popup itself
|
|
@@ -854,17 +1050,70 @@ module Tuile
|
|
|
854
1050
|
def on_focus: () -> void
|
|
855
1051
|
end
|
|
856
1052
|
|
|
1053
|
+
# A clickable button. Activated by Enter, Space, or a left mouse click;
|
|
1054
|
+
# fires the {#on_click} callback. Renders as `[ caption ]` on a single
|
|
1055
|
+
# row; the background is highlighted when the button is focused so the
|
|
1056
|
+
# user can see which button is active.
|
|
1057
|
+
#
|
|
1058
|
+
# Buttons are tab stops — Tab and Shift+Tab will land on them as part of
|
|
1059
|
+
# the standard focus cycle. Click-to-focus also works via the inherited
|
|
1060
|
+
# {Component#handle_mouse}.
|
|
1061
|
+
#
|
|
1062
|
+
# Assign a {#rect} (typically by the surrounding {Layout}) wide enough to
|
|
1063
|
+
# show `[ caption ]`; {#content_size} reports that natural width.
|
|
1064
|
+
class Button < Component
|
|
1065
|
+
# _@param_ `caption` — the button's label.
|
|
1066
|
+
def initialize: (?String caption) -> void
|
|
1067
|
+
|
|
1068
|
+
def focusable?: () -> bool
|
|
1069
|
+
|
|
1070
|
+
def tab_stop?: () -> bool
|
|
1071
|
+
|
|
1072
|
+
# _@return_ — natural width is `caption.length + 4` to fit
|
|
1073
|
+
# `[ caption ]`; height is 1.
|
|
1074
|
+
def content_size: () -> Size
|
|
1075
|
+
|
|
1076
|
+
# _@param_ `key`
|
|
1077
|
+
def handle_key: (String key) -> bool
|
|
1078
|
+
|
|
1079
|
+
# _@param_ `event`
|
|
1080
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
1081
|
+
|
|
1082
|
+
def repaint: () -> void
|
|
1083
|
+
|
|
1084
|
+
# _@return_ — the button's label.
|
|
1085
|
+
attr_accessor caption: String
|
|
1086
|
+
|
|
1087
|
+
# Callback fired when the button is activated (Enter, Space, or
|
|
1088
|
+
# left-click). The callable receives no arguments.
|
|
1089
|
+
#
|
|
1090
|
+
# _@return_ — no-arg callable, or nil.
|
|
1091
|
+
attr_accessor on_click: (Proc | Method)?
|
|
1092
|
+
end
|
|
1093
|
+
|
|
857
1094
|
# A layout doesn't paint anything by itself: its job is to position child
|
|
858
1095
|
# components.
|
|
859
1096
|
#
|
|
860
|
-
#
|
|
861
|
-
#
|
|
862
|
-
#
|
|
1097
|
+
# Children that fully tile the layout's rect repaint themselves and
|
|
1098
|
+
# cover everything; children that leave gaps (e.g. a form with widgets
|
|
1099
|
+
# of varying widths) trigger {Component#repaint}'s default behavior —
|
|
1100
|
+
# the background is cleared and children are re-invalidated so they
|
|
1101
|
+
# paint over a clean surface.
|
|
863
1102
|
class Layout < Component
|
|
864
1103
|
def initialize: () -> void
|
|
865
1104
|
|
|
866
1105
|
def children: () -> ::Array[Component]
|
|
867
1106
|
|
|
1107
|
+
# Layouts are focusable containers — like {Window} and {Popup}, they
|
|
1108
|
+
# don't accept input themselves but they need to participate in the
|
|
1109
|
+
# {HasContent} focus cascade so a Popup wrapping a Layout wrapping a
|
|
1110
|
+
# {TextField} ends up focusing the field rather than parking focus on
|
|
1111
|
+
# the popup. Layouts don't paint any visible chrome of their own
|
|
1112
|
+
# (the auto-cleared background is just blank space), so this has no
|
|
1113
|
+
# mouse-routing consequences — clicks on a gap area land back on the
|
|
1114
|
+
# Layout itself and the on_focus cascade forwards to a tab stop.
|
|
1115
|
+
def focusable?: () -> bool
|
|
1116
|
+
|
|
868
1117
|
# Adds a child component to this layout.
|
|
869
1118
|
#
|
|
870
1119
|
# _@param_ `child`
|
|
@@ -875,8 +1124,6 @@ module Tuile
|
|
|
875
1124
|
|
|
876
1125
|
def content_size: () -> Size
|
|
877
1126
|
|
|
878
|
-
def repaint: () -> void
|
|
879
|
-
|
|
880
1127
|
# Dispatches the event to the child under the mouse cursor.
|
|
881
1128
|
#
|
|
882
1129
|
# _@param_ `event`
|
|
@@ -938,6 +1185,15 @@ module Tuile
|
|
|
938
1185
|
def visible?: () -> bool
|
|
939
1186
|
|
|
940
1187
|
# Fully repaints the window: both frame and contents.
|
|
1188
|
+
#
|
|
1189
|
+
# Window deliberately paints over its entire rect (border around the
|
|
1190
|
+
# edge, content/footer over the interior), so we don't need the
|
|
1191
|
+
# {Component#repaint} default's auto-clear — but we do still want its
|
|
1192
|
+
# "re-invalidate children" effect, since the border overpaints
|
|
1193
|
+
# whatever the content/footer drew on the perimeter. Calling super
|
|
1194
|
+
# handles both: the auto-clear is harmless (we re-paint over it), and
|
|
1195
|
+
# the invalidation queues content + footer for repaint in the same
|
|
1196
|
+
# cycle.
|
|
941
1197
|
def repaint: () -> void
|
|
942
1198
|
|
|
943
1199
|
# _@param_ `key`
|
|
@@ -972,6 +1228,281 @@ module Tuile
|
|
|
972
1228
|
attr_accessor caption: String
|
|
973
1229
|
end
|
|
974
1230
|
|
|
1231
|
+
# A multi-line, word-wrapping text input.
|
|
1232
|
+
#
|
|
1233
|
+
# Sized by the caller — {#rect} is fixed; the area does not grow with
|
|
1234
|
+
# content. Text is wrapped to {Rect#width} columns and any text that
|
|
1235
|
+
# doesn't fit vertically is reached by scrolling: {#top_display_row}
|
|
1236
|
+
# follows the caret so the line being edited stays visible. There is no
|
|
1237
|
+
# horizontal scrolling.
|
|
1238
|
+
#
|
|
1239
|
+
# The caret is a logical index in `0..text.length`. When the caret falls
|
|
1240
|
+
# inside a whitespace run that was absorbed by a soft wrap, it displays
|
|
1241
|
+
# at the end of the previous row (which is visually identical to the
|
|
1242
|
+
# start of the next row in nearly all cases).
|
|
1243
|
+
#
|
|
1244
|
+
# Currently only {#on_change} is wired; Enter inserts a newline as in any
|
|
1245
|
+
# plain `<textarea>` or text editor. A future `on_enter`/`on_submit`
|
|
1246
|
+
# callback may opt out of that by consuming Enter instead.
|
|
1247
|
+
class TextArea < Component
|
|
1248
|
+
ACTIVE_BG_SGR: String
|
|
1249
|
+
INACTIVE_BG_SGR: String
|
|
1250
|
+
|
|
1251
|
+
def initialize: () -> void
|
|
1252
|
+
|
|
1253
|
+
def focusable?: () -> bool
|
|
1254
|
+
|
|
1255
|
+
def tab_stop?: () -> bool
|
|
1256
|
+
|
|
1257
|
+
def cursor_position: () -> Point?
|
|
1258
|
+
|
|
1259
|
+
# _@param_ `key`
|
|
1260
|
+
def handle_key: (String key) -> bool
|
|
1261
|
+
|
|
1262
|
+
# _@param_ `event`
|
|
1263
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
1264
|
+
|
|
1265
|
+
def repaint: () -> void
|
|
1266
|
+
|
|
1267
|
+
def on_width_changed: () -> void
|
|
1268
|
+
|
|
1269
|
+
# _@return_ — cached wrap of {#text} for the
|
|
1270
|
+
# current {Rect#width}. Each entry is `{start:, length:}`.
|
|
1271
|
+
def display_rows: () -> ::Array[::Hash[Symbol, Integer]]
|
|
1272
|
+
|
|
1273
|
+
# Greedy word-wrap. Whitespace at a soft-wrap break point is absorbed
|
|
1274
|
+
# (not rendered on either row). A token longer than {Rect#width} hard-
|
|
1275
|
+
# wraps inside the token. Newlines force a hard break and the wrap
|
|
1276
|
+
# restarts on the next character.
|
|
1277
|
+
def compute_display_rows: () -> ::Array[::Hash[Symbol, Integer]]
|
|
1278
|
+
|
|
1279
|
+
# Trims trailing space/tab characters off a row's visible length so the
|
|
1280
|
+
# whitespace at a soft-wrap point is absorbed (not rendered) rather than
|
|
1281
|
+
# left at the end of the row. Without this, soft-wrapping `"foo bar"`
|
|
1282
|
+
# to width 4 would yield row 0 length 4 (`"foo "`) and the natural
|
|
1283
|
+
# end-of-row caret position would coincide with row 1's start.
|
|
1284
|
+
#
|
|
1285
|
+
# _@param_ `row_start`
|
|
1286
|
+
#
|
|
1287
|
+
# _@param_ `row_chars`
|
|
1288
|
+
#
|
|
1289
|
+
# _@return_ — new row_chars.
|
|
1290
|
+
def trim_trailing_whitespace: (Integer row_start, Integer row_chars) -> Integer
|
|
1291
|
+
|
|
1292
|
+
# _@param_ `caret`
|
|
1293
|
+
#
|
|
1294
|
+
# _@return_ — `[row_index, column]` for `caret`.
|
|
1295
|
+
def caret_to_display: (Integer caret) -> [Integer, Integer]
|
|
1296
|
+
|
|
1297
|
+
# _@param_ `delta` — `+1` for down, `-1` for up.
|
|
1298
|
+
def move_caret_vertical: (Integer delta) -> void
|
|
1299
|
+
|
|
1300
|
+
def move_caret_to_row_start: () -> void
|
|
1301
|
+
|
|
1302
|
+
def move_caret_to_row_end: () -> void
|
|
1303
|
+
|
|
1304
|
+
# _@param_ `char`
|
|
1305
|
+
#
|
|
1306
|
+
# _@return_ — always true.
|
|
1307
|
+
def insert_char: (String char) -> bool
|
|
1308
|
+
|
|
1309
|
+
def delete_before_caret: () -> void
|
|
1310
|
+
|
|
1311
|
+
def delete_at_caret: () -> void
|
|
1312
|
+
|
|
1313
|
+
# Keeps the caret visible by scrolling vertically.
|
|
1314
|
+
def adjust_top_display_row: () -> void
|
|
1315
|
+
|
|
1316
|
+
# _@param_ `key`
|
|
1317
|
+
def printable?: (String key) -> bool
|
|
1318
|
+
|
|
1319
|
+
# Same semantics as {TextField}'s ctrl+left.
|
|
1320
|
+
def word_left: () -> Integer
|
|
1321
|
+
|
|
1322
|
+
# Same semantics as {TextField}'s ctrl+right.
|
|
1323
|
+
def word_right: () -> Integer
|
|
1324
|
+
|
|
1325
|
+
# _@return_ — current text contents (may contain embedded `\n`).
|
|
1326
|
+
attr_accessor text: String
|
|
1327
|
+
|
|
1328
|
+
# _@return_ — caret index in `0..text.length`.
|
|
1329
|
+
attr_accessor caret: Integer
|
|
1330
|
+
|
|
1331
|
+
# _@return_ — index of the topmost display row currently visible.
|
|
1332
|
+
attr_reader top_display_row: Integer
|
|
1333
|
+
|
|
1334
|
+
# Optional callback fired whenever {#text} changes. Receives the new text
|
|
1335
|
+
# as a single argument. Not fired by {#caret=} (text unchanged), not
|
|
1336
|
+
# fired by a no-op setter, and not fired by a re-wrap caused by a width
|
|
1337
|
+
# change ({#text} itself is unchanged).
|
|
1338
|
+
#
|
|
1339
|
+
# _@return_ — one-arg callable, or nil.
|
|
1340
|
+
attr_accessor on_change: (Proc | Method)?
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
# A read-only viewer for prose: chunks of formatted text that scroll
|
|
1344
|
+
# vertically. Shape-wise a hybrid between {Label} (string-shaped content
|
|
1345
|
+
# via {#text=}) and {List} (scroll keys, optional scrollbar, auto-scroll).
|
|
1346
|
+
#
|
|
1347
|
+
# Text is modeled as a {StyledString}: embedded `\n` are hard line breaks,
|
|
1348
|
+
# lines wider than the viewport are word-wrapped via {StyledString#wrap}
|
|
1349
|
+
# (style spans are preserved across wrap boundaries — unlike the older
|
|
1350
|
+
# ANSI-as-bytes wrapping, color does *not* get dropped on continuation
|
|
1351
|
+
# rows). {#text=} accepts a {String} (parsed via {StyledString.parse},
|
|
1352
|
+
# so embedded ANSI is honored) or a {StyledString} directly; {#text}
|
|
1353
|
+
# always returns the {StyledString}. Use {#append} for incremental "log
|
|
1354
|
+
# line" style updates; turn on {#auto_scroll} to keep the latest content
|
|
1355
|
+
# in view.
|
|
1356
|
+
#
|
|
1357
|
+
# TextView is meant to be the content of a {Window} — focus indication and
|
|
1358
|
+
# keyboard-hint surfacing rely on the surrounding window chrome.
|
|
1359
|
+
class TextView < Component
|
|
1360
|
+
def initialize: () -> void
|
|
1361
|
+
|
|
1362
|
+
# _@return_ — the current text. Defaults to an empty
|
|
1363
|
+
# {StyledString}. Internally the text is stored as an array of hard
|
|
1364
|
+
# lines so {#append} can stay O(appended) instead of re-scanning the
|
|
1365
|
+
# whole buffer; the joined {StyledString} returned here is
|
|
1366
|
+
# reconstructed on first read after a mutation and cached, so
|
|
1367
|
+
# repeated reads are O(1) but the first read after {#append} pays
|
|
1368
|
+
# O(total spans).
|
|
1369
|
+
def text: () -> StyledString
|
|
1370
|
+
|
|
1371
|
+
# Replaces the text. Embedded `\n` characters become hard line breaks.
|
|
1372
|
+
# A `String` is parsed via {StyledString.parse} (so embedded ANSI is
|
|
1373
|
+
# honored); a `StyledString` is used as-is; `nil` is coerced to an
|
|
1374
|
+
# empty {StyledString}.
|
|
1375
|
+
#
|
|
1376
|
+
# _@param_ `value`
|
|
1377
|
+
def text=: ((String | StyledString)? value) -> void
|
|
1378
|
+
|
|
1379
|
+
# Appends `str` as a new physical line. If the current text is empty,
|
|
1380
|
+
# behaves like `text = str`; otherwise prepends a newline so the new
|
|
1381
|
+
# content lands on a fresh line. Accepts the same input forms as
|
|
1382
|
+
# {#text=}.
|
|
1383
|
+
#
|
|
1384
|
+
# Cost is O(appended) rather than O(total) — the existing wrapped
|
|
1385
|
+
# buffer is reused, only the new hard line(s) are wrapped and padded,
|
|
1386
|
+
# and `@content_size` is updated incrementally. The cached
|
|
1387
|
+
# {#text} is invalidated and rebuilt on demand.
|
|
1388
|
+
#
|
|
1389
|
+
# _@param_ `str`
|
|
1390
|
+
def append: ((String | StyledString)? str) -> void
|
|
1391
|
+
|
|
1392
|
+
# Clears the text. Equivalent to `text = ""`.
|
|
1393
|
+
def clear: () -> void
|
|
1394
|
+
|
|
1395
|
+
def focusable?: () -> bool
|
|
1396
|
+
|
|
1397
|
+
def tab_stop?: () -> bool
|
|
1398
|
+
|
|
1399
|
+
# _@param_ `key`
|
|
1400
|
+
def handle_key: (String key) -> bool
|
|
1401
|
+
|
|
1402
|
+
# _@param_ `event`
|
|
1403
|
+
def handle_mouse: (MouseEvent event) -> void
|
|
1404
|
+
|
|
1405
|
+
# Paints the text into {#rect}.
|
|
1406
|
+
#
|
|
1407
|
+
# Skips the {Component#repaint} default's auto-clear: every row is
|
|
1408
|
+
# painted explicitly (with padded blanks past the last line), so the
|
|
1409
|
+
# "fully draw over your rect" contract is met without an upfront wipe.
|
|
1410
|
+
def repaint: () -> void
|
|
1411
|
+
|
|
1412
|
+
# Rewraps the text on width changes. Wrap width depends on
|
|
1413
|
+
# {#rect}`.width` and the scrollbar gutter, both of which trigger
|
|
1414
|
+
# this hook.
|
|
1415
|
+
def on_width_changed: () -> void
|
|
1416
|
+
|
|
1417
|
+
# _@return_ — number of visible lines.
|
|
1418
|
+
def viewport_lines: () -> Integer
|
|
1419
|
+
|
|
1420
|
+
# _@return_ — the max value of {#top_line} for scroll-key clamping.
|
|
1421
|
+
def top_line_max: () -> Integer
|
|
1422
|
+
|
|
1423
|
+
# Recomputes {@physical_lines} for the current text and wrap width,
|
|
1424
|
+
# pre-padding every line to `wrap_width` so {#paintable_line} is just
|
|
1425
|
+
# a lookup + optional scrollbar-char append at paint time (and the
|
|
1426
|
+
# rendered ANSI is cached on each line via {StyledString#to_ansi}'s
|
|
1427
|
+
# memoization, so re-painting on scroll is near-free). Clamps
|
|
1428
|
+
# {@top_line} if the new line count puts it out of range.
|
|
1429
|
+
def rewrap: () -> void
|
|
1430
|
+
|
|
1431
|
+
# Wraps `hard_line` at `width` and appends the padded physical lines
|
|
1432
|
+
# to {@physical_lines}. Empty hard lines (e.g. from a `"\n\n"` run)
|
|
1433
|
+
# and degenerate `width <= 0` both emit a single {@blank_line} row,
|
|
1434
|
+
# matching what `@text.wrap(width).map { |l| pad_to(l, width) }`
|
|
1435
|
+
# would have produced for those cases.
|
|
1436
|
+
#
|
|
1437
|
+
# _@param_ `hard_line` — one hard-broken line (no embedded `"\n"`).
|
|
1438
|
+
#
|
|
1439
|
+
# _@param_ `width`
|
|
1440
|
+
def append_physical_lines: (StyledString hard_line, Integer width) -> void
|
|
1441
|
+
|
|
1442
|
+
# Rebuilds the joined {StyledString} from {@hard_lines}, inserting a
|
|
1443
|
+
# default-styled `"\n"` between hard lines. Called from the {#text}
|
|
1444
|
+
# reader when the cache is cold. Cost is O(total spans).
|
|
1445
|
+
def build_text: () -> StyledString
|
|
1446
|
+
|
|
1447
|
+
# _@return_ — {#content_size} computed from {@hard_lines}.
|
|
1448
|
+
def compute_content_size: () -> Size
|
|
1449
|
+
|
|
1450
|
+
# _@return_ — column width available for wrapped text — viewport
|
|
1451
|
+
# width minus the scrollbar gutter (when visible). `0` when {#rect}'s
|
|
1452
|
+
# width is non-positive, which yields a degenerate "no wrap" result.
|
|
1453
|
+
def wrap_width: () -> Integer
|
|
1454
|
+
|
|
1455
|
+
# _@param_ `delta` — negative scrolls up, positive scrolls down.
|
|
1456
|
+
def move_top_line_by: (Integer delta) -> void
|
|
1457
|
+
|
|
1458
|
+
# _@param_ `target` — desired top line; clamped to `[0, top_line_max]`.
|
|
1459
|
+
def move_top_line_to: (Integer target) -> void
|
|
1460
|
+
|
|
1461
|
+
def update_top_line_if_auto_scroll: () -> void
|
|
1462
|
+
|
|
1463
|
+
def scrollbar_visible?: () -> bool
|
|
1464
|
+
|
|
1465
|
+
# Pads `line` with trailing default-styled spaces out to `width` display
|
|
1466
|
+
# columns. Callers rely on {StyledString#wrap} having already
|
|
1467
|
+
# constrained the line to `<= width`, so no truncation is performed.
|
|
1468
|
+
# `width <= 0` returns {StyledString::EMPTY} to handle the degenerate
|
|
1469
|
+
# `wrap_width == 0` case (rect.width == 1 with scrollbar).
|
|
1470
|
+
#
|
|
1471
|
+
# _@param_ `line`
|
|
1472
|
+
#
|
|
1473
|
+
# _@param_ `width`
|
|
1474
|
+
def pad_to: (StyledString line, Integer width) -> StyledString
|
|
1475
|
+
|
|
1476
|
+
# _@param_ `index` — 0-based index into `@physical_lines`.
|
|
1477
|
+
#
|
|
1478
|
+
# _@param_ `row_in_viewport` — 0-based row within the viewport.
|
|
1479
|
+
#
|
|
1480
|
+
# _@param_ `scrollbar`
|
|
1481
|
+
#
|
|
1482
|
+
# _@return_ — paintable ANSI-encoded line exactly `rect.width`
|
|
1483
|
+
# columns wide. Body lines come pre-padded from {#rewrap}, so this
|
|
1484
|
+
# reduces to a memoized {StyledString#to_ansi} read plus an
|
|
1485
|
+
# ASCII-string concat of the scrollbar glyph when one is present.
|
|
1486
|
+
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> String
|
|
1487
|
+
|
|
1488
|
+
# _@return_ — index of the first visible physical line.
|
|
1489
|
+
attr_accessor top_line: Integer
|
|
1490
|
+
|
|
1491
|
+
# _@return_ — `:gone` or `:visible`.
|
|
1492
|
+
attr_accessor scrollbar_visibility: Symbol
|
|
1493
|
+
|
|
1494
|
+
# _@return_ — if true, mutating the text scrolls the viewport so
|
|
1495
|
+
# the last line stays in view. Default `false`.
|
|
1496
|
+
attr_accessor auto_scroll: bool
|
|
1497
|
+
|
|
1498
|
+
# _@return_ — longest hard-line's display width × number of hard
|
|
1499
|
+
# lines. Reported on the *unwrapped* text — wrap-aware sizing would
|
|
1500
|
+
# be circular (width depends on width). Empty text returns
|
|
1501
|
+
# `Size.new(0, 0)`. Maintained incrementally by {#text=} and
|
|
1502
|
+
# {#append}, so reads are O(1).
|
|
1503
|
+
attr_reader content_size: Size
|
|
1504
|
+
end
|
|
1505
|
+
|
|
975
1506
|
# Shows a log. Construct your logger pointed at a {LogWindow::IO} to route
|
|
976
1507
|
# log lines into this window:
|
|
977
1508
|
#
|
|
@@ -1015,10 +1546,15 @@ module Tuile
|
|
|
1015
1546
|
# positioned by {Screen} after each repaint cycle when this component is
|
|
1016
1547
|
# focused; see {Component#cursor_position}.
|
|
1017
1548
|
class TextField < Component
|
|
1549
|
+
ACTIVE_BG_SGR: String
|
|
1550
|
+
INACTIVE_BG_SGR: String
|
|
1551
|
+
|
|
1018
1552
|
def initialize: () -> void
|
|
1019
1553
|
|
|
1020
1554
|
def focusable?: () -> bool
|
|
1021
1555
|
|
|
1556
|
+
def tab_stop?: () -> bool
|
|
1557
|
+
|
|
1022
1558
|
def cursor_position: () -> Point?
|
|
1023
1559
|
|
|
1024
1560
|
# _@param_ `key`
|
|
@@ -1044,6 +1580,16 @@ module Tuile
|
|
|
1044
1580
|
# _@param_ `key`
|
|
1045
1581
|
def printable?: (String key) -> bool
|
|
1046
1582
|
|
|
1583
|
+
# Caret target for ctrl+left: skip whitespace going left, then a run of
|
|
1584
|
+
# non-whitespace. Lands at the beginning of the current word, or the
|
|
1585
|
+
# beginning of the previous word if already there.
|
|
1586
|
+
def word_left: () -> Integer
|
|
1587
|
+
|
|
1588
|
+
# Caret target for ctrl+right: skip non-whitespace going right, then a
|
|
1589
|
+
# run of whitespace. Lands at the beginning of the next word, or at the
|
|
1590
|
+
# end of the text if no further word exists.
|
|
1591
|
+
def word_right: () -> Integer
|
|
1592
|
+
|
|
1047
1593
|
# _@return_ — current text contents.
|
|
1048
1594
|
attr_accessor text: String
|
|
1049
1595
|
|
|
@@ -1433,13 +1979,26 @@ module Tuile
|
|
|
1433
1979
|
|
|
1434
1980
|
# Focus repair when a child detaches. Default {Component#on_child_removed}
|
|
1435
1981
|
# would refocus to `self` (the pane), which isn't a useful focus target.
|
|
1436
|
-
# Instead, route focus to the
|
|
1437
|
-
# snapshotted when this popup was opened
|
|
1438
|
-
#
|
|
1982
|
+
# Instead, route focus to the first interactable widget in the now-topmost
|
|
1983
|
+
# popup; falling back to the focus snapshotted when this popup was opened
|
|
1984
|
+
# (if still attached and still focusable); then to the first interactable
|
|
1985
|
+
# widget in {#content}; then to {#content} itself; then nil.
|
|
1986
|
+
#
|
|
1987
|
+
# "First interactable widget" = first {Component#tab_stop?} in pre-order;
|
|
1988
|
+
# if a scope has no tab stops at all (a borderless ESC-to-close popup, or
|
|
1989
|
+
# tiled content made entirely of {Label}s), we focus the scope's root so
|
|
1990
|
+
# `q`/ESC still has somewhere to dispatch from.
|
|
1439
1991
|
#
|
|
1440
1992
|
# _@param_ `child`
|
|
1441
1993
|
def on_child_removed: (Component child) -> void
|
|
1442
1994
|
|
|
1995
|
+
# First {Component#tab_stop?} in `root`'s subtree (pre-order), falling
|
|
1996
|
+
# back to `root` itself when the subtree has no tab stops. Returns `nil`
|
|
1997
|
+
# if `root` is `nil`.
|
|
1998
|
+
#
|
|
1999
|
+
# _@param_ `root`
|
|
2000
|
+
def first_tab_stop_or_root: (Component? root) -> Component?
|
|
2001
|
+
|
|
1443
2002
|
# _@return_ — the tiled content component.
|
|
1444
2003
|
attr_accessor content: Component?
|
|
1445
2004
|
|
|
@@ -1451,6 +2010,352 @@ module Tuile
|
|
|
1451
2010
|
attr_reader status_bar: Component::Label
|
|
1452
2011
|
end
|
|
1453
2012
|
|
|
2013
|
+
# An immutable string-with-styling, modeled as a sequence of {Span}s where
|
|
2014
|
+
# each span carries a complete {Style} (`fg`, `bg`, `bold`, `italic`,
|
|
2015
|
+
# `underline`). Spans are non-overlapping and fully tile the string — every
|
|
2016
|
+
# character has exactly one resolved style, no overlay layers to merge.
|
|
2017
|
+
#
|
|
2018
|
+
# Where this differs from threading SGR escapes through a plain `String`:
|
|
2019
|
+
# slicing, wrapping, and concatenation operate on the structured spans, so
|
|
2020
|
+
# they never have to "figure out what SGR state is active at column N" —
|
|
2021
|
+
# the answer is just the containing span's `style`. The flip side is one
|
|
2022
|
+
# extra type to construct (or parse) before doing styled-text math.
|
|
2023
|
+
#
|
|
2024
|
+
# ## Constructors
|
|
2025
|
+
#
|
|
2026
|
+
# ```ruby
|
|
2027
|
+
# StyledString.new # empty
|
|
2028
|
+
# StyledString.plain("hello") # default style
|
|
2029
|
+
# StyledString.styled("hello", fg: :red, bold: true)
|
|
2030
|
+
# StyledString.parse("\e[31mhello\e[0m world") # ANSI → spans
|
|
2031
|
+
# ```
|
|
2032
|
+
#
|
|
2033
|
+
# ## Algebra
|
|
2034
|
+
#
|
|
2035
|
+
# All operations return a fresh {StyledString} — the underlying spans are
|
|
2036
|
+
# frozen and shared. `+` coerces a `String` operand via {.parse}.
|
|
2037
|
+
#
|
|
2038
|
+
# ```ruby
|
|
2039
|
+
# a + b # concatenate
|
|
2040
|
+
# ss.slice(2, 5) # 5 display columns starting at column 2
|
|
2041
|
+
# ss.slice(2..5) # range (inclusive end)
|
|
2042
|
+
# ss.lines # split on "\n" → Array<StyledString>
|
|
2043
|
+
# ss.each_char_with_style { |ch, style| ... }
|
|
2044
|
+
# ```
|
|
2045
|
+
#
|
|
2046
|
+
# ## Rendering
|
|
2047
|
+
#
|
|
2048
|
+
# - `#to_s` — plain text, no SGR.
|
|
2049
|
+
# - `#to_ansi` — minimal-diff SGR rendering, ending with `\e[0m` only when
|
|
2050
|
+
# the last span carried a non-default style. Transitions to the default
|
|
2051
|
+
# style emit `\e[0m` (shorter than re-emitting every off-code).
|
|
2052
|
+
#
|
|
2053
|
+
# ## Parser
|
|
2054
|
+
#
|
|
2055
|
+
# {.parse} is strict by design: it recognizes only the SGR codes
|
|
2056
|
+
# corresponding to {Style}'s supported attributes (fg/bg/bold/italic/
|
|
2057
|
+
# underline). Anything else — unmodeled attributes (dim, blink, reverse,
|
|
2058
|
+
# strike, conceal, double-underline, overline, ...), unknown SGR codes, or
|
|
2059
|
+
# non-SGR escapes (cursor moves, OSC) — raises {ParseError}. This keeps the
|
|
2060
|
+
# round-trip parse(to_ansi(x)) == x contract honest.
|
|
2061
|
+
class StyledString
|
|
2062
|
+
EMPTY: StyledString
|
|
2063
|
+
|
|
2064
|
+
# sord duck - #to_s looks like a duck type with an equivalent RBS interface, replacing with _ToS
|
|
2065
|
+
# _@param_ `text`
|
|
2066
|
+
def self.plain: (_ToS text) -> StyledString
|
|
2067
|
+
|
|
2068
|
+
# sord duck - #to_s looks like a duck type with an equivalent RBS interface, replacing with _ToS
|
|
2069
|
+
# _@param_ `text`
|
|
2070
|
+
#
|
|
2071
|
+
# _@param_ `style_kwargs` — forwarded to {Style.new}.
|
|
2072
|
+
def self.styled: (_ToS text, **::Hash[Symbol, Object] style_kwargs) -> StyledString
|
|
2073
|
+
|
|
2074
|
+
# Parses an ANSI/SGR-coded string into a {StyledString}. A {StyledString}
|
|
2075
|
+
# input is returned as-is. `nil` and the empty string both fast-path to
|
|
2076
|
+
# {EMPTY}. Strings without any `\e` byte fast-path to a single
|
|
2077
|
+
# default-styled span.
|
|
2078
|
+
#
|
|
2079
|
+
# _@param_ `input`
|
|
2080
|
+
def self.parse: ((String | StyledString)? input) -> StyledString
|
|
2081
|
+
|
|
2082
|
+
# _@param_ `spans`
|
|
2083
|
+
def initialize: (?::Array[Span] spans) -> void
|
|
2084
|
+
|
|
2085
|
+
# Total display width in terminal columns, accounting for Unicode wide
|
|
2086
|
+
# characters (fullwidth CJK = 2 columns, combining marks = 0, etc.).
|
|
2087
|
+
# Memoized — safe because spans are frozen and immutable.
|
|
2088
|
+
def display_width: () -> Integer
|
|
2089
|
+
|
|
2090
|
+
def empty?: () -> bool
|
|
2091
|
+
|
|
2092
|
+
# Plain text concatenation across all spans — no SGR codes.
|
|
2093
|
+
def to_s: () -> String
|
|
2094
|
+
|
|
2095
|
+
# Rendered ANSI string. Minimal-diff between adjacent spans: only the
|
|
2096
|
+
# attributes that changed are emitted. A transition to the default style
|
|
2097
|
+
# emits `\e[0m` (one code) instead of the longer "turn each attribute
|
|
2098
|
+
# off" form. Always closes with `\e[0m` when the last span carried a
|
|
2099
|
+
# non-default style, so the styled run doesn't bleed into subsequent
|
|
2100
|
+
# output. Memoized — safe because spans are frozen and immutable.
|
|
2101
|
+
def to_ansi: () -> String
|
|
2102
|
+
|
|
2103
|
+
# _@param_ `other`
|
|
2104
|
+
def ==: (Object other) -> bool
|
|
2105
|
+
|
|
2106
|
+
def hash: () -> Integer
|
|
2107
|
+
|
|
2108
|
+
# Concatenation. A `String` operand is parsed via {.parse} before joining
|
|
2109
|
+
# (so embedded ANSI escapes round-trip through spans).
|
|
2110
|
+
#
|
|
2111
|
+
# _@param_ `other`
|
|
2112
|
+
def +: ((StyledString | String) other) -> StyledString
|
|
2113
|
+
|
|
2114
|
+
# Substring by display columns, preserving spans. Characters whose column
|
|
2115
|
+
# range only partially overlaps the slice (e.g. a 2-column CJK character
|
|
2116
|
+
# straddling the start or end boundary) are dropped — never split.
|
|
2117
|
+
#
|
|
2118
|
+
# Accepts either `slice(start_col, len_col)` or `slice(range)`. Both
|
|
2119
|
+
# forms support negative indices counting from the end of the string.
|
|
2120
|
+
#
|
|
2121
|
+
# _@param_ `start_col`
|
|
2122
|
+
#
|
|
2123
|
+
# _@param_ `len_col`
|
|
2124
|
+
def slice: (Integer start_col, Integer len_col) -> StyledString
|
|
2125
|
+
|
|
2126
|
+
# Truncates to a target column width, appending an ellipsis when
|
|
2127
|
+
# characters were dropped. The ellipsis counts toward the target — the
|
|
2128
|
+
# returned {StyledString}'s `display_width` never exceeds
|
|
2129
|
+
# `display_width`. When `self` already fits, `self` is returned. When
|
|
2130
|
+
# `display_width` is smaller than the ellipsis's own width, the ellipsis
|
|
2131
|
+
# is sliced down to fit and no original content is included.
|
|
2132
|
+
#
|
|
2133
|
+
# _@param_ `display_width` — target column width.
|
|
2134
|
+
#
|
|
2135
|
+
# _@param_ `ellipsis` — appended when truncation occurs. Defaults to the Unicode horizontal-ellipsis `…` (one column). A `String` is parsed via {.parse}, so ANSI in it is preserved.
|
|
2136
|
+
def ellipsize: (Integer display_width, ?(String | StyledString) ellipsis) -> StyledString
|
|
2137
|
+
|
|
2138
|
+
# Splits on `"\n"`, preserving spans on each side. A trailing newline
|
|
2139
|
+
# produces a trailing empty {StyledString} (matches `split("\n", -1)`).
|
|
2140
|
+
# An empty {StyledString} returns a single empty entry, like `"".split`.
|
|
2141
|
+
def lines: () -> ::Array[StyledString]
|
|
2142
|
+
|
|
2143
|
+
# Word-wraps to physical lines that each fit within `width` display
|
|
2144
|
+
# columns, preserving spans and styles across breaks. Greedy word-wrap,
|
|
2145
|
+
# hard-break for words wider than `width`, leading whitespace dropped on
|
|
2146
|
+
# wrapped continuations, hard `"\n"` breaks preserved as separate output
|
|
2147
|
+
# lines.
|
|
2148
|
+
#
|
|
2149
|
+
# Whitespace runs are space or tab; other characters are treated as word
|
|
2150
|
+
# content. When a single character is wider than `width` (e.g. a 2-column
|
|
2151
|
+
# CJK character with `width = 1`), it is still emitted on its own line at
|
|
2152
|
+
# its natural width. The "no line exceeds `width`" guarantee therefore
|
|
2153
|
+
# holds whenever every character is at most `width` columns wide.
|
|
2154
|
+
#
|
|
2155
|
+
# _@param_ `width` — target column width. `nil` or `<= 0` skips wrapping and returns each hard-line as-is, so callers can pass a stale viewport width without crashing.
|
|
2156
|
+
#
|
|
2157
|
+
# _@return_ — one entry per physical (output) line.
|
|
2158
|
+
# An empty receiver returns `[]`.
|
|
2159
|
+
def wrap: (Integer? width) -> ::Array[StyledString]
|
|
2160
|
+
|
|
2161
|
+
# Yields each character (per `String#each_char`) along with the {Style}
|
|
2162
|
+
# it carries. Returns an `Enumerator` without a block.
|
|
2163
|
+
def each_char_with_style: () -> (::Enumerator[untyped] | self)
|
|
2164
|
+
|
|
2165
|
+
# Returns a new {StyledString} with `bg` applied to every span, preserving
|
|
2166
|
+
# each span's text and other style attributes (`fg`, `bold`, `italic`,
|
|
2167
|
+
# `underline`). Useful for row-level highlights — the new bg overlays
|
|
2168
|
+
# without dropping foreground colors the original styling carried.
|
|
2169
|
+
#
|
|
2170
|
+
# _@param_ `bg` — background color, in any of the forms accepted by {Style.new}. `nil` clears bg back to the terminal default.
|
|
2171
|
+
def with_bg: ((Symbol | Integer | ::Array[Integer])? bg) -> StyledString
|
|
2172
|
+
|
|
2173
|
+
def inspect: () -> String
|
|
2174
|
+
|
|
2175
|
+
def build_ansi: () -> String
|
|
2176
|
+
|
|
2177
|
+
# _@param_ `spans`
|
|
2178
|
+
def normalize: (::Array[Span] spans) -> ::Array[Span]
|
|
2179
|
+
|
|
2180
|
+
# _@param_ `from`
|
|
2181
|
+
#
|
|
2182
|
+
# _@param_ `to`
|
|
2183
|
+
def sgr_diff: (Style from, Style to) -> String
|
|
2184
|
+
|
|
2185
|
+
# _@param_ `color`
|
|
2186
|
+
#
|
|
2187
|
+
# _@param_ `base` — base SGR code — 30 for fg, 40 for bg.
|
|
2188
|
+
#
|
|
2189
|
+
# _@param_ `ext` — extended-color SGR code — 38 for fg, 48 for bg.
|
|
2190
|
+
def color_codes: ((Symbol | Integer | ::Array[Integer])? color, base: Integer, ext: Integer) -> ::Array[Integer]
|
|
2191
|
+
|
|
2192
|
+
# _@param_ `start_or_range`
|
|
2193
|
+
#
|
|
2194
|
+
# _@param_ `len`
|
|
2195
|
+
#
|
|
2196
|
+
# _@param_ `total` — receiver's full display width.
|
|
2197
|
+
#
|
|
2198
|
+
# _@return_ — normalized `[start_col, len_col]`.
|
|
2199
|
+
def resolve_slice_bounds: ((Integer | ::Range[untyped]) start_or_range, Integer? len, Integer total) -> [Integer, Integer]
|
|
2200
|
+
|
|
2201
|
+
# _@param_ `start`
|
|
2202
|
+
#
|
|
2203
|
+
# _@param_ `len`
|
|
2204
|
+
def slice_spans: (Integer start, Integer len) -> StyledString
|
|
2205
|
+
|
|
2206
|
+
# _@param_ `hard_line` — one hard-broken line — no embedded `"\n"`.
|
|
2207
|
+
#
|
|
2208
|
+
# _@param_ `width`
|
|
2209
|
+
def wrap_one: (StyledString hard_line, Integer width) -> ::Array[StyledString]
|
|
2210
|
+
|
|
2211
|
+
# _@param_ `hard_line`
|
|
2212
|
+
#
|
|
2213
|
+
# _@return_ — tokens shaped `[type, chars, w]` where `type` is
|
|
2214
|
+
# `:space` or `:word`, `chars` is an `Array<[String, Style, Integer]>`
|
|
2215
|
+
# (char, style, display width), and `w` is the token's total width.
|
|
2216
|
+
def tokenize_for_wrap: (StyledString hard_line) -> ::Array[::Array[untyped]]
|
|
2217
|
+
|
|
2218
|
+
# _@param_ `chars` — `[char, style, width]` triples.
|
|
2219
|
+
#
|
|
2220
|
+
# _@param_ `width`
|
|
2221
|
+
#
|
|
2222
|
+
# _@return_ — each inner Array is a `chars`-shaped chunk.
|
|
2223
|
+
def hard_break_chars: (::Array[::Array[untyped]] chars, Integer width) -> ::Array[::Array[::Array[untyped]]]
|
|
2224
|
+
|
|
2225
|
+
# _@param_ `chars` — `[char, style, width]` triples.
|
|
2226
|
+
def chars_to_styled: (::Array[::Array[untyped]] chars) -> StyledString
|
|
2227
|
+
|
|
2228
|
+
# _@param_ `text`
|
|
2229
|
+
#
|
|
2230
|
+
# _@param_ `start_col`
|
|
2231
|
+
#
|
|
2232
|
+
# _@param_ `len_col`
|
|
2233
|
+
def slice_text_by_columns: (String text, Integer start_col, Integer len_col) -> String
|
|
2234
|
+
|
|
2235
|
+
# _@return_ — the frozen, normalized span list — no empty-text
|
|
2236
|
+
# entries, no two adjacent entries sharing a style.
|
|
2237
|
+
attr_reader spans: ::Array[Span]
|
|
2238
|
+
|
|
2239
|
+
# Raised by {.parse} on malformed or unsupported escape sequences.
|
|
2240
|
+
class ParseError < Tuile::Error
|
|
2241
|
+
end
|
|
2242
|
+
|
|
2243
|
+
# A frozen value type describing the visual style of a {Span}.
|
|
2244
|
+
#
|
|
2245
|
+
# `fg` and `bg` accept:
|
|
2246
|
+
# - `nil` — the terminal default (SGR 39 / 49)
|
|
2247
|
+
# - a symbol from {COLOR_SYMBOLS} — 8 standard + 8 bright ANSI colors
|
|
2248
|
+
# - an Integer 0..255 — 256-color palette index (SGR 38;5;N / 48;5;N)
|
|
2249
|
+
# - an `[r, g, b]` Array of three 0..255 Integers — 24-bit RGB
|
|
2250
|
+
#
|
|
2251
|
+
# @!attribute [r] fg
|
|
2252
|
+
# @return [Symbol, Integer, Array<Integer>, nil]
|
|
2253
|
+
# @!attribute [r] bg
|
|
2254
|
+
# @return [Symbol, Integer, Array<Integer>, nil]
|
|
2255
|
+
# @!attribute [r] bold
|
|
2256
|
+
# @return [Boolean]
|
|
2257
|
+
# @!attribute [r] italic
|
|
2258
|
+
# @return [Boolean]
|
|
2259
|
+
# @!attribute [r] underline
|
|
2260
|
+
# @return [Boolean]
|
|
2261
|
+
class Style
|
|
2262
|
+
COLOR_SYMBOLS: ::Array[Symbol]
|
|
2263
|
+
DEFAULT: Style
|
|
2264
|
+
|
|
2265
|
+
# _@param_ `fg`
|
|
2266
|
+
#
|
|
2267
|
+
# _@param_ `bg`
|
|
2268
|
+
#
|
|
2269
|
+
# _@param_ `bold`
|
|
2270
|
+
#
|
|
2271
|
+
# _@param_ `italic`
|
|
2272
|
+
#
|
|
2273
|
+
# _@param_ `underline`
|
|
2274
|
+
def self.new: (
|
|
2275
|
+
?fg: (Symbol | Integer | ::Array[Integer])?,
|
|
2276
|
+
?bg: (Symbol | Integer | ::Array[Integer])?,
|
|
2277
|
+
?bold: bool,
|
|
2278
|
+
?italic: bool,
|
|
2279
|
+
?underline: bool
|
|
2280
|
+
) -> Style
|
|
2281
|
+
|
|
2282
|
+
# _@param_ `color`
|
|
2283
|
+
#
|
|
2284
|
+
# _@param_ `which`
|
|
2285
|
+
def self.validate_color!: (Object color, Symbol which) -> void
|
|
2286
|
+
|
|
2287
|
+
def default?: () -> bool
|
|
2288
|
+
|
|
2289
|
+
# Returns a new {Style} with the given attributes overridden.
|
|
2290
|
+
#
|
|
2291
|
+
# _@param_ `overrides`
|
|
2292
|
+
def merge: (**::Hash[Symbol, Object] overrides) -> Style
|
|
2293
|
+
|
|
2294
|
+
attr_reader fg: (Symbol | Integer | ::Array[Integer])?
|
|
2295
|
+
|
|
2296
|
+
attr_reader bg: (Symbol | Integer | ::Array[Integer])?
|
|
2297
|
+
|
|
2298
|
+
attr_reader bold: bool
|
|
2299
|
+
|
|
2300
|
+
attr_reader italic: bool
|
|
2301
|
+
|
|
2302
|
+
attr_reader underline: bool
|
|
2303
|
+
end
|
|
2304
|
+
|
|
2305
|
+
# A maximal run of text sharing a single {Style}. `text` is plain — it
|
|
2306
|
+
# never contains ANSI escape sequences. Spans inside a {StyledString} are
|
|
2307
|
+
# normalized: no empty text, no two adjacent spans share a style.
|
|
2308
|
+
#
|
|
2309
|
+
# @!attribute [r] text
|
|
2310
|
+
# @return [String] frozen plain text.
|
|
2311
|
+
# @!attribute [r] style
|
|
2312
|
+
# @return [Style]
|
|
2313
|
+
class Span
|
|
2314
|
+
# _@param_ `text`
|
|
2315
|
+
#
|
|
2316
|
+
# _@param_ `style`
|
|
2317
|
+
def initialize: (text: String, style: Style) -> void
|
|
2318
|
+
|
|
2319
|
+
# _@return_ — frozen plain text.
|
|
2320
|
+
attr_reader text: String
|
|
2321
|
+
|
|
2322
|
+
attr_reader style: Style
|
|
2323
|
+
end
|
|
2324
|
+
|
|
2325
|
+
# @api private
|
|
2326
|
+
# Hand-rolled SGR parser. State machine over a {StringScanner}: plain
|
|
2327
|
+
# text accumulates into the current span; each `\e[...m` flushes the
|
|
2328
|
+
# current span and updates the running {Style}. Anything outside the
|
|
2329
|
+
# supported SGR alphabet raises {ParseError}.
|
|
2330
|
+
class Parser
|
|
2331
|
+
STANDARD_COLORS: ::Array[Symbol]
|
|
2332
|
+
BRIGHT_COLORS: ::Array[Symbol]
|
|
2333
|
+
|
|
2334
|
+
# _@param_ `input`
|
|
2335
|
+
def initialize: (String input) -> void
|
|
2336
|
+
|
|
2337
|
+
def parse: () -> StyledString
|
|
2338
|
+
|
|
2339
|
+
def consume_text: () -> void
|
|
2340
|
+
|
|
2341
|
+
def consume_escape: () -> void
|
|
2342
|
+
|
|
2343
|
+
# _@param_ `params_str`
|
|
2344
|
+
def apply_sgr: (String params_str) -> void
|
|
2345
|
+
|
|
2346
|
+
# _@param_ `codes`
|
|
2347
|
+
#
|
|
2348
|
+
# _@param_ `index`
|
|
2349
|
+
#
|
|
2350
|
+
# _@param_ `target` — either `:fg` or `:bg`.
|
|
2351
|
+
#
|
|
2352
|
+
# _@return_ — how many SGR codes were consumed (3 for 256-color, 5 for RGB).
|
|
2353
|
+
def consume_extended_color: (::Array[Integer] codes, Integer index, Symbol target) -> Integer
|
|
2354
|
+
|
|
2355
|
+
def flush: () -> void
|
|
2356
|
+
end
|
|
2357
|
+
end
|
|
2358
|
+
|
|
1454
2359
|
# A "synchronous" event queue – no loop is run, submitted blocks are run right
|
|
1455
2360
|
# away and submitted events are thrown away. Intended for testing only.
|
|
1456
2361
|
class FakeEventQueue
|