v / vlib / term / ui / termios_nix.c.v
765 lines · 706 sloc · 17.55 KB · 8e4dcc699acd2e529cb14313f265aebabfd59c28
Raw
1// Copyright (c) 2020-2024 Raúl Hernández. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module ui
5
6import os
7import strings
8import time
9import term.termios
10
11#include <signal.h>
12
13pub struct C.winsize {
14 ws_row u16
15 ws_col u16
16}
17
18const termios_at_startup = get_termios()
19
20const kitty_keyboard_flags = 0b10 | 0b1000 | 0b10000
21
22@[inline]
23fn get_termios() termios.Termios {
24 mut t := termios.Termios{}
25 termios.tcgetattr(C.STDIN_FILENO, mut t)
26 return t
27}
28
29@[inline]
30fn get_terminal_size() (u16, u16) {
31 winsz := C.winsize{}
32 termios.ioctl(0, u64(termios.flag(C.TIOCGWINSZ)), voidptr(&winsz))
33 return winsz.ws_row, winsz.ws_col
34}
35
36fn restore_terminal_state_signal(_ os.Signal) {
37 restore_terminal_state()
38}
39
40fn restore_terminal_state() {
41 termios_reset()
42 mut c := ctx_ptr
43 if unsafe { c != 0 } {
44 c.paused = true
45 load_title()
46 }
47 os.flush()
48}
49
50fn (mut ctx Context) termios_setup() ! {
51 // store the current title, so restore_terminal_state can get it back
52 save_title()
53
54 if !ctx.cfg.skip_init_checks && !(os.is_atty(C.STDIN_FILENO) != 0
55 && os.is_atty(C.STDOUT_FILENO) != 0) {
56 return error('not running under a TTY')
57 }
58
59 mut tios := get_termios()
60
61 if ctx.cfg.capture_events {
62 // Set raw input mode by unsetting ICANON and ECHO,
63 // as well as disable e.g. ctrl+c and ctrl.z
64 tios.c_iflag &=
65 termios.invert(termios.flag(int(C.IGNBRK) | int(C.BRKINT) | int(C.PARMRK) | int(C.IXON)))
66 tios.c_lflag &=
67 termios.invert(termios.flag(int(C.ICANON) | int(C.ISIG) | int(C.ECHO) | int(C.IEXTEN) | int(C.TOSTOP)))
68 } else {
69 // Set raw input mode by unsetting ICANON and ECHO
70 tios.c_lflag &= termios.invert(termios.flag(int(C.ICANON) | int(C.ECHO)))
71 }
72
73 if ctx.cfg.hide_cursor {
74 ctx.hide_cursor()
75 ctx.flush()
76 }
77
78 if ctx.cfg.window_title != '' {
79 print('\x1b]0;${ctx.cfg.window_title}\x07')
80 flush_stdout()
81 }
82
83 if !ctx.cfg.skip_init_checks {
84 // prevent blocking during the feature detections, but allow enough time for the terminal
85 // to send back the relevant input data
86 tios.c_cc[C.VTIME] = 1
87 tios.c_cc[C.VMIN] = 0
88 termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut tios)
89 // feature-test the SU spec
90 sx, sy := get_cursor_position()
91 print('${bsu}${esu}')
92 flush_stdout()
93 ex, ey := get_cursor_position()
94 if sx == ex && sy == ey {
95 // the terminal either ignored or handled the sequence properly, enable SU
96 ctx.enable_su = true
97 } else {
98 ctx.draw_line(sx, sy, ex, ey)
99 ctx.set_cursor_position(sx, sy)
100 ctx.flush()
101 }
102 // feature-test rgb (truecolor) support
103 ctx.enable_rgb = supports_truecolor()
104 }
105 // Prevent stdin from blocking by making its read time 0
106 tios.c_cc[C.VTIME] = 0
107 tios.c_cc[C.VMIN] = 0
108 termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut tios)
109 if ctx.cfg.mouse_enabled {
110 // enable mouse input
111 print('\x1b[?1003h\x1b[?1006h')
112 flush_stdout()
113 }
114 if ctx.cfg.use_alternate_buffer {
115 // switch to the alternate buffer
116 print('\x1b[?1049h')
117 flush_stdout()
118 // clear the terminal and set the cursor to the origin
119 print('\x1b[2J\x1b[3J\x1b[1;1H')
120 flush_stdout()
121 }
122 if ctx.cfg.capture_events {
123 // Ask supporting terminals to report press/repeat/release in CSI-u form.
124 print('\x1b[>${kitty_keyboard_flags}u')
125 flush_stdout()
126 }
127 ctx.window_height, ctx.window_width = get_terminal_size()
128
129 // Reset console on exit
130 at_exit(restore_terminal_state) or {}
131 os.signal_opt(.tstp, restore_terminal_state_signal) or {}
132 os.signal_opt(.cont, fn (_ os.Signal) {
133 mut c := ctx_ptr
134 if unsafe { c != 0 } {
135 c.termios_setup() or { panic(err) }
136 c.window_height, c.window_width = get_terminal_size()
137 mut event := &Event{
138 typ: .resized
139 width: c.window_width
140 height: c.window_height
141 }
142 c.paused = false
143 c.event(event)
144 }
145 }) or {}
146 for code in ctx.cfg.reset {
147 os.signal_opt(code, fn (_ os.Signal) {
148 mut c := ctx_ptr
149 if unsafe { c != 0 } {
150 c.cleanup()
151 }
152 exit(0)
153 }) or {}
154 }
155
156 os.signal_opt(.winch, fn (_ os.Signal) {
157 mut c := ctx_ptr
158 if unsafe { c != 0 } {
159 c.window_height, c.window_width = get_terminal_size()
160
161 mut event := &Event{
162 typ: .resized
163 width: c.window_width
164 height: c.window_height
165 }
166 c.event(event)
167 }
168 }) or {}
169
170 os.flush()
171}
172
173fn get_cursor_position() (int, int) {
174 print('\033[6n')
175 flush_stdout()
176 mut s := ''
177 unsafe {
178 buf := malloc_noscan(25)
179 len := C.read(C.STDIN_FILENO, buf, 24)
180 buf[len] = 0
181 s = tos(buf, len)
182 }
183 if s.len == 0 {
184 return -1, -1
185 }
186 a := s[2..].split(';')
187 if a.len != 2 {
188 return -1, -1
189 }
190 return a[0].int(), a[1].int()
191}
192
193fn supports_truecolor() bool {
194 // faster/simpler, but less reliable, check
195 if os.getenv('COLORTERM') in ['truecolor', '24bit'] {
196 return true
197 }
198 // set the bg color to some arbitrary value (#010203), assumed not to be the default
199 print('\x1b[48:2:1:2:3m')
200 flush_stdout()
201 // andquery the current color
202 print('\x1bP\$qm\x1b\\')
203 flush_stdout()
204 mut s := ''
205 unsafe {
206 buf := malloc_noscan(25)
207 len := C.read(C.STDIN_FILENO, buf, 24)
208 buf[len] = 0
209 s = tos(buf, len)
210 }
211 return s.contains('1:2:3')
212}
213
214fn termios_reset() {
215 // C.TCSANOW ??
216 mut startup := termios_at_startup
217 termios.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, mut startup)
218 c := ctx_ptr
219 if unsafe { c != 0 } && c.cfg.capture_events {
220 // Pop the keyboard mode stack before leaving the current screen.
221 print('\x1b[<u')
222 }
223 print('\x1b[?1003l\x1b[?1006l\x1b[?25h')
224 flush_stdout()
225 if unsafe { c != 0 } && c.cfg.use_alternate_buffer {
226 print('\x1b[?1049l')
227 }
228 os.flush()
229}
230
231///////////////////////////////////////////
232// TODO: do multiple sleep/read cycles, rather than one big one
233fn (mut ctx Context) termios_loop() {
234 frame_time := 1_000_000 / ctx.cfg.frame_rate
235 mut init_called := false
236 mut sw := time.new_stopwatch(auto_start: false)
237 mut sleep_len := 0
238 for {
239 if !init_called {
240 ctx.init()
241 init_called = true
242 }
243 // println('SLEEPING: ${sleep_len}')
244 if sleep_len > 0 {
245 time.sleep(sleep_len * time.microsecond)
246 }
247 if !ctx.paused {
248 sw.restart()
249 if ctx.cfg.event_fn != none {
250 unsafe {
251 len := C.read(C.STDIN_FILENO, &u8(ctx.read_buf.data) + ctx.read_buf.len,
252 ctx.read_buf.cap - ctx.read_buf.len)
253 ctx.resize_arr(ctx.read_buf.len + len)
254 }
255 if ctx.read_buf.len > 0 {
256 ctx.parse_events()
257 }
258 }
259 ctx.frame()
260 sw.pause()
261 e := sw.elapsed().microseconds()
262 sleep_len = frame_time - int(e)
263
264 ctx.frame_count++
265 }
266 }
267}
268
269fn (mut ctx Context) parse_events() {
270 // Stop this from getting stuck in rare cases where something isn't parsed correctly
271 mut nr_iters := 0
272 for ctx.read_buf.len > 0 {
273 nr_iters++
274 if nr_iters > 100 {
275 ctx.shift(1)
276 }
277 mut event := &Event(unsafe { nil })
278 if ctx.read_buf[0] == 0x1b {
279 e, len := escape_sequence(ctx.read_buf.bytestr())
280 event = unsafe { e }
281 ctx.shift(len)
282 } else {
283 if ctx.read_all_bytes {
284 e, len := multi_char(ctx.read_buf.bytestr())
285 event = unsafe { e }
286 ctx.shift(len)
287 } else {
288 event = single_char(ctx.read_buf.bytestr())
289 ctx.shift(1)
290 }
291 }
292 if unsafe { event != 0 } {
293 ctx.event(event)
294 nr_iters = 0
295 }
296 }
297}
298
299fn single_char(buf string) &Event {
300 ch := buf[0]
301
302 mut event := &Event{
303 typ: .key_down
304 ascii: ch
305 code: unsafe { KeyCode(ch) }
306 utf8: ch.ascii_str()
307 }
308
309 match ch {
310 // special handling for `ctrl + letter`
311 // TODO: Fix assoc in V and remove this workaround :/
312 // 1 ... 26 { event = Event{ ...event, code: KeyCode(96 | ch), modifiers: .ctrl } }
313 // 65 ... 90 { event = Event{ ...event, code: KeyCode(32 | ch), modifiers: .shift } }
314 // The bit `or`s here are really just `+`'s, just written in this way for a tiny performance improvement
315 // don't treat tab, enter as ctrl+i, ctrl+j
316 1...8, 11...26 {
317 event = &Event{
318 typ: event.typ
319 ascii: event.ascii
320 utf8: event.utf8
321 code: unsafe { KeyCode(96 | ch) }
322 modifiers: .ctrl
323 }
324 }
325 65...90 {
326 event = &Event{
327 typ: event.typ
328 ascii: event.ascii
329 utf8: event.utf8
330 code: unsafe { KeyCode(32 | ch) }
331 modifiers: .shift
332 }
333 }
334 else {}
335 }
336
337 return event
338}
339
340fn multi_char(buf string) (&Event, int) {
341 ch := buf[0]
342
343 mut event := &Event{
344 typ: .key_down
345 ascii: ch
346 code: unsafe { KeyCode(ch) }
347 utf8: buf
348 }
349
350 match ch {
351 // special handling for `ctrl + letter`
352 // TODO: Fix assoc in V and remove this workaround :/
353 // 1 ... 26 { event = Event{ ...event, code: KeyCode(96 | ch), modifiers: .ctrl } }
354 // 65 ... 90 { event = Event{ ...event, code: KeyCode(32 | ch), modifiers: .shift } }
355 // The bit `or`s here are really just `+`'s, just written in this way for a tiny performance improvement
356 // don't treat tab, enter as ctrl+i, ctrl+j
357 1...8, 11...26 {
358 event = &Event{
359 typ: event.typ
360 ascii: event.ascii
361 utf8: event.utf8
362 code: unsafe { KeyCode(96 | ch) }
363 modifiers: .ctrl
364 }
365 }
366 65...90 {
367 event = &Event{
368 typ: event.typ
369 ascii: event.ascii
370 utf8: event.utf8
371 code: unsafe { KeyCode(32 | ch) }
372 modifiers: .shift
373 }
374 }
375 else {}
376 }
377
378 return event, buf.len
379}
380
381// Gets an entire, independent escape sequence from the buffer
382// Normally, this just means reading until the first letter, but there are some exceptions...
383fn escape_end(buf string) int {
384 mut i := 0
385 for {
386 if i + 1 == buf.len {
387 return buf.len
388 }
389
390 if buf[i].is_letter() || buf[i] == `~` {
391 if buf[i] == `O` && i + 2 <= buf.len {
392 n := buf[i + 1]
393 if (n >= `A` && n <= `D`) || (n >= `P` && n <= `S`) || n == `F` || n == `H` {
394 return i + 2
395 }
396 }
397 return i + 1
398 // escape hatch to avoid potential issues/crashes, although ideally this should never eval to true
399 } else if buf[i + 1] == 0x1b {
400 return i + 1
401 }
402 i++
403 }
404 // this point should be unreachable
405 assert false
406 return 0
407}
408
409@[inline]
410fn modifiers_from_report_param(param int) Modifiers {
411 flags := if param > 0 { param - 1 } else { 0 }
412 mut modifiers := unsafe { Modifiers(0) }
413 if flags & 0b001 != 0 {
414 modifiers.set(.shift)
415 }
416 if flags & 0b010 != 0 {
417 modifiers.set(.alt)
418 }
419 if flags & 0b100 != 0 {
420 modifiers.set(.ctrl)
421 }
422 return modifiers
423}
424
425@[inline]
426fn key_event_type_from_report_param(param int) EventType {
427 return match param {
428 3 { .key_up }
429 else { .key_down }
430 }
431}
432
433@[inline]
434fn parse_key_report_param(param string) (Modifiers, EventType) {
435 if param.len == 0 {
436 return unsafe { Modifiers(0) }, EventType.key_down
437 }
438 parts := param.split(':')
439 modifiers := modifiers_from_report_param(parts[0].int())
440 event_type := if parts.len > 1 {
441 key_event_type_from_report_param(parts[1].int())
442 } else {
443 EventType.key_down
444 }
445 return modifiers, event_type
446}
447
448fn utf8_from_reported_text(param string) string {
449 if param.len == 0 {
450 return ''
451 }
452 mut builder := strings.new_builder(param.len)
453 for part in param.split(':') {
454 codepoint := part.int()
455 if codepoint <= 0 || codepoint > 0x10ffff {
456 continue
457 }
458 builder.write_string(utf32_to_str(u32(codepoint)))
459 }
460 return builder.str()
461}
462
463@[inline]
464fn event_from_reported_key(codepoint int, raw string, modifiers Modifiers, event_type EventType, text string) &Event {
465 mut utf8 := raw
466 if text.len > 0 {
467 utf8 = text
468 }
469 mut ascii := u8(0)
470 if text.len == 1 {
471 ascii = text[0]
472 }
473 if codepoint <= 0 || codepoint > 0x10ffff {
474 return &Event{
475 typ: event_type
476 ascii: ascii
477 utf8: utf8
478 modifiers: modifiers
479 }
480 }
481 if codepoint <= 255 {
482 ch := u8(codepoint)
483 if ch == `\r` {
484 return &Event{
485 typ: event_type
486 ascii: ch
487 code: .enter
488 utf8: utf8
489 modifiers: modifiers
490 }
491 }
492 base := single_char(ch.ascii_str())
493 event_ascii := if ascii != 0 { ascii } else { base.ascii }
494 return &Event{
495 typ: event_type
496 ascii: event_ascii
497 code: base.code
498 utf8: utf8
499 modifiers: base.modifiers | modifiers
500 }
501 }
502 return &Event{
503 typ: event_type
504 ascii: ascii
505 utf8: utf8
506 modifiers: modifiers
507 }
508}
509
510fn parse_csi_u_key_sequence(single string, buf string) &Event {
511 if buf.len < 4 || buf[0] != `[` || buf[buf.len - 1] != `u` {
512 return unsafe { nil }
513 }
514 parts := buf[1..buf.len - 1].split(';')
515 if parts.len < 1 || parts.len > 3 {
516 return unsafe { nil }
517 }
518 codepoint := parts[0].split(':')[0].int()
519 mut modifiers := unsafe { Modifiers(0) }
520 mut event_type := EventType.key_down
521 if parts.len > 1 {
522 modifiers, event_type = parse_key_report_param(parts[1])
523 }
524 text := if parts.len > 2 { utf8_from_reported_text(parts[2]) } else { '' }
525 return event_from_reported_key(codepoint, single, modifiers, event_type, text)
526}
527
528fn parse_modify_other_keys_sequence(single string, buf string) &Event {
529 if buf.len < 7 || buf[0] != `[` || buf[buf.len - 1] != `~` {
530 return unsafe { nil }
531 }
532 parts := buf[1..buf.len - 1].split(';')
533 if parts.len != 3 || parts[0] != '27' {
534 return unsafe { nil }
535 }
536 return event_from_reported_key(parts[2].int(), single,
537 modifiers_from_report_param(parts[1].int()), .key_down, '')
538}
539
540@[inline]
541fn key_code_from_csi_final(final u8) KeyCode {
542 return match final {
543 `A` { .up }
544 `B` { .down }
545 `C` { .right }
546 `D` { .left }
547 `F` { .end }
548 `H` { .home }
549 `P` { .f1 }
550 `Q` { .f2 }
551 `R` { .f3 }
552 `S` { .f4 }
553 else { .null }
554 }
555}
556
557@[inline]
558fn key_code_from_tilde_param(param string) KeyCode {
559 return match param {
560 '2' { .insert }
561 '3' { .delete }
562 '5' { .page_up }
563 '6' { .page_down }
564 '11' { .f1 }
565 '12' { .f2 }
566 '13' { .f3 }
567 '14' { .f4 }
568 '15' { .f5 }
569 '17' { .f6 }
570 '18' { .f7 }
571 '19' { .f8 }
572 '20' { .f9 }
573 '21' { .f10 }
574 '23' { .f11 }
575 '24' { .f12 }
576 else { .null }
577 }
578}
579
580fn parse_csi_modified_key_sequence(buf string) (KeyCode, Modifiers, EventType, bool) {
581 if buf.len < 5 || buf[0] != `[` {
582 return KeyCode.null, unsafe { Modifiers(0) }, EventType.key_down, false
583 }
584 final := buf[buf.len - 1]
585 params := buf[1..buf.len - 1].split(';')
586 if params.len != 2 {
587 return KeyCode.null, unsafe { Modifiers(0) }, EventType.key_down, false
588 }
589 modifiers, event_type := parse_key_report_param(params[1])
590 if final == `~` {
591 code := key_code_from_tilde_param(params[0])
592 return code, modifiers, event_type, code != .null
593 }
594 code := key_code_from_csi_final(final)
595 return code, modifiers, event_type, code != .null
596}
597
598fn escape_sequence(buf_ string) (&Event, int) {
599 end := escape_end(buf_)
600 single := buf_[..end] // read until the end of the sequence
601 buf := single[1..] // skip the escape character
602
603 if buf.len == 0 {
604 return &Event{
605 typ: .key_down
606 ascii: 27
607 code: .escape
608 utf8: single
609 }, 1
610 }
611
612 if buf.len == 1 {
613 c := single_char(buf)
614 mut modifiers := c.modifiers
615 modifiers.set(.alt)
616 return &Event{
617 typ: c.typ
618 ascii: c.ascii
619 code: c.code
620 utf8: single
621 modifiers: modifiers
622 }, 2
623 }
624 // ----------------
625 // Mouse events
626 // ----------------
627 // Documentation: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
628 if buf.len > 2 && buf[1] == `<` {
629 split := buf[2..].split(';')
630 if split.len < 3 {
631 return &Event(unsafe { nil }), 0
632 }
633
634 typ, x, y := split[0].int(), split[1].int(), split[2].int()
635 lo := typ & 0b00011
636 hi := typ & 0b11100
637
638 mut modifiers := unsafe { Modifiers(0) }
639 if hi & 4 != 0 {
640 modifiers.set(.shift)
641 }
642 if hi & 8 != 0 {
643 modifiers.set(.alt)
644 }
645 if hi & 16 != 0 {
646 modifiers.set(.ctrl)
647 }
648
649 match typ {
650 0...31 {
651 last := buf[buf.len - 1]
652 button := if lo < 3 { unsafe { MouseButton(lo + 1) } } else { MouseButton.unknown }
653 event := if last == `m` || lo == 3 {
654 EventType.mouse_up
655 } else {
656 EventType.mouse_down
657 }
658
659 return &Event{
660 typ: event
661 x: x
662 y: y
663 button: button
664 modifiers: modifiers
665 utf8: single
666 }, end
667 }
668 32...63 {
669 button, event := if lo < 3 {
670 unsafe { MouseButton(lo + 1), EventType.mouse_drag }
671 } else {
672 MouseButton.unknown, EventType.mouse_move
673 }
674
675 return &Event{
676 typ: event
677 x: x
678 y: y
679 button: button
680 modifiers: modifiers
681 utf8: single
682 }, end
683 }
684 64...95 {
685 direction := if typ & 1 == 0 { Direction.down } else { Direction.up }
686 return &Event{
687 typ: .mouse_scroll
688 x: x
689 y: y
690 direction: direction
691 modifiers: modifiers
692 utf8: single
693 }, end
694 }
695 else {
696 return &Event{
697 typ: .unknown
698 utf8: single
699 }, end
700 }
701 }
702 }
703 // ----------------------------
704 // Special key combinations
705 // ----------------------------
706
707 e := parse_csi_u_key_sequence(single, buf)
708 if unsafe { e != nil } {
709 return e, end
710 }
711 e2 := parse_modify_other_keys_sequence(single, buf)
712 if unsafe { e2 != nil } {
713 return e2, end
714 }
715
716 mut code := KeyCode.null
717 mut modifiers := unsafe { Modifiers(0) }
718 mut event_type := EventType.key_down
719 match buf {
720 '[A', 'OA' { code = .up }
721 '[B', 'OB' { code = .down }
722 '[C', 'OC' { code = .right }
723 '[D', 'OD' { code = .left }
724 '[5~', '[[5~' { code = .page_up }
725 '[6~', '[[6~' { code = .page_down }
726 '[F', 'OF', '[4~', '[[8~' { code = .end }
727 '[H', 'OH', '[1~', '[[7~' { code = .home }
728 '[2~' { code = .insert }
729 '[3~' { code = .delete }
730 'OP', '[11~' { code = .f1 }
731 'OQ', '[12~' { code = .f2 }
732 'OR', '[13~' { code = .f3 }
733 'OS', '[14~' { code = .f4 }
734 '[15~' { code = .f5 }
735 '[17~' { code = .f6 }
736 '[18~' { code = .f7 }
737 '[19~' { code = .f8 }
738 '[20~' { code = .f9 }
739 '[21~' { code = .f10 }
740 '[23~' { code = .f11 }
741 '[24~' { code = .f12 }
742 else {}
743 }
744
745 if buf == '[Z' {
746 code = .tab
747 modifiers.set(.shift)
748 }
749
750 if code == .null {
751 parsed_code, parsed_modifiers, parsed_event_type, ok := parse_csi_modified_key_sequence(buf)
752 if ok {
753 code = parsed_code
754 modifiers = parsed_modifiers
755 event_type = parsed_event_type
756 }
757 }
758
759 return &Event{
760 typ: event_type
761 code: code
762 utf8: single
763 modifiers: modifiers
764 }, end
765}
766