diff --git a/cli/cli.odin b/cli/cli.odin index 7b7f3ee..e53ae98 100644 --- a/cli/cli.odin +++ b/cli/cli.odin @@ -16,36 +16,42 @@ HISTORY_MAX :: 2048 HISTORY_FILE :: "skal.history" Term :: struct { - input_buffer: [dynamic]byte, - pos: u32, - min_pos: u32, - history: [HISTORY_MAX]string, + input_buffer: [dynamic]rune, + pos: i32, + min_pos: i32, + history: [dynamic][]rune, + history_pos: Maybe(i32), + written_line: Maybe([]rune), orig_mode: posix.termios } -term := Term{pos = 0, min_pos = 0} +term := Term{pos = 0, min_pos = 0, history_pos = nil} init_cli :: proc () { mem_err: mem.Allocator_Error - term.input_buffer, mem_err = make([dynamic]byte, context.allocator) //todo deinit + term.input_buffer, mem_err = make([dynamic]rune, context.allocator) //todo deinit + log.assertf(mem_err == nil, "Memory allocation failed") + + term.history, mem_err = make([dynamic][]rune, context.allocator) //todo deinit + log.assertf(mem_err == nil, "Memory allocation failed") + home_path := string(posix.getenv("HOME")) log.assertf(home_path != "", "Home path not found") - history_file := filepath.join({home_path, HISTORY_FILE}, context.allocator) //todo deinit + history_file := filepath.join({home_path, HISTORY_FILE}, context.allocator) log.assertf(mem_err == nil, "Memory allocation failed") defer delete(history_file) history_content, err := os.read_entire_file_from_filename_or_err(history_file, context.allocator) - if err != nil { fmt.printfln("skal: Couldn't read history file, continuing without history...") - defer delete(history_content) return } + defer delete(history_content) history_content_it := string(history_content) for line in strings.split_lines_iterator(&history_content_it) { - fmt.printfln(line) + append(&term.history, utf8.string_to_runes(line)) } } @@ -98,7 +104,7 @@ get_prompt_prefix :: proc() -> string { @(private) get_input :: proc() -> string { - return string(term.input_buffer[:]) + return utf8.runes_to_string(term.input_buffer[:]) } @(private) @@ -108,12 +114,13 @@ reset_prompt :: proc() { @(private) handle_esc_seq :: proc(in_stream: ^io.Stream) { - next_ch, _, err := io.read_rune(in_stream^) + NAV_SEQ_START :: '[' + + next_rn, _, err := io.read_rune(in_stream^) log.assertf(err == nil, "Couldn't read from stdin") - NAV_SEQ_START :: '[' - if next_ch == NAV_SEQ_START do handle_nav(in_stream) + if next_rn == NAV_SEQ_START do handle_nav(in_stream) } @(private) @@ -123,14 +130,53 @@ handle_nav :: proc(in_stream: ^io.Stream) { RIGHT :: 'C' LEFT :: 'D' - ch, _, err := io.read_rune(in_stream^) + rn, _, err := io.read_rune(in_stream^) log.assertf(err == nil, "Couldn't read from stdin") - switch ch { + switch rn { case UP: + if term.history_pos == nil { + term.written_line = slice.clone(term.input_buffer[:], context.temp_allocator) + } + + for len(term.input_buffer) > 0 { + backwards() + } + history_pos := term.history_pos.? or_else i32(len(term.history)) + history_pos = max(history_pos-1, 0) + term.history_pos = history_pos + + new_input := term.history[history_pos] + append(&term.input_buffer, ..new_input[:]) + fmt.printf("%s", term.input_buffer[:]) + + term.pos = term.min_pos + i32(len(term.input_buffer)) case DOWN: + history_pos, ok := term.history_pos.? + if !ok do return + + for len(term.input_buffer) > 0 { + backwards() + } + + history_pos += 1 + new_input: []rune + if history_pos > i32(len(term.history)-1) { + term.history_pos = nil + history_pos = i32(len(term.history)-1) + new_input, ok = term.written_line.?; assert(ok) + term.written_line = nil + } else { + term.history_pos = history_pos + new_input = term.history[history_pos] + } + + append(&term.input_buffer, ..new_input[:]) + fmt.printf("%s", term.input_buffer[:]) + + term.pos = term.min_pos + i32(len(term.input_buffer)) case RIGHT: - if u32(len(term.input_buffer)) + term.min_pos > term.pos { + if i32(len(term.input_buffer)) + term.min_pos > term.pos { fmt.print("\x1b[C") term.pos += 1 } @@ -143,16 +189,20 @@ handle_nav :: proc(in_stream: ^io.Stream) { } } -// This is not correct for multisize runes backwards :: proc() { DELETE_AND_REVERSE :: "\b \b" - _, rm_size := utf8.decode_last_rune(term.input_buffer[:]) - if rm_size > 0 && term.pos > term.min_pos { - resize(&term.input_buffer, len(term.input_buffer)-rm_size) + if term.pos == term.min_pos do return + + last_rune := term.input_buffer[len(term.input_buffer)-1] + _, _, width := utf8.grapheme_count(utf8.runes_to_string({last_rune})) + resize(&term.input_buffer, len(term.input_buffer)-1) + + for _ in 0.. string { enable_raw_mode() skip_and_clear() in_stream := os.stream_from_handle(os.stdin) - + for { - ch, size, err := io.read_rune(in_stream) + rn, size, err := io.read_rune(in_stream) log.assertf(err == nil, "Couldn't read from stdin") - switch { - case ch == '\n': + switch rn { + case '\n': fmt.println() return get_input() - case ch == '\f': + case '\f': fmt.println() - return "clear" + return "clear" // This is a terminal history wipe, usually isn't - case ch == '\u007f': + case '\u007f': backwards() - case ch == '\x1b': + case '\x1b': handle_esc_seq(&in_stream) - - case ch == utf8.RUNE_EOF: + case utf8.RUNE_EOF: fallthrough - case ch == '\x04': + case '\x04': fmt.println() skip_and_clear() - case: - bytes, _ := utf8.encode_rune(ch) - append(&term.input_buffer, ..bytes[:size]) - term.pos += u32(size) + case: // Bug: if user moved to earlier character + append(&term.input_buffer, rn) + term.pos += 1 - fmt.print(ch) + fmt.print(rn) } }