github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/cli/term/reader_unix.go (about) 1 //go:build !windows && !plan9 2 // +build !windows,!plan9 3 4 package term 5 6 import ( 7 "os" 8 "time" 9 10 "github.com/markusbkk/elvish/pkg/ui" 11 ) 12 13 // reader reads terminal escape sequences and decodes them into events. 14 type reader struct { 15 fr fileReader 16 } 17 18 func newReader(f *os.File) *reader { 19 fr, err := newFileReader(f) 20 if err != nil { 21 // TODO(xiaq): Do not panic. 22 panic(err) 23 } 24 return &reader{fr} 25 } 26 27 func (rd *reader) ReadEvent() (Event, error) { 28 return readEvent(rd.fr) 29 } 30 31 func (rd *reader) ReadRawEvent() (Event, error) { 32 r, err := readRune(rd.fr, -1) 33 return K(r), err 34 } 35 36 func (rd *reader) Close() { 37 rd.fr.Stop() 38 rd.fr.Close() 39 } 40 41 // Used by readRune in readOne to signal end of current sequence. 42 const runeEndOfSeq rune = -1 43 44 // Timeout for bytes in escape sequences. Modern terminal emulators send escape 45 // sequences very fast, so 10ms is more than sufficient. SSH connections on a 46 // slow link might be problematic though. 47 var keySeqTimeout = 10 * time.Millisecond 48 49 func readEvent(rd byteReaderWithTimeout) (event Event, err error) { 50 var r rune 51 r, err = readRune(rd, -1) 52 if err != nil { 53 return 54 } 55 56 currentSeq := string(r) 57 // Attempts to read a rune within a timeout of keySeqTimeout. It returns 58 // runeEndOfSeq if there is any error; the caller should terminate the 59 // current sequence when it sees that value. 60 readRune := 61 func() rune { 62 r, e := readRune(rd, keySeqTimeout) 63 if e != nil { 64 return runeEndOfSeq 65 } 66 currentSeq += string(r) 67 return r 68 } 69 badSeq := func(msg string) { 70 err = seqError{msg, currentSeq} 71 } 72 73 switch r { 74 case 0x1b: // ^[ Escape 75 r2 := readRune() 76 // According to https://unix.stackexchange.com/a/73697, rxvt and derivatives 77 // prepend another ESC to a CSI-style or G3-style sequence to signal Alt. 78 // If that happens, remember this now; it will be later picked up when parsing 79 // those two kinds of sequences. 80 // 81 // issue #181 82 hasTwoLeadingESC := false 83 if r2 == 0x1b { 84 hasTwoLeadingESC = true 85 r2 = readRune() 86 } 87 if r2 == runeEndOfSeq { 88 // TODO(xiaq): Error is swallowed. 89 // Nothing follows. Taken as a lone Escape. 90 event = KeyEvent{'[', ui.Ctrl} 91 break 92 } 93 switch r2 { 94 case '[': 95 // A '[' follows. CSI style function key sequence. 96 r = readRune() 97 if r == runeEndOfSeq { 98 event = KeyEvent{'[', ui.Alt} 99 return 100 } 101 102 nums := make([]int, 0, 2) 103 var starter rune 104 105 // Read an optional starter. 106 switch r { 107 case '<': 108 starter = r 109 r = readRune() 110 case 'M': 111 // Mouse event. 112 cb := readRune() 113 if cb == runeEndOfSeq { 114 badSeq("incomplete mouse event") 115 return 116 } 117 cx := readRune() 118 if cx == runeEndOfSeq { 119 badSeq("incomplete mouse event") 120 return 121 } 122 cy := readRune() 123 if cy == runeEndOfSeq { 124 badSeq("incomplete mouse event") 125 return 126 } 127 down := true 128 button := int(cb & 3) 129 if button == 3 { 130 down = false 131 button = -1 132 } 133 mod := mouseModify(int(cb)) 134 event = MouseEvent{ 135 Pos{int(cy) - 32, int(cx) - 32}, down, button, mod} 136 return 137 } 138 CSISeq: 139 for { 140 switch { 141 case r == ';': 142 nums = append(nums, 0) 143 case '0' <= r && r <= '9': 144 if len(nums) == 0 { 145 nums = append(nums, 0) 146 } 147 cur := len(nums) - 1 148 nums[cur] = nums[cur]*10 + int(r-'0') 149 case r == runeEndOfSeq: 150 // Incomplete CSI. 151 badSeq("incomplete CSI") 152 return 153 default: // Treat as a terminator. 154 break CSISeq 155 } 156 157 r = readRune() 158 } 159 if starter == 0 && r == 'R' { 160 // Cursor position report. 161 if len(nums) != 2 { 162 badSeq("bad CPR") 163 return 164 } 165 event = CursorPosition{nums[0], nums[1]} 166 } else if starter == '<' && (r == 'm' || r == 'M') { 167 // SGR-style mouse event. 168 if len(nums) != 3 { 169 badSeq("bad SGR mouse event") 170 return 171 } 172 down := r == 'M' 173 button := nums[0] & 3 174 mod := mouseModify(nums[0]) 175 event = MouseEvent{Pos{nums[2], nums[1]}, down, button, mod} 176 } else if r == '~' && len(nums) == 1 && (nums[0] == 200 || nums[0] == 201) { 177 b := nums[0] == 200 178 event = PasteSetting(b) 179 } else { 180 k := parseCSI(nums, r, currentSeq) 181 if k == (ui.Key{}) { 182 badSeq("bad CSI") 183 } else { 184 if hasTwoLeadingESC { 185 k.Mod |= ui.Alt 186 } 187 event = KeyEvent(k) 188 } 189 } 190 case 'O': 191 // An 'O' follows. G3 style function key sequence: read one rune. 192 r = readRune() 193 if r == runeEndOfSeq { 194 // Nothing follows after 'O'. Taken as Alt-O. 195 event = KeyEvent{'O', ui.Alt} 196 return 197 } 198 k, ok := g3Seq[r] 199 if ok { 200 if hasTwoLeadingESC { 201 k.Mod |= ui.Alt 202 } 203 event = KeyEvent(k) 204 } else { 205 badSeq("bad G3") 206 } 207 default: 208 // Something other than '[' or 'O' follows. Taken as an 209 // Alt-modified key, possibly also modified by Ctrl. 210 k := ctrlModify(r2) 211 k.Mod |= ui.Alt 212 event = KeyEvent(k) 213 } 214 default: 215 event = KeyEvent(ctrlModify(r)) 216 } 217 return 218 } 219 220 // Determines whether a rune corresponds to a Ctrl-modified key and returns the 221 // ui.Key the rune represents. 222 func ctrlModify(r rune) ui.Key { 223 switch r { 224 // TODO(xiaq): Are the following special cases universal? 225 case 0x0: 226 return ui.K('`', ui.Ctrl) // ^@ 227 case 0x1e: 228 return ui.K('6', ui.Ctrl) // ^^ 229 case 0x1f: 230 return ui.K('/', ui.Ctrl) // ^_ 231 case ui.Tab, ui.Enter, ui.Backspace: // ^I ^J ^? 232 // Ambiguous Ctrl keys; prefer the non-Ctrl form as they are more likely. 233 return ui.K(r) 234 default: 235 // Regular ui.Ctrl sequences. 236 if 0x1 <= r && r <= 0x1d { 237 return ui.K(r+0x40, ui.Ctrl) 238 } 239 } 240 return ui.K(r) 241 } 242 243 // Tables for key sequences. Comments document which terminal emulators are 244 // known to generate which sequences. The terminal emulators tested are 245 // categorized into xterm (including actual xterm, libvte-based terminals, 246 // Konsole and Terminal.app unless otherwise noted), urxvt, tmux. 247 248 // G3-style key sequences: \eO followed by exactly one character. For instance, 249 // \eOP is F1. These are pretty limited in that they cannot be extended to 250 // support modifier keys, other than a leading \e for Alt (e.g. \e\eOP is 251 // Alt-F1). Terminals that send G3-style key sequences typically switch to 252 // sending a CSI-style key sequence when a non-Alt modifier key is pressed. 253 var g3Seq = map[rune]ui.Key{ 254 // xterm, tmux -- only in Vim, depends on termios setting? 255 // NOTE(xiaq): According to urxvt's manpage, \eO[ABCD] sequences are used for 256 // Ctrl-Shift-modified arrow keys; however, this doesn't seem to be true for 257 // urxvt 9.22 packaged by Debian; those keys simply send the same sequence 258 // as Ctrl-modified keys (\eO[abcd]). 259 'A': ui.K(ui.Up), 'B': ui.K(ui.Down), 'C': ui.K(ui.Right), 'D': ui.K(ui.Left), 260 'H': ui.K(ui.Home), 'F': ui.K(ui.End), 'M': ui.K(ui.Insert), 261 // urxvt 262 'a': ui.K(ui.Up, ui.Ctrl), 'b': ui.K(ui.Down, ui.Ctrl), 263 'c': ui.K(ui.Right, ui.Ctrl), 'd': ui.K(ui.Left, ui.Ctrl), 264 // xterm, urxvt, tmux 265 'P': ui.K(ui.F1), 'Q': ui.K(ui.F2), 'R': ui.K(ui.F3), 'S': ui.K(ui.F4), 266 } 267 268 // Tables for CSI-style key sequences. A CSI sequence is \e[ followed by zero or 269 // more numerical arguments (separated by semicolons), ending in a non-numeric, 270 // non-semicolon rune. They are used for many purposes, and CSI-style key 271 // sequences are a subset of them. 272 // 273 // There are several variants of CSI-style key sequences; see comments above the 274 // respective tables. In all variants, modifier keys are encoded in numerical 275 // arguments; see xtermModify. Note that although the set of possible sequences 276 // make it possible to express a very complete set of key combinations, they are 277 // not always sent by terminals. For instance, many (if not most) terminals will 278 // send the same sequence for Up when Shift-Up is pressed, even if Shift-Up is 279 // expressible using the escape sequences described below. 280 281 // CSI-style key sequences identified by the last rune. For instance, \e[A is 282 // Up. When modified, two numerical arguments are added, the first always beging 283 // 1 and the second identifying the modifier. For instance, \e[1;5A is Ctrl-Up. 284 var csiSeqByLast = map[rune]ui.Key{ 285 // xterm, urxvt, tmux 286 'A': ui.K(ui.Up), 'B': ui.K(ui.Down), 'C': ui.K(ui.Right), 'D': ui.K(ui.Left), 287 // urxvt 288 'a': ui.K(ui.Up, ui.Shift), 'b': ui.K(ui.Down, ui.Shift), 289 'c': ui.K(ui.Right, ui.Shift), 'd': ui.K(ui.Left, ui.Shift), 290 // xterm (Terminal.app only sends those in alternate screen) 291 'H': ui.K(ui.Home), 'F': ui.K(ui.End), 292 // xterm, urxvt, tmux 293 'Z': ui.K(ui.Tab, ui.Shift), 294 } 295 296 // CSI-style key sequences ending with '~' with by one or two numerical 297 // arguments. The first argument identifies the key, and the optional second 298 // argument identifies the modifier. For instance, \e[3~ is Delete, and \e[3;5~ 299 // is Ctrl-Delete. 300 // 301 // An alternative encoding of the modifier key, only known to be used by urxvt 302 // (or for that matter, likely also rxvt) is to change the last rune: '$' for 303 // Shift, '^' for Ctrl, and '@' for Ctrl+Shift. The numeric argument is kept 304 // unchanged. For instance, \e[3^ is Ctrl-Delete. 305 var csiSeqTilde = map[int]rune{ 306 // tmux (NOTE: urxvt uses the pair for Find/Select) 307 1: ui.Home, 4: ui.End, 308 // xterm (Terminal.app sends ^M for Fn+Enter), urxvt, tmux 309 2: ui.Insert, 310 // xterm, urxvt, tmux 311 3: ui.Delete, 312 // xterm (Terminal.app only sends those in alternate screen), urxvt, tmux 313 // NOTE: called Prior/Next in urxvt manpage 314 5: ui.PageUp, 6: ui.PageDown, 315 // urxvt 316 7: ui.Home, 8: ui.End, 317 // urxvt 318 11: ui.F1, 12: ui.F2, 13: ui.F3, 14: ui.F4, 319 // xterm, urxvt, tmux 320 // NOTE: 16 and 22 are unused 321 15: ui.F5, 17: ui.F6, 18: ui.F7, 19: ui.F8, 322 20: ui.F9, 21: ui.F10, 23: ui.F11, 24: ui.F12, 323 } 324 325 // CSI-style key sequences ending with '~', with the first argument always 27, 326 // the second argument identifying the modifier, and the third argument 327 // identifying the key. For instance, \e[27;5;9~ is Ctrl-Tab. 328 // 329 // NOTE(xiaq): The list is taken blindly from xterm-keys.c in the tmux source 330 // tree. I do not have a keyboard-terminal combination that generate such 331 // sequences, but assumably they are generated by some terminals for numpad 332 // inputs. 333 var csiSeqTilde27 = map[int]rune{ 334 9: '\t', 13: '\r', 335 33: '!', 35: '#', 39: '\'', 40: '(', 41: ')', 43: '+', 44: ',', 45: '-', 336 46: '.', 337 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', 54: '6', 55: '7', 338 56: '8', 57: '9', 339 58: ':', 59: ';', 60: '<', 61: '=', 62: '>', 63: ';', 340 } 341 342 // parseCSI parses a CSI-style key sequence. See comments above for all the 3 343 // variants this function handles. 344 func parseCSI(nums []int, last rune, seq string) ui.Key { 345 if k, ok := csiSeqByLast[last]; ok { 346 if len(nums) == 0 { 347 // Unmodified: \e[A (Up) 348 return k 349 } else if len(nums) == 2 && nums[0] == 1 { 350 // Modified: \e[1;5A (Ctrl-Up) 351 return xtermModify(k, nums[1], seq) 352 } else { 353 return ui.Key{} 354 } 355 } 356 357 switch last { 358 case '~': 359 if len(nums) == 1 || len(nums) == 2 { 360 if r, ok := csiSeqTilde[nums[0]]; ok { 361 k := ui.K(r) 362 if len(nums) == 1 { 363 // Unmodified: \e[5~ (e.g. PageUp) 364 return k 365 } 366 // Modified: \e[5;5~ (e.g. Ctrl-PageUp) 367 return xtermModify(k, nums[1], seq) 368 } 369 } else if len(nums) == 3 && nums[0] == 27 { 370 if r, ok := csiSeqTilde27[nums[2]]; ok { 371 k := ui.K(r) 372 return xtermModify(k, nums[1], seq) 373 } 374 } 375 case '$', '^', '@': 376 // Modified by urxvt; see comment above csiSeqTilde. 377 if len(nums) == 1 { 378 if r, ok := csiSeqTilde[nums[0]]; ok { 379 var mod ui.Mod 380 switch last { 381 case '$': 382 mod = ui.Shift 383 case '^': 384 mod = ui.Ctrl 385 case '@': 386 mod = ui.Shift | ui.Ctrl 387 } 388 return ui.K(r, mod) 389 } 390 } 391 } 392 393 return ui.Key{} 394 } 395 396 func xtermModify(k ui.Key, mod int, seq string) ui.Key { 397 if mod < 0 || mod > 16 { 398 // Out of range 399 return ui.Key{} 400 } 401 if mod == 0 { 402 return k 403 } 404 modFlags := mod - 1 405 if modFlags&0x1 != 0 { 406 k.Mod |= ui.Shift 407 } 408 if modFlags&0x2 != 0 { 409 k.Mod |= ui.Alt 410 } 411 if modFlags&0x4 != 0 { 412 k.Mod |= ui.Ctrl 413 } 414 if modFlags&0x8 != 0 { 415 // This should be Meta, but we currently conflate Meta and Alt. 416 k.Mod |= ui.Alt 417 } 418 return k 419 } 420 421 func mouseModify(n int) ui.Mod { 422 var mod ui.Mod 423 if n&4 != 0 { 424 mod |= ui.Shift 425 } 426 if n&8 != 0 { 427 mod |= ui.Alt 428 } 429 if n&16 != 0 { 430 mod |= ui.Ctrl 431 } 432 return mod 433 }