v2 / vlib / readline / readline_nix.c.v
673 lines · 619 sloc · 17.78 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4//
5// Serves as more advanced input method
6// based on the work of https://github.com/AmokHuginnsson/replxx
7//
8module readline
9
10import term.termios
11import term
12import os
13import encoding.utf8.east_asian
14
15fn C.raise(sig i32)
16
17fn C.getppid() i32
18
19// Action defines what actions to be executed.
20enum Action {
21 eof
22 nothing
23 insert_character
24 commit_line
25 delete_left
26 delete_right
27 delete_word_left
28 delete_line
29 move_cursor_left
30 move_cursor_right
31 move_cursor_start
32 move_cursor_end
33 move_cursor_word_left
34 move_cursor_word_right
35 history_previous
36 history_next
37 overwrite
38 clear_screen
39 suspend
40 completion
41}
42
43// enable_raw_mode enables the raw mode of the terminal.
44// In raw mode all key presses are directly sent to the program and no interpretation is done.
45// Please note that `enable_raw_mode` catches the `SIGUSER` (CTRL + C) signal.
46// For a method that does please see `enable_raw_mode_nosig`.
47pub fn (mut r Readline) enable_raw_mode() {
48 if termios.tcgetattr(0, mut r.orig_termios) != 0 {
49 r.is_tty = false
50 r.is_raw = false
51 return
52 }
53 mut raw := r.orig_termios
54 // println('> r.orig_termios: ${r.orig_termios}')
55 // println('> raw: ${raw}')
56 raw.c_iflag &=
57 termios.invert(termios.flag(int(C.BRKINT) | int(C.ICRNL) | int(C.INPCK) | int(C.ISTRIP) | int(C.IXON)))
58 raw.c_cflag |= termios.flag(C.CS8)
59 raw.c_lflag &=
60 termios.invert(termios.flag(int(C.ECHO) | int(C.ICANON) | int(C.IEXTEN) | int(C.ISIG)))
61 raw.c_cc[C.VMIN] = u8(1)
62 raw.c_cc[C.VTIME] = u8(0)
63 termios.tcsetattr(0, C.TCSADRAIN, mut raw)
64 // println('> after raw: ${raw}')
65 r.is_raw = true
66 r.is_tty = true
67}
68
69// enable_raw_mode_nosig enables the raw mode of the terminal.
70// In raw mode all key presses are directly sent to the program and no interpretation is done.
71// Please note that `enable_raw_mode_nosig` does not catch the `SIGUSER` (CTRL + C) signal
72// as opposed to `enable_raw_mode`.
73pub fn (mut r Readline) enable_raw_mode_nosig() {
74 if termios.tcgetattr(0, mut r.orig_termios) != 0 {
75 r.is_tty = false
76 r.is_raw = false
77 return
78 }
79 mut raw := r.orig_termios
80 raw.c_iflag &=
81 termios.invert(termios.flag(int(C.BRKINT) | int(C.ICRNL) | int(C.INPCK) | int(C.ISTRIP) | int(C.IXON)))
82 raw.c_cflag |= termios.flag(C.CS8)
83 raw.c_lflag &= termios.invert(termios.flag(int(C.ECHO) | int(C.ICANON) | int(C.IEXTEN)))
84 raw.c_cc[C.VMIN] = u8(1)
85 raw.c_cc[C.VTIME] = u8(0)
86 termios.tcsetattr(0, C.TCSADRAIN, mut raw)
87 r.is_raw = true
88 r.is_tty = true
89}
90
91// disable_raw_mode disables the raw mode of the terminal.
92// For a description of raw mode please see the `enable_raw_mode` method.
93pub fn (mut r Readline) disable_raw_mode() {
94 if r.is_raw {
95 termios.tcsetattr(0, C.TCSADRAIN, mut r.orig_termios)
96 r.is_raw = false
97 }
98}
99
100// read_char reads a single character.
101pub fn (r Readline) read_char() !int {
102 return int(term.utf8_getchar() or { return err })
103}
104
105// read_line_utf8 blocks execution in a loop and awaits user input
106// characters from a terminal until `EOF` or `Enter` key is encountered
107// in the input stream.
108// read_line_utf8 returns the complete UTF-8 input line as an UTF-32 encoded `[]rune` or
109// an error if the line is empty.
110// The `prompt` `string` is output as a prefix text for the input capturing.
111// read_line_utf8 is the main method of the `readline` module and `Readline` struct.
112pub fn (mut r Readline) read_line_utf8(prompt string) ![]rune {
113 r.current = []rune{}
114 r.cursor = 0
115 r.prompt = prompt
116 r.search_index = 0
117 r.prompt_offset = get_prompt_offset(prompt)
118 if r.previous_lines.len <= 1 {
119 r.previous_lines << []rune{}
120 r.previous_lines << []rune{}
121 } else {
122 r.previous_lines[0] = []rune{}
123 }
124 if !r.is_raw {
125 r.enable_raw_mode()
126 }
127 print(r.prompt)
128 for {
129 flush_stdout()
130 c := r.read_char() or { return err }
131 a := r.analyse(c)
132 if r.execute(a, c) {
133 break
134 }
135 }
136 r.previous_lines[0] = []rune{}
137 r.search_index = 0
138 r.disable_raw_mode()
139 if r.current.len == 0 {
140 return error('empty line')
141 } else {
142 if r.current.last() == `\n` {
143 r.current.pop()
144 }
145 }
146 return r.current
147}
148
149// read_line does the same as `read_line_utf8` but returns user input as a `string`.
150// (As opposed to `[]rune` returned by `read_line_utf8`).
151pub fn (mut r Readline) read_line(prompt string) !string {
152 s := r.read_line_utf8(prompt)!
153 return s.string()
154}
155
156// read_line_utf8 blocks execution in a loop and awaits user input
157// characters from a terminal until `EOF` or `Enter` key is encountered
158// in the input stream.
159// read_line_utf8 returns the complete UTF-8 input line as an UTF-32 encoded `[]rune` or
160// an error if the line is empty.
161// The `prompt` `string` is output as a prefix text for the input capturing.
162// read_line_utf8 is the main method of the `readline` module and `Readline` struct.
163// NOTE that this version of `read_line_utf8` is a standalone function without
164// persistent functionalities (e.g. history).
165pub fn read_line_utf8(prompt string) ![]rune {
166 mut r := Readline{}
167 s := r.read_line_utf8(prompt)!
168 return s
169}
170
171// read_line does the same as `read_line_utf8` but returns user input as a `string`.
172// (As opposed to `[]rune` as returned by `read_line_utf8`).
173// NOTE that this version of `read_line` is a standalone function without
174// persistent functionalities (e.g. history).
175pub fn read_line(prompt string) !string {
176 mut r := Readline{}
177 s := r.read_line(prompt)!
178 return s
179}
180
181// analyse returns an `Action` based on the type of input byte given in `c`.
182fn (mut r Readline) analyse(c int) Action {
183 if c > 255 {
184 return Action.insert_character
185 }
186 match u8(c) {
187 `\0`, 0x3, 0x4, 255 {
188 return .eof
189 } // NUL, End of Text, End of Transmission
190 `\n`, `\r` {
191 r.last_prefix_completion.clear()
192 return .commit_line
193 }
194 `\t` {
195 return .completion
196 }
197 `\f` {
198 return .clear_screen
199 } // CTRL + L
200 `\b`, 127 {
201 return .delete_left
202 } // BS, DEL
203 27 {
204 return r.analyse_control()
205 } // ESC
206 21 {
207 return .delete_line
208 } // CTRL + U
209 23 {
210 return .delete_word_left
211 } // CTRL + W
212 1 {
213 return .move_cursor_start
214 } // ^A
215 5 {
216 return .move_cursor_end
217 } // ^E
218 26 {
219 return .suspend
220 } // CTRL + Z, SUB
221 else {
222 if c >= ` ` {
223 r.last_prefix_completion.clear()
224 return Action.insert_character
225 }
226 return Action.nothing
227 }
228 }
229}
230
231// analyse_control returns an `Action` based on the type of input read by `read_char`.
232fn (r Readline) analyse_control() Action {
233 c := r.read_char() or { panic('Control sequence incomplete') }
234 match u8(c) {
235 `[` {
236 sequence := r.read_char() or { panic('Control sequence incomplete') }
237 match u8(sequence) {
238 `C` { return .move_cursor_right }
239 `D` { return .move_cursor_left }
240 `B` { return .history_next }
241 `A` { return .history_previous }
242 `H` { return .move_cursor_start }
243 `F` { return .move_cursor_end }
244 `1` { return r.analyse_extended_control() }
245 `2`, `3` { return r.analyse_extended_control_no_eat(u8(sequence)) }
246 else {}
247 }
248 }
249 else {}
250 }
251
252 return .nothing
253}
254
255// analyse_extended_control returns an `Action` based on the type of input read by `read_char`.
256// analyse_extended_control specialises in cursor control.
257fn (r Readline) analyse_extended_control() Action {
258 r.read_char() or { panic('Control sequence incomplete') } // Removes ;
259 c := r.read_char() or { panic('Control sequence incomplete') }
260 match u8(c) {
261 `5` {
262 direction := r.read_char() or { panic('Control sequence incomplete') }
263 match u8(direction) {
264 `C` { return .move_cursor_word_right }
265 `D` { return .move_cursor_word_left }
266 else {}
267 }
268 }
269 else {}
270 }
271
272 return .nothing
273}
274
275// analyse_extended_control_no_eat returns an `Action` based on the type of input byte given in `c`.
276// analyse_extended_control_no_eat specialises in detection of delete and insert keys.
277fn (r Readline) analyse_extended_control_no_eat(last_c u8) Action {
278 c := r.read_char() or { panic('Control sequence incomplete') }
279 match u8(c) {
280 `~` {
281 match last_c {
282 `3` { return .delete_right } // Suppr key
283 `2` { return .overwrite }
284 else {}
285 }
286 }
287 else {}
288 }
289
290 return .nothing
291}
292
293// execute executes the corresponding methods on `Readline` based on `a Action` and `c int` arguments.
294fn (mut r Readline) execute(a Action, c int) bool {
295 match a {
296 .eof { return r.eof() }
297 .insert_character { r.insert_character(c) }
298 .commit_line { return r.commit_line() }
299 .delete_left { r.delete_character() }
300 .delete_right { r.suppr_character() }
301 .delete_line { r.delete_line() }
302 .delete_word_left { r.delete_word_left() }
303 .move_cursor_left { r.move_cursor_left() }
304 .move_cursor_right { r.move_cursor_right() }
305 .move_cursor_start { r.move_cursor_start() }
306 .move_cursor_end { r.move_cursor_end() }
307 .move_cursor_word_left { r.move_cursor_word_left() }
308 .move_cursor_word_right { r.move_cursor_word_right() }
309 .history_previous { r.history_previous() }
310 .history_next { r.history_next() }
311 .overwrite { r.switch_overwrite() }
312 .clear_screen { r.clear_screen() }
313 .suspend { r.suspend() }
314 .completion { r.completion() }
315 else {}
316 }
317
318 return false
319}
320
321// get_screen_columns returns the number of columns (`width`) in the terminal.
322fn get_screen_columns() int {
323 ws := Winsize{}
324 cols := if unsafe { C.ioctl(1, C.TIOCGWINSZ, &ws) } == -1 { 80 } else { int(ws.ws_col) }
325 return cols
326}
327
328// shift_cursor warps the cursor to `xpos` with `yoffset`.
329fn shift_cursor(xpos int, yoffset int) {
330 if yoffset != 0 {
331 if yoffset > 0 {
332 term.cursor_down(yoffset)
333 } else {
334 term.cursor_up(-yoffset)
335 }
336 }
337 // Absolute X position
338 print('\x1b[${xpos + 1}G')
339}
340
341// calculate_screen_position returns a position `[x, y]int` based on various terminal attributes.
342fn calculate_screen_position(x_in int, y_in int, screen_columns int, char_count int, inp []int) []int {
343 mut out := inp.clone()
344 mut x := x_in
345 mut y := y_in
346 out[0] = x
347 out[1] = y
348 for chars_remaining := char_count; chars_remaining > 0; {
349 chars_this_row := if (x + chars_remaining) < screen_columns {
350 chars_remaining
351 } else {
352 screen_columns - x
353 }
354 out[0] = x + chars_this_row
355 out[1] = y
356 chars_remaining -= chars_this_row
357 x = 0
358 y++
359 }
360 if out[0] == screen_columns {
361 out[0] = 0
362 out[1]++
363 }
364 return out
365}
366
367// get_prompt_offset computes the length of the `prompt` `string` argument.
368fn get_prompt_offset(prompt string) int {
369 mut len := 0
370 for i := 0; i < prompt.len; i++ {
371 if prompt[i] == `\e` {
372 for ; i < prompt.len && prompt[i] != `m`; i++ {
373 }
374 } else {
375 len = len + 1
376 }
377 }
378 return prompt.len - len
379}
380
381// refresh_line redraws the current line, including the prompt.
382fn (mut r Readline) refresh_line() {
383 mut end_of_input := [0, 0]
384 last_prompt_line := if r.prompt.contains('\n') {
385 r.prompt.all_after_last('\n')
386 } else {
387 r.prompt
388 }
389 last_prompt_width := east_asian.display_width(last_prompt_line, 1)
390 current_width := east_asian.display_width(r.current.string(), 1)
391 cursor_prefix_width := east_asian.display_width(r.current[..r.cursor].string(), 1)
392
393 end_of_input = calculate_screen_position(last_prompt_width, 0, get_screen_columns(),
394 current_width, end_of_input)
395 end_of_input[1] += r.current.filter(it == `\n`).len
396 mut cursor_pos := [0, 0]
397 cursor_pos = calculate_screen_position(last_prompt_width, 0, get_screen_columns(),
398 cursor_prefix_width, cursor_pos)
399 shift_cursor(0, -r.cursor_row_offset)
400 term.erase_toend()
401 print(last_prompt_line)
402 print(r.current.string())
403 if end_of_input[0] == 0 && end_of_input[1] > 0 {
404 print('\n')
405 }
406 shift_cursor(cursor_pos[0], -(end_of_input[1] - cursor_pos[1]))
407 r.cursor_row_offset = cursor_pos[1]
408}
409
410// eof ends the line *without* a newline.
411fn (mut r Readline) eof() bool {
412 r.previous_lines.insert(1, r.current)
413 r.cursor = r.current.len
414 if r.is_tty {
415 r.refresh_line()
416 }
417 return true
418}
419
420// insert_character inserts the character `c` at current cursor position.
421fn (mut r Readline) insert_character(c int) {
422 if !r.overwrite || r.cursor == r.current.len {
423 r.current.insert(r.cursor, c)
424 } else {
425 r.current[r.cursor] = rune(c)
426 }
427 r.cursor++
428 // Refresh the line to add the new character
429 if r.is_tty {
430 r.refresh_line()
431 }
432}
433
434// Removes the character behind cursor.
435fn (mut r Readline) delete_character() {
436 if r.cursor <= 0 {
437 return
438 }
439 r.cursor--
440 r.current.delete(r.cursor)
441 r.refresh_line()
442 r.completion_clear()
443}
444
445fn (mut r Readline) delete_word_left() {
446 if r.cursor == 0 {
447 return
448 }
449
450 orig_cursor := r.cursor
451 if r.cursor >= r.current.len {
452 r.cursor = r.current.len - 1
453 }
454
455 if r.current[r.cursor] != ` ` && r.current[r.cursor - 1] == ` ` {
456 r.cursor--
457 }
458
459 if r.current[r.cursor] == ` ` {
460 for r.cursor > 0 && r.current[r.cursor] == ` ` {
461 r.cursor--
462 }
463 for r.cursor > 0 && r.current[r.cursor - 1] != ` ` {
464 r.cursor--
465 }
466 } else {
467 for r.cursor > 0 {
468 if r.current[r.cursor - 1] == ` ` {
469 break
470 }
471 r.cursor--
472 }
473 }
474
475 r.current.delete_many(r.cursor, orig_cursor - r.cursor)
476 r.refresh_line()
477 r.completion_clear()
478}
479
480fn (mut r Readline) delete_line() {
481 r.current = []
482 r.cursor = 0
483 r.refresh_line()
484 r.completion_clear()
485}
486
487// suppr_character removes (suppresses) the character in front of the cursor.
488fn (mut r Readline) suppr_character() {
489 if r.cursor >= r.current.len {
490 return
491 }
492 r.current.delete(r.cursor)
493 r.refresh_line()
494}
495
496// commit_line adds a line break and then stops the main loop.
497fn (mut r Readline) commit_line() bool {
498 r.previous_lines.insert(1, r.current)
499 r.current << `\n`
500 r.cursor = r.current.len
501 if r.is_tty {
502 r.refresh_line()
503 println('')
504 }
505 return true
506}
507
508// move_cursor_left moves the cursor relative one cell to the left.
509fn (mut r Readline) move_cursor_left() {
510 if r.cursor > 0 {
511 r.cursor--
512 r.refresh_line()
513 }
514}
515
516// move_cursor_right moves the cursor relative one cell to the right.
517fn (mut r Readline) move_cursor_right() {
518 if r.cursor < r.current.len {
519 r.cursor++
520 r.refresh_line()
521 }
522}
523
524// move_cursor_start moves the cursor to the beginning of the current line.
525fn (mut r Readline) move_cursor_start() {
526 r.cursor = 0
527 r.refresh_line()
528}
529
530// move_cursor_end moves the cursor to the end of the current line.
531fn (mut r Readline) move_cursor_end() {
532 r.cursor = r.current.len
533 r.refresh_line()
534}
535
536// is_break_character returns true if the character is considered as a word-breaking character.
537fn (r Readline) is_break_character(c string) bool {
538 break_characters := ' \t\v\f\a\b\r\n`~!@#$%^&*()-=+[{]}\\|;:\'",<.>/?'
539 return break_characters.contains(c)
540}
541
542// move_cursor_word_left moves the cursor relative one word length worth to the left.
543fn (mut r Readline) move_cursor_word_left() {
544 if r.cursor > 0 {
545 for ; r.cursor > 0 && r.is_break_character(r.current[r.cursor - 1].str()); r.cursor-- {
546 }
547 for ; r.cursor > 0 && !r.is_break_character(r.current[r.cursor - 1].str()); r.cursor-- {
548 }
549 r.refresh_line()
550 }
551}
552
553// move_cursor_word_right moves the cursor relative one word length worth to the right.
554fn (mut r Readline) move_cursor_word_right() {
555 if r.cursor < r.current.len {
556 for ; r.cursor < r.current.len && r.is_break_character(r.current[r.cursor].str()); r.cursor++ {
557 }
558 for ; r.cursor < r.current.len && !r.is_break_character(r.current[r.cursor].str()); r.cursor++ {
559 }
560 r.refresh_line()
561 }
562}
563
564// switch_overwrite toggles Readline `overwrite` mode on/off.
565fn (mut r Readline) switch_overwrite() {
566 r.overwrite = !r.overwrite
567}
568
569// clear_screen clears the current terminal window contents and positions the cursor at top left.
570fn (mut r Readline) clear_screen() {
571 term.set_cursor_position(x: 1, y: 1)
572 term.erase_clear()
573 r.refresh_line()
574}
575
576// history_previous sets current line to the content of the previous line in the history buffer.
577fn (mut r Readline) history_previous() {
578 if r.search_index + 2 >= r.previous_lines.len {
579 return
580 }
581 if r.search_index == 0 {
582 r.previous_lines[0] = r.current
583 }
584 r.search_index++
585 prev_line := r.previous_lines[r.search_index]
586 if r.skip_empty && prev_line == [] {
587 r.history_previous()
588 } else {
589 r.current = prev_line
590 r.cursor = r.current.len
591 r.refresh_line()
592 }
593}
594
595// history_next sets current line to the content of the next line in the history buffer.
596fn (mut r Readline) history_next() {
597 if r.search_index <= 0 {
598 return
599 }
600 r.search_index--
601 r.current = r.previous_lines[r.search_index]
602 r.cursor = r.current.len
603 r.refresh_line()
604}
605
606// completion implements the tab completion feature
607fn (mut r Readline) completion() {
608 // check if completion is used
609 if r.completion_list.len == 0 && r.completion_callback == unsafe { nil } {
610 return
611 }
612 // use last prefix for completion or current input
613 prefix := if r.last_prefix_completion.len > 0 { r.last_prefix_completion } else { r.current }
614 if prefix.len == 0 {
615 return
616 }
617 // filtering by prefix
618 opts := if r.completion_list.len > 0 {
619 sprefix := prefix.string()
620 r.completion_list.filter(it.starts_with(sprefix))
621 } else if r.completion_callback != unsafe { nil } {
622 r.completion_callback(prefix.string())
623 } else {
624 []string{}
625 }
626 if opts.len == 0 {
627 r.completion_clear()
628 return
629 }
630
631 // moving for next possible completion match using saved prefix
632 if r.last_prefix_completion.len != 0 {
633 if opts.len > r.last_completion_offset + 1 {
634 r.last_completion_offset += 1
635 } else {
636 // reset for initial option in completion list
637 r.last_completion_offset = 0
638 }
639 } else {
640 // save current prefix before tab'ing
641 r.last_prefix_completion = r.current
642 }
643
644 // set the text to the current completion match in the completion list
645 r.current = opts[r.last_completion_offset].runes()
646 r.cursor = r.current.len
647 r.refresh_line()
648}
649
650// completion_clear resets the completion state
651fn (mut r Readline) completion_clear() {
652 r.last_prefix_completion.clear()
653 r.last_completion_offset = 0
654}
655
656// suspend sends the `SIGSTOP` signal to the terminal.
657fn (mut r Readline) suspend() {
658 is_standalone := os.getenv('VCHILD') != 'true'
659 r.disable_raw_mode()
660 if !is_standalone {
661 // We have to SIGSTOP the parent v process
662 unsafe {
663 ppid := C.getppid()
664 C.kill(ppid, C.SIGSTOP)
665 }
666 }
667 unsafe { C.raise(C.SIGSTOP) }
668 r.enable_raw_mode()
669 r.refresh_line()
670 if r.is_tty {
671 r.refresh_line()
672 }
673}
674