tuile 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +51 -24
  4. data/book/01-first-app.md +186 -0
  5. data/book/02-repaint.md +177 -0
  6. data/book/03-layout.md +379 -0
  7. data/book/04-event-loop.md +209 -0
  8. data/book/05-focus.md +188 -0
  9. data/book/06-theming.md +250 -0
  10. data/book/07-components.md +231 -0
  11. data/book/08-testing.md +186 -0
  12. data/book/README.md +79 -0
  13. data/examples/hello_world.rb +1 -2
  14. data/examples/sampler.rb +109 -0
  15. data/lib/tuile/ansi.rb +16 -0
  16. data/lib/tuile/buffer.rb +481 -0
  17. data/lib/tuile/component/button.rb +3 -14
  18. data/lib/tuile/component/has_content.rb +0 -6
  19. data/lib/tuile/component/info_window.rb +4 -2
  20. data/lib/tuile/component/label.rb +15 -23
  21. data/lib/tuile/component/layout.rb +0 -21
  22. data/lib/tuile/component/list.rb +10 -37
  23. data/lib/tuile/component/log_window.rb +6 -5
  24. data/lib/tuile/component/picker_window.rb +4 -2
  25. data/lib/tuile/component/popup.rb +85 -55
  26. data/lib/tuile/component/text_area.rb +1 -1
  27. data/lib/tuile/component/text_field.rb +1 -1
  28. data/lib/tuile/component/text_input.rb +25 -9
  29. data/lib/tuile/component/text_view.rb +6 -30
  30. data/lib/tuile/component/window.rb +77 -112
  31. data/lib/tuile/component.rb +16 -71
  32. data/lib/tuile/event_queue.rb +31 -10
  33. data/lib/tuile/fake_event_queue.rb +21 -5
  34. data/lib/tuile/fake_screen.rb +14 -1
  35. data/lib/tuile/fraction.rb +46 -0
  36. data/lib/tuile/screen.rb +98 -105
  37. data/lib/tuile/screen_pane.rb +82 -19
  38. data/lib/tuile/styled_string.rb +40 -30
  39. data/lib/tuile/version.rb +1 -1
  40. data/sig/tuile.rbs +653 -282
  41. metadata +13 -3
  42. data/lib/tuile/sizing.rb +0 -59
