tuile 0.1.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.
data/sig/tuile.rbs ADDED
@@ -0,0 +1,1502 @@
1
+ # Tuile is a small component-oriented terminal UI framework, built on top of
2
+ # the TTY toolkit. The name is French for a roof tile — a small piece that
3
+ # composes into a larger whole, which mirrors how Tuile UIs are built from
4
+ # {Component}s nested under a single {Screen}.
5
+ module Tuile
6
+ VERSION: String
7
+
8
+ def self.logger: () -> Logger
9
+
10
+ # The logger Tuile writes to. Defaults to a null logger, so the gem is
11
+ # silent unless the host app opts in via `Tuile.logger = ...`. Any object
12
+ # duck-typing the stdlib `Logger` interface (`debug/info/warn/error/fatal`
13
+ # taking a string) works — including `TTY::Logger`.
14
+ def self.logger=: (Logger value) -> Logger
15
+
16
+ class Error < StandardError
17
+ end
18
+
19
+ # Constants for keys returned by {.getkey} and helpers for reading them from
20
+ # stdin. The constants are the raw escape sequences emitted by the terminal;
21
+ # see https://en.wikipedia.org/wiki/ANSI_escape_code for the encoding.
22
+ module Keys
23
+ DOWN_ARROW: String
24
+ UP_ARROW: String
25
+ DOWN_ARROWS: ::Array[String]
26
+ UP_ARROWS: ::Array[String]
27
+ LEFT_ARROW: String
28
+ RIGHT_ARROW: String
29
+ ESC: String
30
+ HOME: String
31
+ END_: String
32
+ PAGE_UP: String
33
+ PAGE_DOWN: String
34
+ BACKSPACE: String
35
+ DELETE: String
36
+ CTRL_H: String
37
+ BACKSPACES: ::Array[String]
38
+ CTRL_U: String
39
+ CTRL_D: String
40
+ ENTER: String
41
+
42
+ # Grabs a key from stdin and returns it. Blocks until the key is obtained.
43
+ # Reads a full ESC key sequence; see constants above for some values returned
44
+ # by this function.
45
+ #
46
+ # _@return_ — key, such as {DOWN_ARROW}.
47
+ def self.getkey: () -> String
48
+ end
49
+
50
+ # A rectangle, with integer `left`, `top`, `width` and `height`, all 0-based.
51
+ #
52
+ # @!attribute [r] left
53
+ # @return [Integer] left edge, 0-based.
54
+ # @!attribute [r] top
55
+ # @return [Integer] top edge, 0-based.
56
+ # @!attribute [r] width
57
+ # @return [Integer] width.
58
+ # @!attribute [r] height
59
+ # @return [Integer] height.
60
+ class Rect
61
+ def to_s: () -> String
62
+
63
+ # _@return_ — true if either {#width} or {#height} is zero or negative.
64
+ def empty?: () -> bool
65
+
66
+ # _@param_ `point` — new top-left corner.
67
+ #
68
+ # _@return_ — positioned at the new `left`/`top`.
69
+ def at: (Point point) -> Rect
70
+
71
+ # Centers the rectangle — keeps {#width} and {#height} but modifies
72
+ # {#top} and {#left} so that the rectangle is centered on a screen.
73
+ #
74
+ # _@param_ `screen_size` — screen size
75
+ #
76
+ # _@return_ — moved rectangle.
77
+ def centered: (Size screen_size) -> Rect
78
+
79
+ # Clamp both width and height and return a rectangle.
80
+ #
81
+ # _@param_ `max_size` — the max size
82
+ def clamp: (Size max_size) -> Rect
83
+
84
+ # _@param_ `point`
85
+ def contains?: (Point point) -> bool
86
+
87
+ def size: () -> Size
88
+
89
+ def top_left: () -> Point
90
+
91
+ # _@return_ — left edge, 0-based.
92
+ attr_reader left: Integer
93
+
94
+ # _@return_ — top edge, 0-based.
95
+ attr_reader top: Integer
96
+
97
+ # _@return_ — width.
98
+ attr_reader width: Integer
99
+
100
+ # _@return_ — height.
101
+ attr_reader height: Integer
102
+ end
103
+
104
+ # A size with integer `width` and `height`.
105
+ #
106
+ # @!attribute [r] width
107
+ # @return [Integer] width.
108
+ # @!attribute [r] height
109
+ # @return [Integer] height.
110
+ class Size
111
+ ZERO: Size
112
+
113
+ def to_s: () -> String
114
+
115
+ # _@return_ — true if either {#width} or {#height} is zero or negative.
116
+ def empty?: () -> bool
117
+
118
+ # _@param_ `width`
119
+ #
120
+ # _@param_ `height`
121
+ def plus: (Integer width, Integer height) -> Size
122
+
123
+ # Clamp both width and height and return a size.
124
+ #
125
+ # _@param_ `max_size` — the max size
126
+ def clamp: (Size max_size) -> Size
127
+
128
+ # Clamp height and return a size.
129
+ #
130
+ # _@param_ `max_height` — the max height
131
+ def clamp_height: (Integer max_height) -> Size
132
+
133
+ # _@return_ — width.
134
+ attr_reader width: Integer
135
+
136
+ # _@return_ — height.
137
+ attr_reader height: Integer
138
+ end
139
+
140
+ # A point with `x` and `y` integer coordinates, both 0-based.
141
+ #
142
+ # @!attribute [r] x
143
+ # @return [Integer] x coordinate, 0-based.
144
+ # @!attribute [r] y
145
+ # @return [Integer] y coordinate, 0-based.
146
+ class Point
147
+ def to_s: () -> String
148
+
149
+ # _@return_ — x coordinate, 0-based.
150
+ attr_reader x: Integer
151
+
152
+ # _@return_ — y coordinate, 0-based.
153
+ attr_reader y: Integer
154
+ end
155
+
156
+ # The TTY screen. There is exactly one screen per app.
157
+ #
158
+ # A screen runs the event loop; call {#run_event_loop} to do that.
159
+ #
160
+ # A screen holds the screen lock; any UI modifications must be called from
161
+ # the event queue.
162
+ #
163
+ # All UI lives under a single {ScreenPane} owned by the screen. Set tiled
164
+ # content via {#content=}; the pane fills the entire terminal and is
165
+ # responsible for laying out its children.
166
+ #
167
+ # Modal popups are supported too, via {Component::Popup#open}. They
168
+ # auto-size to their wrapped content and are drawn centered over the
169
+ # tiled content.
170
+ #
171
+ # The drawing procedure is very simple: when a window needs repaint, it
172
+ # invalidates itself, but won't draw immediately. After the keyboard press
173
+ # event processing is done in the event loop, {#repaint} is called which
174
+ # then repaints all invalidated windows. This prevents repeated paintings.
175
+ class Screen
176
+ # rubocop:disable Style/ClassVars
177
+ def initialize: () -> void
178
+
179
+ # _@return_ — the singleton instance.
180
+ def self.instance: () -> Screen
181
+
182
+ # _@return_ — tiled content (forwarded to {ScreenPane}).
183
+ def content: () -> Component?
184
+
185
+ # _@param_ `content`
186
+ def content=: (Component content) -> void
187
+
188
+ # _@return_ — currently active popup components (forwarded
189
+ # to {ScreenPane}). The array must not be modified!
190
+ def popups: () -> ::Array[Component]
191
+
192
+ # Checks that the UI lock is held and the current code runs in the "UI
193
+ # thread".
194
+ def check_locked: () -> void
195
+
196
+ # Clears the TTY screen.
197
+ def clear: () -> void
198
+
199
+ # Invalidates a component: causes the component to be repainted on next
200
+ # call to {#repaint}.
201
+ #
202
+ # _@param_ `component`
203
+ def invalidate: (Component component) -> void
204
+
205
+ # Internal — use {Component::Popup#open} instead. Adds the popup to
206
+ # {#pane}, centers and focuses it.
207
+ #
208
+ # _@param_ `window`
209
+ def add_popup: (Component::Popup window) -> void
210
+
211
+ # Runs event loop – waits for keys and sends them to active window. The
212
+ # function exits when the 'ESC' or 'q' key is pressed.
213
+ def run_event_loop: () -> void
214
+
215
+ # _@return_ — current active tiled component.
216
+ def active_window: () -> Component?
217
+
218
+ # Internal — use {Component::Popup#close} instead. Removes the popup
219
+ # from {#pane}, repairs focus, and repaints the scene.
220
+ #
221
+ # Does nothing if the window is not open on this screen.
222
+ #
223
+ # _@param_ `window`
224
+ def remove_popup: (Component::Popup window) -> void
225
+
226
+ # Internal — use {Component::Popup#open?} instead.
227
+ #
228
+ # _@param_ `window`
229
+ #
230
+ # _@return_ — true if this popup is currently mounted.
231
+ def has_popup?: (Component::Popup window) -> bool
232
+
233
+ # Testing only — creates new screen, locks the UI, and prevents any
234
+ # redraws, so that test TTY is not painted over. {FakeScreen#initialize}
235
+ # self-installs as the singleton, so subsequent {Screen.instance} calls
236
+ # return the same object.
237
+ def self.fake: () -> FakeScreen
238
+
239
+ def close: () -> void
240
+
241
+ def self.close: () -> void
242
+
243
+ # Prints given strings.
244
+ #
245
+ # _@param_ `args` — stuff to print.
246
+ def print: (*String args) -> void
247
+
248
+ # Repaints the screen; tries to be as effective as possible, by only
249
+ # considering invalidated windows.
250
+ def repaint: () -> void
251
+
252
+ # Returns the absolute screen coordinates where the hardware cursor should
253
+ # sit, or nil if it should be hidden. Only the {#focused} component owns
254
+ # the cursor: there can be multiple active components (the focus path),
255
+ # but only one focused.
256
+ def cursor_position: () -> Point?
257
+
258
+ # Collects a component and all its descendants in tree order
259
+ # (parent before children).
260
+ #
261
+ # _@param_ `component`
262
+ def collect_subtree: (Component component) -> ::Array[Component]
263
+
264
+ # Hides or moves the hardware cursor based on the current focus state.
265
+ def position_cursor: () -> void
266
+
267
+ # Recalculates positions of all windows, and repaints the scene.
268
+ # Automatically called whenever terminal size changes. Call when the app
269
+ # starts. {#size} provides correct size of the terminal.
270
+ def layout: () -> void
271
+
272
+ # Called after a popup is closed. Since a popup can cover any window,
273
+ # top-level component or other popups, we need to redraw everything.
274
+ def needs_full_repaint: () -> void
275
+
276
+ # A key has been pressed on the keyboard. Handle it, or forward to active
277
+ # window.
278
+ #
279
+ # _@param_ `key`
280
+ #
281
+ # _@return_ — true if the key was handled by some window.
282
+ def handle_key: (String key) -> bool
283
+
284
+ # Finds target window and calls {Component::Window#handle_mouse}.
285
+ #
286
+ # _@param_ `event`
287
+ def handle_mouse: (MouseEvent event) -> void
288
+
289
+ def event_loop: () -> void
290
+
291
+ # _@return_ — the structural root of the component tree.
292
+ attr_reader pane: ScreenPane
293
+
294
+ # Handler invoked when a {StandardError} escapes an event handler inside
295
+ # the event loop (e.g. a {Component::TextField}'s `on_change` raises).
296
+ #
297
+ # The default re-raises, so the exception propagates out of
298
+ # {#run_event_loop} and crashes the script with a stacktrace — unhandled
299
+ # exceptions are bugs and should be surfaced loudly.
300
+ #
301
+ # Replace it when the host has somewhere visible to put errors, e.g. a
302
+ # {Component::LogWindow} wired to {Tuile.logger}:
303
+ #
304
+ # screen.on_error = lambda do |e|
305
+ # Tuile.logger.error("#{e.class}: #{e.message}\n#{e.backtrace&.join("\n")}")
306
+ # end
307
+ #
308
+ # The handler runs on the event-loop thread with the UI lock held.
309
+ # Returning normally keeps the loop alive; raising from within the handler
310
+ # tears the loop down and propagates out of {#run_event_loop}.
311
+ #
312
+ # _@return_ — one-arg callable receiving the {StandardError} instance.
313
+ attr_accessor on_error: Proc
314
+
315
+ # _@return_ — current screen size.
316
+ attr_reader size: Size
317
+
318
+ # _@return_ — the event queue.
319
+ attr_reader event_queue: EventQueue
320
+
321
+ # _@return_ — currently focused component.
322
+ attr_accessor focused: Component?
323
+ end
324
+
325
+ # A UI component which is positioned on the screen and draws characters into
326
+ # its bounding rectangle (in {#repaint}).
327
+ #
328
+ # Component is considered invisible if {#rect} is empty or one of left/top is
329
+ # negative. The component won't draw when invisible.
330
+ class Component
331
+ def initialize: () -> void
332
+
333
+ # _@return_ — the screen which owns this component.
334
+ def screen: () -> Screen
335
+
336
+ # Focuses this component. Equivalent to `screen.focused = self`.
337
+ def focus: () -> void
338
+
339
+ # Repaints the component. Default implementation does nothing.
340
+ #
341
+ # The component must fully draw over {#rect}, and must not draw outside of
342
+ # {#rect}.
343
+ #
344
+ # Tip: use {#clear_background} to clear component background before painting.
345
+ def repaint: () -> void
346
+
347
+ # Called when a character is pressed on the keyboard.
348
+ #
349
+ # Also called for inactive components. Inactive component should just return
350
+ # false.
351
+ #
352
+ # Default implementation searches for a component with {#key_shortcut} and
353
+ # focuses it. The shortcut search is suppressed while the focused component
354
+ # owns the hardware cursor (e.g. a {Component::TextField} the user is
355
+ # typing into) so that hotkeys don't steal printable keys from the editor.
356
+ #
357
+ # _@param_ `key` — a key.
358
+ #
359
+ # _@return_ — true if the key was handled, false if not.
360
+ def handle_key: (String key) -> bool
361
+
362
+ # _@param_ `key` — keyboard key to look up.
363
+ #
364
+ # _@return_ — the component whose {#key_shortcut} matches `key`,
365
+ # or nil.
366
+ def find_shortcut_component: (String key) -> Component?
367
+
368
+ # Handles mouse event. Default implementation focuses this component when
369
+ # clicked (if {#focusable?}).
370
+ #
371
+ # _@param_ `event`
372
+ def handle_mouse: (MouseEvent event) -> void
373
+
374
+ # _@return_ — true if the component is on the active chain — i.e. it
375
+ # is the focused component or an ancestor of it. Set by {Screen#focused=}.
376
+ def active?: () -> bool
377
+
378
+ # _@param_ `active` — true if active. Set by {Screen#focused=} as it marks the focus chain (root → focused); not meant to be called directly.
379
+ def active=: (bool active) -> void
380
+
381
+ # Whether this component is a valid focus target. `false` by default —
382
+ # passive components like {Label} are decoration and don't accept focus.
383
+ # The flag gates click-to-focus ({#handle_mouse}) and the focus-cascade
384
+ # in container components ({HasContent#on_focus}, {Layout#on_focus}).
385
+ # Independent from {#active?}: every component carries the active flag, but
386
+ # only focusable ones can become a focus target that puts themselves and
387
+ # their ancestors on the active chain.
388
+ #
389
+ # _@return_ — true if this component can be focused.
390
+ def focusable?: () -> bool
391
+
392
+ # _@return_ — the distance from the root component; 0 if {#parent}
393
+ # is nil.
394
+ def depth: () -> Integer
395
+
396
+ # _@return_ — the root component of this component hierarchy.
397
+ def root: () -> Component
398
+
399
+ # List of child components, defaults to an empty array.
400
+ #
401
+ # _@return_ — child components. Must not be mutated! May be
402
+ # empty.
403
+ def children: () -> ::Array[Component]
404
+
405
+ # Calls block for this component and for every descendant component.
406
+ def on_tree: () ?{ (Component component) -> void } -> void
407
+
408
+ # Called when the component receives focus.
409
+ def on_focus: () -> void
410
+
411
+ # _@return_ — true if this component's tree is currently mounted on
412
+ # the {Screen}, i.e. its root is the {ScreenPane}.
413
+ def attached?: () -> bool
414
+
415
+ # Called by container components after `child` has been detached from
416
+ # `self.children` (its `parent` is already nil and it is no longer in the
417
+ # children list). Default behavior repairs dangling focus: if the focused
418
+ # component lived inside the removed subtree, focus shifts to `self` so the
419
+ # cursor doesn't dangle on a detached component. No-op if `self` is not
420
+ # attached to the screen — focus state in a detached subtree is moot.
421
+ #
422
+ # _@param_ `child` — the just-detached child.
423
+ def on_child_removed: (Component child) -> void
424
+
425
+ # The {Size} big enough to show the entire component contents without
426
+ # scrolling. Plain components have no intrinsic content and report
427
+ # {Size::ZERO}; container/decorative components (e.g. {Label}, {List},
428
+ # {Layout}, {Window}) override this to fold in their content's natural
429
+ # extent. Used by callers like {Component::Popup} to auto-size to
430
+ # whatever content was assigned, regardless of its concrete type.
431
+ def content_size: () -> Size
432
+
433
+ # Where the hardware terminal cursor should sit when this component is the
434
+ # cursor owner. Returns `nil` to indicate the cursor should be hidden. The
435
+ # {Screen} positions the hardware cursor after each repaint cycle by
436
+ # consulting the {Screen#focused} component only.
437
+ #
438
+ # _@return_ — absolute screen coordinates, or nil to hide.
439
+ def cursor_position: () -> Point?
440
+
441
+ # _@return_ — formatted keyboard hint surfaced in the status bar by
442
+ # {Screen} when this component is the active tiled window or the
443
+ # topmost popup. Empty by default; override to advertise shortcuts.
444
+ def keyboard_hint: () -> String
445
+
446
+ # Called whenever the component width changes. Does nothing by default.
447
+ def on_width_changed: () -> void
448
+
449
+ # Invalidates the component: {Screen} records this component as
450
+ # needs-repaint and once all events are processed, will call {#repaint}.
451
+ def invalidate: () -> void
452
+
453
+ # Clears the background: prints spaces into all characters occupied by the
454
+ # component's rect.
455
+ def clear_background: () -> void
456
+
457
+ # _@return_ — the rectangle the component occupies on screen.
458
+ attr_accessor rect: Rect
459
+
460
+ # A global keyboard shortcut. When pressed, will focus this component.
461
+ #
462
+ # _@return_ — shortcut, `nil` by default.
463
+ attr_accessor key_shortcut: String?
464
+
465
+ # _@return_ — the parent component or nil if the component has
466
+ # no parent.
467
+ attr_accessor parent: Component?
468
+
469
+ # A scrollable list of String items with cursor support.
470
+ #
471
+ # Items are lines painted directly into the component's {#rect}. Lines are
472
+ # automatically clipped horizontally. Vertical scrolling is supported via
473
+ # {#top_line}; the list can also automatically scroll to the bottom if
474
+ # {#auto_scroll} is enabled.
475
+ #
476
+ # Cursor is supported; call {#cursor=} to change cursor behavior. The
477
+ # cursor responds to arrows, `jk`, Home/End, Ctrl+U/D and scrolls the list
478
+ # automatically.
479
+ class List < Component
480
+ def initialize: () -> void
481
+
482
+ # Sets new lines. Each entry is coerced via `#to_s`, split on `\n` into
483
+ # separate lines, and trailing whitespace stripped — symmetric with
484
+ # {#add_lines}, so the stored `@lines` is always `Array<String>`.
485
+ #
486
+ # _@param_ `lines` — new lines. Entries need only respond to `#to_s`.
487
+ def lines=: (::Array[untyped] lines) -> void
488
+
489
+ # Without a block, returns the current lines. With a block, fully
490
+ # re-populates the list:
491
+ # ```ruby
492
+ # list.lines do |buffer|
493
+ # buffer << "Hello!"
494
+ # end
495
+ # ```
496
+ #
497
+ # _@return_ — current lines (when called without a block).
498
+ def lines: () ?{ (::Array[String] buffer) -> void } -> ::Array[String]
499
+
500
+ # Adds a line.
501
+ #
502
+ # _@param_ `line`
503
+ def add_line: (String line) -> void
504
+
505
+ # Appends given lines. Each entry is coerced via `#to_s`, split on `\n`
506
+ # into separate lines, and trailing whitespace stripped — symmetric with
507
+ # {#lines=}.
508
+ #
509
+ # _@param_ `lines` — entries need only respond to `#to_s`.
510
+ def add_lines: (::Array[untyped] lines) -> void
511
+
512
+ def content_size: () -> Size
513
+
514
+ def focusable?: () -> bool
515
+
516
+ # _@param_ `key` — a key.
517
+ #
518
+ # _@return_ — true if the key was handled.
519
+ def handle_key: (String key) -> bool
520
+
521
+ # Moves the cursor to the next line whose text contains `query`
522
+ # (case-insensitive substring match). Search wraps around the end of the
523
+ # list. Only lines reachable by the current {#cursor} are considered.
524
+ #
525
+ # _@param_ `query` — substring to match. Empty query never matches.
526
+ #
527
+ # _@param_ `include_current` — when true, the current cursor position is eligible (useful when the query has just changed and the current line may still match); when false, the search starts after the current position (useful for "find next" key bindings that should advance past the current).
528
+ #
529
+ # _@return_ — true if a match was found.
530
+ def select_next: (String query, ?include_current: bool) -> bool
531
+
532
+ # Mirror of {#select_next} that walks the list backwards.
533
+ #
534
+ # _@param_ `query`
535
+ #
536
+ # _@param_ `include_current`
537
+ #
538
+ # _@return_ — true if a match was found.
539
+ def select_prev: (String query, ?include_current: bool) -> bool
540
+
541
+ # _@param_ `event`
542
+ def handle_mouse: (MouseEvent event) -> void
543
+
544
+ # Paints the list items into {#rect}.
545
+ def repaint: () -> void
546
+
547
+ # _@return_ — true if the cursor sits on a real content line.
548
+ def cursor_on_item?: () -> bool
549
+
550
+ # Calls {#on_item_chosen} with the cursor's current `(index, line)`.
551
+ # Caller must ensure {#cursor_on_item?}.
552
+ def fire_item_chosen: () -> void
553
+
554
+ # _@param_ `query`
555
+ #
556
+ # _@param_ `include_current`
557
+ #
558
+ # _@param_ `reverse`
559
+ def search_and_go: (String query, include_current: bool, reverse: bool) -> bool
560
+
561
+ # Rotates `candidates` (sorted ascending) so iteration starts from the
562
+ # position appropriate for "find next" / "find prev" with optional
563
+ # inclusion of the current.
564
+ #
565
+ # _@param_ `candidates`
566
+ #
567
+ # _@param_ `current`
568
+ #
569
+ # _@param_ `include_current`
570
+ #
571
+ # _@param_ `reverse`
572
+ def order_for_search: (
573
+ ::Array[Integer] candidates,
574
+ Integer current,
575
+ include_current: bool,
576
+ reverse: bool
577
+ ) -> ::Array[Integer]
578
+
579
+ # Scrolls the viewport so the cursor is visible.
580
+ def move_viewport_to_cursor: () -> void
581
+
582
+ # _@return_ — the max value of {#top_line}.
583
+ def top_line_max: () -> Integer
584
+
585
+ # _@return_ — the number of visible lines.
586
+ def viewport_lines: () -> Integer
587
+
588
+ # Scrolls the list.
589
+ #
590
+ # _@param_ `delta` — negative scrolls up, positive scrolls down.
591
+ def move_top_line_by: (Integer delta) -> void
592
+
593
+ # If auto-scrolling, recalculate the top line.
594
+ def update_top_line_if_auto_scroll: () -> void
595
+
596
+ # _@return_ — whether the scrollbar should be drawn right now.
597
+ def scrollbar_visible?: () -> bool
598
+
599
+ # Trims string exactly to `width` columns.
600
+ #
601
+ # _@param_ `str`
602
+ #
603
+ # _@param_ `width`
604
+ def trim_to: (String str, Integer width) -> String
605
+
606
+ # _@param_ `index` — 0-based index into {#lines}.
607
+ #
608
+ # _@param_ `row_in_viewport` — 0-based row within the viewport.
609
+ #
610
+ # _@param_ `width` — number of columns the line should occupy.
611
+ #
612
+ # _@param_ `scrollbar` — scrollbar instance, or nil if not shown.
613
+ #
614
+ # _@return_ — paintable line exactly `width` columns wide;
615
+ # highlighted if cursor is here.
616
+ def paintable_line: (
617
+ Integer index,
618
+ Integer row_in_viewport,
619
+ Integer width,
620
+ VerticalScrollBar? scrollbar
621
+ ) -> String
622
+
623
+ # _@return_ — callback fired when an item is chosen — by pressing
624
+ # Enter on the cursor's item, or by left-clicking an item. Called as
625
+ # `proc.call(index, line)` with the chosen 0-based index and its line.
626
+ # Never fires when the cursor's position is outside the content (e.g.
627
+ # {Cursor::None}, or empty content).
628
+ attr_accessor on_item_chosen: Proc?
629
+
630
+ # _@return_ — if true and a line is added or new content is set,
631
+ # auto-scrolls to the bottom.
632
+ attr_accessor auto_scroll: bool
633
+
634
+ # _@return_ — top line of the viewport. 0 or positive.
635
+ attr_accessor top_line: Integer
636
+
637
+ # _@return_ — the list's cursor.
638
+ attr_accessor cursor: Cursor
639
+
640
+ # _@return_ — scrollbar visibility: `:gone` or `:visible`.
641
+ attr_accessor scrollbar_visibility: Symbol
642
+
643
+ # _@return_ — when true, the cursor highlight is painted even while
644
+ # the list is inactive (e.g. when focus is on a sibling search field).
645
+ # Defaults to false.
646
+ attr_accessor show_cursor_when_inactive: bool
647
+
648
+ # Tracks cursor position within the list.
649
+ class Cursor
650
+ # _@param_ `position` — the initial cursor position.
651
+ def initialize: (?position: Integer) -> void
652
+
653
+ # _@param_ `line_count` — number of lines in the list.
654
+ #
655
+ # _@return_ — positions the cursor can land on, in
656
+ # ascending order.
657
+ def candidate_positions: (Integer line_count) -> ::Array[Integer]
658
+
659
+ # _@param_ `key` — pressed keyboard key.
660
+ #
661
+ # _@param_ `line_count` — number of lines in the list.
662
+ #
663
+ # _@param_ `viewport_lines` — number of visible lines.
664
+ #
665
+ # _@return_ — true if the cursor moved.
666
+ def handle_key: (String key, Integer line_count, Integer viewport_lines) -> bool
667
+
668
+ # _@param_ `line` — cursor is hovering over this line.
669
+ #
670
+ # _@param_ `event` — the event.
671
+ #
672
+ # _@param_ `line_count` — number of lines in the list.
673
+ #
674
+ # _@return_ — true if the event was handled.
675
+ def handle_mouse: (Integer line, MouseEvent event, Integer line_count) -> bool
676
+
677
+ # Moves the cursor to the new position. Public only because of testing.
678
+ #
679
+ # _@param_ `new_position` — new 0-based cursor position.
680
+ #
681
+ # _@return_ — true if the position changed.
682
+ def go: (Integer new_position) -> bool
683
+
684
+ # _@param_ `lines`
685
+ #
686
+ # _@param_ `line_count`
687
+ def go_down_by: (Integer lines, Integer line_count) -> bool
688
+
689
+ # _@param_ `lines`
690
+ def go_up_by: (Integer lines) -> bool
691
+
692
+ def go_to_first: () -> bool
693
+
694
+ # _@param_ `line_count`
695
+ def go_to_last: (Integer line_count) -> bool
696
+
697
+ # _@return_ — 0-based line index of the current cursor position.
698
+ attr_reader position: Integer
699
+
700
+ # No cursor — cursor is disabled.
701
+ class None < Cursor
702
+ def initialize: () -> void
703
+
704
+ # _@param_ `_key`
705
+ #
706
+ # _@param_ `_line_count`
707
+ #
708
+ # _@param_ `_viewport_lines`
709
+ def handle_key: (String _key, Integer _line_count, Integer _viewport_lines) -> bool
710
+
711
+ # _@param_ `_line`
712
+ #
713
+ # _@param_ `_event`
714
+ #
715
+ # _@param_ `_line_count`
716
+ def handle_mouse: (Integer _line, MouseEvent _event, Integer _line_count) -> bool
717
+
718
+ # _@param_ `_line_count`
719
+ def candidate_positions: (Integer _line_count) -> ::Array[Integer]
720
+ end
721
+
722
+ # Cursor which can only land on specific allowed lines.
723
+ class Limited < Cursor
724
+ # _@param_ `positions` — allowed positions. Must not be empty.
725
+ #
726
+ # _@param_ `position` — initial position.
727
+ def initialize: (::Array[Integer] positions, ?position: Integer) -> void
728
+
729
+ # _@param_ `line`
730
+ #
731
+ # _@param_ `event`
732
+ #
733
+ # _@param_ `_line_count`
734
+ def handle_mouse: (Integer line, MouseEvent event, Integer _line_count) -> bool
735
+
736
+ # _@param_ `line_count`
737
+ def candidate_positions: (Integer line_count) -> ::Array[Integer]
738
+
739
+ # _@param_ `lines`
740
+ #
741
+ # _@param_ `line_count`
742
+ def go_down_by: (Integer lines, Integer line_count) -> bool
743
+
744
+ # _@param_ `lines`
745
+ def go_up_by: (Integer lines) -> bool
746
+
747
+ def go_to_first: () -> bool
748
+
749
+ # _@param_ `_line_count`
750
+ def go_to_last: (Integer _line_count) -> bool
751
+ end
752
+ end
753
+ end
754
+
755
+ # A label which shows static text. No word-wrapping; clips long lines.
756
+ class Label < Component
757
+ def initialize: () -> void
758
+
759
+ # _@param_ `text` — draws this text. May contain ANSI formatting. Clipped automatically.
760
+ def text=: (String? text) -> void
761
+
762
+ def content_size: () -> Size
763
+
764
+ def repaint: () -> void
765
+
766
+ def on_width_changed: () -> void
767
+
768
+ def update_clipped_text: () -> void
769
+ end
770
+
771
+ # A modal overlay that wraps any {Component} as its content. Popup itself
772
+ # paints nothing — it's a transparent host that handles modality
773
+ # ({#open} / {#close} / {#open?}, ESC/q to close), centers itself on the
774
+ # screen, and auto-sizes to the wrapped content.
775
+ #
776
+ # The wrapped content fills the popup's full {#rect}; if you want a frame
777
+ # and caption, wrap a {Component::Window} (or any subclass — including
778
+ # {Component::LogWindow}) and let it draw its own border:
779
+ #
780
+ # window = Component::Window.new("Help")
781
+ # window.content = Component::List.new.tap { _1.lines = lines }
782
+ # Component::Popup.new(content: window).open
783
+ #
784
+ # Bare content also works (a {Component::Label}, a {Component::List}…), in
785
+ # which case the popup is borderless.
786
+ #
787
+ # `q` and ESC close the popup. Any nested {Component::TextField} that owns
788
+ # the hardware cursor swallows printable keys first via the standard
789
+ # cursor-owner suppression in {Component#handle_key}, so typing `q` into a
790
+ # text field doesn't dismiss the popup.
791
+ class Popup < Component
792
+ include Tuile::Component::HasContent
793
+
794
+ # _@param_ `content` — initial content; can be set later via {#content=}. When provided here, the popup auto-sizes to fit.
795
+ def initialize: (?content: Component?) -> void
796
+
797
+ def focusable?: () -> bool
798
+
799
+ # Mounts this popup on the {Screen}.
800
+ def open: () -> void
801
+
802
+ # Constructs and opens a popup in one call.
803
+ #
804
+ # _@param_ `content`
805
+ #
806
+ # _@return_ — the opened popup.
807
+ def self.open: (?content: Component?) -> Popup
808
+
809
+ # Removes this popup from the {Screen}. No-op if not currently open.
810
+ def close: () -> void
811
+
812
+ # _@return_ — true if this popup is currently mounted on the screen.
813
+ def open?: () -> bool
814
+
815
+ # Recenters the popup on the screen, preserving its current width/height.
816
+ # Called automatically by the screen's layout pass and by {#content=}
817
+ # when the popup is open.
818
+ def center: () -> void
819
+
820
+ # _@return_ — max height the popup will grow to fit its content,
821
+ # defaults to 12. Override in a subclass to allow taller popups.
822
+ def max_height: () -> Integer
823
+
824
+ # Sets the popup's content and auto-sizes the popup to fit.
825
+ #
826
+ # _@param_ `new_content`
827
+ def content=: (Component? new_content) -> void
828
+
829
+ # Hint for the status bar: own "q Close" plus the wrapped content's hint.
830
+ def keyboard_hint: () -> String
831
+
832
+ # _@param_ `key`
833
+ #
834
+ # _@return_ — true if the key was handled.
835
+ def handle_key: (String key) -> bool
836
+
837
+ # Content fills the popup's full rect — Popup has no border to subtract.
838
+ #
839
+ # _@param_ `content`
840
+ def layout: (Component content) -> void
841
+
842
+ # Recompute width/height from {#content}'s natural size and recenter
843
+ # if currently open. Called whenever content is (re)assigned.
844
+ def update_rect: () -> void
845
+
846
+ # _@param_ `event`
847
+ def handle_mouse: (MouseEvent event) -> void
848
+
849
+ def children: () -> ::Array[Component]
850
+
851
+ # _@param_ `rect`
852
+ def rect=: (Rect rect) -> void
853
+
854
+ def on_focus: () -> void
855
+ end
856
+
857
+ # A layout doesn't paint anything by itself: its job is to position child
858
+ # components.
859
+ #
860
+ # All children must completely cover the contents of a layout: that way,
861
+ # the layout itself doesn't have to draw and no clipping algorithm is
862
+ # necessary.
863
+ class Layout < Component
864
+ def initialize: () -> void
865
+
866
+ def children: () -> ::Array[Component]
867
+
868
+ # Adds a child component to this layout.
869
+ #
870
+ # _@param_ `child`
871
+ def add: ((Component | ::Array[Component]) child) -> void
872
+
873
+ # _@param_ `child`
874
+ def remove: (Component child) -> void
875
+
876
+ def content_size: () -> Size
877
+
878
+ def repaint: () -> void
879
+
880
+ # Dispatches the event to the child under the mouse cursor.
881
+ #
882
+ # _@param_ `event`
883
+ def handle_mouse: (MouseEvent event) -> void
884
+
885
+ # Called when a character is pressed on the keyboard.
886
+ #
887
+ # _@param_ `key` — a key.
888
+ #
889
+ # _@return_ — true if the key was handled, false if not.
890
+ def handle_key: (String key) -> bool
891
+
892
+ def on_focus: () -> void
893
+
894
+ # Absolute layout. Extend this class, register any children, and
895
+ # override {Component#rect=} to reposition the children.
896
+ class Absolute < Layout
897
+ end
898
+ end
899
+
900
+ # A window with a frame, a {#caption} and a content {Component}. Doesn't
901
+ # support overlapping with other windows: it paints its entire contents and
902
+ # doesn't clip if there are other overlapping windows.
903
+ #
904
+ # The window's `content` is unset by default; assign one via {#content=}.
905
+ #
906
+ # Window is considered invisible if {#rect} is empty or one of left/top is
907
+ # negative. The window won't draw when invisible.
908
+ class Window < Component
909
+ include Tuile::Component::HasContent
910
+
911
+ # _@param_ `caption`
912
+ def initialize: (?String caption) -> void
913
+
914
+ def focusable?: () -> bool
915
+
916
+ def children: () -> ::Array[Component]
917
+
918
+ # _@param_ `key`
919
+ def handle_key: (String key) -> bool
920
+
921
+ # _@param_ `event`
922
+ def handle_mouse: (MouseEvent event) -> void
923
+
924
+ # _@param_ `new_rect`
925
+ def rect=: (Rect new_rect) -> void
926
+
927
+ # _@param_ `value`
928
+ def scrollbar=: (bool value) -> void
929
+
930
+ # _@return_ — the size needed to fit the window's content, footer
931
+ # (width only — footer overlays the bottom border), and caption,
932
+ # plus the 2-character border. Returns {Size}`.new(2, 2)` when the
933
+ # window has no content, footer, or caption.
934
+ def content_size: () -> Size
935
+
936
+ # _@return_ — true if {#rect} is off screen and the window won't
937
+ # paint.
938
+ def visible?: () -> bool
939
+
940
+ # Fully repaints the window: both frame and contents.
941
+ def repaint: () -> void
942
+
943
+ # _@param_ `key`
944
+ def key_shortcut=: (String? key) -> void
945
+
946
+ # _@param_ `content`
947
+ def layout: (Component content) -> void
948
+
949
+ # Paints the window border.
950
+ def repaint_border: () -> void
951
+
952
+ # The caption text as it appears in the rendered border, including the
953
+ # shortcut prefix when {#key_shortcut} is set.
954
+ def frame_caption: () -> String
955
+
956
+ # Builds the border as a single string with embedded cursor-positioning
957
+ # escapes, mirroring the layout {TTY::Box.frame} used to produce. Title
958
+ # is clipped to fit the inner width so the box never overflows {#rect}.
959
+ #
960
+ # _@param_ `caption`
961
+ def build_frame: (String caption) -> String
962
+
963
+ def layout_footer: () -> void
964
+
965
+ def on_focus: () -> void
966
+
967
+ # _@return_ — optional component overlaying the bottom border
968
+ # row.
969
+ attr_accessor footer: Component?
970
+
971
+ # _@return_ — the current caption, empty by default.
972
+ attr_accessor caption: String
973
+ end
974
+
975
+ # Shows a log. Construct your logger pointed at a {LogWindow::IO} to route
976
+ # log lines into this window:
977
+ #
978
+ # log_window = Tuile::Component::LogWindow.new
979
+ # logger = Logger.new(Tuile::Component::LogWindow::IO.new(log_window))
980
+ #
981
+ # Any logger that writes formatted lines to an IO works the same way —
982
+ # for example `TTY::Logger` configured with the `:console` handler and
983
+ # `output: LogWindow::IO.new(window)`.
984
+ class LogWindow < Tuile::Component::Window
985
+ # _@param_ `caption`
986
+ def initialize: (?String caption) -> void
987
+
988
+ # IO-shaped adapter that forwards each log line to the owning {LogWindow}.
989
+ # Implements both {#write} (stdlib `Logger`) and {#puts} (loggers that
990
+ # call `output.puts`, e.g. `TTY::Logger`).
991
+ class IO
992
+ # _@param_ `window`
993
+ def initialize: (LogWindow window) -> void
994
+
995
+ # _@param_ `string`
996
+ def write: (String string) -> void
997
+
998
+ # _@param_ `string`
999
+ def puts: (String string) -> void
1000
+
1001
+ # Stdlib `Logger` only treats an object as an IO target when it
1002
+ # responds to both {#write} and {#close}; otherwise it tries to
1003
+ # interpret it as a filename. This is a no-op.
1004
+ def close: () -> void
1005
+ end
1006
+ end
1007
+
1008
+ # A single-line text input field with hardware-cursor caret.
1009
+ #
1010
+ # The field does not scroll. Any keystroke that would make {#text} longer
1011
+ # than `rect.width - 1` (the last column is reserved for the caret past the
1012
+ # last char) is rejected.
1013
+ #
1014
+ # The caret is a logical index in `0..text.length`. The hardware cursor is
1015
+ # positioned by {Screen} after each repaint cycle when this component is
1016
+ # focused; see {Component#cursor_position}.
1017
+ class TextField < Component
1018
+ def initialize: () -> void
1019
+
1020
+ def focusable?: () -> bool
1021
+
1022
+ def cursor_position: () -> Point?
1023
+
1024
+ # _@param_ `key`
1025
+ def handle_key: (String key) -> bool
1026
+
1027
+ # _@param_ `event`
1028
+ def handle_mouse: (MouseEvent event) -> void
1029
+
1030
+ def repaint: () -> void
1031
+
1032
+ def on_width_changed: () -> void
1033
+
1034
+ # Maximum number of characters {#text} can hold given current width.
1035
+ def max_text_length: () -> Integer
1036
+
1037
+ # _@param_ `char`
1038
+ def insert: (String char) -> bool
1039
+
1040
+ def delete_before_caret: () -> void
1041
+
1042
+ def delete_at_caret: () -> void
1043
+
1044
+ # _@param_ `key`
1045
+ def printable?: (String key) -> bool
1046
+
1047
+ # _@return_ — current text contents.
1048
+ attr_accessor text: String
1049
+
1050
+ # _@return_ — caret index in `0..text.length`.
1051
+ attr_accessor caret: Integer
1052
+
1053
+ # Optional callback fired when ESC is pressed. When set, ESC is consumed
1054
+ # by the field; when nil, ESC falls through to the parent (default
1055
+ # behavior).
1056
+ #
1057
+ # _@return_ — no-arg callable, or nil.
1058
+ attr_accessor on_escape: (Proc | Method)?
1059
+
1060
+ # Optional callback fired whenever {#text} changes. Receives the new text
1061
+ # as a single argument. Not fired by {#caret=} (text unchanged) and not
1062
+ # fired when a setter is a no-op.
1063
+ #
1064
+ # _@return_ — one-arg callable, or nil.
1065
+ attr_accessor on_change: (Proc | Method)?
1066
+
1067
+ # Optional callback fired when the UP arrow key is pressed. When set, UP
1068
+ # is consumed by the field; when nil, UP falls through to the parent
1069
+ # (default behavior). Only triggered by {Keys::UP_ARROW}, not by `k`,
1070
+ # since `k` is a printable character inserted into {#text}.
1071
+ #
1072
+ # _@return_ — no-arg callable, or nil.
1073
+ attr_accessor on_key_up: (Proc | Method)?
1074
+
1075
+ # Optional callback fired when the DOWN arrow key is pressed. When set,
1076
+ # DOWN is consumed by the field; when nil, DOWN falls through to the
1077
+ # parent (default behavior). Only triggered by {Keys::DOWN_ARROW}, not by
1078
+ # `j`, since `j` is a printable character inserted into {#text}.
1079
+ #
1080
+ # _@return_ — no-arg callable, or nil.
1081
+ attr_accessor on_key_down: (Proc | Method)?
1082
+
1083
+ # Optional callback fired when ENTER is pressed. When set, ENTER is
1084
+ # consumed by the field; when nil, ENTER falls through to the parent
1085
+ # (default behavior).
1086
+ #
1087
+ # _@return_ — no-arg callable, or nil.
1088
+ attr_accessor on_enter: (Proc | Method)?
1089
+ end
1090
+
1091
+ # A mixin interface for a component with one child tops. The host must
1092
+ # provide a protected `layout(content)` method which repositions the
1093
+ # content component; the mixin manages `@content` itself.
1094
+ module HasContent
1095
+ # _@param_ `key` — a key.
1096
+ #
1097
+ # _@return_ — true if the key was handled, false if not.
1098
+ def handle_key: (String key) -> bool
1099
+
1100
+ # _@param_ `event`
1101
+ def handle_mouse: (MouseEvent event) -> void
1102
+
1103
+ def children: () -> ::Array[Component]
1104
+
1105
+ # _@param_ `rect`
1106
+ def rect=: (Rect rect) -> void
1107
+
1108
+ def on_focus: () -> void
1109
+
1110
+ # _@return_ — the current content component.
1111
+ attr_accessor content: Component?
1112
+ end
1113
+
1114
+ # A {Window} preconfigured with a {List} of static lines. Useful for
1115
+ # showing read-only information.
1116
+ #
1117
+ # Usable tiled (just add to a {Layout}) or as a popup via {.open}, which
1118
+ # wraps it in a {Popup}.
1119
+ class InfoWindow < Tuile::Component::Window
1120
+ # _@param_ `caption`
1121
+ #
1122
+ # _@param_ `lines` — initial content; each entry may contain Rainbow formatting.
1123
+ def initialize: (?String caption, ?::Array[String] lines) -> void
1124
+
1125
+ # Opens the info window as a popup.
1126
+ #
1127
+ # _@param_ `caption`
1128
+ #
1129
+ # _@param_ `lines` — the content, may contain formatting.
1130
+ #
1131
+ # _@return_ — the opened popup.
1132
+ def self.open: (String caption, ::Array[String] lines) -> Popup
1133
+ end
1134
+
1135
+ # A {Window} that lists options identified by single keyboard keys, asks
1136
+ # the user to pick one, and fires a callback with the picked key.
1137
+ #
1138
+ # Usable tiled (just add to a {Layout} and read picks via the block) or
1139
+ # as a popup via {.open}, which wraps it in a {Popup} that closes itself
1140
+ # after a pick. ESC / `q` close without firing the callback.
1141
+ class PickerWindow < Tuile::Component::Window
1142
+ MAX_ITEMS: Integer
1143
+
1144
+ # _@param_ `caption` — the window caption.
1145
+ #
1146
+ # _@param_ `options` — pairs of keyboard key and option caption. No Rainbow formatting must be used.
1147
+ def initialize: (String caption, ::Array[[String, String]] options) ?{ (String key) -> void } -> void
1148
+
1149
+ # _@param_ `key`
1150
+ def handle_key: (String key) -> bool
1151
+
1152
+ def keyboard_hint: () -> String
1153
+
1154
+ # Opens a picker as a popup. Picking an option fires `block`, then
1155
+ # closes the popup; ESC / `q` close without firing `block`.
1156
+ #
1157
+ # _@param_ `caption`
1158
+ #
1159
+ # _@param_ `options`
1160
+ #
1161
+ # _@return_ — the wrapping popup.
1162
+ def self.open: (String caption, ::Array[[String, String]] options) ?{ (String key) -> void } -> Popup
1163
+
1164
+ # _@param_ `key`
1165
+ def select_option: (String key) -> void
1166
+
1167
+ # Callback invoked after the user picks an option (after the block
1168
+ # fires). The {Popup} returned by {.open} sets this to its own `close`.
1169
+ attr_accessor on_pick: Proc?
1170
+
1171
+ # One picker option.
1172
+ #
1173
+ # @!attribute [r] key
1174
+ # @return [String] the keyboard key that picks this option.
1175
+ # @!attribute [r] caption
1176
+ # @return [String] the option caption.
1177
+ class Option
1178
+ # _@return_ — the keyboard key that picks this option.
1179
+ attr_reader key: String
1180
+
1181
+ # _@return_ — the option caption.
1182
+ attr_reader caption: String
1183
+ end
1184
+ end
1185
+ end
1186
+
1187
+ # An event queue. The idea is that all UI-related updates run from the thread
1188
+ # which runs the event queue only; this removes any need for locking and/or
1189
+ # need for thread-safety mechanisms.
1190
+ #
1191
+ # Any events (keypress, timer, term resize – WINCH) are captured in background
1192
+ # threads; instead of processing the events directly the events are pushed
1193
+ # into the event queue: this causes the events to be processed centrally,
1194
+ # by a single thread only.
1195
+ class EventQueue
1196
+ # _@param_ `listen_for_keys` — if true, fires {KeyEvent}.
1197
+ def initialize: (?listen_for_keys: bool) -> void
1198
+
1199
+ # Posts event into the event queue. The event may be of any type. Since the
1200
+ # event is passed between threads, the event object should be frozen.
1201
+ #
1202
+ # The function may be called from any thread.
1203
+ #
1204
+ # _@param_ `event` — the event to post to the queue, should be frozen.
1205
+ def post: (Object event) -> void
1206
+
1207
+ # Submits block to be run in the event queue. Returns immediately.
1208
+ #
1209
+ # The function may be called from any thread.
1210
+ def submit: () -> void
1211
+
1212
+ # Awaits until the event queue is empty (all events have been processed).
1213
+ def await_empty: () -> void
1214
+
1215
+ # Runs the event loop and blocks. Must be run from at most one thread at the
1216
+ # same time. Blocks until some thread calls {#stop}. Calls block for all
1217
+ # events submitted via {#post}; the block is always called from the thread
1218
+ # running this function.
1219
+ #
1220
+ # Any exception raised by block is re-thrown, causing this function to
1221
+ # terminate.
1222
+ def run_loop: () ?{ (Object event) -> void } -> void
1223
+
1224
+ # _@return_ — true if this thread is running inside an event queue.
1225
+ def locked?: () -> bool
1226
+
1227
+ # Stops ongoing {#run_loop}. The stop may not be immediate: {#run_loop} may
1228
+ # process a bunch of events before terminating.
1229
+ #
1230
+ # Can be called from any thread, including the thread which runs the event
1231
+ # loop.
1232
+ def stop: () -> void
1233
+
1234
+ def event_loop: () -> void
1235
+
1236
+ # Starts listening for stdin, firing {KeyEvent} on keypress.
1237
+ def start_key_thread: () -> void
1238
+
1239
+ # Trap the WINCH signal (TTY resize signal) and fire {TTYSizeEvent}.
1240
+ def trap_winch: () -> void
1241
+
1242
+ # A keypress event. See {Keys} for a list of key codes.
1243
+ #
1244
+ # @!attribute [r] key
1245
+ # @return [String] key code.
1246
+ class KeyEvent
1247
+ # _@return_ — key code.
1248
+ attr_reader key: String
1249
+ end
1250
+
1251
+ # An error event, causes {EventQueue#run_loop} to throw `StandardError` with
1252
+ # {#error} as its origin.
1253
+ #
1254
+ # @!attribute [r] error
1255
+ # @return [StandardError] the underlying error.
1256
+ class ErrorEvent
1257
+ # _@return_ — the underlying error.
1258
+ attr_reader error: StandardError
1259
+ end
1260
+
1261
+ # TTY has been resized. Contains the current width and height of the TTY
1262
+ # terminal.
1263
+ #
1264
+ # @!attribute [r] width
1265
+ # @return [Integer] terminal width in columns.
1266
+ # @!attribute [r] height
1267
+ # @return [Integer] terminal height in rows.
1268
+ class TTYSizeEvent
1269
+ # _@param_ `width`
1270
+ #
1271
+ # _@param_ `height`
1272
+ def initialize: (width: Integer, height: Integer) -> void
1273
+
1274
+ # _@return_ — event with current TTY size.
1275
+ def self.create: () -> TTYSizeEvent
1276
+
1277
+ def size: () -> Size
1278
+
1279
+ # _@return_ — terminal width in columns.
1280
+ attr_reader width: Integer
1281
+
1282
+ # _@return_ — terminal height in rows.
1283
+ attr_reader height: Integer
1284
+ end
1285
+
1286
+ # Emitted once when the queue is cleared, all messages are processed and the
1287
+ # event loop will block waiting for more messages. Perfect time for
1288
+ # repainting windows.
1289
+ class EmptyQueueEvent
1290
+ include Singleton
1291
+ end
1292
+ end
1293
+
1294
+ # Testing only — a screen which doesn't paint anything and pretends that the
1295
+ # lock is held. This way, the TTY running the tests is not painted over.
1296
+ #
1297
+ # Intended for unit-testing individual components: instantiate a component,
1298
+ # mutate it, and assert against {#prints} or {#invalidated?}. It does not
1299
+ # run an event loop, so it is *not* suitable for system-testing whole apps
1300
+ # — for that, drive the real script through a PTY (see `spec/examples/`).
1301
+ #
1302
+ # Call {Screen.fake} to initialize the fake screen easily. Typical usage:
1303
+ #
1304
+ # before { Screen.fake }
1305
+ # after { Screen.close }
1306
+ #
1307
+ # it "paints its content" do
1308
+ # label = Component::Label.new.tap { |l| l.text = "hi" }
1309
+ # Screen.instance.content = Component::Window.new("Greeting").tap { |w| w.content = label }
1310
+ # Screen.instance.repaint
1311
+ # assert_includes Screen.instance.prints.join, "hi"
1312
+ # end
1313
+ class FakeScreen < Tuile::Screen
1314
+ def initialize: () -> void
1315
+
1316
+ def check_locked: () -> void
1317
+
1318
+ def clear: () -> void
1319
+
1320
+ # Doesn't print anything: collects all strings in {#prints}.
1321
+ #
1322
+ # _@param_ `args`
1323
+ def print: (*String args) -> void
1324
+
1325
+ # _@param_ `component` — the component to check.
1326
+ def invalidated?: (Component component) -> bool
1327
+
1328
+ def invalidated_clear: () -> void
1329
+
1330
+ # _@return_ — whatever {#print} printed so far.
1331
+ attr_reader prints: ::Array[String]
1332
+ end
1333
+
1334
+ # A mouse event.
1335
+ #
1336
+ # @!attribute [r] button
1337
+ # @return [Symbol, nil] one of `:left`, `:middle`, `:right`, `:scroll_up`,
1338
+ # `:scroll_down`; `nil` if not known.
1339
+ # @!attribute [r] x
1340
+ # @return [Integer] x coordinate, 0-based.
1341
+ # @!attribute [r] y
1342
+ # @return [Integer] y coordinate, 0-based.
1343
+ class MouseEvent
1344
+ # _@return_ — the event's position.
1345
+ def point: () -> Point
1346
+
1347
+ # Checks whether given key is a mouse event key
1348
+ #
1349
+ # _@param_ `key` — key read via {Keys.getkey}
1350
+ #
1351
+ # _@return_ — true if it is a mouse event
1352
+ def self.mouse_event?: (String key) -> bool
1353
+
1354
+ # _@param_ `key` — key read via {Keys.getkey}
1355
+ def self.parse: (String key) -> MouseEvent?
1356
+
1357
+ def self.start_tracking: () -> String
1358
+
1359
+ def self.stop_tracking: () -> String
1360
+
1361
+ # _@return_ — one of `:left`, `:middle`, `:right`, `:scroll_up`,
1362
+ # `:scroll_down`; `nil` if not known.
1363
+ attr_reader button: Symbol?
1364
+
1365
+ # _@return_ — x coordinate, 0-based.
1366
+ attr_reader x: Integer
1367
+
1368
+ # _@return_ — y coordinate, 0-based.
1369
+ attr_reader y: Integer
1370
+ end
1371
+
1372
+ # The structural root of the {Screen}'s component tree.
1373
+ #
1374
+ # {Screen} is a singleton runtime owner (event loop, lock, terminal IO,
1375
+ # invalidation set). All actual UI lives under a {ScreenPane}: the tiled
1376
+ # {#content}, the modal {#popups} stack, and the bottom {#status_bar}.
1377
+ # Putting them under a single Component parent gives focus traversal a real
1378
+ # root, makes {Component#attached?} a one-liner, and lets popup-focus repair
1379
+ # fall out of the standard {Component#on_child_removed} hook.
1380
+ #
1381
+ # The pane is not a {Component::Layout}: popups deliberately overlap content
1382
+ # (Z-ordered, full overdraw, no clipping) and key/mouse dispatch follows
1383
+ # modal-popup rules rather than active-child dispatch.
1384
+ class ScreenPane < Tuile::Component
1385
+ def initialize: () -> void
1386
+
1387
+ def focusable?: () -> bool
1388
+
1389
+ # Children for tree traversal: content first, popups in stacking order,
1390
+ # status bar last.
1391
+ def children: () -> ::Array[Component]
1392
+
1393
+ # Adds a popup, centers it, focuses it, and invalidates it for repaint.
1394
+ #
1395
+ # _@param_ `window`
1396
+ def add_popup: (Component::Popup window) -> void
1397
+
1398
+ # Removes a popup. If the popup held focus, focus shifts to the now-topmost
1399
+ # remaining popup, falling back to the focus snapshotted when the popup
1400
+ # was opened (if still attached), then to {#content}, then to nil.
1401
+ #
1402
+ # _@param_ `window`
1403
+ def remove_popup: (Component window) -> void
1404
+
1405
+ # _@param_ `window`
1406
+ #
1407
+ # _@return_ — true if this pane currently hosts the popup.
1408
+ def has_popup?: (Component window) -> bool
1409
+
1410
+ # Re-lays out children whenever the pane's own rect changes.
1411
+ #
1412
+ # _@param_ `new_rect`
1413
+ def rect=: (Rect new_rect) -> void
1414
+
1415
+ # Lays out content (full pane minus the bottom row) and the status bar
1416
+ # (bottom row). Popups self-position via {Component::Popup#center}.
1417
+ def layout: () -> void
1418
+
1419
+ # Pane paints nothing itself; its children paint over the entire rect.
1420
+ def repaint: () -> void
1421
+
1422
+ # Topmost popup is modal: it eats keys. Falls through to content only
1423
+ # when no popup is open.
1424
+ def handle_key: (String key) -> bool
1425
+
1426
+ # Mouse events check popups in reverse stacking order (topmost first), and
1427
+ # fall through to content only when no popup is hit *and* there are no
1428
+ # popups open. This preserves modal click-blocking: an open popup eats
1429
+ # clicks even outside its rect.
1430
+ #
1431
+ # _@param_ `event`
1432
+ def handle_mouse: (MouseEvent event) -> void
1433
+
1434
+ # Focus repair when a child detaches. Default {Component#on_child_removed}
1435
+ # would refocus to `self` (the pane), which isn't a useful focus target.
1436
+ # Instead, route focus to the now-topmost popup, then to the prior focus
1437
+ # snapshotted when this popup was opened (if still attached), then to
1438
+ # content, then nil.
1439
+ #
1440
+ # _@param_ `child`
1441
+ def on_child_removed: (Component child) -> void
1442
+
1443
+ # _@return_ — the tiled content component.
1444
+ attr_accessor content: Component?
1445
+
1446
+ # _@return_ — modal popups in stacking order; last is
1447
+ # topmost. The array must not be mutated by callers.
1448
+ attr_reader popups: ::Array[Component]
1449
+
1450
+ # _@return_ — the bottom status bar.
1451
+ attr_reader status_bar: Component::Label
1452
+ end
1453
+
1454
+ # A "synchronous" event queue – no loop is run, submitted blocks are run right
1455
+ # away and submitted events are thrown away. Intended for testing only.
1456
+ class FakeEventQueue
1457
+ def locked?: () -> bool
1458
+
1459
+ def stop: () -> void
1460
+
1461
+ def run_loop: () -> void
1462
+
1463
+ def await_empty: () -> void
1464
+
1465
+ def submit: () -> void
1466
+
1467
+ # _@param_ `event`
1468
+ def post: (Object event) -> void
1469
+ end
1470
+
1471
+ # A vertical scrollbar that computes which character to draw at each row.
1472
+ #
1473
+ # Uses `█` for the handle (filled track) and `░` for the empty track. There
1474
+ # are no up/down arrows; the full height is used as the track. Handle
1475
+ # geometry is precomputed in the constructor as {#handle_height},
1476
+ # {#handle_start}, and {#handle_end}.
1477
+ class VerticalScrollBar
1478
+ # _@param_ `height` — number of rows in the scrollbar (== viewport height).
1479
+ #
1480
+ # _@param_ `line_count` — total number of content lines.
1481
+ #
1482
+ # _@param_ `top_line` — index of the first visible content line.
1483
+ def initialize: (Integer height, line_count: Integer, top_line: Integer) -> void
1484
+
1485
+ # Returns the scrollbar character for the given viewport row.
1486
+ #
1487
+ # _@param_ `row_in_viewport` — 0-based row index within the viewport.
1488
+ #
1489
+ # _@return_ — single scrollbar character.
1490
+ def scrollbar_char: (Integer row_in_viewport) -> String
1491
+
1492
+ # _@return_ — number of track rows the handle occupies (height >= 1
1493
+ # only).
1494
+ attr_reader handle_height: Integer
1495
+
1496
+ # _@return_ — 0-based row where the handle starts (height >= 1 only).
1497
+ attr_reader handle_start: Integer
1498
+
1499
+ # _@return_ — 0-based row where the handle ends (height >= 1 only).
1500
+ attr_reader handle_end: Integer
1501
+ end
1502
+ end