tuile 0.6.0 → 0.8.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 +16 -0
- data/README.md +1 -1
- data/examples/sampler.rb +112 -3
- data/ideas/back-buffer.md +217 -0
- data/lib/tuile/ansi.rb +16 -0
- data/lib/tuile/buffer.rb +412 -0
- data/lib/tuile/component/button.rb +2 -5
- data/lib/tuile/component/has_content.rb +0 -6
- data/lib/tuile/component/label.rb +8 -8
- data/lib/tuile/component/layout.rb +1 -13
- data/lib/tuile/component/list.rb +45 -23
- data/lib/tuile/component/log_window.rb +21 -5
- data/lib/tuile/component/picker_window.rb +8 -6
- data/lib/tuile/component/popup.rb +69 -13
- data/lib/tuile/component/text_area.rb +1 -1
- data/lib/tuile/component/text_field.rb +1 -1
- data/lib/tuile/component/text_input.rb +25 -9
- data/lib/tuile/component/text_view.rb +30 -10
- data/lib/tuile/component/window.rb +21 -38
- data/lib/tuile/component.rb +30 -26
- data/lib/tuile/fake_screen.rb +14 -1
- data/lib/tuile/keys.rb +2 -6
- data/lib/tuile/rect.rb +12 -0
- data/lib/tuile/screen.rb +109 -113
- data/lib/tuile/screen_pane.rb +81 -20
- data/lib/tuile/styled_string.rb +164 -59
- data/lib/tuile/version.rb +1 -1
- data/mise.toml +2 -0
- data/sig/tuile.rbs +639 -133
- metadata +10 -4
data/sig/tuile.rbs
CHANGED
|
@@ -23,6 +23,8 @@ module Tuile
|
|
|
23
23
|
# same form.
|
|
24
24
|
module Ansi
|
|
25
25
|
RESET: String
|
|
26
|
+
SYNC_BEGIN: String
|
|
27
|
+
SYNC_END: String
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
# Constants for keys returned by {.getkey} and helpers for reading them from
|
|
@@ -136,6 +138,13 @@ module Tuile
|
|
|
136
138
|
# _@param_ `point`
|
|
137
139
|
def contains?: (Point point) -> bool
|
|
138
140
|
|
|
141
|
+
# _@param_ `other` — another rectangle.
|
|
142
|
+
#
|
|
143
|
+
# _@return_ — true if `other` lies entirely within this rectangle.
|
|
144
|
+
# Uses the same half-open edges as {#contains?} (right/bottom exclusive).
|
|
145
|
+
# An {#empty? empty} `other` covers no cells, so it is trivially contained.
|
|
146
|
+
def contains_rect?: (Rect other) -> bool
|
|
147
|
+
|
|
139
148
|
def size: () -> Size
|
|
140
149
|
|
|
141
150
|
def top_left: () -> Point
|
|
@@ -519,6 +528,286 @@ module Tuile
|
|
|
519
528
|
attr_reader custom: ::Hash[Symbol, Color]
|
|
520
529
|
end
|
|
521
530
|
|
|
531
|
+
# An in-memory grid of styled cells mirroring the terminal screen. This is
|
|
532
|
+
# the back buffer behind flicker-free rendering: components paint into it
|
|
533
|
+
# (via {#set_line} / {#set_char} / {#fill}) instead of writing escape
|
|
534
|
+
# sequences straight to the terminal, and {#flush} emits the minimal escape
|
|
535
|
+
# string needed to bring a terminal — one that already matches the buffer's
|
|
536
|
+
# state as of the previous flush — up to date. Only cells that actually
|
|
537
|
+
# changed are emitted, so nothing flickers regardless of terminal/multiplexer
|
|
538
|
+
# synchronized-output support. See `ideas/back-buffer.md`.
|
|
539
|
+
#
|
|
540
|
+
# Coordinates are 0-based `(x, y)` = `(column, row)`, matching
|
|
541
|
+
# {Component#rect} and `TTY::Cursor.move_to`.
|
|
542
|
+
#
|
|
543
|
+
# ## Dirty tracking
|
|
544
|
+
#
|
|
545
|
+
# Every mutator compares the incoming grapheme+style against what's already
|
|
546
|
+
# there and records the cell dirty only when it differs — so both mutation
|
|
547
|
+
# and {#flush} cost scale with what actually changed, never with the buffer
|
|
548
|
+
# size. There is deliberately no per-frame whole-buffer clear or copy;
|
|
549
|
+
# un-touched cells retain the previous frame's value.
|
|
550
|
+
#
|
|
551
|
+
# The bookkeeping avoids hashing and full-grid scans: a dirty flag **on each
|
|
552
|
+
# cell** (O(1) set, no `Set` bucket math, no separate array), a per-row
|
|
553
|
+
# boolean so {#flush} scans only the rows that changed, and one global flag
|
|
554
|
+
# so {#dirty?} and the "nothing changed" early-out are O(1). {#flush} clears
|
|
555
|
+
# every flag it consumes.
|
|
556
|
+
#
|
|
557
|
+
# Cells are **mutable and pre-allocated**: the grid builds its {Cell}s once
|
|
558
|
+
# (at construction and {#resize}) and rewrites them in place, so a normal
|
|
559
|
+
# paint allocates nothing per cell. That is why {Cell} is a plain mutable
|
|
560
|
+
# object rather than a frozen value type. The empty state of a cell is a
|
|
561
|
+
# space in the default style.
|
|
562
|
+
#
|
|
563
|
+
# ## Wide characters
|
|
564
|
+
#
|
|
565
|
+
# A 2-column glyph (fullwidth CJK, most emoji) occupies its origin cell plus a
|
|
566
|
+
# **continuation** cell to its right (an empty-grapheme {Cell} the flush emits
|
|
567
|
+
# nothing for, since the glyph itself advances the cursor two columns).
|
|
568
|
+
# Overwriting either half of a wide glyph blanks the orphaned half, so the
|
|
569
|
+
# grid never holds a dangling continuation or a headless one.
|
|
570
|
+
class Buffer
|
|
571
|
+
DEFAULT_STYLE: StyledString::Style
|
|
572
|
+
|
|
573
|
+
# _@param_ `size` — grid dimensions in columns × rows.
|
|
574
|
+
def initialize: (Size size) -> void
|
|
575
|
+
|
|
576
|
+
# _@return_ — grid dimensions.
|
|
577
|
+
def size: () -> Size
|
|
578
|
+
|
|
579
|
+
# _@param_ `x` — column.
|
|
580
|
+
#
|
|
581
|
+
# _@param_ `y` — row.
|
|
582
|
+
#
|
|
583
|
+
# _@return_ — the live cell at `(x, y)` (do not mutate — paint via
|
|
584
|
+
# {#set_char} / {#set_line} so dirty tracking stays correct), or nil when
|
|
585
|
+
# out of bounds.
|
|
586
|
+
def cell: (Integer x, Integer y) -> Cell?
|
|
587
|
+
|
|
588
|
+
# _@return_ — true if any cell has changed since the last {#flush}.
|
|
589
|
+
def dirty?: () -> bool
|
|
590
|
+
|
|
591
|
+
# Writes one grapheme cluster at `(x, y)`. A 2-column glyph also writes a
|
|
592
|
+
# continuation cell at `(x + 1, y)`; a wide glyph that would overflow the
|
|
593
|
+
# last column is replaced by a blank (terminals can't render a half-clipped
|
|
594
|
+
# wide glyph). Zero-width input (a lone combining mark) is ignored — it has
|
|
595
|
+
# no cell of its own. Out-of-bounds writes are dropped.
|
|
596
|
+
#
|
|
597
|
+
# _@param_ `x` — column.
|
|
598
|
+
#
|
|
599
|
+
# _@param_ `y` — row.
|
|
600
|
+
#
|
|
601
|
+
# _@param_ `grapheme` — one grapheme cluster.
|
|
602
|
+
#
|
|
603
|
+
# _@param_ `style`
|
|
604
|
+
def set_char: (
|
|
605
|
+
Integer x,
|
|
606
|
+
Integer y,
|
|
607
|
+
String grapheme,
|
|
608
|
+
?StyledString::Style style
|
|
609
|
+
) -> void
|
|
610
|
+
|
|
611
|
+
# Writes a {StyledString} starting at `(x, y)`, advancing by each grapheme's
|
|
612
|
+
# display width and clipping at the right edge. The workhorse that replaces
|
|
613
|
+
# the old `screen.print(TTY::Cursor.move_to(x, y), styled.to_ansi)` per-row
|
|
614
|
+
# paint. Newlines in the string are not handled — pass one physical line.
|
|
615
|
+
#
|
|
616
|
+
# _@param_ `x` — starting column.
|
|
617
|
+
#
|
|
618
|
+
# _@param_ `y` — row.
|
|
619
|
+
#
|
|
620
|
+
# _@param_ `styled`
|
|
621
|
+
def set_line: (Integer x, Integer y, StyledString styled) -> void
|
|
622
|
+
|
|
623
|
+
# Fills the intersection of `rect` and the buffer with blank cells in
|
|
624
|
+
# `style` — the cell-grid equivalent of clearing a background. Only `bg`
|
|
625
|
+
# shows; the grapheme is a space.
|
|
626
|
+
#
|
|
627
|
+
# _@param_ `rect`
|
|
628
|
+
#
|
|
629
|
+
# _@param_ `style`
|
|
630
|
+
def fill: (Rect rect, ?StyledString::Style style) -> void
|
|
631
|
+
|
|
632
|
+
# Blanks the entire buffer in `style`. A flat pass over every cell — no
|
|
633
|
+
# rect math or nested loops, since it covers the whole grid. Only cells
|
|
634
|
+
# that actually change are marked dirty (and their rows), so a {#flush}
|
|
635
|
+
# after clearing an already-blank buffer emits nothing.
|
|
636
|
+
#
|
|
637
|
+
# _@param_ `style`
|
|
638
|
+
def clear: (?StyledString::Style style) -> void
|
|
639
|
+
|
|
640
|
+
# Marks every cell dirty, so the next {#flush} re-emits the whole grid.
|
|
641
|
+
# Used after a resize and whenever the terminal contents become unknown
|
|
642
|
+
# (e.g. the screen was cleared underneath us).
|
|
643
|
+
def mark_all_dirty: () -> void
|
|
644
|
+
|
|
645
|
+
# Resizes the grid to `size`, reallocating blank cells and marking the
|
|
646
|
+
# whole buffer dirty — after a resize the terminal contents are undefined,
|
|
647
|
+
# so the next flush redraws from scratch.
|
|
648
|
+
#
|
|
649
|
+
# _@param_ `size`
|
|
650
|
+
def resize: (Size size) -> void
|
|
651
|
+
|
|
652
|
+
# Emits the minimal escape sequence that updates a terminal — already
|
|
653
|
+
# matching this buffer as of the previous flush — to the current contents,
|
|
654
|
+
# then clears the dirty flags. Returns `""` when nothing changed.
|
|
655
|
+
#
|
|
656
|
+
# Scans only dirty rows; within a row, consecutive dirty cells form one run
|
|
657
|
+
# (one `TTY::Cursor.move_to` followed by their graphemes), with a running
|
|
658
|
+
# {StyledString::Style#sgr_to} diff so only changed attributes are sent
|
|
659
|
+
# (continuation cells emit nothing). The sequence always ends in the default
|
|
660
|
+
# style ({Ansi::RESET} when needed), the invariant the next flush relies on:
|
|
661
|
+
# the terminal's SGR state is default at flush boundaries.
|
|
662
|
+
#
|
|
663
|
+
# _@return_ — the escape sequence to write to the terminal.
|
|
664
|
+
def flush: () -> String
|
|
665
|
+
|
|
666
|
+
# _@param_ `y` — row.
|
|
667
|
+
#
|
|
668
|
+
# _@return_ — the plain text of row `y` (continuation cells contribute
|
|
669
|
+
# nothing, so wide glyphs read as their single cluster). Intended for
|
|
670
|
+
# tests; see {FakeScreen}.
|
|
671
|
+
def row_text: (Integer y) -> String
|
|
672
|
+
|
|
673
|
+
# _@param_ `y` — row.
|
|
674
|
+
#
|
|
675
|
+
# _@return_ — row `y` rendered to ANSI across its full width — the
|
|
676
|
+
# minimal-SGR encoding of its cells, equivalent to what a component's
|
|
677
|
+
# `set_line` of the whole row would have printed. Intended for tests that
|
|
678
|
+
# assert on styled output (see {FakeScreen}); empty for an out-of-range row.
|
|
679
|
+
def row_ansi: (Integer y) -> String
|
|
680
|
+
|
|
681
|
+
# _@param_ `rect`
|
|
682
|
+
#
|
|
683
|
+
# _@return_ — the plain text of each row within `rect`'s column
|
|
684
|
+
# range, top to bottom. The region equivalent of {#row_text}, for asserting
|
|
685
|
+
# what a component painted into its own rect. Intended for tests.
|
|
686
|
+
def region_text: (Rect rect) -> ::Array[String]
|
|
687
|
+
|
|
688
|
+
# _@param_ `rect`
|
|
689
|
+
#
|
|
690
|
+
# _@return_ — each row within `rect` rendered to ANSI, top to
|
|
691
|
+
# bottom — byte-identical to what a component's per-row `set_line` over
|
|
692
|
+
# that rect emitted. The region equivalent of {#row_ansi}. Intended for
|
|
693
|
+
# tests asserting styled output.
|
|
694
|
+
def region_ansi: (Rect rect) -> ::Array[String]
|
|
695
|
+
|
|
696
|
+
# (Re)allocates a blank grid of `size` with clean dirty state. Callers
|
|
697
|
+
# follow with {#mark_all_dirty} when the terminal doesn't match the new
|
|
698
|
+
# grid — construction and {#resize} both do.
|
|
699
|
+
#
|
|
700
|
+
# _@param_ `size`
|
|
701
|
+
def allocate_grid: (Size size) -> void
|
|
702
|
+
|
|
703
|
+
# Emits the dirty cells of row `y` into `out`, breaking a run at each clean
|
|
704
|
+
# cell, and returns the running style at the end of the row.
|
|
705
|
+
#
|
|
706
|
+
# _@param_ `out` — accumulator.
|
|
707
|
+
#
|
|
708
|
+
# _@param_ `y`
|
|
709
|
+
#
|
|
710
|
+
# _@param_ `style` — style the terminal currently holds.
|
|
711
|
+
def flush_row: (String _out, Integer y, StyledString::Style style) -> StyledString::Style
|
|
712
|
+
|
|
713
|
+
# _@param_ `rect`
|
|
714
|
+
#
|
|
715
|
+
# _@return_ — cells within `rect`, row-major, clamped to
|
|
716
|
+
# the grid (out-of-bounds positions yield a blank cell).
|
|
717
|
+
def region_cells: (Rect rect) -> ::Array[::Array[Cell]]
|
|
718
|
+
|
|
719
|
+
# _@param_ `x` — column
|
|
720
|
+
#
|
|
721
|
+
# _@param_ `y` — row
|
|
722
|
+
#
|
|
723
|
+
# _@return_ — flat-array index for `(x, y)`.
|
|
724
|
+
def index: (Integer x, Integer y) -> Integer
|
|
725
|
+
|
|
726
|
+
# _@param_ `x` — column
|
|
727
|
+
#
|
|
728
|
+
# _@param_ `y` — row
|
|
729
|
+
#
|
|
730
|
+
# _@return_ — true when `(x, y)` falls within the grid.
|
|
731
|
+
def in_bounds?: (Integer x, Integer y) -> bool
|
|
732
|
+
|
|
733
|
+
# Rewrites the cell at `(x, y)` in place, marking it (and its row) dirty
|
|
734
|
+
# only when grapheme or style actually changes. Caller guarantees `(x, y)`
|
|
735
|
+
# is in bounds.
|
|
736
|
+
#
|
|
737
|
+
# _@param_ `x` — column
|
|
738
|
+
#
|
|
739
|
+
# _@param_ `y` — row
|
|
740
|
+
#
|
|
741
|
+
# _@param_ `grapheme` — the new grapheme cluster
|
|
742
|
+
#
|
|
743
|
+
# _@param_ `style` — the new style
|
|
744
|
+
def write_cell: (
|
|
745
|
+
Integer x,
|
|
746
|
+
Integer y,
|
|
747
|
+
String grapheme,
|
|
748
|
+
StyledString::Style style
|
|
749
|
+
) -> void
|
|
750
|
+
|
|
751
|
+
# If `(x, y)` is half of a wide glyph, blanks the *other* half, so a write
|
|
752
|
+
# that lands on either half doesn't strand the remaining one.
|
|
753
|
+
#
|
|
754
|
+
# _@param_ `x` — column
|
|
755
|
+
#
|
|
756
|
+
# _@param_ `y` — row
|
|
757
|
+
def repair_orphans: (Integer x, Integer y) -> void
|
|
758
|
+
|
|
759
|
+
attr_reader width: Integer
|
|
760
|
+
|
|
761
|
+
attr_reader height: Integer
|
|
762
|
+
|
|
763
|
+
# One screen cell: a single grapheme cluster, the {StyledString::Style} it's
|
|
764
|
+
# drawn in, and a dirty flag. Mutable by design (see {Buffer} "Dirty
|
|
765
|
+
# tracking") — the grid rewrites cells in place. A continuation cell (right
|
|
766
|
+
# half of a wide glyph) carries an empty grapheme — see {#continuation?}.
|
|
767
|
+
class Cell
|
|
768
|
+
# _@param_ `grapheme`
|
|
769
|
+
#
|
|
770
|
+
# _@param_ `style`
|
|
771
|
+
def initialize: (String grapheme, StyledString::Style style) -> void
|
|
772
|
+
|
|
773
|
+
# _@return_ — true if this is the right half of a wide glyph, which
|
|
774
|
+
# {Buffer#flush} skips (the glyph to the left already moved the cursor
|
|
775
|
+
# past it).
|
|
776
|
+
def continuation?: () -> bool
|
|
777
|
+
|
|
778
|
+
# Sets the cell's content, flipping {#dirty} on when grapheme or style
|
|
779
|
+
# actually changes (an already-dirty cell stays dirty). Returns the
|
|
780
|
+
# resulting dirty flag, so callers can aggregate row/buffer dirty state in
|
|
781
|
+
# one step. The single mutation path behind {Buffer#set_char} / {#fill} /
|
|
782
|
+
# {#clear}.
|
|
783
|
+
#
|
|
784
|
+
# _@param_ `grapheme`
|
|
785
|
+
#
|
|
786
|
+
# _@param_ `style`
|
|
787
|
+
#
|
|
788
|
+
# _@return_ — {#dirty} after the write.
|
|
789
|
+
def set: (String grapheme, StyledString::Style style) -> bool
|
|
790
|
+
|
|
791
|
+
# Content equality (grapheme + style); the dirty flag is bookkeeping and
|
|
792
|
+
# is deliberately excluded.
|
|
793
|
+
#
|
|
794
|
+
# _@param_ `other`
|
|
795
|
+
def ==: (Object other) -> bool
|
|
796
|
+
|
|
797
|
+
# Read-only: mutate content through {#set} so dirty tracking stays correct.
|
|
798
|
+
#
|
|
799
|
+
# _@return_ — one grapheme cluster, `" "` for blank, or `""` for a
|
|
800
|
+
# wide-glyph continuation.
|
|
801
|
+
attr_reader grapheme: String
|
|
802
|
+
|
|
803
|
+
attr_reader style: StyledString::Style
|
|
804
|
+
|
|
805
|
+
# _@return_ — true if this cell changed since the last {Buffer#flush}.
|
|
806
|
+
# {Buffer} flips it (off as it flushes, on via {Buffer#mark_all_dirty}).
|
|
807
|
+
attr_accessor dirty: bool
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
522
811
|
# The TTY screen. There is exactly one screen per app.
|
|
523
812
|
#
|
|
524
813
|
# A screen runs the event loop; call {#run_event_loop} to do that.
|
|
@@ -665,6 +954,15 @@ module Tuile
|
|
|
665
954
|
# _@param_ `window`
|
|
666
955
|
def remove_popup: (Component::Popup window) -> void
|
|
667
956
|
|
|
957
|
+
# Invalidates the entire attached tree, forcing every component to repaint
|
|
958
|
+
# on the next cycle. Needed whenever something overdraws the scene without
|
|
959
|
+
# clipping and then exposes what was underneath — a closing popup
|
|
960
|
+
# ({#remove_popup}), or a popup that shrinks or moves so its new {#rect} no
|
|
961
|
+
# longer covers the cells it previously painted ({Component::Popup#rect=}).
|
|
962
|
+
# The popup-only fast path in {#repaint} can't clear those vacated cells on
|
|
963
|
+
# its own, so we accept the cost of a full repaint.
|
|
964
|
+
def needs_full_repaint: () -> void
|
|
965
|
+
|
|
668
966
|
# Internal — use {Component::Popup#open?} instead.
|
|
669
967
|
#
|
|
670
968
|
# _@param_ `window`
|
|
@@ -682,23 +980,12 @@ module Tuile
|
|
|
682
980
|
|
|
683
981
|
def self.close: () -> void
|
|
684
982
|
|
|
685
|
-
#
|
|
686
|
-
#
|
|
687
|
-
#
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
691
|
-
#
|
|
692
|
-
# Outside repaint, writes go straight to stdout. We deliberately
|
|
693
|
-
# don't raise on a "print outside repaint" — that would be a useful
|
|
694
|
-
# guardrail against components painting outside the repaint loop,
|
|
695
|
-
# but it'd force terminal-housekeeping writes (`Screen#clear`,
|
|
696
|
-
# mouse-tracking start/stop, cursor-show on teardown) to bypass
|
|
697
|
-
# this method entirely and write directly to `$stdout`. {FakeScreen}
|
|
698
|
-
# overrides `print` to capture every byte into its `@prints` array,
|
|
699
|
-
# and tests that exercise `run_event_loop` against a real {Screen}
|
|
700
|
-
# would otherwise leak escape sequences to the test runner's stdout.
|
|
701
|
-
# Keeping `print` as the single sink preserves that override seam.
|
|
983
|
+
# Writes terminal-housekeeping escapes straight to stdout: {#clear},
|
|
984
|
+
# mouse-tracking start/stop, the color-scheme notify toggles, cursor-show
|
|
985
|
+
# on teardown. Component painting does *not* go through here anymore — it
|
|
986
|
+
# writes into {#buffer}, which {#repaint} diffs and {#emit}s. {FakeScreen}
|
|
987
|
+
# overrides this (and {#emit}) to capture into `@prints` instead of the
|
|
988
|
+
# test runner's stdout.
|
|
702
989
|
#
|
|
703
990
|
# _@param_ `args` — stuff to print.
|
|
704
991
|
def print: (*String args) -> void
|
|
@@ -741,24 +1028,22 @@ module Tuile
|
|
|
741
1028
|
# _@return_ — true if focus moved.
|
|
742
1029
|
def cycle_focus: (forward: bool) -> bool
|
|
743
1030
|
|
|
744
|
-
#
|
|
745
|
-
#
|
|
746
|
-
#
|
|
747
|
-
|
|
748
|
-
def collect_subtree: (Component component) -> ::Array[Component]
|
|
1031
|
+
# The escape sequence positioning the hardware cursor for the current focus
|
|
1032
|
+
# state: hidden when nothing owns it, else moved to the focused component's
|
|
1033
|
+
# {Component#cursor_position} and shown. Appended to each frame's flush.
|
|
1034
|
+
def cursor_sequence: () -> String
|
|
749
1035
|
|
|
750
|
-
#
|
|
751
|
-
|
|
1036
|
+
# Writes an assembled frame (escape string) to the terminal. The single
|
|
1037
|
+
# sink for repaint output; {FakeScreen} overrides it to capture instead.
|
|
1038
|
+
#
|
|
1039
|
+
# _@param_ `str`
|
|
1040
|
+
def emit: (String str) -> void
|
|
752
1041
|
|
|
753
1042
|
# Recalculates positions of all windows, and repaints the scene.
|
|
754
1043
|
# Automatically called whenever terminal size changes. Call when the app
|
|
755
1044
|
# starts. {#size} provides correct size of the terminal.
|
|
756
1045
|
def layout: () -> void
|
|
757
1046
|
|
|
758
|
-
# Called after a popup is closed. Since a popup can cover any window,
|
|
759
|
-
# top-level component or other popups, we need to redraw everything.
|
|
760
|
-
def needs_full_repaint: () -> void
|
|
761
|
-
|
|
762
1047
|
# A key has been pressed on the keyboard. Handle it, or forward to active
|
|
763
1048
|
# window.
|
|
764
1049
|
#
|
|
@@ -769,10 +1054,12 @@ module Tuile
|
|
|
769
1054
|
# doesn't trap them.
|
|
770
1055
|
# 2. App-level shortcuts from {#register_global_shortcut}. An entry
|
|
771
1056
|
# registered with `over_popups: true` always fires; one with the
|
|
772
|
-
# default `over_popups: false` fires only when no popup is open
|
|
773
|
-
# (otherwise the popup receives the key normally).
|
|
774
|
-
#
|
|
775
|
-
#
|
|
1057
|
+
# default `over_popups: false` fires only when no modal popup is open
|
|
1058
|
+
# (otherwise the modal popup receives the key normally). A non-modal
|
|
1059
|
+
# overlay doesn't suppress global shortcuts.
|
|
1060
|
+
# 3. {ScreenPane#handle_key}, which captures a matching {#key_shortcut}
|
|
1061
|
+
# in the active scope, then delivers the key to {#focused} and bubbles
|
|
1062
|
+
# it up the focus chain.
|
|
776
1063
|
#
|
|
777
1064
|
# _@param_ `key`
|
|
778
1065
|
#
|
|
@@ -789,6 +1076,10 @@ module Tuile
|
|
|
789
1076
|
# _@return_ — the structural root of the component tree.
|
|
790
1077
|
attr_reader pane: ScreenPane
|
|
791
1078
|
|
|
1079
|
+
# _@return_ — the back buffer components paint into
|
|
1080
|
+
# ({Buffer#set_line} / {Buffer#fill} / {Buffer#set_char}).
|
|
1081
|
+
attr_reader buffer: Buffer
|
|
1082
|
+
|
|
792
1083
|
# Handler invoked when a {StandardError} escapes an event handler inside
|
|
793
1084
|
# the event loop (e.g. a {Component::TextField}'s `on_change` raises).
|
|
794
1085
|
#
|
|
@@ -938,20 +1229,23 @@ module Tuile
|
|
|
938
1229
|
# Only called when the component is attached.
|
|
939
1230
|
def repaint: () -> void
|
|
940
1231
|
|
|
941
|
-
# Called when a character is pressed on the keyboard.
|
|
942
|
-
#
|
|
943
|
-
#
|
|
944
|
-
#
|
|
1232
|
+
# Called when a character is pressed on the keyboard. The default does
|
|
1233
|
+
# nothing and reports the key as unhandled; input components
|
|
1234
|
+
# ({Component::TextField}, {Component::List}, {Component::Button}, …)
|
|
1235
|
+
# override it to act on keys they care about.
|
|
945
1236
|
#
|
|
946
|
-
#
|
|
947
|
-
#
|
|
948
|
-
#
|
|
949
|
-
#
|
|
1237
|
+
# Dispatch is owned by {ScreenPane#handle_key}: a {#key_shortcut} match
|
|
1238
|
+
# anywhere in the active scope is captured first (suppressed while a
|
|
1239
|
+
# cursor-owner is mid-edit), then the key is delivered to {Screen#focused}
|
|
1240
|
+
# and bubbles up its ancestor chain until some component handles it. A
|
|
1241
|
+
# component therefore only ever receives keys when it is on the focus chain
|
|
1242
|
+
# — or when app code hands it a key directly — so it acts on the key alone
|
|
1243
|
+
# and must never gate on its own {#active?} state.
|
|
950
1244
|
#
|
|
951
|
-
# _@param_ `
|
|
1245
|
+
# _@param_ `_key` — a key.
|
|
952
1246
|
#
|
|
953
1247
|
# _@return_ — true if the key was handled, false if not.
|
|
954
|
-
def handle_key: (String
|
|
1248
|
+
def handle_key: (String _key) -> bool
|
|
955
1249
|
|
|
956
1250
|
# _@param_ `key` — keyboard key to look up.
|
|
957
1251
|
#
|
|
@@ -1057,6 +1351,18 @@ module Tuile
|
|
|
1057
1351
|
# topmost popup. Empty by default; override to advertise shortcuts.
|
|
1058
1352
|
def keyboard_hint: () -> String
|
|
1059
1353
|
|
|
1354
|
+
# Advice to a wrapping {Component::Popup} on the minimum height this
|
|
1355
|
+
# component prefers to occupy when shown in a popup. `nil` (the default)
|
|
1356
|
+
# means no preference — the popup uses its own {Component::Popup#min_height}.
|
|
1357
|
+
# Override in a content component that should not collapse to a couple of
|
|
1358
|
+
# rows when sparse (e.g. {Component::LogWindow}).
|
|
1359
|
+
def popup_min_height: () -> Integer?
|
|
1360
|
+
|
|
1361
|
+
# Advice to a wrapping {Component::Popup} on the maximum height this
|
|
1362
|
+
# component may grow to when shown in a popup. `nil` (the default) means
|
|
1363
|
+
# no preference — the popup uses its own {Component::Popup#max_height}.
|
|
1364
|
+
def popup_max_height: () -> Integer?
|
|
1365
|
+
|
|
1060
1366
|
# Called whenever the component width changes. Does nothing by default.
|
|
1061
1367
|
def on_width_changed: () -> void
|
|
1062
1368
|
|
|
@@ -1141,6 +1447,12 @@ module Tuile
|
|
|
1141
1447
|
class List < Component
|
|
1142
1448
|
def initialize: () -> void
|
|
1143
1449
|
|
|
1450
|
+
# _@return_ — whether {#auto_scroll} is currently tailing. True
|
|
1451
|
+
# while the viewport sits at the last line; flips to false the moment
|
|
1452
|
+
# the user scrolls up, and back to true once they scroll to the bottom
|
|
1453
|
+
# again. Only consulted when {#auto_scroll} is enabled.
|
|
1454
|
+
def following?: () -> bool
|
|
1455
|
+
|
|
1144
1456
|
# Sets new lines. Each entry is coerced into a {StyledString} (a
|
|
1145
1457
|
# `String` is parsed via {StyledString.parse}, so embedded ANSI is
|
|
1146
1458
|
# honored; a {StyledString} is used as-is; anything else is stringified
|
|
@@ -1309,6 +1621,10 @@ module Tuile
|
|
|
1309
1621
|
# _@return_ — the max value of {#top_line}.
|
|
1310
1622
|
def top_line_max: () -> Integer
|
|
1311
1623
|
|
|
1624
|
+
# _@return_ — whether the viewport is pinned to the last line.
|
|
1625
|
+
# Drives {#following?}: re-evaluated on every {#top_line=}.
|
|
1626
|
+
def at_bottom?: () -> bool
|
|
1627
|
+
|
|
1312
1628
|
# _@return_ — the number of visible lines.
|
|
1313
1629
|
def viewport_lines: () -> Integer
|
|
1314
1630
|
|
|
@@ -1325,6 +1641,11 @@ module Tuile
|
|
|
1325
1641
|
# which would leave `top_line` past the last item once a real rect
|
|
1326
1642
|
# arrives. {#on_width_changed} re-runs this hook when the rect grows so
|
|
1327
1643
|
# the snap-to-bottom intent is preserved.
|
|
1644
|
+
#
|
|
1645
|
+
# Gated on {#following?}: once the user scrolls up off the bottom the
|
|
1646
|
+
# cursor snap and viewport pin are both skipped, so reading older
|
|
1647
|
+
# content is not interrupted by incoming lines. {#top_line=} re-arms
|
|
1648
|
+
# `@follow` when the viewport returns to the bottom.
|
|
1328
1649
|
def update_top_line_if_auto_scroll: () -> void
|
|
1329
1650
|
|
|
1330
1651
|
# _@return_ — whether the scrollbar should be drawn right now.
|
|
@@ -1358,9 +1679,9 @@ module Tuile
|
|
|
1358
1679
|
#
|
|
1359
1680
|
# _@param_ `scrollbar` — scrollbar instance, or nil if not shown.
|
|
1360
1681
|
#
|
|
1361
|
-
# _@return_ — paintable
|
|
1362
|
-
#
|
|
1363
|
-
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) ->
|
|
1682
|
+
# _@return_ — paintable line exactly `rect.width` columns wide;
|
|
1683
|
+
# highlighted if cursor is here.
|
|
1684
|
+
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> StyledString
|
|
1364
1685
|
|
|
1365
1686
|
# _@return_ — callback fired when an item is chosen — by pressing
|
|
1366
1687
|
# Enter on the cursor's item, or by left-clicking an item. Called as
|
|
@@ -1380,7 +1701,10 @@ module Tuile
|
|
|
1380
1701
|
attr_accessor on_cursor_changed: Proc?
|
|
1381
1702
|
|
|
1382
1703
|
# _@return_ — if true and a line is added or new content is set,
|
|
1383
|
-
# auto-scrolls to the bottom
|
|
1704
|
+
# auto-scrolls to the bottom — but only while the viewport is already
|
|
1705
|
+
# pinned to the last line (see {#following?}). Scroll up to read older
|
|
1706
|
+
# content and appends stop yanking you back down; scroll back to the
|
|
1707
|
+
# bottom and tailing resumes.
|
|
1384
1708
|
attr_accessor auto_scroll: bool
|
|
1385
1709
|
|
|
1386
1710
|
# _@return_ — top line of the viewport. 0 or positive.
|
|
@@ -1544,11 +1868,11 @@ module Tuile
|
|
|
1544
1868
|
def compute_content_size: () -> Size
|
|
1545
1869
|
|
|
1546
1870
|
# Recomputes {@clipped_lines} for the current text and rect width.
|
|
1547
|
-
# Each line is ellipsized to fit
|
|
1548
|
-
# the full width,
|
|
1549
|
-
#
|
|
1550
|
-
#
|
|
1551
|
-
#
|
|
1871
|
+
# Each line is ellipsized to fit and padded with trailing spaces out to
|
|
1872
|
+
# the full width, so {#repaint} is just a lookup + {Buffer#set_line} per
|
|
1873
|
+
# row. {@blank_line} covers rows past the last text line. When {#bg} is
|
|
1874
|
+
# set, every produced line (and the blank row) has the bg applied
|
|
1875
|
+
# uniformly.
|
|
1552
1876
|
def update_clipped_lines: () -> void
|
|
1553
1877
|
|
|
1554
1878
|
# _@param_ `line`
|
|
@@ -1569,10 +1893,20 @@ module Tuile
|
|
|
1569
1893
|
attr_accessor bg: (Color | Symbol | Integer | ::Array[Integer])?
|
|
1570
1894
|
end
|
|
1571
1895
|
|
|
1572
|
-
#
|
|
1573
|
-
# paints nothing — it's a transparent host that handles
|
|
1574
|
-
# ({#open} / {#close} / {#open?}, ESC/q to close)
|
|
1575
|
-
#
|
|
1896
|
+
# An overlay that wraps any {Component} as its content. Popup itself
|
|
1897
|
+
# paints nothing — it's a transparent host that handles its lifecycle
|
|
1898
|
+
# ({#open} / {#close} / {#open?}, ESC/q to close) and auto-sizes to the
|
|
1899
|
+
# wrapped content.
|
|
1900
|
+
#
|
|
1901
|
+
# Modal by default: it centers on the screen, grabs focus, eats keys, and
|
|
1902
|
+
# blocks clicks beneath it. Pass `modal: false` for a non-modal overlay
|
|
1903
|
+
# that floats above the content (still painted on top, still auto-sized)
|
|
1904
|
+
# without taking focus or capturing input — the caller positions it (via
|
|
1905
|
+
# {#rect=}) and drives it from app code. That is the building block for an
|
|
1906
|
+
# autocomplete/slash-command list anchored to a {Component::TextField} or
|
|
1907
|
+
# {Component::TextArea} caret: typing keeps focus (and the cursor) in the
|
|
1908
|
+
# input, an {Component::TextInput#on_change} listener refills the list, and
|
|
1909
|
+
# an {Component::TextInput#on_key} interceptor forwards Up/Down/Enter to it.
|
|
1576
1910
|
#
|
|
1577
1911
|
# The wrapped content fills the popup's full {#rect}; if you want a frame
|
|
1578
1912
|
# and caption, wrap a {Component::Window} (or any subclass — including
|
|
@@ -1593,10 +1927,26 @@ module Tuile
|
|
|
1593
1927
|
include Tuile::Component::HasContent
|
|
1594
1928
|
|
|
1595
1929
|
# _@param_ `content` — initial content; can be set later via {#content=}. When provided here, the popup auto-sizes to fit.
|
|
1596
|
-
|
|
1930
|
+
#
|
|
1931
|
+
# _@param_ `modal` — true (default) for a centered, focus-grabbing, input-capturing modal; false for a non-modal overlay the caller positions and drives (see the class docs).
|
|
1932
|
+
def initialize: (?content: Component?, ?modal: bool) -> void
|
|
1933
|
+
|
|
1934
|
+
# _@return_ — whether this popup is modal. See {#initialize}.
|
|
1935
|
+
def modal?: () -> bool
|
|
1597
1936
|
|
|
1598
1937
|
def focusable?: () -> bool
|
|
1599
1938
|
|
|
1939
|
+
# Reassigns the popup's rect, escalating to a full scene repaint when an
|
|
1940
|
+
# open popup shrinks or moves so its new rect no longer covers the cells
|
|
1941
|
+
# it previously painted. A popup overdraws the scene without clipping and
|
|
1942
|
+
# nothing clears underneath it, so {Screen#repaint}'s popup-only fast path
|
|
1943
|
+
# would repaint into the new rect and leave the vacated cells showing
|
|
1944
|
+
# stale content. When the new rect fully covers the old one (the popup
|
|
1945
|
+
# only grew), the fast path is correct and the full repaint is skipped.
|
|
1946
|
+
#
|
|
1947
|
+
# _@param_ `new_rect`
|
|
1948
|
+
def rect=: (Rect new_rect) -> void
|
|
1949
|
+
|
|
1600
1950
|
# Mounts this popup on the {Screen}. Recomputes the popup's size from
|
|
1601
1951
|
# the current content first, so reopening a popup whose content has
|
|
1602
1952
|
# grown or shrunk while closed picks up the new size.
|
|
@@ -1620,10 +1970,21 @@ module Tuile
|
|
|
1620
1970
|
# when the popup is open.
|
|
1621
1971
|
def center: () -> void
|
|
1622
1972
|
|
|
1623
|
-
# _@return_ — max height the popup will grow to fit its content
|
|
1624
|
-
#
|
|
1973
|
+
# _@return_ — max height the popup will grow to fit its content.
|
|
1974
|
+
# Defers to the content's {Component#popup_max_height} advice when it
|
|
1975
|
+
# gives one, else defaults to 12. Override in a subclass to allow
|
|
1976
|
+
# taller popups regardless of content.
|
|
1625
1977
|
def max_height: () -> Integer
|
|
1626
1978
|
|
|
1979
|
+
# _@return_ — min height the popup occupies even when its content
|
|
1980
|
+
# is shorter. Defers to the content's {Component#popup_min_height}
|
|
1981
|
+
# advice when it gives one, else defaults to 0 (size purely to
|
|
1982
|
+
# content) — so a {Component::LogWindow} stays readable while only a
|
|
1983
|
+
# few lines are in without callers wiring up a subclass. Override in a
|
|
1984
|
+
# subclass to keep any popup from collapsing to a couple of rows.
|
|
1985
|
+
# Capped at the same 4/5-of-screen ceiling {#update_rect} applies.
|
|
1986
|
+
def min_height: () -> Integer
|
|
1987
|
+
|
|
1627
1988
|
# Sets the popup's content and auto-sizes the popup to fit.
|
|
1628
1989
|
#
|
|
1629
1990
|
# _@param_ `new_content`
|
|
@@ -1640,6 +2001,10 @@ module Tuile
|
|
|
1640
2001
|
# Hint for the status bar: own "q Close" plus the wrapped content's hint.
|
|
1641
2002
|
def keyboard_hint: () -> String
|
|
1642
2003
|
|
|
2004
|
+
# `q` and ESC close the popup. The popup sits on the focus chain of
|
|
2005
|
+
# whatever it wraps, so the key reaches here by bubbling up from the
|
|
2006
|
+
# focused content after that content declined to handle it.
|
|
2007
|
+
#
|
|
1643
2008
|
# _@param_ `key`
|
|
1644
2009
|
#
|
|
1645
2010
|
# _@return_ — true if the key was handled.
|
|
@@ -1652,6 +2017,11 @@ module Tuile
|
|
|
1652
2017
|
|
|
1653
2018
|
# Recompute width/height from {#content}'s natural size and recenter
|
|
1654
2019
|
# if currently open. Called whenever content is (re)assigned.
|
|
2020
|
+
#
|
|
2021
|
+
# Computes the final (centered) rect and assigns it in one step rather
|
|
2022
|
+
# than positioning at the origin and then centering: the intermediate
|
|
2023
|
+
# origin rect rarely covers the previous one, which would make
|
|
2024
|
+
# {#rect=}'s shrink/move detection fire a full repaint on every resize.
|
|
1655
2025
|
def update_rect: () -> void
|
|
1656
2026
|
|
|
1657
2027
|
# _@param_ `event`
|
|
@@ -1659,9 +2029,6 @@ module Tuile
|
|
|
1659
2029
|
|
|
1660
2030
|
def children: () -> ::Array[Component]
|
|
1661
2031
|
|
|
1662
|
-
# _@param_ `rect`
|
|
1663
|
-
def rect=: (Rect rect) -> void
|
|
1664
|
-
|
|
1665
2032
|
def on_focus: () -> void
|
|
1666
2033
|
end
|
|
1667
2034
|
|
|
@@ -1743,13 +2110,6 @@ module Tuile
|
|
|
1743
2110
|
# _@param_ `event`
|
|
1744
2111
|
def handle_mouse: (MouseEvent event) -> void
|
|
1745
2112
|
|
|
1746
|
-
# Called when a character is pressed on the keyboard.
|
|
1747
|
-
#
|
|
1748
|
-
# _@param_ `key` — a key.
|
|
1749
|
-
#
|
|
1750
|
-
# _@return_ — true if the key was handled, false if not.
|
|
1751
|
-
def handle_key: (String key) -> bool
|
|
1752
|
-
|
|
1753
2113
|
def on_focus: () -> void
|
|
1754
2114
|
|
|
1755
2115
|
# Absolute layout. Extend this class, register any children, and
|
|
@@ -1777,9 +2137,6 @@ module Tuile
|
|
|
1777
2137
|
|
|
1778
2138
|
def children: () -> ::Array[Component]
|
|
1779
2139
|
|
|
1780
|
-
# _@param_ `key`
|
|
1781
|
-
def handle_key: (String key) -> bool
|
|
1782
|
-
|
|
1783
2140
|
# _@param_ `event`
|
|
1784
2141
|
def handle_mouse: (MouseEvent event) -> void
|
|
1785
2142
|
|
|
@@ -1823,20 +2180,15 @@ module Tuile
|
|
|
1823
2180
|
# _@param_ `content`
|
|
1824
2181
|
def layout: (Component content) -> void
|
|
1825
2182
|
|
|
1826
|
-
# Paints the window border.
|
|
2183
|
+
# Paints the window border into the {Screen#buffer}. Title is clipped to
|
|
2184
|
+
# the inner width so the box never overflows {#rect}; when the window is
|
|
2185
|
+
# active the whole border is drawn in {Theme#active_border_color}.
|
|
1827
2186
|
def repaint_border: () -> void
|
|
1828
2187
|
|
|
1829
2188
|
# The caption text as it appears in the rendered border, including the
|
|
1830
2189
|
# shortcut prefix when {#key_shortcut} is set.
|
|
1831
2190
|
def frame_caption: () -> String
|
|
1832
2191
|
|
|
1833
|
-
# Builds the border as a single string with embedded cursor-positioning
|
|
1834
|
-
# escapes, mirroring the layout {TTY::Box.frame} used to produce. Title
|
|
1835
|
-
# is clipped to fit the inner width so the box never overflows {#rect}.
|
|
1836
|
-
#
|
|
1837
|
-
# _@param_ `caption`
|
|
1838
|
-
def build_frame: (String caption) -> String
|
|
1839
|
-
|
|
1840
2192
|
# Recomputes the window's natural size: content's natural size (or the
|
|
1841
2193
|
# caption, whichever is wider) plus the 2-character border. The footer
|
|
1842
2194
|
# is deliberately excluded — see {#on_child_content_size_changed}. A
|
|
@@ -1985,6 +2337,12 @@ module Tuile
|
|
|
1985
2337
|
# O(total spans).
|
|
1986
2338
|
def text: () -> StyledString
|
|
1987
2339
|
|
|
2340
|
+
# _@return_ — whether {#auto_scroll} is currently tailing. True
|
|
2341
|
+
# while the viewport sits at the last line; flips to false the moment
|
|
2342
|
+
# the user scrolls up, and back to true once they scroll to the bottom
|
|
2343
|
+
# again. Only consulted when {#auto_scroll} is enabled.
|
|
2344
|
+
def following?: () -> bool
|
|
2345
|
+
|
|
1988
2346
|
# Replaces the text. Embedded `\n` characters become hard line breaks.
|
|
1989
2347
|
# A `String` is parsed via {StyledString.parse} (so embedded ANSI is
|
|
1990
2348
|
# honored); a `StyledString` is used as-is; `nil` is coerced to an
|
|
@@ -2316,8 +2674,16 @@ module Tuile
|
|
|
2316
2674
|
# _@param_ `target` — desired top line; clamped to `[0, top_line_max]`.
|
|
2317
2675
|
def move_top_line_to: (Integer target) -> void
|
|
2318
2676
|
|
|
2677
|
+
# Gated on {#following?}: once the user scrolls up off the bottom the
|
|
2678
|
+
# viewport pin is skipped, so reading older content is not interrupted
|
|
2679
|
+
# by incoming lines. {#top_line=} re-arms `@follow` when the viewport
|
|
2680
|
+
# returns to the bottom.
|
|
2319
2681
|
def update_top_line_if_auto_scroll: () -> void
|
|
2320
2682
|
|
|
2683
|
+
# _@return_ — whether the viewport is pinned to the last line.
|
|
2684
|
+
# Drives {#following?}: re-evaluated on every {#top_line=}.
|
|
2685
|
+
def at_bottom?: () -> bool
|
|
2686
|
+
|
|
2321
2687
|
def scrollbar_visible?: () -> bool
|
|
2322
2688
|
|
|
2323
2689
|
# Pads `line` with trailing default-styled spaces out to `width` display
|
|
@@ -2337,11 +2703,10 @@ module Tuile
|
|
|
2337
2703
|
#
|
|
2338
2704
|
# _@param_ `scrollbar`
|
|
2339
2705
|
#
|
|
2340
|
-
# _@return_ — paintable
|
|
2341
|
-
#
|
|
2342
|
-
#
|
|
2343
|
-
|
|
2344
|
-
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> String
|
|
2706
|
+
# _@return_ — paintable line exactly `rect.width` columns wide.
|
|
2707
|
+
# Body lines come pre-padded from {#rewrap}, so this reduces to a lookup
|
|
2708
|
+
# plus a concat of the scrollbar glyph when one is present.
|
|
2709
|
+
def paintable_line: (Integer index, Integer row_in_viewport, VerticalScrollBar? scrollbar) -> StyledString
|
|
2345
2710
|
|
|
2346
2711
|
# _@return_ — index of the first visible physical line.
|
|
2347
2712
|
attr_accessor top_line: Integer
|
|
@@ -2350,7 +2715,10 @@ module Tuile
|
|
|
2350
2715
|
attr_accessor scrollbar_visibility: Symbol
|
|
2351
2716
|
|
|
2352
2717
|
# _@return_ — if true, mutating the text scrolls the viewport so
|
|
2353
|
-
# the last line stays in view
|
|
2718
|
+
# the last line stays in view — but only while the viewport is already
|
|
2719
|
+
# pinned to the last line (see {#following?}). Scroll up to read older
|
|
2720
|
+
# content and appends stop yanking you back down; scroll back to the
|
|
2721
|
+
# bottom and tailing resumes. Default `false`.
|
|
2354
2722
|
attr_accessor auto_scroll: bool
|
|
2355
2723
|
|
|
2356
2724
|
# _@return_ — longest hard-line's display width × number of hard
|
|
@@ -2504,6 +2872,18 @@ module Tuile
|
|
|
2504
2872
|
# _@param_ `caption`
|
|
2505
2873
|
def initialize: (?String caption) -> void
|
|
2506
2874
|
|
|
2875
|
+
# Keep the log pane at least half the screen tall even when only a few
|
|
2876
|
+
# lines have been logged: a {Component::Popup} sizes to its content, which
|
|
2877
|
+
# would collapse a near-empty log to two or three rows. Advice consulted
|
|
2878
|
+
# by {Component::Popup#min_height} when this window is a popup's content.
|
|
2879
|
+
def popup_min_height: () -> Integer
|
|
2880
|
+
|
|
2881
|
+
# Let a busy log grow past the popup's base 12-row cap (up to the
|
|
2882
|
+
# 4/5-of-screen ceiling {Component::Popup#update_rect} applies) so the
|
|
2883
|
+
# diagnostic stream stays scrollable in a tall window. Advice consulted
|
|
2884
|
+
# by {Component::Popup#max_height} when this window is a popup's content.
|
|
2885
|
+
def popup_max_height: () -> Integer
|
|
2886
|
+
|
|
2507
2887
|
# Appends given line to the log. Can be called from any thread. Does nothing if nil is passed in.
|
|
2508
2888
|
#
|
|
2509
2889
|
# _@param_ `string` — the line (or multiple lines) to log.
|
|
@@ -2622,22 +3002,24 @@ module Tuile
|
|
|
2622
3002
|
|
|
2623
3003
|
def tab_stop?: () -> bool
|
|
2624
3004
|
|
|
2625
|
-
# Handles a key.
|
|
2626
|
-
#
|
|
2627
|
-
#
|
|
3005
|
+
# Handles a key. An {#on_key} interceptor (if set) gets first refusal —
|
|
3006
|
+
# a truthy return consumes the key — otherwise it delegates to
|
|
3007
|
+
# {#handle_text_input_key}. Dispatch ({ScreenPane#handle_key}) only routes
|
|
3008
|
+
# keys here when this input is on the focus chain, so there is no
|
|
3009
|
+
# {#active?} gate.
|
|
2628
3010
|
#
|
|
2629
3011
|
# _@param_ `key`
|
|
2630
3012
|
def handle_key: (String key) -> bool
|
|
2631
3013
|
|
|
2632
3014
|
# Renders `text` on the field's background well, looked up from the
|
|
2633
|
-
# current {Screen#theme} at paint time: {Theme#
|
|
2634
|
-
# input is on the active (focus) chain, {Theme#
|
|
3015
|
+
# current {Screen#theme} at paint time: {Theme#active_bg_color} when this
|
|
3016
|
+
# input is on the active (focus) chain, {Theme#input_bg_color} otherwise —
|
|
2635
3017
|
# visibly a field either way, distinctly highlighted when active.
|
|
2636
3018
|
#
|
|
2637
3019
|
# _@param_ `text`
|
|
2638
3020
|
#
|
|
2639
|
-
# _@return_ —
|
|
2640
|
-
def background: (String text) ->
|
|
3021
|
+
# _@return_ — text on the field's background well.
|
|
3022
|
+
def background: (String text) -> StyledString
|
|
2641
3023
|
|
|
2642
3024
|
# Input filter for {#text=}. Subclasses override to truncate or reject
|
|
2643
3025
|
# invalid input. Default coerces to String.
|
|
@@ -2700,6 +3082,21 @@ module Tuile
|
|
|
2700
3082
|
# _@return_ — one-arg callable, or nil.
|
|
2701
3083
|
attr_accessor on_change: (Proc | Method)?
|
|
2702
3084
|
|
|
3085
|
+
# Optional interceptor consulted before the input's own key handling.
|
|
3086
|
+
# Receives the pressed key; return a truthy value to consume it (the
|
|
3087
|
+
# input then ignores that key), falsy to let normal editing proceed.
|
|
3088
|
+
#
|
|
3089
|
+
# The keyboard analog of {#on_change}: it lets app code layer behavior
|
|
3090
|
+
# onto an input without subclassing. The motivating case is an
|
|
3091
|
+
# autocomplete / slash-command overlay (a non-modal {Component::Popup}):
|
|
3092
|
+
# while it is open the interceptor claims Up/Down/Enter/ESC and forwards
|
|
3093
|
+
# them to the overlay's list, but lets ordinary characters fall through
|
|
3094
|
+
# so typing keeps editing the field (and {#on_change} keeps refilling the
|
|
3095
|
+
# list).
|
|
3096
|
+
#
|
|
3097
|
+
# _@return_ — one-arg callable, or nil.
|
|
3098
|
+
attr_accessor on_key: (Proc | Method)?
|
|
3099
|
+
|
|
2703
3100
|
# Callback fired when ESC is pressed. Defaults to a closure that clears
|
|
2704
3101
|
# focus (`screen.focused = nil`) so ESC visibly cancels text entry instead
|
|
2705
3102
|
# of bubbling to the parent — and, in particular, instead of reaching the
|
|
@@ -2714,11 +3111,6 @@ module Tuile
|
|
|
2714
3111
|
# provide a protected `layout(content)` method which repositions the
|
|
2715
3112
|
# content component; the mixin manages `@content` itself.
|
|
2716
3113
|
module HasContent
|
|
2717
|
-
# _@param_ `key` — a key.
|
|
2718
|
-
#
|
|
2719
|
-
# _@return_ — true if the key was handled, false if not.
|
|
2720
|
-
def handle_key: (String key) -> bool
|
|
2721
|
-
|
|
2722
3114
|
# _@param_ `event`
|
|
2723
3115
|
def handle_mouse: (MouseEvent event) -> void
|
|
2724
3116
|
|
|
@@ -2768,6 +3160,11 @@ module Tuile
|
|
|
2768
3160
|
# _@param_ `options` — pairs of keyboard key and option caption. No Rainbow formatting must be used.
|
|
2769
3161
|
def initialize: (String caption, ::Array[[String, String]] options) ?{ (String key) -> void } -> void
|
|
2770
3162
|
|
|
3163
|
+
# Handles an option-key press. Reached by bubbling: the inner {List}
|
|
3164
|
+
# (the focused component) sees the key first and handles cursor/Enter
|
|
3165
|
+
# picks; anything it declines bubbles up here, where a key matching an
|
|
3166
|
+
# option's `key` picks that option.
|
|
3167
|
+
#
|
|
2771
3168
|
# _@param_ `key`
|
|
2772
3169
|
def handle_key: (String key) -> bool
|
|
2773
3170
|
|
|
@@ -3088,6 +3485,13 @@ module Tuile
|
|
|
3088
3485
|
# _@param_ `args`
|
|
3089
3486
|
def print: (*String args) -> void
|
|
3090
3487
|
|
|
3488
|
+
# Captures the assembled repaint frame instead of writing to the test
|
|
3489
|
+
# runner's TTY. Lands in {#prints} so cursor/sync escapes can be asserted;
|
|
3490
|
+
# painted content is read from {#buffer}.
|
|
3491
|
+
#
|
|
3492
|
+
# _@param_ `str`
|
|
3493
|
+
def emit: (String str) -> void
|
|
3494
|
+
|
|
3091
3495
|
# _@param_ `component` — the component to check.
|
|
3092
3496
|
def invalidated?: (Component component) -> bool
|
|
3093
3497
|
|
|
@@ -3098,7 +3502,10 @@ module Tuile
|
|
|
3098
3502
|
# steal its input) and pin the deterministic default.
|
|
3099
3503
|
def detect_scheme: () -> Symbol
|
|
3100
3504
|
|
|
3101
|
-
# _@return_ — whatever {#print}
|
|
3505
|
+
# _@return_ — whatever {#print} / {#emit} produced so far.
|
|
3506
|
+
# Component painting lands in {#buffer}, not here — assert on
|
|
3507
|
+
# {Buffer#row_text} / {Buffer#row_ansi} / {Buffer#cell} for content, and
|
|
3508
|
+
# on `prints` for cursor and housekeeping escapes.
|
|
3102
3509
|
attr_reader prints: ::Array[String]
|
|
3103
3510
|
end
|
|
3104
3511
|
|
|
@@ -3176,7 +3583,11 @@ module Tuile
|
|
|
3176
3583
|
# status bar last.
|
|
3177
3584
|
def children: () -> ::Array[Component]
|
|
3178
3585
|
|
|
3179
|
-
# Adds a popup
|
|
3586
|
+
# Adds a popup and invalidates it for repaint. A modal popup is centered
|
|
3587
|
+
# and grabs focus; a non-modal overlay ({Component::Popup#modal?} false) is
|
|
3588
|
+
# left wherever the caller positions it and does *not* take focus, so the
|
|
3589
|
+
# component that was focused keeps the cursor and keeps receiving keys —
|
|
3590
|
+
# the overlay floats above the content, driven from app code.
|
|
3180
3591
|
#
|
|
3181
3592
|
# _@param_ `window`
|
|
3182
3593
|
def add_popup: (Component::Popup window) -> void
|
|
@@ -3193,26 +3604,52 @@ module Tuile
|
|
|
3193
3604
|
# _@return_ — true if this pane currently hosts the popup.
|
|
3194
3605
|
def has_popup?: (Component window) -> bool
|
|
3195
3606
|
|
|
3607
|
+
# _@return_ — the topmost *modal* popup, or nil when
|
|
3608
|
+
# only non-modal overlays (or no popups) are open. This is the "modal
|
|
3609
|
+
# owner": the popup that scopes key dispatch, blocks mouse clicks, owns
|
|
3610
|
+
# the status bar, and confines Tab cycling. Non-modal overlays are
|
|
3611
|
+
# excluded — they float above the content without capturing input.
|
|
3612
|
+
def modal_popup: () -> Component::Popup?
|
|
3613
|
+
|
|
3196
3614
|
# Re-lays out children whenever the pane's own rect changes.
|
|
3197
3615
|
#
|
|
3198
3616
|
# _@param_ `new_rect`
|
|
3199
3617
|
def rect=: (Rect new_rect) -> void
|
|
3200
3618
|
|
|
3201
3619
|
# Lays out content (full pane minus the bottom row) and the status bar
|
|
3202
|
-
# (bottom row).
|
|
3620
|
+
# (bottom row). Modal popups self-recenter via {Component::Popup#center};
|
|
3621
|
+
# non-modal overlays keep the position their owner assigned.
|
|
3203
3622
|
def layout: () -> void
|
|
3204
3623
|
|
|
3205
3624
|
# Pane paints nothing itself; its children paint over the entire rect.
|
|
3206
3625
|
def repaint: () -> void
|
|
3207
3626
|
|
|
3208
|
-
#
|
|
3209
|
-
# when
|
|
3627
|
+
# Dispatches a key in two phases, both scoped to the topmost *modal* popup
|
|
3628
|
+
# (when one is open) or else the tiled {#content}. Non-modal overlays are
|
|
3629
|
+
# never the scope: focus stays in the content beneath them, and the overlay
|
|
3630
|
+
# is driven by app code (which forwards keys to it explicitly), so it
|
|
3631
|
+
# doesn't appear in this path at all.
|
|
3632
|
+
#
|
|
3633
|
+
# 1. *Capture* — a {Component#key_shortcut} match anywhere in the scope
|
|
3634
|
+
# focuses that component and consumes the key. Suppressed while a
|
|
3635
|
+
# cursor-owner ({Screen#cursor_position}) is mid-edit, so typing into a
|
|
3636
|
+
# {Component::TextField} isn't hijacked by a sibling's shortcut.
|
|
3637
|
+
# 2. *Delivery* — the key is handed to {Screen#focused} and bubbles up its
|
|
3638
|
+
# ancestor chain to the scope root; the first component to return true
|
|
3639
|
+
# wins. Focus that is nil or sits outside the scope receives nothing,
|
|
3640
|
+
# which is what keeps an open modal popup modal.
|
|
3641
|
+
#
|
|
3642
|
+
# _@param_ `key`
|
|
3643
|
+
#
|
|
3644
|
+
# _@return_ — true if the key was handled.
|
|
3210
3645
|
def handle_key: (String key) -> bool
|
|
3211
3646
|
|
|
3212
3647
|
# Mouse events check popups in reverse stacking order (topmost first), and
|
|
3213
|
-
# fall through to content only when no popup is hit *and*
|
|
3214
|
-
#
|
|
3215
|
-
#
|
|
3648
|
+
# fall through to content only when no popup is hit *and* no modal popup is
|
|
3649
|
+
# open. This preserves modal click-blocking — an open modal eats clicks
|
|
3650
|
+
# even outside its rect — while a non-modal overlay blocks nothing: clicks
|
|
3651
|
+
# inside it route to it (e.g. click-to-select), clicks elsewhere reach the
|
|
3652
|
+
# content beneath.
|
|
3216
3653
|
#
|
|
3217
3654
|
# _@param_ `event`
|
|
3218
3655
|
def handle_mouse: (MouseEvent event) -> void
|
|
@@ -3220,7 +3657,7 @@ module Tuile
|
|
|
3220
3657
|
# Focus repair when a child detaches. Default {Component#on_child_removed}
|
|
3221
3658
|
# would refocus to `self` (the pane), which isn't a useful focus target.
|
|
3222
3659
|
# Instead, route focus to the first interactable widget in the now-topmost
|
|
3223
|
-
# popup; falling back to the focus snapshotted when this popup was opened
|
|
3660
|
+
# modal popup; falling back to the focus snapshotted when this popup was opened
|
|
3224
3661
|
# (if still attached and still focusable); then to the first interactable
|
|
3225
3662
|
# widget in {#content}; then to {#content} itself; then nil.
|
|
3226
3663
|
#
|
|
@@ -3232,6 +3669,19 @@ module Tuile
|
|
|
3232
3669
|
# _@param_ `child`
|
|
3233
3670
|
def on_child_removed: (Component child) -> void
|
|
3234
3671
|
|
|
3672
|
+
# Delivers `key` to {Screen#focused} and bubbles it up the ancestor chain,
|
|
3673
|
+
# stopping at (and including) `scope`. Delivers to no one — returning false
|
|
3674
|
+
# — when focus is nil or sits outside `scope`; the latter is what makes an
|
|
3675
|
+
# open popup modal, since focus is always inside it and content beneath
|
|
3676
|
+
# never receives keys.
|
|
3677
|
+
#
|
|
3678
|
+
# _@param_ `key`
|
|
3679
|
+
#
|
|
3680
|
+
# _@param_ `scope` — the modal scope root (topmost popup or content).
|
|
3681
|
+
#
|
|
3682
|
+
# _@return_ — true if some component on the chain handled the key.
|
|
3683
|
+
def bubble_key: (String key, Component scope) -> bool
|
|
3684
|
+
|
|
3235
3685
|
# First {Component#tab_stop?} in `root`'s subtree (pre-order), falling
|
|
3236
3686
|
# back to `root` itself when the subtree has no tab stops. Returns `nil`
|
|
3237
3687
|
# if `root` is `nil`.
|
|
@@ -3242,8 +3692,9 @@ module Tuile
|
|
|
3242
3692
|
# _@return_ — the tiled content component.
|
|
3243
3693
|
attr_accessor content: Component?
|
|
3244
3694
|
|
|
3245
|
-
# _@return_ —
|
|
3246
|
-
# topmost.
|
|
3695
|
+
# _@return_ — overlay popups in stacking order; last is
|
|
3696
|
+
# topmost. Holds both modal popups and non-modal overlays
|
|
3697
|
+
# ({Component::Popup#modal?}). The array must not be mutated by callers.
|
|
3247
3698
|
attr_reader popups: ::Array[Component]
|
|
3248
3699
|
|
|
3249
3700
|
# _@return_ — the bottom status bar.
|
|
@@ -3252,7 +3703,7 @@ module Tuile
|
|
|
3252
3703
|
|
|
3253
3704
|
# An immutable string-with-styling, modeled as a sequence of {Span}s where
|
|
3254
3705
|
# each span carries a complete {Style} (`fg`, `bg`, `bold`, `italic`,
|
|
3255
|
-
# `underline`). Spans are non-overlapping and fully tile the string — every
|
|
3706
|
+
# `underline`, `strikethrough`). Spans are non-overlapping and fully tile the string — every
|
|
3256
3707
|
# character has exactly one resolved style, no overlay layers to merge.
|
|
3257
3708
|
#
|
|
3258
3709
|
# Where this differs from threading SGR escapes through a plain `String`:
|
|
@@ -3292,12 +3743,21 @@ module Tuile
|
|
|
3292
3743
|
#
|
|
3293
3744
|
# ## Parser
|
|
3294
3745
|
#
|
|
3295
|
-
# {.parse} is strict by
|
|
3746
|
+
# {.parse} is strict by default: it recognizes only the SGR codes
|
|
3296
3747
|
# corresponding to {Style}'s supported attributes (fg/bg/bold/italic/
|
|
3297
|
-
# underline). Anything else — unmodeled attributes (dim, blink,
|
|
3298
|
-
#
|
|
3748
|
+
# underline/strikethrough). Anything else — unmodeled attributes (dim, blink,
|
|
3749
|
+
# reverse, conceal, double-underline, overline, ...), unknown SGR codes, or
|
|
3299
3750
|
# non-SGR escapes (cursor moves, OSC) — raises {ParseError}. This keeps the
|
|
3300
3751
|
# round-trip parse(to_ansi(x)) == x contract honest.
|
|
3752
|
+
#
|
|
3753
|
+
# Pass `lenient: true` to instead **discard** everything the parser can't
|
|
3754
|
+
# model and keep going — recognized fg/bg/bold/italic/underline/strikethrough codes still
|
|
3755
|
+
# apply, and any unmodeled SGR code, malformed extended color, non-SGR CSI
|
|
3756
|
+
# (cursor moves, `\e[K`), OSC/DCS/string sequence, or stray escape is
|
|
3757
|
+
# silently dropped. This is the mode for piping in colored output you don't
|
|
3758
|
+
# control (e.g. `git --color` through a pager): "give me the colors, throw
|
|
3759
|
+
# the rest away." It is lossy by design — `parse(x, lenient: true)` does not
|
|
3760
|
+
# round-trip back to `x`.
|
|
3301
3761
|
class StyledString
|
|
3302
3762
|
EMPTY: StyledString
|
|
3303
3763
|
|
|
@@ -3317,7 +3777,9 @@ module Tuile
|
|
|
3317
3777
|
# default-styled span.
|
|
3318
3778
|
#
|
|
3319
3779
|
# _@param_ `input`
|
|
3320
|
-
|
|
3780
|
+
#
|
|
3781
|
+
# _@param_ `lenient` — when true, unmodeled SGR codes and non-SGR escapes are discarded instead of raising — see {StyledString} "## Parser". Lossy: the result no longer round-trips to `input`.
|
|
3782
|
+
def self.parse: ((String | StyledString)? input, ?lenient: bool) -> StyledString
|
|
3321
3783
|
|
|
3322
3784
|
# _@param_ `spans`
|
|
3323
3785
|
def initialize: (?::Array[Span] spans) -> void
|
|
@@ -3404,7 +3866,7 @@ module Tuile
|
|
|
3404
3866
|
|
|
3405
3867
|
# Returns a new {StyledString} with `bg` applied to every span, preserving
|
|
3406
3868
|
# each span's text and other style attributes (`fg`, `bold`, `italic`,
|
|
3407
|
-
# `underline`). Useful for row-level highlights — the new bg overlays
|
|
3869
|
+
# `underline`, `strikethrough`). Useful for row-level highlights — the new bg overlays
|
|
3408
3870
|
# without dropping foreground colors the original styling carried.
|
|
3409
3871
|
#
|
|
3410
3872
|
# _@param_ `bg` — background color, coerced via {Color.coerce}. `nil` clears bg back to the terminal default.
|
|
@@ -3412,7 +3874,7 @@ module Tuile
|
|
|
3412
3874
|
|
|
3413
3875
|
# Returns a new {StyledString} with `fg` applied to every span, preserving
|
|
3414
3876
|
# each span's text and other style attributes (`bg`, `bold`, `italic`,
|
|
3415
|
-
# `underline`). The new fg overlays without dropping background colors or
|
|
3877
|
+
# `underline`, `strikethrough`). The new fg overlays without dropping background colors or
|
|
3416
3878
|
# text attributes the original styling carried.
|
|
3417
3879
|
#
|
|
3418
3880
|
# _@param_ `fg` — foreground color, coerced via {Color.coerce}. `nil` clears fg back to the terminal default.
|
|
@@ -3425,19 +3887,6 @@ module Tuile
|
|
|
3425
3887
|
# _@param_ `spans`
|
|
3426
3888
|
def normalize: (::Array[Span] spans) -> ::Array[Span]
|
|
3427
3889
|
|
|
3428
|
-
# _@param_ `from`
|
|
3429
|
-
#
|
|
3430
|
-
# _@param_ `to`
|
|
3431
|
-
def sgr_diff: (Style from, Style to) -> String
|
|
3432
|
-
|
|
3433
|
-
# _@param_ `color`
|
|
3434
|
-
#
|
|
3435
|
-
# _@param_ `target` — `:fg` or `:bg`.
|
|
3436
|
-
#
|
|
3437
|
-
# _@return_ — SGR codes; `[39]` / `[49]` for the "default" reset
|
|
3438
|
-
# when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
|
|
3439
|
-
def color_codes: (Color? color, target: Symbol) -> ::Array[Integer]
|
|
3440
|
-
|
|
3441
3890
|
# _@param_ `start_or_range`
|
|
3442
3891
|
#
|
|
3443
3892
|
# _@param_ `len`
|
|
@@ -3505,6 +3954,8 @@ module Tuile
|
|
|
3505
3954
|
# @return [Boolean]
|
|
3506
3955
|
# @!attribute [r] underline
|
|
3507
3956
|
# @return [Boolean]
|
|
3957
|
+
# @!attribute [r] strikethrough
|
|
3958
|
+
# @return [Boolean]
|
|
3508
3959
|
class Style
|
|
3509
3960
|
DEFAULT: Style
|
|
3510
3961
|
|
|
@@ -3517,12 +3968,15 @@ module Tuile
|
|
|
3517
3968
|
# _@param_ `italic`
|
|
3518
3969
|
#
|
|
3519
3970
|
# _@param_ `underline`
|
|
3971
|
+
#
|
|
3972
|
+
# _@param_ `strikethrough`
|
|
3520
3973
|
def self.new: (
|
|
3521
3974
|
?fg: (Color | Symbol | Integer | ::Array[Integer])?,
|
|
3522
3975
|
?bg: (Color | Symbol | Integer | ::Array[Integer])?,
|
|
3523
3976
|
?bold: bool,
|
|
3524
3977
|
?italic: bool,
|
|
3525
|
-
?underline: bool
|
|
3978
|
+
?underline: bool,
|
|
3979
|
+
?strikethrough: bool
|
|
3526
3980
|
) -> Style
|
|
3527
3981
|
|
|
3528
3982
|
def default?: () -> bool
|
|
@@ -3532,6 +3986,27 @@ module Tuile
|
|
|
3532
3986
|
# _@param_ `overrides`
|
|
3533
3987
|
def merge: (**::Hash[Symbol, Object] overrides) -> Style
|
|
3534
3988
|
|
|
3989
|
+
# Minimal SGR escape that transitions a terminal already showing `self`
|
|
3990
|
+
# into `other`: only the attributes that differ are emitted. Returns
|
|
3991
|
+
# `""` when the styles are identical (nothing to do), and {Ansi::RESET}
|
|
3992
|
+
# (`\e[0m`, one code) when `other` is the default style — shorter than
|
|
3993
|
+
# turning each attribute off individually.
|
|
3994
|
+
#
|
|
3995
|
+
# Shared by {StyledString#to_ansi} (diffing span-to-span from the default
|
|
3996
|
+
# style) and {Buffer}'s flush (diffing cell-to-cell against the style the
|
|
3997
|
+
# terminal currently holds), so both emit identical minimal sequences.
|
|
3998
|
+
#
|
|
3999
|
+
# _@param_ `other` — the style to transition to.
|
|
4000
|
+
def sgr_to: (Style other) -> String
|
|
4001
|
+
|
|
4002
|
+
# _@param_ `color`
|
|
4003
|
+
#
|
|
4004
|
+
# _@param_ `target` — either `:fg` or `:bg`.
|
|
4005
|
+
#
|
|
4006
|
+
# _@return_ — SGR codes; `[39]` / `[49]` for the "default"
|
|
4007
|
+
# reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
|
|
4008
|
+
def color_codes: (Color? color, target: Symbol) -> ::Array[Integer]
|
|
4009
|
+
|
|
3535
4010
|
attr_reader fg: Color?
|
|
3536
4011
|
|
|
3537
4012
|
attr_reader bg: Color?
|
|
@@ -3541,6 +4016,8 @@ module Tuile
|
|
|
3541
4016
|
attr_reader italic: bool
|
|
3542
4017
|
|
|
3543
4018
|
attr_reader underline: bool
|
|
4019
|
+
|
|
4020
|
+
attr_reader strikethrough: bool
|
|
3544
4021
|
end
|
|
3545
4022
|
|
|
3546
4023
|
# A maximal run of text sharing a single {Style}. `text` is plain — it
|
|
@@ -3566,14 +4043,18 @@ module Tuile
|
|
|
3566
4043
|
# @api private
|
|
3567
4044
|
# Hand-rolled SGR parser. State machine over a {StringScanner}: plain
|
|
3568
4045
|
# text accumulates into the current span; each `\e[...m` flushes the
|
|
3569
|
-
# current span and updates the running {Style}.
|
|
3570
|
-
# supported SGR alphabet raises {ParseError}
|
|
4046
|
+
# current span and updates the running {Style}. In strict mode anything
|
|
4047
|
+
# outside the supported SGR alphabet raises {ParseError}; in lenient mode
|
|
4048
|
+
# it is consumed and discarded (see {StyledString} "## Parser").
|
|
3571
4049
|
class Parser
|
|
3572
4050
|
STANDARD_COLORS: ::Array[Symbol]
|
|
3573
4051
|
BRIGHT_COLORS: ::Array[Symbol]
|
|
4052
|
+
STRING_INTRODUCERS: ::Array[String]
|
|
3574
4053
|
|
|
3575
4054
|
# _@param_ `input`
|
|
3576
|
-
|
|
4055
|
+
#
|
|
4056
|
+
# _@param_ `lenient` — when true, discard unmodeled SGR codes and non-SGR escapes instead of raising {ParseError}.
|
|
4057
|
+
def initialize: (String input, ?lenient: bool) -> void
|
|
3577
4058
|
|
|
3578
4059
|
def parse: () -> StyledString
|
|
3579
4060
|
|
|
@@ -3581,6 +4062,27 @@ module Tuile
|
|
|
3581
4062
|
|
|
3582
4063
|
def consume_escape: () -> void
|
|
3583
4064
|
|
|
4065
|
+
# Consumes a CSI sequence (`\e[` already eaten). A well-formed SGR
|
|
4066
|
+
# (`\e[...m` with numeric/`;` params and no intermediates) is applied;
|
|
4067
|
+
# anything else is a non-SGR or malformed CSI — raises in strict mode,
|
|
4068
|
+
# swallowed in lenient. Scans the full CSI grammar (parameter bytes
|
|
4069
|
+
# `\x30-\x3F`, intermediate bytes `\x20-\x2F`, final byte) so lenient
|
|
4070
|
+
# mode consumes the whole sequence even for private-marker forms like
|
|
4071
|
+
# `\e[?25l`.
|
|
4072
|
+
def consume_csi: () -> void
|
|
4073
|
+
|
|
4074
|
+
# Lenient-only: discards a non-CSI escape (`\e` and `intro` already
|
|
4075
|
+
# eaten). OSC/DCS/string sequences run to their string terminator; an
|
|
4076
|
+
# nF escape (`\e( B`) eats its intermediates plus one final byte; any
|
|
4077
|
+
# other Fe/Fp/Fs escape was complete in `intro` alone.
|
|
4078
|
+
#
|
|
4079
|
+
# _@param_ `intro` — the byte after `\e` (never `"["`).
|
|
4080
|
+
def consume_non_csi: (String intro) -> void
|
|
4081
|
+
|
|
4082
|
+
# Lenient-only: swallows an OSC/DCS/string-sequence payload up to and
|
|
4083
|
+
# including its terminator (BEL, or ST `\e\\`), or to EOS if unterminated.
|
|
4084
|
+
def consume_string_sequence: () -> void
|
|
4085
|
+
|
|
3584
4086
|
# _@param_ `params_str`
|
|
3585
4087
|
def apply_sgr: (String params_str) -> void
|
|
3586
4088
|
|
|
@@ -3590,7 +4092,11 @@ module Tuile
|
|
|
3590
4092
|
#
|
|
3591
4093
|
# _@param_ `target` — either `:fg` or `:bg`.
|
|
3592
4094
|
#
|
|
3593
|
-
# _@return_ — how many SGR codes were consumed
|
|
4095
|
+
# _@return_ — how many SGR codes were consumed. In lenient mode a
|
|
4096
|
+
# malformed color is skipped rather than applied, but the same count is
|
|
4097
|
+
# returned (3 for 256-color, 5 for RGB) so the running index advances
|
|
4098
|
+
# past its operands; an unknown selector skips just `38`/`48` + the
|
|
4099
|
+
# selector byte (2), letting the rest be reprocessed.
|
|
3594
4100
|
def consume_extended_color: (::Array[Integer] codes, Integer index, Symbol target) -> Integer
|
|
3595
4101
|
|
|
3596
4102
|
def flush: () -> void
|