Module: Charming::Escape

Defined in:
lib/charming/escape.rb

Overview

Escape is the out-of-band terminal channel: escape sequences written straight to the terminal, before each frame, bypassing the line-based renderer (which measures width and would shred raw control sequences). It is the shared substrate for several primitives — image transmissions (Image::Transmit), clipboard writes, desktop notifications, and the window title.

Sequences are gathered during an event's dispatch via a thread-local collector (Escape.collecting / Escape.register) and attached to the Response; the Runtime flushes them through backend.write_escape. Any object that responds to #payload (a string) can ride the channel; the builders here return Sequence value objects and sanitize interpolated text so user content can't break out of the sequence.

Defined Under Namespace

Classes: Sequence

Constant Summary collapse

BUCKET_KEY =

Thread-local key under which the active collection bucket is stored.

:charming_escape_bucket

Class Method Summary collapse

Class Method Details

.bellObject

Builds a terminal-bell sequence (BEL).



63
64
65
# File 'lib/charming/escape.rb', line 63

def bell
  Sequence.new(payload: "\a")
end

.clipboard(text, target: "c") ⇒ Object

Builds an OSC 52 clipboard-write sequence setting the target selection ("c" clipboard, "p" primary) to text. The text is base64-encoded, so any bytes are safe.



46
47
48
# File 'lib/charming/escape.rb', line 46

def clipboard(text, target: "c")
  Sequence.new(payload: "\e]52;#{target};#{[text.to_s].pack("m0")}\a")
end

.collectingObject

Runs block with a fresh collection bucket active and returns the bucket — the Sequences registered via register during the block. The block's own return value is ignored (capture it via a closure). Nesting is supported: an inner collection shadows the outer.



25
26
27
28
29
30
31
32
33
# File 'lib/charming/escape.rb', line 25

def collecting
  previous = Thread.current[BUCKET_KEY]
  bucket = []
  Thread.current[BUCKET_KEY] = bucket
  yield
  bucket
ensure
  Thread.current[BUCKET_KEY] = previous
end

.notification(body, title: nil) ⇒ Object

Builds a desktop-notification sequence: OSC 777 (title + body) when a title is given, else OSC 9 (body only) — the broadly supported baseline (Ghostty, iTerm2, Kitty).



52
53
54
55
56
57
58
59
60
# File 'lib/charming/escape.rb', line 52

def notification(body, title: nil)
  payload =
    if title
      "\e]777;notify;#{sanitize(title)};#{sanitize(body)}\e\\"
    else
      "\e]9;#{sanitize(body)}\a"
    end
  Sequence.new(payload: payload)
end

.register(sequence) ⇒ Object

Registers sequence with the active collection bucket so it is flushed to the backend. A no-op (outside a collection, or when sequence is nil) so callers can register freely.



37
38
39
40
41
42
# File 'lib/charming/escape.rb', line 37

def register(sequence)
  return sequence unless sequence

  Thread.current[BUCKET_KEY]&.push(sequence)
  sequence
end

.title(text) ⇒ Object

Builds an OSC 0 sequence setting the terminal window (and icon) title to text.



68
69
70
# File 'lib/charming/escape.rb', line 68

def title(text)
  Sequence.new(payload: "\e]0;#{sanitize(text)}\a")
end