Module: Tuile::Keys

Defined in:
lib/tuile/keys.rb,
sig/tuile.rbs

Overview

Constants for keys returned by Keys.getkey and helpers for reading them from stdin. The constants are the raw escape sequences emitted by the terminal; see https://en.wikipedia.org/wiki/ANSI_escape_code for the encoding.

Constant Summary collapse

DOWN_ARROW =

Returns:

  • (String)
"\e[B"
UP_ARROW =

Returns:

  • (String)
"\e[A"
DOWN_ARROWS =

Returns:

  • (Array<String>)
[DOWN_ARROW, "j"].freeze
UP_ARROWS =

Returns:

  • (Array<String>)
[UP_ARROW, "k"].freeze
LEFT_ARROW =

Returns:

  • (String)
"\e[D"
RIGHT_ARROW =

Returns:

  • (String)
"\e[C"
CTRL_LEFT_ARROW =

Returns:

  • (String)
"\e[1;5D"
CTRL_RIGHT_ARROW =

Returns:

  • (String)
"\e[1;5C"
ESC =

Returns:

  • (String)
"\e"
HOME =

Returns:

  • (String)
"\e[H"
END_ =

Returns:

  • (String)
"\e[F"
HOMES =

Home-key sequences. xterm-style (HOME) is the modern default, but the Linux console, rxvt, and tmux/screen in their default configuration emit the VT220-style \e[1~ instead. Components that handle Home should match against this array so users see consistent behavior regardless of which sequence their terminal emits.

Returns:

  • (Array<String>)
[HOME, "\e[1~"].freeze
ENDS_ =

End-key sequences. See HOMES for why two are recognized.

Returns:

  • (Array<String>)
[END_, "\e[4~"].freeze
PAGE_UP =

Returns:

  • (String)
"\e[5~"
PAGE_DOWN =

Returns:

  • (String)
"\e[6~"
BACKSPACE =

Returns:

  • (String)
"\x7f"
DELETE =

Returns:

  • (String)
"\e[3~"
CTRL_A =

Ctrl+letter sends bytes 0x01..0x1a. Note that CTRL_H == "\b", CTRL_I == TAB, CTRL_J == "\n", and CTRL_M == ENTER — terminals deliver these key combinations indistinguishably from the corresponding named keys.

Returns:

  • (String)
"\x01"
CTRL_B =

Returns:

  • (String)
"\x02"
CTRL_C =

Returns:

  • (String)
"\x03"
CTRL_D =

Returns:

  • (String)
"\x04"
CTRL_E =

Returns:

  • (String)
"\x05"
CTRL_F =

Returns:

  • (String)
"\x06"
CTRL_G =

Returns:

  • (String)
"\x07"
CTRL_H =

Returns:

  • (String)
"\b"
CTRL_I =

Returns:

  • (String)
"\t"
CTRL_J =

Returns:

  • (String)
"\n"
CTRL_K =

Returns:

  • (String)
"\x0b"
CTRL_L =

Returns:

  • (String)
"\x0c"
CTRL_M =

Returns:

  • (String)
"\r"
CTRL_N =

Returns:

  • (String)
"\x0e"
CTRL_O =

Returns:

  • (String)
"\x0f"
CTRL_P =

Returns:

  • (String)
"\x10"
CTRL_Q =

Returns:

  • (String)
"\x11"
CTRL_R =

Returns:

  • (String)
"\x12"
CTRL_S =

Returns:

  • (String)
"\x13"
CTRL_T =

Returns:

  • (String)
"\x14"
CTRL_U =

Returns:

  • (String)
"\x15"
CTRL_V =

Returns:

  • (String)
"\x16"
CTRL_W =

Returns:

  • (String)
"\x17"
CTRL_X =

Returns:

  • (String)
"\x18"
CTRL_Y =

Returns:

  • (String)
"\x19"
CTRL_Z =

Returns:

  • (String)
"\x1a"
BACKSPACES =

Returns:

  • (Array<String>)
[BACKSPACE, CTRL_H].freeze
ENTER =

Returns:

  • (String)
"\r"
TAB =

Returns:

  • (String)
"\t"
SHIFT_TAB =

The terminal sequence emitted by Shift+Tab in xterm-style terminals (CSI Z). Used by Screen for reverse focus traversal.

Returns:

  • (String)
"\e[Z"
BRACKETED_PASTE_ON =

Enables bracketed paste (DEC private mode 2004): the terminal wraps pasted text in PASTE_STARTPASTE_END instead of feeding it as keystrokes. Terminals that don't know the mode ignore it, so Screen#run_event_loop prints it unconditionally.

Returns:

  • (String)
"\e[?2004h"
BRACKETED_PASTE_OFF =

Disables bracketed paste. See BRACKETED_PASTE_ON.

Returns:

  • (String)
"\e[?2004l"
PASTE_START =

Opens a bracketed paste. getkey returns it like any other key; the payload behind it is read by read_paste.

Returns:

  • (String)
"\e[200~"
PASTE_END =

Closes a bracketed paste. Never surfaces as a key — read_paste consumes it as the payload terminator.

Returns:

  • (String)
"\e[201~"

Class Method Summary collapse

Class Method Details

.getkeyString

Grabs a key from stdin and returns it. Blocks until the key is obtained. Reads a full ESC key sequence; see constants above for some values returned by this function.

@return — key, such as DOWN_ARROW.

Returns:

  • (String)


155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/tuile/keys.rb', line 155

def self.getkey
  char = $stdin.getch
  return char unless char == Keys::ESC

  # Escape sequence. Try to read more data.
  begin
    # Read up to 5 bytes: that's the maximum tail length of any escape
    # sequence Tuile recognizes after the initial \e (X10 mouse `[Mbxy`,
    # CTRL+arrow `[1;5D`, etc.). Reading 6 here would over-read into the
    # next sequence on tight mouse-event bursts — we'd silently steal
    # the next event's leading \e and the rest of it would surface as
    # individual printable keypresses in focused inputs.
    char += $stdin.read_nonblock(5)
  rescue IO::EAGAINWaitReadable
    # The "ESC" key pressed => only the \e char is emitted.
    return char
  end

  # If `read_nonblock` returned a partial X10 mouse-report prefix (the
  # sequence is fixed-length: 3 bytes after `\e[M`), drain the remainder
  # with a blocking read so the parser downstream sees a complete event
  # instead of leaking tail bytes as keypresses.
  char += $stdin.read(6 - char.bytesize) if char.start_with?("\e[M") && char.bytesize < 6

  # Private-mode CSI reports (`\e[?` params… final byte in 0x40..0x7E)
  # can outgrow the 5-byte gulp above — the mode-2031 color-scheme
  # notification `\e[?997;1n` (see {EventQueue::ColorSchemeEvent}) is 8
  # bytes after the `\e`. Drain to the final byte with blocking 1-byte
  # reads so the tail doesn't surface as phantom keypresses. Keyboard
  # sequences never start with `\e[?`, so this can't eat a regular key.
  char += $stdin.read(1) while char.start_with?("\e[?") && !char.match?(/[\x40-\x7e]\z/)

  # OSC replies (the ~22-byte background report, {TerminalBackground})
  # outgrow the gulp too, and end in BEL or ST rather than at a fixed
  # length. Byte-at-a-time is required, not merely tidy: ST *is* `\e\\`,
  # so a gulping read would swallow it plus the keys typed behind it.
  # Keyboard sequences never start with `\e]`, so this eats no real key.
  char += $stdin.read(1) while char.start_with?("\e]") && !char.end_with?("\a", "\e\\")

  char
end

.normalize_paste(text) ⇒ String

Rewrites a raw paste payload into the one convention callers see: \n line endings, valid UTF-8.

Keys.normalize_paste("a\r\nb\rc")   # => "a\nb\nc"

Both are terminal-layer artifacts, not text: a terminal with bracketed paste off rewrites the clipboard's \n to \r so a paste looks like typing, and several keep doing it inside the brackets — so the byte a line break arrives as is not something a component should have to know. Invalid bytes are scrubbed to U+FFFD, which is what lets Component#handle_paste take any clipboard, a binary file included, without the grapheme-cluster walk raising downstream.

Control characters other than \n are left alone: they are content, and what a text buffer may hold is Component::AbstractStringField's call, not this layer's.

@param text — raw payload.

Parameters:

  • text (String)

Returns:

  • (String)


242
# File 'lib/tuile/keys.rb', line 242

def self.normalize_paste(text) = text.scrub.gsub(/\r\n?/, "\n")

.printable?(key) ⇒ Boolean

True iff key is a single printable character — a one-character string whose codepoint is not in Unicode's C (Other) category. Rejects multi- character escape sequences (UP_ARROW, mouse events, …), control bytes (TAB, ENTER, ESC, CTRL_A..CTRL_Z, BACKSPACE), and the empty string; accepts ASCII letters/digits/punctuation/space and non-ASCII printables like "é".

Used by Screen#register_global_shortcut to reject keys that would collide with typing, and by Component::TextField to decide whether to insert a key at the caret.

@param key

Parameters:

  • key (String)

Returns:

  • (Boolean)


147
148
149
# File 'lib/tuile/keys.rb', line 147

def self.printable?(key)
  key.length == 1 && !key.match?(/\p{C}/)
end

.read_pasteString

Reads the body of a bracketed paste, having just read PASTE_START, and returns it normalize_pasted:

Keys.getkey                    # => "\e[200~"
Keys.read_paste                # => "one\ntwo"   (terminator consumed)

Reads raw, one byte at a time, rather than looping on getkey: a pasted \e would send getkey gulping five bytes of the payload as an escape tail, surfacing them as phantom keypresses. One byte at a time is also what keeps the terminator from being over-read — nothing past PASTE_END is consumed, so typing that lands behind a paste survives.

Blocks until the terminator arrives; returns what it has at EOF.

@return — the pasted text, UTF-8, without the brackets.

Returns:

  • (String)


211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/tuile/keys.rb', line 211

def self.read_paste
  buffer = +"".b
  terminator = PASTE_END.b
  until buffer.end_with?(terminator)
    byte = $stdin.read(1)
    break if byte.nil?

    buffer << byte
  end
  buffer.delete_suffix!(terminator)
  normalize_paste(buffer.force_encoding(Encoding::UTF_8))
end