tuile 0.4.0 → 0.5.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 +13 -0
- data/lib/tuile/color.rb +127 -0
- data/lib/tuile/component/label.rb +33 -3
- data/lib/tuile/component/text_view.rb +693 -54
- data/lib/tuile/event_queue.rb +104 -9
- data/lib/tuile/fake_event_queue.rb +69 -0
- data/lib/tuile/screen.rb +2 -0
- data/lib/tuile/styled_string.rb +28 -61
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +533 -52
- metadata +2 -1
data/sig/tuile.rbs
CHANGED
|
@@ -188,6 +188,70 @@ module Tuile
|
|
|
188
188
|
attr_reader height: Integer
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
+
# An immutable terminal color. Accepts the three forms ANSI/SGR understands:
|
|
192
|
+
#
|
|
193
|
+
# - a Symbol from {COLOR_SYMBOLS} — 8 standard + 8 bright named colors
|
|
194
|
+
# (SGR 30..37 / 90..97 for fg, 40..47 / 100..107 for bg)
|
|
195
|
+
# - an Integer 0..255 — the 256-color palette (SGR 38;5;N / 48;5;N)
|
|
196
|
+
# - an Array of three Integers 0..255 — 24-bit RGB (SGR 38;2;R;G;B / 48;2;R;G;B)
|
|
197
|
+
#
|
|
198
|
+
# A constant per named color is pre-defined (`Color::RED`, `Color::BRIGHT_BLUE`,
|
|
199
|
+
# …) so callers can reach for `Color::RED` instead of building one each time.
|
|
200
|
+
# {.coerce} accepts anything {.new} accepts plus `nil` (terminal default) and
|
|
201
|
+
# an existing {Color} (returned as-is), so APIs that accept colors typically
|
|
202
|
+
# take `[Color, nil]` and pass through {.coerce}.
|
|
203
|
+
#
|
|
204
|
+
# ```ruby
|
|
205
|
+
# Color.new(:red) # named
|
|
206
|
+
# Color.new(42) # 256-color palette
|
|
207
|
+
# Color.new([255, 100, 0]) # RGB
|
|
208
|
+
# Color::RED # constant
|
|
209
|
+
# Color.coerce(:red) # accepts raw forms, returns Color
|
|
210
|
+
# Color.coerce(nil) # nil → nil
|
|
211
|
+
# ```
|
|
212
|
+
#
|
|
213
|
+
# {#to_ansi} renders a full SGR escape (`"\e[31m"`); {#sgr_codes} returns the
|
|
214
|
+
# raw numeric codes so callers (notably {StyledString}) can combine them with
|
|
215
|
+
# other SGR attributes in a single sequence.
|
|
216
|
+
class Color
|
|
217
|
+
COLOR_SYMBOLS: ::Array[Symbol]
|
|
218
|
+
|
|
219
|
+
# Coerces the input to a {Color}. `nil` passes through unchanged (callers
|
|
220
|
+
# use `nil` for the terminal default); an existing {Color} is returned
|
|
221
|
+
# as-is; otherwise the value is fed to {.new}.
|
|
222
|
+
#
|
|
223
|
+
# _@param_ `value`
|
|
224
|
+
def self.coerce: ((Color | Symbol | Integer | ::Array[Integer])? value) -> Color?
|
|
225
|
+
|
|
226
|
+
# _@param_ `value` — see class-level docs for the three accepted forms.
|
|
227
|
+
def initialize: ((Symbol | Integer | ::Array[Integer]) value) -> void
|
|
228
|
+
|
|
229
|
+
# SGR parameter codes for emitting this color as either a foreground
|
|
230
|
+
# (`target: :fg`) or background (`target: :bg`). Returned as an array so
|
|
231
|
+
# callers can splice them into a multi-attribute SGR (e.g. bold + color).
|
|
232
|
+
#
|
|
233
|
+
# _@param_ `target` — `:fg` or `:bg`.
|
|
234
|
+
def sgr_codes: (?Symbol target) -> ::Array[Integer]
|
|
235
|
+
|
|
236
|
+
# Full SGR escape sequence for this color (e.g. `"\e[31m"`). Useful for
|
|
237
|
+
# `print`-style direct emission; for composing with other attributes use
|
|
238
|
+
# {#sgr_codes} instead.
|
|
239
|
+
#
|
|
240
|
+
# _@param_ `target` — `:fg` or `:bg`.
|
|
241
|
+
def to_ansi: (?Symbol target) -> String
|
|
242
|
+
|
|
243
|
+
# _@param_ `other`
|
|
244
|
+
def ==: (Object other) -> bool
|
|
245
|
+
|
|
246
|
+
def hash: () -> Integer
|
|
247
|
+
|
|
248
|
+
def inspect: () -> String
|
|
249
|
+
|
|
250
|
+
# The underlying raw representation — a Symbol, Integer, or frozen
|
|
251
|
+
# Array<Integer>.
|
|
252
|
+
attr_reader value: (Symbol | Integer | ::Array[Integer])
|
|
253
|
+
end
|
|
254
|
+
|
|
191
255
|
# A point with `x` and `y` integer coordinates, both 0-based.
|
|
192
256
|
#
|
|
193
257
|
# @!attribute [r] x
|
|
@@ -1109,9 +1173,13 @@ module Tuile
|
|
|
1109
1173
|
# Each line is ellipsized to fit, padded with trailing spaces out to
|
|
1110
1174
|
# the full width, and pre-rendered to ANSI so {#repaint} is just a
|
|
1111
1175
|
# lookup + screen.print per row. {@blank_line} covers rows past the
|
|
1112
|
-
# last text line.
|
|
1176
|
+
# last text line. When {#bg} is set, every produced line (and the
|
|
1177
|
+
# blank row) has the bg applied uniformly.
|
|
1113
1178
|
def update_clipped_lines: () -> void
|
|
1114
1179
|
|
|
1180
|
+
# _@param_ `line`
|
|
1181
|
+
def apply_bg: (StyledString line) -> StyledString
|
|
1182
|
+
|
|
1115
1183
|
# _@param_ `line`
|
|
1116
1184
|
#
|
|
1117
1185
|
# _@param_ `width`
|
|
@@ -1120,6 +1188,11 @@ module Tuile
|
|
|
1120
1188
|
# _@return_ — the current text. Defaults to an empty
|
|
1121
1189
|
# {StyledString}.
|
|
1122
1190
|
attr_accessor text: (StyledString | String)?
|
|
1191
|
+
|
|
1192
|
+
# _@return_ — background color applied uniformly across every
|
|
1193
|
+
# painted row (including padding past the text). `nil` (default)
|
|
1194
|
+
# leaves whatever bg the text's own styling carries.
|
|
1195
|
+
attr_accessor bg: (Color | Symbol | Integer | ::Array[Integer])?
|
|
1123
1196
|
end
|
|
1124
1197
|
|
|
1125
1198
|
# A modal overlay that wraps any {Component} as its content. Popup itself
|
|
@@ -1513,9 +1586,27 @@ module Tuile
|
|
|
1513
1586
|
# honored); a `StyledString` is used as-is; `nil` is coerced to an
|
|
1514
1587
|
# empty {StyledString}.
|
|
1515
1588
|
#
|
|
1589
|
+
# Detaches every existing {Region} (including the original default)
|
|
1590
|
+
# and installs a fresh internal default region that owns all the new
|
|
1591
|
+
# hard lines. Any handle the caller was holding becomes detached and
|
|
1592
|
+
# raises on use — see {Region#attached?}. The no-op short-circuit
|
|
1593
|
+
# (matching value, same {StyledString}) preserves existing regions.
|
|
1594
|
+
#
|
|
1516
1595
|
# _@param_ `value`
|
|
1517
1596
|
def text=: ((String | StyledString)? value) -> void
|
|
1518
1597
|
|
|
1598
|
+
# Creates a new empty {Region} at the spatial tail of the document
|
|
1599
|
+
# and returns its handle. Subsequent {#append} / {#<<} / {#add_line}
|
|
1600
|
+
# calls route through this new region (since it is now the spatial
|
|
1601
|
+
# tail). Earlier regions keep their content and their handles stay
|
|
1602
|
+
# valid; their {Region#range} shifts as later regions grow.
|
|
1603
|
+
#
|
|
1604
|
+
# Apps streaming logically-distinct sections (e.g. an LLM's "thinking"
|
|
1605
|
+
# vs. "assistant" output) create one region per section, hold the
|
|
1606
|
+
# handles, and call `region.append` / `region.text=` directly when
|
|
1607
|
+
# they need to grow or rewrite an earlier section.
|
|
1608
|
+
def create_region: () -> Region
|
|
1609
|
+
|
|
1519
1610
|
# _@return_ — true iff {#text} is empty (no hard lines).
|
|
1520
1611
|
def empty?: () -> bool
|
|
1521
1612
|
|
|
@@ -1568,6 +1659,56 @@ module Tuile
|
|
|
1568
1659
|
# _@param_ `n` — number of hard lines to drop; must be >= 0.
|
|
1569
1660
|
def remove_last_n_lines: (Integer n) -> void
|
|
1570
1661
|
|
|
1662
|
+
# Replaces a contiguous range of hard lines with the parsed content
|
|
1663
|
+
# of `str`. The replacement is parsed exactly like {#text=} and
|
|
1664
|
+
# {#append}: a {String} is run through {StyledString.parse} (so
|
|
1665
|
+
# embedded ANSI is honored), a {StyledString} is used as-is, `nil`
|
|
1666
|
+
# behaves like an empty replacement (the range is deleted). Embedded
|
|
1667
|
+
# `"\n"` in the replacement produces multiple hard lines, so a single
|
|
1668
|
+
# `replace` can grow or shrink the buffer.
|
|
1669
|
+
#
|
|
1670
|
+
# `range` selects which hard lines to swap out:
|
|
1671
|
+
#
|
|
1672
|
+
# - an `Integer` `n` is shorthand for `n..n` (replace one existing
|
|
1673
|
+
# line — `n` must be in `[0, hard-line count)`);
|
|
1674
|
+
# - a non-empty `Range` of hard-line indices replaces those lines;
|
|
1675
|
+
# - an empty `Range` (e.g. `2...2`, or the canonical end-insertion
|
|
1676
|
+
# `hard_lines.size...hard_lines.size`) is *insertion* at that
|
|
1677
|
+
# position — no lines are removed. {#insert} is a thin alias for
|
|
1678
|
+
# this case.
|
|
1679
|
+
#
|
|
1680
|
+
# Endpoints must be non-negative integers; `begin` may equal
|
|
1681
|
+
# `hard-line count` (insertion at the end), `end` may not exceed
|
|
1682
|
+
# `hard-line count - 1`. `nil` endpoints (beginless / endless ranges)
|
|
1683
|
+
# are not accepted.
|
|
1684
|
+
#
|
|
1685
|
+
# Cost is roughly `O(from + length + new content)`: the splice
|
|
1686
|
+
# updates only the affected slice of the physical-row buffer, using
|
|
1687
|
+
# the per-hard-line wrap-count cache to locate the starting offset
|
|
1688
|
+
# without re-wrapping preceding lines. Lines outside the splice are
|
|
1689
|
+
# never re-wrapped. {#top_line} is clamped if the new line count
|
|
1690
|
+
# puts it past the end; {#auto_scroll} pins it to the bottom as
|
|
1691
|
+
# usual. The call is a no-op (no invalidation) when the parsed
|
|
1692
|
+
# replacement equals the covered range (vacuously true for an empty
|
|
1693
|
+
# range plus empty replacement, so `replace(n...n, "")` is a cheap
|
|
1694
|
+
# no-op).
|
|
1695
|
+
#
|
|
1696
|
+
# _@param_ `range` — hard-line indices to replace.
|
|
1697
|
+
#
|
|
1698
|
+
# _@param_ `str` — replacement content.
|
|
1699
|
+
def replace: ((::Range[untyped] | Integer) range, (String | StyledString)? str) -> void
|
|
1700
|
+
|
|
1701
|
+
# Inserts `str` at hard-line index `at`. Equivalent to
|
|
1702
|
+
# `replace(at...at, str)` — a no-removal splice that grows the buffer
|
|
1703
|
+
# by the parsed line count. `at == hard-line count` is allowed and
|
|
1704
|
+
# appends at the end; for that case {#append} / {#add_line} are
|
|
1705
|
+
# usually more idiomatic.
|
|
1706
|
+
#
|
|
1707
|
+
# _@param_ `at` — 0-based hard-line index in `[0, hard-line count]`.
|
|
1708
|
+
#
|
|
1709
|
+
# _@param_ `str` — content to insert.
|
|
1710
|
+
def insert: (Integer at, (String | StyledString)? str) -> void
|
|
1711
|
+
|
|
1571
1712
|
# Clears the text. Equivalent to `text = ""`.
|
|
1572
1713
|
def clear: () -> void
|
|
1573
1714
|
|
|
@@ -1593,41 +1734,164 @@ module Tuile
|
|
|
1593
1734
|
# this hook.
|
|
1594
1735
|
def on_width_changed: () -> void
|
|
1595
1736
|
|
|
1737
|
+
# Validates and unpacks a {#replace}-style range argument into
|
|
1738
|
+
# inclusive `[from, to]` line indices. An `Integer` `n` becomes
|
|
1739
|
+
# `[n, n]` (which must point at an existing line — `Integer` is
|
|
1740
|
+
# never insertion sugar). A `Range` is normalized for
|
|
1741
|
+
# `exclude_end?`; `to == from - 1` is a valid empty range
|
|
1742
|
+
# (insertion at `from`), and `from` may equal `size` for
|
|
1743
|
+
# end-insertion. Shared by {#replace} and {Region#replace};
|
|
1744
|
+
# `size` is the buffer or region line count, and `what` is the
|
|
1745
|
+
# entity name woven into error messages.
|
|
1746
|
+
#
|
|
1747
|
+
# _@param_ `range`
|
|
1748
|
+
#
|
|
1749
|
+
# _@param_ `size`
|
|
1750
|
+
#
|
|
1751
|
+
# _@param_ `what`
|
|
1752
|
+
def normalize_replace_range: ((::Range[untyped] | Integer) range, ?Integer size, ?String what) -> [Integer, Integer]
|
|
1753
|
+
|
|
1754
|
+
# Hard-line index where `region` begins in {@hard_lines} — derived
|
|
1755
|
+
# by summing the line counts of all regions that precede it.
|
|
1756
|
+
#
|
|
1757
|
+
# _@param_ `region`
|
|
1758
|
+
def region_start_index: (Region region) -> Integer
|
|
1759
|
+
|
|
1760
|
+
# Joined {StyledString} of the hard lines that `region` owns. Mirrors
|
|
1761
|
+
# {#text} but scoped to one region.
|
|
1762
|
+
#
|
|
1763
|
+
# _@param_ `region`
|
|
1764
|
+
def text_for_region: (Region region) -> StyledString
|
|
1765
|
+
|
|
1766
|
+
# Replaces all of `region`'s hard lines with the parsed content of
|
|
1767
|
+
# `value`. Symmetric with {#text=}, scoped to one region. Empty/nil
|
|
1768
|
+
# content empties the region (no visible blank line). Works on
|
|
1769
|
+
# already-empty regions (insertion at the region's position).
|
|
1770
|
+
#
|
|
1771
|
+
# _@param_ `region`
|
|
1772
|
+
#
|
|
1773
|
+
# _@param_ `value`
|
|
1774
|
+
def set_region_text: (Region region, (String | StyledString)? value) -> void
|
|
1775
|
+
|
|
1776
|
+
# Region-scoped {#replace}. Validates `range` against
|
|
1777
|
+
# `region.line_count`, translates region-relative indices to
|
|
1778
|
+
# absolute buffer indices, splices, and updates the region's count.
|
|
1779
|
+
#
|
|
1780
|
+
# _@param_ `region`
|
|
1781
|
+
#
|
|
1782
|
+
# _@param_ `range`
|
|
1783
|
+
#
|
|
1784
|
+
# _@param_ `str`
|
|
1785
|
+
def replace_in_region: (Region region, (::Range[untyped] | Integer) range, (String | StyledString)? str) -> void
|
|
1786
|
+
|
|
1787
|
+
# Verbatim append into `region`.
|
|
1788
|
+
#
|
|
1789
|
+
# _@param_ `region`
|
|
1790
|
+
#
|
|
1791
|
+
# _@param_ `str`
|
|
1792
|
+
def append_to_region: (Region region, (String | StyledString)? str) -> void
|
|
1793
|
+
|
|
1794
|
+
# Drops the last `n` hard lines from `region`'s tail via
|
|
1795
|
+
# {#splice_hard_lines}. `n` is clamped to the region's current
|
|
1796
|
+
# line count; callers guarantee `n > 0` and the region is
|
|
1797
|
+
# non-empty (the {Region#remove_last_n_lines} guard handles the
|
|
1798
|
+
# no-op cases).
|
|
1799
|
+
#
|
|
1800
|
+
# _@param_ `region`
|
|
1801
|
+
#
|
|
1802
|
+
# _@param_ `n`
|
|
1803
|
+
def remove_last_n_from_region: (Region region, Integer n) -> void
|
|
1804
|
+
|
|
1805
|
+
# Drops `region` from {@regions}: its hard lines are removed via
|
|
1806
|
+
# {#splice_hard_lines}, the handle is detached, and the always-one
|
|
1807
|
+
# default is restored if the removal would have left zero regions.
|
|
1808
|
+
# Skips the rewrap / invalidate work when the region was empty
|
|
1809
|
+
# (the buffer didn't change), but always detaches.
|
|
1810
|
+
#
|
|
1811
|
+
# _@param_ `region`
|
|
1812
|
+
def remove_region: (Region region) -> void
|
|
1813
|
+
|
|
1814
|
+
# Adjusts region line counts after a {@hard_lines} splice that
|
|
1815
|
+
# removed `removed_count` lines at index `from` and inserted
|
|
1816
|
+
# `added_count` in their place. Two passes:
|
|
1817
|
+
#
|
|
1818
|
+
# 1. Subtract each region's overlap with the removed range (uses
|
|
1819
|
+
# the original counts to compute positions). Remember the first
|
|
1820
|
+
# region that lost lines — that's the natural home for the
|
|
1821
|
+
# replacement content.
|
|
1822
|
+
# 2. Credit `added_count` to that region. For pure insertions (no
|
|
1823
|
+
# removal), there's no "first overlapping region" to pick from;
|
|
1824
|
+
# walk regions and credit the latest one starting at `from` (the
|
|
1825
|
+
# boundary tiebreaker matches the spatial-tail-routing of
|
|
1826
|
+
# {#append}). Past-the-end inserts fall back to the tail region.
|
|
1827
|
+
#
|
|
1828
|
+
# _@param_ `from`
|
|
1829
|
+
#
|
|
1830
|
+
# _@param_ `removed_count`
|
|
1831
|
+
#
|
|
1832
|
+
# _@param_ `added_count`
|
|
1833
|
+
def update_region_counts: (Integer from, Integer removed_count, Integer added_count) -> void
|
|
1834
|
+
|
|
1596
1835
|
# _@return_ — number of visible lines.
|
|
1597
1836
|
def viewport_lines: () -> Integer
|
|
1598
1837
|
|
|
1599
1838
|
# _@return_ — the max value of {#top_line} for scroll-key clamping.
|
|
1600
1839
|
def top_line_max: () -> Integer
|
|
1601
1840
|
|
|
1602
|
-
#
|
|
1603
|
-
#
|
|
1604
|
-
#
|
|
1605
|
-
#
|
|
1606
|
-
#
|
|
1607
|
-
# {@top_line} if the new line count puts it out of
|
|
1841
|
+
# Full rebuild of {@physical_lines} and {@hard_line_wrap_counts}
|
|
1842
|
+
# from {@hard_lines}. Called when wrap width changes (which
|
|
1843
|
+
# invalidates every cached row count) and from {#text=} (which
|
|
1844
|
+
# replaces the whole logical model). Mid-buffer mutators splice
|
|
1845
|
+
# incrementally via {#splice_hard_lines} and do *not* go through
|
|
1846
|
+
# here. Clamps {@top_line} if the new line count puts it out of
|
|
1847
|
+
# range.
|
|
1608
1848
|
def rewrap: () -> void
|
|
1609
1849
|
|
|
1610
|
-
# Wraps `hard_line` at `width` and
|
|
1611
|
-
#
|
|
1612
|
-
# and degenerate `width <= 0` both emit a single {@blank_line}
|
|
1613
|
-
# matching what `@text.wrap(width).map { |l| pad_to(l, width) }`
|
|
1614
|
-
# would have produced
|
|
1850
|
+
# Wraps `hard_line` at `width` and returns the padded physical rows
|
|
1851
|
+
# alongside the row count. Empty hard lines (e.g. from a `"\n\n"`
|
|
1852
|
+
# run) and degenerate `width <= 0` both emit a single {@blank_line}
|
|
1853
|
+
# row, matching what `@text.wrap(width).map { |l| pad_to(l, width) }`
|
|
1854
|
+
# would have produced.
|
|
1615
1855
|
#
|
|
1616
|
-
# _@param_ `hard_line`
|
|
1856
|
+
# _@param_ `hard_line`
|
|
1617
1857
|
#
|
|
1618
1858
|
# _@param_ `width`
|
|
1619
|
-
def
|
|
1859
|
+
def wrap_hard_line: (StyledString hard_line, Integer width) -> [::Array[StyledString], Integer]
|
|
1620
1860
|
|
|
1621
|
-
#
|
|
1622
|
-
#
|
|
1623
|
-
# input). Used by {#append} when extending the last hard line: its
|
|
1624
|
-
# old wrapped rows are dropped, then the extended hard line is
|
|
1625
|
-
# re-wrapped and appended.
|
|
1861
|
+
# Appends `hard_line` to the tail of {@hard_lines}, updating the
|
|
1862
|
+
# wrap-count cache and {@physical_lines} in lockstep.
|
|
1626
1863
|
#
|
|
1627
1864
|
# _@param_ `hard_line`
|
|
1628
1865
|
#
|
|
1629
1866
|
# _@param_ `width`
|
|
1630
|
-
def
|
|
1867
|
+
def push_hard_line: (StyledString hard_line, Integer width) -> void
|
|
1868
|
+
|
|
1869
|
+
# Pops the last hard line, the corresponding cache entry, and the
|
|
1870
|
+
# physical rows that hard line contributed. Returns the popped
|
|
1871
|
+
# hard line.
|
|
1872
|
+
def pop_hard_line: () -> StyledString
|
|
1873
|
+
|
|
1874
|
+
# Splices `new_hard_lines` into the buffer in place of the `count`
|
|
1875
|
+
# hard lines starting at index `from`. Updates {@hard_lines},
|
|
1876
|
+
# {@hard_line_wrap_counts}, and {@physical_lines} consistently.
|
|
1877
|
+
# The starting physical-row offset is computed in O(`from`) integer
|
|
1878
|
+
# adds via the cache — no wraps of preceding hard lines. Wraps are
|
|
1879
|
+
# done only for the new content, so total cost is
|
|
1880
|
+
# `O(from + count + new_hard_lines.sum(&:display_width))`.
|
|
1881
|
+
#
|
|
1882
|
+
# _@param_ `from`
|
|
1883
|
+
#
|
|
1884
|
+
# _@param_ `count` — number of existing hard lines to remove.
|
|
1885
|
+
#
|
|
1886
|
+
# _@param_ `new_hard_lines`
|
|
1887
|
+
def splice_hard_lines: (Integer from, Integer count, ::Array[StyledString] new_hard_lines) -> void
|
|
1888
|
+
|
|
1889
|
+
# _@param_ `idx`
|
|
1890
|
+
#
|
|
1891
|
+
# _@return_ — the {@physical_lines} index where the hard line
|
|
1892
|
+
# at {@hard_lines}`[idx]` starts. O(`idx`) integer adds via the
|
|
1893
|
+
# wrap-count cache.
|
|
1894
|
+
def phys_offset_at: (Integer idx) -> Integer
|
|
1631
1895
|
|
|
1632
1896
|
# Rebuilds the joined {StyledString} from {@hard_lines}, inserting a
|
|
1633
1897
|
# default-styled `"\n"` between hard lines. Called from the {#text}
|
|
@@ -1691,6 +1955,136 @@ module Tuile
|
|
|
1691
1955
|
# `Size.new(0, 0)`. Maintained incrementally by {#text=} and
|
|
1692
1956
|
# {#append}, so reads are O(1).
|
|
1693
1957
|
attr_reader content_size: Size
|
|
1958
|
+
|
|
1959
|
+
# A logical section of a {TextView}'s text — a contiguous run of
|
|
1960
|
+
# hard lines the app wants to address as a unit (e.g. an LLM's
|
|
1961
|
+
# "thinking" output vs. its assistant message). The view always
|
|
1962
|
+
# has at least one region, an internal default that owns whatever
|
|
1963
|
+
# hard lines aren't claimed by an app-created region.
|
|
1964
|
+
#
|
|
1965
|
+
# Apps don't construct regions directly; call {TextView#create_region}
|
|
1966
|
+
# to get one. The handle stays valid as long as the region is
|
|
1967
|
+
# attached — i.e. until {TextView#text=} (or {TextView#clear}) wipes
|
|
1968
|
+
# the slate and installs a fresh internal default. Detached regions
|
|
1969
|
+
# raise {RuntimeError} on every mutator and reader.
|
|
1970
|
+
#
|
|
1971
|
+
# A region's position is derived from its sibling order and counts,
|
|
1972
|
+
# so growing or shrinking an earlier region implicitly shifts the
|
|
1973
|
+
# ranges of all later regions. Empty regions occupy zero rows but
|
|
1974
|
+
# still hold a position in the sequence; `region.text = ""` collapses
|
|
1975
|
+
# a region's visible footprint without detaching it. Pre-creating
|
|
1976
|
+
# empty placeholder regions is supported and is the natural pattern
|
|
1977
|
+
# for "I'll fill this in later" layouts.
|
|
1978
|
+
class Region
|
|
1979
|
+
# _@param_ `view` — the owning view (never `nil` at construction).
|
|
1980
|
+
#
|
|
1981
|
+
# _@param_ `line_count` — number of hard lines this region owns.
|
|
1982
|
+
def initialize: (TextView view, ?Integer line_count) -> void
|
|
1983
|
+
|
|
1984
|
+
# _@return_ — `true` while the region is owned by its
|
|
1985
|
+
# {TextView}. Becomes `false` permanently once detached
|
|
1986
|
+
# (typically by {TextView#text=} / {TextView#clear}).
|
|
1987
|
+
def attached?: () -> bool
|
|
1988
|
+
|
|
1989
|
+
# _@return_ — true iff the region owns zero hard lines.
|
|
1990
|
+
# Empty regions render nothing — they still hold a position in
|
|
1991
|
+
# the sequence, so subsequent mutations route to them as usual.
|
|
1992
|
+
def empty?: () -> bool
|
|
1993
|
+
|
|
1994
|
+
# _@return_ — the joined content of just this region's
|
|
1995
|
+
# hard lines. Empty regions return {StyledString::EMPTY}.
|
|
1996
|
+
def text: () -> StyledString
|
|
1997
|
+
|
|
1998
|
+
# Replaces all of this region's hard lines with the parsed content
|
|
1999
|
+
# of `value`. Accepts the same inputs as {TextView#text=}; empty
|
|
2000
|
+
# or `nil` content collapses the region to zero hard lines.
|
|
2001
|
+
#
|
|
2002
|
+
# _@param_ `value`
|
|
2003
|
+
def text=: ((String | StyledString)? value) -> void
|
|
2004
|
+
|
|
2005
|
+
# Verbatim append into this region's tail. Same semantics as
|
|
2006
|
+
# {TextView#append} but scoped to the region: embedded `"\n"`
|
|
2007
|
+
# creates new hard lines within the region, no-leading-newline
|
|
2008
|
+
# input extends the region's last hard line. Empty / `nil` input
|
|
2009
|
+
# is a no-op (but still raises when detached). When the region is
|
|
2010
|
+
# the spatial tail of the view, this uses the incremental
|
|
2011
|
+
# {TextView#append} path; mid-document regions splice the affected
|
|
2012
|
+
# slice of the physical-row buffer (lines outside the region are
|
|
2013
|
+
# not re-wrapped).
|
|
2014
|
+
#
|
|
2015
|
+
# _@param_ `str`
|
|
2016
|
+
def append: ((String | StyledString)? str) -> void
|
|
2017
|
+
|
|
2018
|
+
# _@return_ — the hard-line indices this region currently
|
|
2019
|
+
# occupies — `start...(start + line_count)`. Empty regions
|
|
2020
|
+
# return a degenerate exclusive range at their position (e.g.
|
|
2021
|
+
# `5...5`). The result is computed on each call and so always
|
|
2022
|
+
# reflects sibling mutations.
|
|
2023
|
+
def range: () -> ::Range[untyped]
|
|
2024
|
+
|
|
2025
|
+
# Removes this region from its view. The region's hard lines (if
|
|
2026
|
+
# any) are deleted from the buffer — subsequent regions' ranges
|
|
2027
|
+
# shift up by `line_count` — and the handle detaches permanently.
|
|
2028
|
+
# The view keeps its always-≥1-region invariant: if this was the
|
|
2029
|
+
# only remaining region, a fresh internal default is installed
|
|
2030
|
+
# (the app doesn't get a handle to it; call
|
|
2031
|
+
# {TextView#create_region} again to start tracking).
|
|
2032
|
+
#
|
|
2033
|
+
# Idempotent: calling `remove` on an already-detached region is a
|
|
2034
|
+
# silent no-op (unlike the other mutators, which raise). This
|
|
2035
|
+
# lets cleanup paths blindly call `remove` without first checking
|
|
2036
|
+
# {#attached?}.
|
|
2037
|
+
def remove: () -> void
|
|
2038
|
+
|
|
2039
|
+
# Appends `str` as a new entry in this region: starts a fresh
|
|
2040
|
+
# hard line first (when the region is non-empty), then appends
|
|
2041
|
+
# `str`. Scoped equivalent of {TextView#add_line}. On an empty
|
|
2042
|
+
# region behaves like {#append}.
|
|
2043
|
+
#
|
|
2044
|
+
# _@param_ `str`
|
|
2045
|
+
def add_line: ((String | StyledString)? str) -> void
|
|
2046
|
+
|
|
2047
|
+
# Replaces a contiguous range of this region's hard lines with the
|
|
2048
|
+
# parsed content of `str`. Region-scoped counterpart of
|
|
2049
|
+
# {TextView#replace}: indices are 0-based **within the region**
|
|
2050
|
+
# (so `replace(0, "x")` rewrites the region's first line, not
|
|
2051
|
+
# the buffer's). Same range conventions apply — `Integer`,
|
|
2052
|
+
# inclusive/exclusive `Range`, empty range as insertion at
|
|
2053
|
+
# `begin`, and `begin == line_count` for end-insertion.
|
|
2054
|
+
#
|
|
2055
|
+
# _@param_ `range` — region-relative hard-line indices.
|
|
2056
|
+
#
|
|
2057
|
+
# _@param_ `str` — replacement content.
|
|
2058
|
+
def replace: ((::Range[untyped] | Integer) range, (String | StyledString)? str) -> void
|
|
2059
|
+
|
|
2060
|
+
# Inserts `str` at region-relative hard-line index `at`.
|
|
2061
|
+
# Equivalent to `replace(at...at, str)`. Region-scoped counterpart
|
|
2062
|
+
# of {TextView#insert}; `at == line_count` is allowed and appends
|
|
2063
|
+
# at the region's tail.
|
|
2064
|
+
#
|
|
2065
|
+
# _@param_ `at` — region-relative index in `[0, line_count]`.
|
|
2066
|
+
#
|
|
2067
|
+
# _@param_ `str`
|
|
2068
|
+
def insert: (Integer at, (String | StyledString)? str) -> void
|
|
2069
|
+
|
|
2070
|
+
# Drops the last `n` hard lines from this region's tail.
|
|
2071
|
+
# Subsequent regions' ranges shift up by the number actually
|
|
2072
|
+
# dropped. `n` is clamped to {#line_count}, so passing a large
|
|
2073
|
+
# `n` empties the region — the handle stays attached (use
|
|
2074
|
+
# {#remove} when the goal is to drop the region itself).
|
|
2075
|
+
# `n == 0` and an already-empty region are no-ops.
|
|
2076
|
+
#
|
|
2077
|
+
# _@param_ `n`
|
|
2078
|
+
def remove_last_n_lines: (Integer n) -> void
|
|
2079
|
+
|
|
2080
|
+
def detach!: () -> void
|
|
2081
|
+
|
|
2082
|
+
def check_attached: () -> void
|
|
2083
|
+
|
|
2084
|
+
# _@return_ — number of hard lines this region owns. Safe to
|
|
2085
|
+
# read on a detached region (no error raised).
|
|
2086
|
+
attr_accessor line_count: (Integer | untyped)
|
|
2087
|
+
end
|
|
1694
2088
|
end
|
|
1695
2089
|
|
|
1696
2090
|
# Shows a log. Construct your logger pointed at a {LogWindow::IO} to route
|
|
@@ -2032,13 +2426,39 @@ module Tuile
|
|
|
2032
2426
|
# Awaits until the event queue is empty (all events have been processed).
|
|
2033
2427
|
def await_empty: () -> void
|
|
2034
2428
|
|
|
2429
|
+
# Schedules `block` to fire on the event-loop thread roughly `fps` times
|
|
2430
|
+
# per second, passing a 0-based monotonically increasing tick counter. Use
|
|
2431
|
+
# it for animations (e.g. a `/-\|` spinner in a {Component::Label}) or
|
|
2432
|
+
# periodic UI refresh from a background task.
|
|
2433
|
+
#
|
|
2434
|
+
# The returned {Ticker} controls the schedule — call {Ticker#cancel} to
|
|
2435
|
+
# stop it.
|
|
2436
|
+
#
|
|
2437
|
+
# **Errors:** if `block` raises, the {Ticker} cancels itself and the
|
|
2438
|
+
# exception flows through the normal event-loop error path — i.e.
|
|
2439
|
+
# {Screen#on_error} for the default Tuile setup. Auto-cancel prevents a
|
|
2440
|
+
# broken block from spamming `on_error` at the tick rate.
|
|
2441
|
+
#
|
|
2442
|
+
# Tickers reuse `concurrent-ruby`'s shared timer thread
|
|
2443
|
+
# ({Concurrent}.global_timer_set) — adding more tickers does not add more
|
|
2444
|
+
# threads, just more work on the shared scheduler.
|
|
2445
|
+
#
|
|
2446
|
+
# _@param_ `fps` — firings per second, must be positive. Fractional values are fine (`fps: 0.5` ⇒ one tick every two seconds).
|
|
2447
|
+
def tick: (Numeric fps) ?{ (Integer tick) -> void } -> Ticker
|
|
2448
|
+
|
|
2035
2449
|
# Runs the event loop and blocks. Must be run from at most one thread at the
|
|
2036
2450
|
# same time. Blocks until some thread calls {#stop}. Calls block for all
|
|
2037
|
-
# events
|
|
2038
|
-
# running this function.
|
|
2451
|
+
# events; the block is always called from the thread running this function.
|
|
2039
2452
|
#
|
|
2040
|
-
# Any exception raised by block is re-thrown, causing this function to
|
|
2041
|
-
# terminate.
|
|
2453
|
+
# Any exception raised by the block is re-thrown, causing this function to
|
|
2454
|
+
# terminate. Wrap the block body in `rescue` if you want to handle errors
|
|
2455
|
+
# without tearing down the loop — see {Screen#event_loop} for an example.
|
|
2456
|
+
#
|
|
2457
|
+
# **Procs are yielded too.** A {#submit}ed block arrives as a `Proc` event;
|
|
2458
|
+
# the consumer is responsible for invoking it (typically `event.call`).
|
|
2459
|
+
# Yielding rather than dispatching inline means a raise inside the
|
|
2460
|
+
# submitted block flows through the consumer's `rescue` like any other
|
|
2461
|
+
# event-handler error, instead of bypassing it.
|
|
2042
2462
|
def run_loop: () ?{ (Object event) -> void } -> void
|
|
2043
2463
|
|
|
2044
2464
|
# _@return_ — true if this thread is running inside an event queue.
|
|
@@ -2109,6 +2529,36 @@ module Tuile
|
|
|
2109
2529
|
class EmptyQueueEvent
|
|
2110
2530
|
include Singleton
|
|
2111
2531
|
end
|
|
2532
|
+
|
|
2533
|
+
# Handle returned by {EventQueue#tick}. Cancel a running ticker via
|
|
2534
|
+
# {#cancel}.
|
|
2535
|
+
#
|
|
2536
|
+
# Internally wraps a `Concurrent::TimerTask` whose firing posts a single
|
|
2537
|
+
# submit-block to the owning {EventQueue}; the user's block therefore
|
|
2538
|
+
# always runs on the event-loop thread and may freely mutate UI. If the
|
|
2539
|
+
# user block raises, the Ticker auto-cancels and the exception is
|
|
2540
|
+
# re-raised so it flows through the loop's normal error handling
|
|
2541
|
+
# ({Screen#on_error} for the default Tuile setup).
|
|
2542
|
+
class Ticker
|
|
2543
|
+
# _@param_ `event_queue` — queue to dispatch tick calls onto.
|
|
2544
|
+
#
|
|
2545
|
+
# _@param_ `fps` — firings per second (positive).
|
|
2546
|
+
#
|
|
2547
|
+
# _@param_ `block` — called as `block.call(tick_count)` on each fire.
|
|
2548
|
+
def initialize: (EventQueue event_queue, Numeric fps, Proc block) -> void
|
|
2549
|
+
|
|
2550
|
+
# _@return_ — true once {#cancel} has been called.
|
|
2551
|
+
def cancelled?: () -> bool
|
|
2552
|
+
|
|
2553
|
+
# Stops the ticker. Idempotent and safe to call from any thread,
|
|
2554
|
+
# including from inside the tick block. Any tick already queued on the
|
|
2555
|
+
# event loop at the moment of cancellation is dropped before the user
|
|
2556
|
+
# block runs.
|
|
2557
|
+
def cancel: () -> void
|
|
2558
|
+
|
|
2559
|
+
# Runs on the event-loop thread.
|
|
2560
|
+
def fire: () -> void
|
|
2561
|
+
end
|
|
2112
2562
|
end
|
|
2113
2563
|
|
|
2114
2564
|
# Testing only — a screen which doesn't paint anything and pretends that the
|
|
@@ -2456,16 +2906,16 @@ module Tuile
|
|
|
2456
2906
|
# `underline`). Useful for row-level highlights — the new bg overlays
|
|
2457
2907
|
# without dropping foreground colors the original styling carried.
|
|
2458
2908
|
#
|
|
2459
|
-
# _@param_ `bg` — background color,
|
|
2460
|
-
def with_bg: ((Symbol | Integer | ::Array[Integer])? bg) -> StyledString
|
|
2909
|
+
# _@param_ `bg` — background color, coerced via {Color.coerce}. `nil` clears bg back to the terminal default.
|
|
2910
|
+
def with_bg: ((Color | Symbol | Integer | ::Array[Integer])? bg) -> StyledString
|
|
2461
2911
|
|
|
2462
2912
|
# Returns a new {StyledString} with `fg` applied to every span, preserving
|
|
2463
2913
|
# each span's text and other style attributes (`bg`, `bold`, `italic`,
|
|
2464
2914
|
# `underline`). The new fg overlays without dropping background colors or
|
|
2465
2915
|
# text attributes the original styling carried.
|
|
2466
2916
|
#
|
|
2467
|
-
# _@param_ `fg` — foreground color,
|
|
2468
|
-
def with_fg: ((Symbol | Integer | ::Array[Integer])? fg) -> StyledString
|
|
2917
|
+
# _@param_ `fg` — foreground color, coerced via {Color.coerce}. `nil` clears fg back to the terminal default.
|
|
2918
|
+
def with_fg: ((Color | Symbol | Integer | ::Array[Integer])? fg) -> StyledString
|
|
2469
2919
|
|
|
2470
2920
|
def inspect: () -> String
|
|
2471
2921
|
|
|
@@ -2481,10 +2931,11 @@ module Tuile
|
|
|
2481
2931
|
|
|
2482
2932
|
# _@param_ `color`
|
|
2483
2933
|
#
|
|
2484
|
-
# _@param_ `
|
|
2934
|
+
# _@param_ `target` — `:fg` or `:bg`.
|
|
2485
2935
|
#
|
|
2486
|
-
# _@
|
|
2487
|
-
|
|
2936
|
+
# _@return_ — SGR codes; `[39]` / `[49]` for the "default" reset
|
|
2937
|
+
# when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
|
|
2938
|
+
def color_codes: (Color? color, target: Symbol) -> ::Array[Integer]
|
|
2488
2939
|
|
|
2489
2940
|
# _@param_ `start_or_range`
|
|
2490
2941
|
#
|
|
@@ -2537,18 +2988,16 @@ module Tuile
|
|
|
2537
2988
|
class ParseError < Tuile::Error
|
|
2538
2989
|
end
|
|
2539
2990
|
|
|
2540
|
-
# A frozen value type describing the visual style of a {Span}.
|
|
2541
|
-
#
|
|
2542
|
-
#
|
|
2543
|
-
#
|
|
2544
|
-
#
|
|
2545
|
-
# - an Integer 0..255 — 256-color palette index (SGR 38;5;N / 48;5;N)
|
|
2546
|
-
# - an `[r, g, b]` Array of three 0..255 Integers — 24-bit RGB
|
|
2991
|
+
# A frozen value type describing the visual style of a {Span}. Colors are
|
|
2992
|
+
# stored as {Color} instances (or `nil` for the terminal default); inputs
|
|
2993
|
+
# to {.new} and {#merge} are coerced via {Color.coerce}, so the four
|
|
2994
|
+
# accepted color forms — `nil`, Symbol, Integer 0..255, RGB Array — work
|
|
2995
|
+
# transparently.
|
|
2547
2996
|
#
|
|
2548
2997
|
# @!attribute [r] fg
|
|
2549
|
-
# @return [
|
|
2998
|
+
# @return [Color, nil]
|
|
2550
2999
|
# @!attribute [r] bg
|
|
2551
|
-
# @return [
|
|
3000
|
+
# @return [Color, nil]
|
|
2552
3001
|
# @!attribute [r] bold
|
|
2553
3002
|
# @return [Boolean]
|
|
2554
3003
|
# @!attribute [r] italic
|
|
@@ -2556,12 +3005,11 @@ module Tuile
|
|
|
2556
3005
|
# @!attribute [r] underline
|
|
2557
3006
|
# @return [Boolean]
|
|
2558
3007
|
class Style
|
|
2559
|
-
COLOR_SYMBOLS: ::Array[Symbol]
|
|
2560
3008
|
DEFAULT: Style
|
|
2561
3009
|
|
|
2562
|
-
# _@param_ `fg`
|
|
3010
|
+
# _@param_ `fg` — coerced via {Color.coerce}.
|
|
2563
3011
|
#
|
|
2564
|
-
# _@param_ `bg`
|
|
3012
|
+
# _@param_ `bg` — coerced via {Color.coerce}.
|
|
2565
3013
|
#
|
|
2566
3014
|
# _@param_ `bold`
|
|
2567
3015
|
#
|
|
@@ -2569,18 +3017,13 @@ module Tuile
|
|
|
2569
3017
|
#
|
|
2570
3018
|
# _@param_ `underline`
|
|
2571
3019
|
def self.new: (
|
|
2572
|
-
?fg: (Symbol | Integer | ::Array[Integer])?,
|
|
2573
|
-
?bg: (Symbol | Integer | ::Array[Integer])?,
|
|
3020
|
+
?fg: (Color | Symbol | Integer | ::Array[Integer])?,
|
|
3021
|
+
?bg: (Color | Symbol | Integer | ::Array[Integer])?,
|
|
2574
3022
|
?bold: bool,
|
|
2575
3023
|
?italic: bool,
|
|
2576
3024
|
?underline: bool
|
|
2577
3025
|
) -> Style
|
|
2578
3026
|
|
|
2579
|
-
# _@param_ `color`
|
|
2580
|
-
#
|
|
2581
|
-
# _@param_ `which`
|
|
2582
|
-
def self.validate_color!: (Object color, Symbol which) -> void
|
|
2583
|
-
|
|
2584
3027
|
def default?: () -> bool
|
|
2585
3028
|
|
|
2586
3029
|
# Returns a new {Style} with the given attributes overridden.
|
|
@@ -2588,9 +3031,9 @@ module Tuile
|
|
|
2588
3031
|
# _@param_ `overrides`
|
|
2589
3032
|
def merge: (**::Hash[Symbol, Object] overrides) -> Style
|
|
2590
3033
|
|
|
2591
|
-
attr_reader fg:
|
|
3034
|
+
attr_reader fg: Color?
|
|
2592
3035
|
|
|
2593
|
-
attr_reader bg:
|
|
3036
|
+
attr_reader bg: Color?
|
|
2594
3037
|
|
|
2595
3038
|
attr_reader bold: bool
|
|
2596
3039
|
|
|
@@ -2656,6 +3099,8 @@ module Tuile
|
|
|
2656
3099
|
# A "synchronous" event queue – no loop is run, submitted blocks are run right
|
|
2657
3100
|
# away and submitted events are thrown away. Intended for testing only.
|
|
2658
3101
|
class FakeEventQueue
|
|
3102
|
+
def initialize: () -> void
|
|
3103
|
+
|
|
2659
3104
|
def locked?: () -> bool
|
|
2660
3105
|
|
|
2661
3106
|
def stop: () -> void
|
|
@@ -2668,6 +3113,42 @@ module Tuile
|
|
|
2668
3113
|
|
|
2669
3114
|
# _@param_ `event`
|
|
2670
3115
|
def post: (Object event) -> void
|
|
3116
|
+
|
|
3117
|
+
# Mirrors {EventQueue#tick} but timeless: returns a {FakeTicker} that
|
|
3118
|
+
# only fires when a test calls {#tick_once}. The `fps` argument is
|
|
3119
|
+
# validated the same way the real queue validates it, then discarded —
|
|
3120
|
+
# the fake has no clock, so frame cadence is up to the test.
|
|
3121
|
+
#
|
|
3122
|
+
# _@param_ `fps` — firings per second, must be positive. Validated for parity with {EventQueue#tick}; otherwise unused.
|
|
3123
|
+
def tick: (Numeric fps) ?{ (Integer tick) -> void } -> FakeTicker
|
|
3124
|
+
|
|
3125
|
+
# Test helper: fires every live ticker's user block once and prunes
|
|
3126
|
+
# cancelled tickers. No-op when no tickers are registered. Pumps once
|
|
3127
|
+
# per call regardless of any ticker's fps — the fake has no clock, so
|
|
3128
|
+
# tests pump N frames by calling this N times.
|
|
3129
|
+
def tick_once: () -> void
|
|
3130
|
+
|
|
3131
|
+
# Handle returned by {FakeEventQueue#tick}. Mirrors the public surface of
|
|
3132
|
+
# {EventQueue::Ticker} (`cancel`, `cancelled?`) but does not auto-fire —
|
|
3133
|
+
# the host {FakeEventQueue} drives firing via {FakeEventQueue#tick_once}.
|
|
3134
|
+
class FakeTicker
|
|
3135
|
+
# _@param_ `block` — called as `block.call(tick_count)` on each {#fire}.
|
|
3136
|
+
def initialize: (Proc block) -> void
|
|
3137
|
+
|
|
3138
|
+
# _@return_ — true once {#cancel} has been called.
|
|
3139
|
+
def cancelled?: () -> bool
|
|
3140
|
+
|
|
3141
|
+
# Marks the ticker cancelled. Idempotent. Subsequent {#fire} calls are
|
|
3142
|
+
# no-ops; {FakeEventQueue#tick_once} also prunes the ticker on its next
|
|
3143
|
+
# pass.
|
|
3144
|
+
def cancel: () -> void
|
|
3145
|
+
|
|
3146
|
+
# Invokes the user block with the current tick counter, then advances.
|
|
3147
|
+
# No-op when {#cancelled?}. Typically driven by
|
|
3148
|
+
# {FakeEventQueue#tick_once}; safe to call directly from a test that
|
|
3149
|
+
# wants to drive a single ticker.
|
|
3150
|
+
def fire: () -> void
|
|
3151
|
+
end
|
|
2671
3152
|
end
|
|
2672
3153
|
|
|
2673
3154
|
# A vertical scrollbar that computes which character to draw at each row.
|