@@ -0,0 +1,186 @@
1
+ # 8. Testing a Tuile app
2
+
3
+ Every chapter so far has, quietly, also been about this one. When chapter
4
+ 2 said components *invalidate* rather than paint, and write into a back
5
+ *buffer* rather than to the terminal — that was a testing decision as much
6
+ as a rendering one. When chapter 4 insisted everything runs on one thread
7
+ through a queue you could swap out — same. A Tuile app is testable without
8
+ a terminal not because someone bolted on a test mode, but because the
9
+ framework never actually needed the terminal. It needed a `Screen`, a
10
+ buffer, and an event queue, and each of those has an in-memory double that
11
+ behaves like the real thing minus the I/O.
12
+
13
+ So this chapter is the payoff. It shows how to exercise a UI in a plain
14
+ unit test — instantiate a component, poke it, and read back exactly what
15
+ it drew — and, when that isn't enough, how to drive the whole real script
16
+ through a pseudo-terminal.
17
+
18
+ ## The fake screen, and why it resets
19
+
20
+ The singleton `Screen` from chapter 4 is the thing tests replace. Two
21
+ class methods bracket every example:
22
+
23
+ ```ruby
24
+ before { Screen.fake }
25
+ after { Screen.close }
26
+ ```
27
+
28
+ `Screen.fake` installs a {Tuile::FakeScreen} as the process singleton — a
29
+ `Screen` subclass with the terminal amputated. It has a fixed 160×50
30
+ viewport (so geometry is deterministic, independent of whoever's terminal
31
+ runs the suite), it writes nothing to any TTY, its `check_locked` is a
32
+ no-op so you can mutate the UI freely from the test thread without holding
33
+ the UI lock, and its event queue is the synchronous {Tuile::FakeEventQueue}
34
+ (more on that below). It also pins the color scheme to `:dark`, skipping
35
+ the OSC 11 probe from chapter 6 — a probe would otherwise write an escape
36
+ query to the test runner's terminal and swallow its input.
37
+
38
+ `Screen.close` matters as much as `fake` does, and the reason is the
39
+ singleton itself. Because there is exactly one `Screen` per process
40
+ (chapter 4), a screen left standing after one example is the *same* screen
41
+ the next example sees — its tree, its focus, its invalidation set, all
42
+ leaked forward. `close` tears the singleton down so each example starts
43
+ from nothing. Skip the `after` and you get the classic singleton test
44
+ smell: passes in isolation, fails in suite, order-dependent. The pair is
45
+ not boilerplate you can trim.
46
+
47
+ ## Asserting what got painted
48
+
49
+ Here's where the back buffer earns its keep. Recall from chapter 2 that
50
+ components don't emit escape sequences — they write styled cells into
51
+ `Screen#buffer`, and only a flush turns that into wire bytes. In a test
52
+ that means the buffer *is* the rendered screen, sitting in memory, fully
53
+ inspectable, before any diffing or I/O. You assert against it directly.
54
+
55
+ The rhythm is: build the component, give it a `rect`, repaint, read the
56
+ buffer back over that rect.
57
+
58
+ ```ruby
59
+ label = Component::Label.new
60
+ label.rect = Rect.new(0, 0, 10, 1)
61
+ label.text = "hi"
62
+ label.repaint
63
+
64
+ assert_equal ["hi "], Screen.instance.buffer.region_text(label.rect)
65
+ ```
66
+
67
+ {Tuile::Buffer#region_text} returns the plain text of each row in the
68
+ rect, one string per row (trailing pad included — the label fills its
69
+ width). Its sibling {Tuile::Buffer#region_ansi} returns the same rows *with
70
+ their SGR styling* rendered back into ANSI, which is what you assert
71
+ against when the test is about *color* — that a focused field painted its
72
+ active-background, say, or that a theme flip changed a hint's hue. And
73
+ {Tuile::Buffer#cell} gives you a single cell's grapheme and style for a
74
+ pinpoint check. Everything is scoped to a `rect`, so you assert about a
75
+ component's own region without caring what surrounds it.
76
+
77
+ What you do *not* assert content against is `prints`. On a FakeScreen,
78
+ `prints` captures only what actually went "to the wire" — cursor
79
+ positioning, housekeeping escapes, and the assembled frame string. Content
80
+ lives in the buffer; cursor behavior lives in `prints`. Keeping the two
81
+ apart is deliberate, and mixing them up is the most common way a first
82
+ Tuile test goes wrong.
83
+
84
+ ## Driving the system
85
+
86
+ There are two altitudes at which you feed input, and picking the right one
87
+ is most of writing a good Tuile test.
88
+
89
+ **Low: call the component directly.** {Tuile::Component#handle_key} and
90
+ `handle_mouse` are public, and calling them straight tests a component's
91
+ own logic in isolation — no focus, no dispatch, just "given this key, does
92
+ the list move its cursor?" `handle_key` returns whether it consumed the
93
+ key, so you assert on that too:
94
+
95
+ ```ruby
96
+ list.handle_key(Keys::DOWN_ARROW) # exercises the cursor directly
97
+ list.handle_mouse(MouseEvent.new(:left, 5, 2))
98
+ ```
99
+
100
+ **High: go through the screen.** {Tuile::Screen#handle_key} runs the *full*
101
+ dispatch pipeline from chapter 5 — Tab cycling, global shortcuts, the
102
+ subtree `key_shortcut` search, then the focus chain. When your test is
103
+ about routing — that a shortcut jumps focus, that a modal popup traps Tab,
104
+ that a focused text field swallows a key its sibling would otherwise claim
105
+ — you drive `Screen.instance.handle_key` and let the real machinery run.
106
+ Set focus the way production does, with `screen.focused = component` or
107
+ `component.focus`.
108
+
109
+ Two more test-only hooks close the loop. After you mutate something, call
110
+ `Screen.instance.repaint` to flush the pending invalidations into the
111
+ buffer — the coalesced repaint that chapter 2 said happens once per tick,
112
+ triggered by hand because there's no loop running to trigger it. (This is
113
+ a *test* affordance; production code never calls `repaint`, it just
114
+ invalidates and lets the loop coalesce.) And to check invalidation itself
115
+ — that a setter did, or deliberately didn't, mark its component dirty —
116
+ `Screen.instance.invalidated?(component)` and `invalidated_clear` let you
117
+ assert on the set directly.
118
+
119
+ ## Why background code just works
120
+
121
+ Chapter 4's rule was that background threads marshal UI work back with
122
+ `screen.event_queue.submit { … }`. That rule has a happy consequence for
123
+ tests: under the {Tuile::FakeEventQueue}, `submit` **runs its block
124
+ synchronously, right now, on the calling thread.** There is no loop, no
125
+ thread, no waiting. So code that in production hands work across the
126
+ thread boundary — a `LogWindow#log`, a worker posting a result — executes
127
+ inline the moment the test calls it, and the effect is visible on the very
128
+ next line. Posted *events* are simply discarded (a test isn't running the
129
+ loop that would consume them), and `check_locked` passing for free means
130
+ none of this trips the lock guard.
131
+
132
+ The one thing with no clock is animation. `tick` on the fake returns a
133
+ timeless ticker that fires only when the test tells it to: call
134
+ `event_queue.tick_once` to pump every registered ticker one frame. A test
135
+ that wants to advance an animation five frames calls `tick_once` five
136
+ times — frame cadence is the test's to decide, since there's no real time
137
+ passing.
138
+
139
+ ## End to end, through a real terminal
140
+
141
+ Unit tests with the fake cover component logic and rendering, which is
142
+ most of what you write. But some things only exist when the *real* app
143
+ runs: the event loop actually looping, raw-mode key decoding, the WINCH
144
+ trap, a live color-scheme flip. For those, Tuile's own suite spawns the
145
+ real example script in a pseudo-terminal and talks to it like a user
146
+ would. The `spec/examples/` tests are the template.
147
+
148
+ The shape is always the same. Spawn the script under `PTY.spawn`; read its
149
+ output until a glyph you know it paints appears — that single wait proves a
150
+ lot at once (the tree built, a repaint ran, and the loop is now parked in
151
+ the key wait); write a keystroke; assert the process exits cleanly.
152
+
153
+ ```ruby
154
+ PTY.spawn("bundle", "exec", "ruby", "-Ilib", "examples/hello_world.rb") do |reader, writer, pid|
155
+ buffer = String.new
156
+ Timeout.timeout(10) do
157
+ buffer << reader.readpartial(4096) until buffer.include?("Hello, world!")
158
+ end
159
+ writer.write("q")
160
+ Timeout.timeout(5) { Process.wait(pid) }
161
+ assert_equal 0, $CHILD_STATUS.exitstatus
162
+ end
163
+ ```
164
+
165
+ This is the only place the whole stack is exercised together, and it's
166
+ where you'd test something like the mode-2031 flip from chapter 6:
167
+ wait for the dark-theme hint to paint, write the terminal's
168
+ `\e[?997;2n` light report into the PTY, then wait for the *light* hint —
169
+ which passes only if the entire chain (key-thread drain, event parse,
170
+ theme reassignment, full repaint) actually ran end to end. The cost is
171
+ that PTY tests are slower, terminal-dependent, and Linux/macOS only
172
+ (Ruby's stdlib `PTY` isn't on Windows), so they stay a thin top layer over
173
+ a broad base of fake-screen unit tests — the classic pyramid.
174
+
175
+ ---
176
+
177
+ That closes the book. You've followed Tuile from the outside in and back
178
+ out: a tree of components (chapter 1) that repaint through a back buffer
179
+ without flicker (chapter 2), sized top-down by their parents (chapter 3),
180
+ driven by a single-threaded event loop (chapter 4), with keys routed by
181
+ focus (chapter 5) and accents drawn from a terminal-following theme
182
+ (chapter 6); then the library you assemble from (chapter 7), and finally
183
+ the fakes that let you test all of it with no terminal in sight (this
184
+ one). The recurring lesson is the one the name promises: small pieces, each
185
+ doing an obvious thing, composed. The framework is small because the ideas
186
+ are few — and now they're all yours to build on.
data/book/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # The Tuile guide
2
+
3
+ A short, sequential guide to building terminal UIs with Tuile. Each
4
+ chapter builds on the vocabulary the previous one established, so
5
+ reading order matters — the layout chapter assumes the component tree
6
+ from chapter 1, focus assumes the event loop, and so on. Don't jump
7
+ into chapter 5 without chapters 1 and 2 first.
8
+
9
+ If you want the reference instead of the narrative, every public class
10
+ and module is documented with YARD headers in the sources. Run
11
+ `bundle exec rake yard` for a browsable local site, or — once the gem
12
+ is published — see <https://rubydoc.info/gems/tuile>. The division of
13
+ labour: this guide teaches concepts and the *why*; the rdoc carries the
14
+ precise per-method technical truth. When the guide needs a signature it
15
+ links to the rdoc rather than restating it.
16
+
17
+ ## How the guide is shaped
18
+
19
+ Chapters 1–2 are the **base vocabulary** — the component tree and the
20
+ repaint model that every later chapter leans on. Chapter 3 is the heart
21
+ of Tuile's design: layout is top-down and absolute, and the chapter
22
+ argues *why that is enough* — the "C64" case for hand-placed
23
+ coordinates on a character grid — rather than reaching for the
24
+ negotiated min/pref/max machinery of desktop and web toolkits.
25
+
26
+ Chapters 4–6 are the **runtime**: the single-threaded event loop and
27
+ how to do background work safely, focus and keyboard dispatch, and
28
+ theming (including live OS light/dark flips). Chapters 7–8 close out
29
+ **narratively** — a tour of the shipped component toolbox framed around
30
+ when and why to reach for each, and how to test a Tuile app end to end.
31
+ Those two lean on the rdoc for the exact APIs; the guide keeps to the
32
+ walkthroughs and use-cases.
33
+
34
+ The book grows organically — a chapter exists when a concept has earned
35
+ one, not to fill an outline.
36
+
37
+ ## Chapters
38
+
39
+ 1. **[Your first Tuile app](01-first-app.md).** Install, then a
40
+ hello-world walkthrough. Establishes the base vocabulary the rest of
41
+ the guide leans on: the singleton `Tuile::Screen`, the tree of
42
+ `Tuile::Component`s under `ScreenPane`, and the "build a tree, run
43
+ the loop" shape of every Tuile program.
44
+ 2. **[How the screen repaints](02-repaint.md).** Why components never
45
+ write to the terminal directly. `invalidate` → the back buffer →
46
+ a minimal diff → one synchronized flush per tick. The "cover your
47
+ own `rect`" contract, and why the whole model is flicker-free
48
+ without damage tracking or clipping.
49
+ 3. **[Layout: the parent sets the size](03-layout.md).** The heart of
50
+ the design. Top-down, absolute, integer coordinates; a parent
51
+ assigns its children's `rect` and components never negotiate a size.
52
+ The C64 argument for *why simple layouting is enough* on a character
53
+ grid, `Layout::Absolute` and the `rect=` override, `Fraction` for
54
+ sizing a popup against the screen, and resize as a discrete
55
+ recompute. Geometry primitives (`Point` / `Size` / `Rect`) live here.
56
+ 4. **[The event loop and background work](04-event-loop.md).** The
57
+ single-threaded rule: all UI mutation happens on the loop thread.
58
+ The event queue, marshalling work back from a background thread with
59
+ `submit`, and how terminal resize (`SIGWINCH`) is plumbed through the
60
+ same queue rather than handled off the signal.
61
+ 5. **[Focus and the keyboard](05-focus.md).** The focus chain and
62
+ `focusable?`, and the order in which a keystroke is offered to the
63
+ tree — Tab, global shortcuts, a component's `key_shortcut`, then
64
+ `handle_key`. How a focused text field swallows printable keys, and
65
+ how `keyboard_hint` drives the status bar.
66
+ 6. **[Theming](06-theming.md).** Semantic color tokens read at paint
67
+ time, light/dark auto-detection at startup and live OS appearance
68
+ flips, pairing variants in a `ThemeDef`, app-specific custom tokens,
69
+ and rebuilding theme-derived content in `on_theme_changed`.
70
+ 7. **[The component library](07-components.md).** A narrative tour of
71
+ the shipped toolbox — Window, List, the text inputs and views,
72
+ Popup, and the window conveniences — framed around *when and why*
73
+ you reach for each. Signatures stay in the rdoc.
74
+ 8. **[Testing a Tuile app](08-testing.md).** The testing approach:
75
+ `FakeScreen`, asserting against the painted buffer, driving
76
+ invalidation, and PTY-based end-to-end tests of runnable scripts.
77
+
78
+ All eight chapters are currently **stubs** — the skeleton is in place so
79
+ the numbering is stable; prose lands chapter by chapter.
@@ -14,8 +14,7 @@ require "tuile"
14
14
  # Tuile::Screen.instance during invalidate/repaint hooks.
15
15
  screen = Tuile::Screen.new
16
16
 
17
- label = Tuile::Component::Label.new
18
- label.text = "Hello, world!"
17
+ label = Tuile::Component::Label.new("Hello, world!")
19
18
 
20
19
  window = Tuile::Component::Window.new("Tuile")
21
20
  window.content = label
data/examples/sampler.rb CHANGED
@@ -67,6 +67,7 @@ module SamplerExample
67
67
  ["Label", :build_label],
68
68
  ["TextField", :build_text_field],
69
69
  ["TextArea", :build_text_area],
70
+ ["Slash menu", :build_slash_demo],
70
71
  ["TextView", :build_text_view],
71
72
  ["Button", :build_buttons],
72
73
  ["List", :build_list],
@@ -87,6 +88,11 @@ module SamplerExample
87
88
  end
88
89
 
89
90
  def load_entry(idx)
91
+ # The slash-menu demo parks a non-modal overlay on the pane (it lives
92
+ # outside the right pane's content tree), so close it before swapping
93
+ # demos or it would linger over the next one.
94
+ @slash_overlay.close if @slash_overlay&.open?
95
+ @slash_overlay = nil
90
96
  caption, builder = ENTRIES[idx]
91
97
  @right_window.caption = caption
92
98
  @right_window.content = send(builder)
@@ -135,6 +141,64 @@ module SamplerExample
135
141
  end
136
142
  end
137
143
 
144
+ # Slash commands the demo offers; the menu filters these by what's typed.
145
+ SLASH_COMMANDS = %w[/help /list /open /save /clear /quit].freeze
146
+
147
+ # A non-modal Popup used as an autocomplete menu. Focus (and the caret)
148
+ # stays in the TextArea the whole time: an `on_change` listener refills the
149
+ # menu, an `on_key` interceptor forwards Up/Down/Enter/ESC to it while it's
150
+ # open, and the menu floats above the field, anchored to the caret. None of
151
+ # this is baked into TextArea — it's all assembled here from stock hooks.
152
+ def build_slash_demo
153
+ prompt = Tuile::Component::Label.new
154
+ prompt.text = "Non-modal Popup as an autocomplete menu. Type a slash command\n" \
155
+ "(try \"/\" or \"/s\"). The menu floats above the field without taking\n" \
156
+ "focus: Down/Up move the selection, Enter accepts, ESC dismisses, and\n" \
157
+ "ordinary typing keeps editing the field and refilters the menu."
158
+ area = Tuile::Component::TextArea.new
159
+
160
+ list = Tuile::Component::List.new
161
+ list.cursor = Tuile::Component::List::Cursor.new
162
+ list.show_cursor_when_inactive = true # highlight the selection though focus stays in the field
163
+ window = Tuile::Component::Window.new("Commands").tap { _1.content = list }
164
+ overlay = Tuile::Component::Popup.new(content: window, modal: false)
165
+ @slash_overlay = overlay
166
+
167
+ refill = lambda do
168
+ matches = slash_matches(area)
169
+ if matches.empty?
170
+ overlay.close if overlay.open?
171
+ else
172
+ overlay.open unless overlay.open?
173
+ list.lines = matches
174
+ anchor_overlay(overlay, area)
175
+ end
176
+ end
177
+
178
+ area.on_change = ->(_text) { refill.call }
179
+ list.on_item_chosen = ->(_idx, line) { accept_slash_command(area, line.to_s) }
180
+ area.on_key = lambda do |key|
181
+ next false unless overlay.open?
182
+
183
+ case key
184
+ when Tuile::Keys::UP_ARROW, Tuile::Keys::DOWN_ARROW, Tuile::Keys::ENTER
185
+ list.handle_key(key) # works though the list is unfocused — dispatch gates on focus, not the list
186
+ when Tuile::Keys::ESC
187
+ overlay.close
188
+ true
189
+ else
190
+ false
191
+ end
192
+ end
193
+
194
+ panel(prompt, area) do |r|
195
+ inner = inner_rect(r)
196
+ prompt.rect = Tuile::Rect.new(inner.left, inner.top + 1, inner.width, 4)
197
+ area_height = [inner.height - 7, 4].max
198
+ area.rect = Tuile::Rect.new(inner.left, inner.top + 6, inner.width, area_height)
199
+ end
200
+ end
201
+
138
202
  def build_text_view
139
203
  prompt = Tuile::Component::Label.new
140
204
  prompt.text = "Read-only viewer for prose. Word-wraps to width; ANSI formatting passes through.\n" \
@@ -301,6 +365,51 @@ module SamplerExample
301
365
  end
302
366
  end
303
367
 
368
+ # The run of non-space characters ending at the caret, when it starts with
369
+ # "/" — i.e. the slash command being typed — or nil.
370
+ def slash_token(area)
371
+ text = area.text
372
+ caret = area.caret
373
+ start = caret
374
+ start -= 1 while start.positive? && !text[start - 1].match?(/\s/)
375
+ token = text[start...caret].to_s
376
+ token.start_with?("/") ? token : nil
377
+ end
378
+
379
+ # Commands matching the slash token at the caret (empty when not in one).
380
+ def slash_matches(area)
381
+ token = slash_token(area)
382
+ return [] if token.nil?
383
+
384
+ SLASH_COMMANDS.select { _1.start_with?(token) }
385
+ end
386
+
387
+ # Replaces the slash token at the caret with `command` plus a trailing
388
+ # space, then drops the caret after it (which re-fires on_change → refill,
389
+ # so the now-tokenless text closes the menu).
390
+ def accept_slash_command(area, command)
391
+ text = area.text
392
+ caret = area.caret
393
+ start = caret
394
+ start -= 1 while start.positive? && !text[start - 1].match?(/\s/)
395
+ area.text = "#{text[0...start]}#{command} #{text[caret..]}"
396
+ area.caret = start + command.length + 1
397
+ end
398
+
399
+ # Positions the overlay just below the caret, flipping above when there's
400
+ # no room beneath, and clamps it to the screen.
401
+ def anchor_overlay(overlay, area)
402
+ caret = area.cursor_position
403
+ return if caret.nil?
404
+
405
+ screen_size = Tuile::Screen.instance.size
406
+ size = overlay.rect
407
+ top = caret.y + 1
408
+ top = [caret.y - size.height, 0].max if top + size.height > screen_size.height - 1
409
+ left = caret.x.clamp(0, [screen_size.width - size.width, 0].max)
410
+ overlay.rect = Tuile::Rect.new(left, top, size.width, size.height)
411
+ end
412
+
304
413
  # Carves a 2-column padding out of the panel rect so the demo content
305
414
  # doesn't run flush to the window border.
306
415
  def inner_rect(rect)
data/lib/tuile/ansi.rb CHANGED
@@ -11,5 +11,21 @@ module Tuile
11
11
  # background, and text attributes.
12
12
  # @return [String]
13
13
  RESET = "\e[0m"
14
+
15
+ # Begin Synchronized Update (DEC private mode 2026, "Synchronized
16
+ # Output"). The terminal stops refreshing its display and buffers every
17
+ # subsequent write until {SYNC_END}, then composites the whole batch
18
+ # atomically — so a multi-cell repaint is never shown half-drawn. This is
19
+ # what stops flicker when a frame redraws a large region (e.g. the
20
+ # full-scene repaint a shrinking popup forces). Terminals without support
21
+ # ignore the private-mode set, so it's a safe no-op there. {Screen#repaint}
22
+ # wraps its single frame-buffer flush in this pair.
23
+ # @return [String]
24
+ SYNC_BEGIN = "\e[?2026h"
25
+
26
+ # End Synchronized Update — see {SYNC_BEGIN}. Releases the buffered frame
27
+ # and lets the terminal repaint.
28
+ # @return [String]
29
+ SYNC_END = "\e[?2026l"
14
30
  end
15
31
  end