Add input history navigation
This commit is contained in:
parent
dd9831f1b5
commit
50b0e6e641
1 changed files with 92 additions and 39 deletions
131
cli/cli.odin
131
cli/cli.odin
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue