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 =
"\e[B"- UP_ARROW =
"\e[A"- DOWN_ARROWS =
[DOWN_ARROW, "j"].freeze
- UP_ARROWS =
[UP_ARROW, "k"].freeze
- LEFT_ARROW =
"\e[D"- RIGHT_ARROW =
"\e[C"- CTRL_LEFT_ARROW =
"\e[1;5D"- CTRL_RIGHT_ARROW =
"\e[1;5C"- ESC =
"\e"- HOME =
"\e[H"- END_ =
"\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. [HOME, "\e[1~"].freeze
- ENDS_ =
End-key sequences. See HOMES for why two are recognized.
[END_, "\e[4~"].freeze
- PAGE_UP =
"\e[5~"- PAGE_DOWN =
"\e[6~"- BACKSPACE =
"\x7f"- DELETE =
"\e[3~"- CTRL_A =
"\x01"- CTRL_B =
"\x02"- CTRL_C =
"\x03"- CTRL_D =
"\x04"- CTRL_E =
"\x05"- CTRL_F =
"\x06"- CTRL_G =
"\x07"- CTRL_H =
"\b"- CTRL_I =
"\t"- CTRL_J =
"\n"- CTRL_K =
"\x0b"- CTRL_L =
"\x0c"- CTRL_M =
"\r"- CTRL_N =
"\x0e"- CTRL_O =
"\x0f"- CTRL_P =
"\x10"- CTRL_Q =
"\x11"- CTRL_R =
"\x12"- CTRL_S =
"\x13"- CTRL_T =
"\x14"- CTRL_U =
"\x15"- CTRL_V =
"\x16"- CTRL_W =
"\x17"- CTRL_X =
"\x18"- CTRL_Y =
"\x19"- CTRL_Z =
"\x1a"- BACKSPACES =
[BACKSPACE, CTRL_H].freeze
- ENTER =
"\r"- TAB =
"\t"- SHIFT_TAB =
The terminal sequence emitted by Shift+Tab in xterm-style terminals (CSI Z). Used by Screen for reverse focus traversal.
"\e[Z"- BRACKETED_PASTE_ON =
Enables bracketed paste (DEC private mode 2004): the terminal wraps pasted text in PASTE_START … PASTE_END instead of feeding it as keystrokes. Terminals that don't know the mode ignore it, so Screen#run_event_loop prints it unconditionally.
"\e[?2004h"- BRACKETED_PASTE_OFF =
Disables bracketed paste. See BRACKETED_PASTE_ON.
"\e[?2004l"- PASTE_START =
Opens a bracketed paste. getkey returns it like any other key; the payload behind it is read by read_paste.
"\e[200~"- PASTE_END =
Closes a bracketed paste. Never surfaces as a key — read_paste consumes it as the payload terminator.
"\e[201~"
Class Method Summary collapse
-
.getkey ⇒ String
Grabs a key from stdin and returns it.
-
.normalize_paste(text) ⇒ String
Rewrites a raw paste payload into the one convention callers see:
\nline endings, valid UTF-8. -
.printable?(key) ⇒ Boolean
True iff
keyis a single printable character — a one-character string whose codepoint is not in Unicode's C (Other) category. -
.read_paste ⇒ String
Reads the body of a bracketed paste, having just read PASTE_START, and returns it Keys.normalize_pasted:.
Class Method Details
.getkey ⇒ String
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.
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.
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
147 148 149 |
# File 'lib/tuile/keys.rb', line 147 def self.printable?(key) key.length == 1 && !key.match?(/\p{C}/) end |
.read_paste ⇒ String
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.
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 |