Add input history navigation

This commit is contained in:
Hugo Mårdbrink 2025-08-07 23:35:15 +02:00
parent dd9831f1b5
commit 50b0e6e641

View file

@ -16,36 +16,42 @@ HISTORY_MAX :: 2048
HISTORY_FILE :: "skal.history" HISTORY_FILE :: "skal.history"
Term :: struct { Term :: struct {
input_buffer: [dynamic]byte, input_buffer: [dynamic]rune,
pos: u32, pos: i32,
min_pos: u32, min_pos: i32,
history: [HISTORY_MAX]string, history: [dynamic][]rune,
history_pos: Maybe(i32),
written_line: Maybe([]rune),
orig_mode: posix.termios orig_mode: posix.termios
} }
term := Term{pos = 0, min_pos = 0} term := Term{pos = 0, min_pos = 0, history_pos = nil}
init_cli :: proc () { init_cli :: proc () {
mem_err: mem.Allocator_Error 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")) home_path := string(posix.getenv("HOME"))
log.assertf(home_path != "", "Home path not found") 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") log.assertf(mem_err == nil, "Memory allocation failed")
defer delete(history_file) defer delete(history_file)
history_content, err := os.read_entire_file_from_filename_or_err(history_file, context.allocator) history_content, err := os.read_entire_file_from_filename_or_err(history_file, context.allocator)
if err != nil { if err != nil {
fmt.printfln("skal: Couldn't read history file, continuing without history...") fmt.printfln("skal: Couldn't read history file, continuing without history...")
defer delete(history_content)
return return
} }
defer delete(history_content)
history_content_it := string(history_content) history_content_it := string(history_content)
for line in strings.split_lines_iterator(&history_content_it) { 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) @(private)
get_input :: proc() -> string { get_input :: proc() -> string {
return string(term.input_buffer[:]) return utf8.runes_to_string(term.input_buffer[:])
} }
@(private) @(private)
@ -108,12 +114,13 @@ reset_prompt :: proc() {
@(private) @(private)
handle_esc_seq :: proc(in_stream: ^io.Stream) { 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") log.assertf(err == nil, "Couldn't read from stdin")
NAV_SEQ_START :: '[' if next_rn == NAV_SEQ_START do handle_nav(in_stream)
if next_ch == NAV_SEQ_START do handle_nav(in_stream)
} }
@(private) @(private)
@ -123,14 +130,53 @@ handle_nav :: proc(in_stream: ^io.Stream) {
RIGHT :: 'C' RIGHT :: 'C'
LEFT :: 'D' 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") log.assertf(err == nil, "Couldn't read from stdin")
switch ch { switch rn {
case UP: 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: 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: 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") fmt.print("\x1b[C")
term.pos += 1 term.pos += 1
} }
@ -143,16 +189,20 @@ handle_nav :: proc(in_stream: ^io.Stream) {
} }
} }
// This is not correct for multisize runes
backwards :: proc() { backwards :: proc() {
DELETE_AND_REVERSE :: "\b \b" DELETE_AND_REVERSE :: "\b \b"
_, rm_size := utf8.decode_last_rune(term.input_buffer[:]) if term.pos == term.min_pos do return
if rm_size > 0 && term.pos > term.min_pos {
resize(&term.input_buffer, len(term.input_buffer)-rm_size) 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..<width {
fmt.print(DELETE_AND_REVERSE) fmt.print(DELETE_AND_REVERSE)
term.pos -= 1
} }
term.pos -=1
} }
skip_and_clear :: proc() { skip_and_clear :: proc() {
@ -160,49 +210,52 @@ skip_and_clear :: proc() {
prompt_prefix := get_prompt_prefix() prompt_prefix := get_prompt_prefix()
fmt.printf("%s", prompt_prefix) fmt.printf("%s", prompt_prefix)
term.min_pos = u32(len(prompt_prefix)) term.min_pos = i32(len(prompt_prefix))
term.pos = term.min_pos term.pos = term.min_pos
} }
clear_and_print :: proc(input: []rune) {
skip_and_clear()
append(&term.input_buffer, ..input[:])
}
run_prompt :: proc() -> string { run_prompt :: proc() -> string {
enable_raw_mode() enable_raw_mode()
skip_and_clear() skip_and_clear()
in_stream := os.stream_from_handle(os.stdin) in_stream := os.stream_from_handle(os.stdin)
for { 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") log.assertf(err == nil, "Couldn't read from stdin")
switch { switch rn {
case ch == '\n': case '\n':
fmt.println() fmt.println()
return get_input() return get_input()
case ch == '\f': case '\f':
fmt.println() fmt.println()
return "clear" return "clear" // This is a terminal history wipe, usually isn't
case ch == '\u007f': case '\u007f':
backwards() backwards()
case ch == '\x1b': case '\x1b':
handle_esc_seq(&in_stream) handle_esc_seq(&in_stream)
case utf8.RUNE_EOF:
case ch == utf8.RUNE_EOF:
fallthrough fallthrough
case ch == '\x04': case '\x04':
fmt.println() fmt.println()
skip_and_clear() skip_and_clear()
case: case: // Bug: if user moved to earlier character
bytes, _ := utf8.encode_rune(ch) append(&term.input_buffer, rn)
append(&term.input_buffer, ..bytes[:size]) term.pos += 1
term.pos += u32(size)
fmt.print(ch) fmt.print(rn)
} }
} }