diff --git a/cli/cli.odin b/cli/cli.odin index 2fe60ae..382ec57 100644 --- a/cli/cli.odin +++ b/cli/cli.odin @@ -1,5 +1,7 @@ package cli +import "base:runtime" + import "core:io" import "core:os" import "core:log" @@ -16,55 +18,63 @@ INPUT_MAX :: 4096 HISTORY_MAX :: 2048 HISTORY_FILE :: "skal.history" -Term :: struct { - input_buffer: [dynamic]rune, - pos: i32, - min_pos: i32, - history: [dynamic][]rune, - history_pos: Maybe(i32), - history_file: string, - written_line: Maybe([]rune), - orig_mode: posix.termios +History :: struct { + data: [dynamic][]rune, + file: string, + maybe_idx: Maybe(i32), + maybe_cached_input: Maybe([]rune), } -term := Term{pos = 0, min_pos = 0, history_pos = nil, written_line = nil} +Input :: struct { + buffer: [dynamic]rune, + prompt_size: i32, + cursor_offset: i32, +} + +Term :: struct { + orig_mode: posix.termios, + input: Input, + history: History, +} + +term := Term{} init_cli :: proc () { mem_err: mem.Allocator_Error - term.input_buffer, mem_err = make([dynamic]rune, context.allocator) //todo deinit + term.input.buffer, mem_err = make([dynamic]rune, context.allocator) log.assertf(mem_err == nil, "Memory allocation failed") - term.history, mem_err = make([dynamic][]rune, context.allocator) //todo deinit + term.history.data, mem_err = make([dynamic][]rune, context.allocator) log.assertf(mem_err == nil, "Memory allocation failed") home_path := string(posix.getenv("HOME")) log.assertf(home_path != "", "Home path not found") - term.history_file = filepath.join({home_path, HISTORY_FILE}, context.allocator) //todo deinit + term.history.file = filepath.join({home_path, HISTORY_FILE}, context.allocator) log.assertf(mem_err == nil, "Memory allocation failed") - if !os.exists(term.history_file) { - handle, ferr := os.open(term.history_file, os.O_CREATE | os.O_RDWR, 0o600) + USER_PERMISSIONS :: 0o600 + if !os.exists(term.history.file) { + handle, ferr := os.open(term.history.file, os.O_CREATE | os.O_RDWR, USER_PERMISSIONS) log.assertf(ferr == nil, "Failed to create history file") os.close(handle) } - history_content, err := os.read_entire_file_from_filename_or_err(term.history_file, context.allocator) - if err != nil { - fmt.printfln("skal: Couldn't read history file: %s, because of: %s, continuing without history...", term.history_file, err) - return - } + history_content, err := os.read_entire_file_from_filename_or_err(term.history.file, context.allocator) + log.assertf(err == nil, "Failed to read history file") defer delete(history_content) history_content_it := string(history_content) for line in strings.split_lines_iterator(&history_content_it) { - append(&term.history, utf8.string_to_runes(line)) + append(&term.history.data, utf8.string_to_runes(line)) } } deinit_cli :: proc () { - delete(term.input_buffer) + delete(term.input.buffer) + delete(term.history.data) + delete(term.history.file) } disable_raw_mode :: proc "c" () { @@ -109,17 +119,11 @@ get_prompt_prefix :: proc() -> string { return prompt } -@(private) -reset_prompt :: proc() { - clear(&term.input_buffer) -} - @(private) handle_esc_seq :: proc(in_stream: ^io.Stream) { NAV_SEQ_START :: '[' next_rn, _, err := io.read_rune(in_stream^) - log.assertf(err == nil, "Couldn't read from stdin") if next_rn == NAV_SEQ_START do handle_nav(in_stream) @@ -137,132 +141,155 @@ handle_nav :: proc(in_stream: ^io.Stream) { switch rn { case UP: - if term.history_pos == nil { - term.written_line = slice.clone(term.input_buffer[:], context.temp_allocator) + cur_history_idx, scrolling_history := term.history.maybe_idx.? + if !scrolling_history { + term.history.maybe_cached_input = slice.clone(term.input.buffer[:], context.temp_allocator) + } else if cur_history_idx == 0 { + return } - 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 + pop_runes(i32(len(term.input.buffer))) - new_input := term.history[history_pos] - append(&term.input_buffer, ..new_input[:]) - fmt.printf("%s", term.input_buffer[:]) + history_idx := term.history.maybe_idx.? or_else i32(len(term.history.data)) + history_idx -= 1 + term.history.maybe_idx = history_idx + + new_input := term.history.data[history_idx] + write_runes(new_input[:]) - term.pos = term.min_pos + i32(len(term.input_buffer)) case DOWN: - history_pos, ok := term.history_pos.? - if !ok do return + history_idx, scrolling_history := term.history.maybe_idx.? + if !scrolling_history do return - for len(term.input_buffer) > 0 { - backwards() - } + pop_runes(i32(len(term.input.buffer))) - history_pos += 1 - history_pos_max := i32(len(term.history)-1) + history_idx += 1 + history_len := i32(len(term.history.data)) new_input: []rune - if history_pos > history_pos_max { - term.history_pos = nil - history_pos = history_pos_max - new_input, ok = term.written_line.?; assert(ok) - term.written_line = nil + + if history_idx >= history_len { + term.history.maybe_idx = nil + history_idx = history_len + + ok: bool + new_input, ok = term.history.maybe_cached_input.?; assert(ok) + term.history.maybe_cached_input = nil } else { - term.history_pos = history_pos - new_input = term.history[history_pos] + term.history.maybe_idx = history_idx + new_input = term.history.data[history_idx] } + write_runes(new_input[:]) - 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 i32(len(term.input_buffer)) + term.min_pos > term.pos { + if term.input.cursor_offset > 0 { fmt.print("\x1b[C") - term.pos += 1 + term.input.cursor_offset -= 1 } case LEFT: - if term.min_pos < term.pos { + if term.input.cursor_offset < i32(len(term.input.buffer)) { fmt.print("\x1b[D") - term.pos -= 1 + term.input.cursor_offset += 1 } } } -@(private) -backwards :: proc() { - DELETE_AND_REVERSE :: "\b \b" - if term.pos == term.min_pos do return +@(private) +write_runes :: proc(rns: []rune) { + append(&term.input.buffer, ..rns[:]) + fmt.print(utf8.runes_to_string(rns[:])) +} + +@(private) +write_rune :: proc(rn: rune) { + append(&term.input.buffer, rn) + fmt.print(rn) +} + +@(private) +pop_runes :: proc(amount: i32) { + DELETE_AND_REVERSE :: "\b \b" + log.assertf(amount <= i32(len(term.input.buffer)), "Cannot remove more runes that written in buffer") - last_rune := term.input_buffer[len(term.input_buffer)-1] - _, _, width := utf8.grapheme_count(utf8.runes_to_string({last_rune}, context.temp_allocator)) - resize(&term.input_buffer, len(term.input_buffer)-1) + start_idx := i32(len(term.input.buffer)) - amount + runes := term.input.buffer[start_idx:] + _, _, width := utf8.grapheme_count(utf8.runes_to_string(runes, context.temp_allocator)) + resize(&term.input.buffer, i32(len(term.input.buffer)) - amount) for _ in 0.. string { +skip_and_clear :: proc() { + fmt.println() + clear_input() +} + +run_prompt :: proc() -> Maybe(string) { enable_raw_mode() - - skip_and_clear() + clear_input() in_stream := os.stream_from_handle(os.stdin) for { - rn, size, err := io.read_rune(in_stream) + rn, _, err := io.read_rune(in_stream) log.assertf(err == nil, "Couldn't read from stdin") switch rn { case '\n': fmt.println() - if len(term.input_buffer) == 0 do return "" + if len(term.input.buffer) == 0 do return "" - append_history_file(term.input_buffer[:]) - input := utf8.runes_to_string(term.input_buffer[:], context.temp_allocator) + append_history_file(term.input.buffer[:]) + input := utf8.runes_to_string(term.input.buffer[:], context.temp_allocator) return input case '\f': - fmt.println() return "clear" // This is a terminal history wipe, usually isn't case '\u007f': - backwards() + pop_rune_at_cursor() case '\x1b': handle_esc_seq(&in_stream) @@ -270,14 +297,10 @@ run_prompt :: proc() -> string { case utf8.RUNE_EOF: fallthrough case '\x04': - fmt.println() - skip_and_clear() + return nil case: // Bug: if user moved to earlier character - append(&term.input_buffer, rn) - term.pos += 1 - - fmt.print(rn) + write_rune(rn) } } diff --git a/main.odin b/main.odin index dd228f7..0235a64 100644 --- a/main.odin +++ b/main.odin @@ -26,16 +26,23 @@ main :: proc() { shell.init_shell() cli.init_cli() + defer cli.deinit_cli() for true { - input := cli.run_prompt() + defer free_all(context.temp_allocator) + + maybe_input := cli.run_prompt() + input, ok := maybe_input.? + if !ok { + fmt.println() + break + } cmd_seq, parser_err := parser.parse(input) log.assertf(parser_err == nil, "Could not parse input") stop := shell.execute(&cmd_seq) - free_all(context.temp_allocator) if stop == .Stop do break } } diff --git a/shell/shell.odin b/shell/shell.odin index a970415..79f855c 100644 --- a/shell/shell.odin +++ b/shell/shell.odin @@ -9,6 +9,7 @@ import "core:os" import "core:log" import "../parser" +import "../cli" ShellState :: enum { Continue, @@ -16,20 +17,20 @@ ShellState :: enum { } maybe_foreground_pid: Maybe(posix.pid_t) = nil -ctx: runtime.Context handle_ctrl_c :: proc"c"(sig: posix.Signal) { + context = runtime.default_context() foreground_pid, foreground_running := maybe_foreground_pid.? if foreground_running { posix.kill(foreground_pid, .SIGINT) - } else { - // Clear prompt - } + } + + cli.skip_and_clear() } handle_backround_process :: proc"c"(sig: posix.Signal) { - context = ctx + context = runtime.default_context() foreground_pid, foreground_running := maybe_foreground_pid.? if foreground_running do return @@ -49,36 +50,10 @@ handle_backround_process :: proc"c"(sig: posix.Signal) { } init_shell :: proc() { - ctx = context posix.signal(.SIGINT, handle_ctrl_c) posix.signal(.SIGCHLD, handle_backround_process) } -get_prompt :: proc() -> string { - dir := os.get_current_directory(context.temp_allocator) - home := string(posix.getenv("HOME")) - - if strings.contains(dir, home) { - dir, _ = strings.replace(dir, home, "~", 1, context.temp_allocator) - } - - uid := posix.getuid() - pw := posix.getpwuid(uid) - - user: string - if pw == nil { - user = "Skal" - } else { - user = string(pw.pw_name) - } - - prompt_parts := []string {user, " :: ", dir, " ยป "} - prompt, err := strings.concatenate(prompt_parts[:], context.temp_allocator) - log.assertf(err == nil, "Memory allocation failed") - - return prompt -} - pipe_command :: proc(pipe_seq: ^parser.PipeSequence, maybe_fd: Maybe(posix.FD), idx: int) { cmd := pipe_seq.commands[idx] @@ -159,7 +134,7 @@ pipe_command :: proc(pipe_seq: ^parser.PipeSequence, maybe_fd: Maybe(posix.FD), argv[0] = cmd.name for i := 0; i < len(cmd.args); i += 1 do argv[i+1] = cmd.args[i] - + _ = posix.execvp(argv[0], raw_data(argv)) fmt.printfln("skal: command not found: %s", argv[0]) os.exit(1) @@ -169,10 +144,10 @@ pipe_command :: proc(pipe_seq: ^parser.PipeSequence, maybe_fd: Maybe(posix.FD), change_dir :: proc(cmd: ^parser.Command) { if len(cmd.args) > 0 { ch_status := posix.chdir(cmd.args[0]) - if ch_status != .OK do fmt.printf("cd: No such file or directory: %s\n", cmd.args[0]) + if ch_status != .OK do fmt.printfln("cd: No such file or directory: %s", cmd.args[0]) } else { ch_status := posix.chdir(posix.getenv("HOME")) - if ch_status != .OK do fmt.printf("cd: No such file or directory: %s\n", posix.getenv("HOME")) + if ch_status != .OK do fmt.printfln("cd: No such file or directory: %s", posix.getenv("HOME")) } } @@ -219,7 +194,9 @@ execute_cmd_seq :: proc(cmd_seq: ^parser.CommandSequence) { } execute :: proc(cmd_seq: ^parser.CommandSequence) -> ShellState { - if len(cmd_seq^.pipe_sequences) == 0 do return .Continue + if len(cmd_seq^.pipe_sequences) == 0 { + return .Continue + } // if pipe sequence is above 0 it has at least one command first_cmd := cmd_seq^.pipe_sequences[0].commands[0]