Couple cli and shell

This commit is contained in:
Hugo Mårdbrink 2025-08-13 16:18:20 +02:00
parent a3e9ee9807
commit 275e5c3f7d
3 changed files with 142 additions and 135 deletions

View file

@ -1,5 +1,7 @@
package cli package cli
import "base:runtime"
import "core:io" import "core:io"
import "core:os" import "core:os"
import "core:log" import "core:log"
@ -16,55 +18,63 @@ INPUT_MAX :: 4096
HISTORY_MAX :: 2048 HISTORY_MAX :: 2048
HISTORY_FILE :: "skal.history" HISTORY_FILE :: "skal.history"
Term :: struct { History :: struct {
input_buffer: [dynamic]rune, data: [dynamic][]rune,
pos: i32, file: string,
min_pos: i32, maybe_idx: Maybe(i32),
history: [dynamic][]rune, maybe_cached_input: Maybe([]rune),
history_pos: Maybe(i32),
history_file: string,
written_line: Maybe([]rune),
orig_mode: posix.termios
} }
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 () { init_cli :: proc () {
mem_err: mem.Allocator_Error 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") 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") 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")
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") log.assertf(mem_err == nil, "Memory allocation failed")
if !os.exists(term.history_file) { USER_PERMISSIONS :: 0o600
handle, ferr := os.open(term.history_file, os.O_CREATE | os.O_RDWR, 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") log.assertf(ferr == nil, "Failed to create history file")
os.close(handle) os.close(handle)
} }
history_content, err := os.read_entire_file_from_filename_or_err(term.history_file, context.allocator) history_content, err := os.read_entire_file_from_filename_or_err(term.history.file, context.allocator)
if err != nil { log.assertf(err == nil, "Failed to read history file")
fmt.printfln("skal: Couldn't read history file: %s, because of: %s, continuing without history...", term.history_file, err)
return
}
defer delete(history_content) 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) {
append(&term.history, utf8.string_to_runes(line)) append(&term.history.data, utf8.string_to_runes(line))
} }
} }
deinit_cli :: proc () { deinit_cli :: proc () {
delete(term.input_buffer) delete(term.input.buffer)
delete(term.history.data)
delete(term.history.file)
} }
disable_raw_mode :: proc "c" () { disable_raw_mode :: proc "c" () {
@ -109,17 +119,11 @@ get_prompt_prefix :: proc() -> string {
return prompt return prompt
} }
@(private)
reset_prompt :: proc() {
clear(&term.input_buffer)
}
@(private) @(private)
handle_esc_seq :: proc(in_stream: ^io.Stream) { handle_esc_seq :: proc(in_stream: ^io.Stream) {
NAV_SEQ_START :: '[' NAV_SEQ_START :: '['
next_rn, _, err := io.read_rune(in_stream^) 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")
if next_rn == NAV_SEQ_START do handle_nav(in_stream) if next_rn == NAV_SEQ_START do handle_nav(in_stream)
@ -137,132 +141,155 @@ handle_nav :: proc(in_stream: ^io.Stream) {
switch rn { switch rn {
case UP: case UP:
if term.history_pos == nil { cur_history_idx, scrolling_history := term.history.maybe_idx.?
term.written_line = slice.clone(term.input_buffer[:], context.temp_allocator) 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 { pop_runes(i32(len(term.input.buffer)))
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] history_idx := term.history.maybe_idx.? or_else i32(len(term.history.data))
append(&term.input_buffer, ..new_input[:]) history_idx -= 1
fmt.printf("%s", term.input_buffer[:]) 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: case DOWN:
history_pos, ok := term.history_pos.? history_idx, scrolling_history := term.history.maybe_idx.?
if !ok do return if !scrolling_history do return
for len(term.input_buffer) > 0 { pop_runes(i32(len(term.input.buffer)))
backwards()
}
history_pos += 1 history_idx += 1
history_pos_max := i32(len(term.history)-1) history_len := i32(len(term.history.data))
new_input: []rune new_input: []rune
if history_pos > history_pos_max {
term.history_pos = nil if history_idx >= history_len {
history_pos = history_pos_max term.history.maybe_idx = nil
new_input, ok = term.written_line.?; assert(ok) history_idx = history_len
term.written_line = nil
ok: bool
new_input, ok = term.history.maybe_cached_input.?; assert(ok)
term.history.maybe_cached_input = nil
} else { } else {
term.history_pos = history_pos term.history.maybe_idx = history_idx
new_input = term.history[history_pos] 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: case RIGHT:
if i32(len(term.input_buffer)) + term.min_pos > term.pos { if term.input.cursor_offset > 0 {
fmt.print("\x1b[C") fmt.print("\x1b[C")
term.pos += 1 term.input.cursor_offset -= 1
} }
case LEFT: case LEFT:
if term.min_pos < term.pos { if term.input.cursor_offset < i32(len(term.input.buffer)) {
fmt.print("\x1b[D") 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] start_idx := i32(len(term.input.buffer)) - amount
_, _, width := utf8.grapheme_count(utf8.runes_to_string({last_rune}, context.temp_allocator)) runes := term.input.buffer[start_idx:]
resize(&term.input_buffer, len(term.input_buffer)-1) _, _, 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..<width { for _ in 0..<width {
fmt.print(DELETE_AND_REVERSE) fmt.print(DELETE_AND_REVERSE)
} }
}
term.pos -=1 @(private)
pop_rune_at_cursor :: proc() {
DELETE_AND_REVERSE :: "\b \b"
if len(term.input.buffer) == 0 do return
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)
for _ in 0..<width {
fmt.print(DELETE_AND_REVERSE)
}
} }
@(private) @(private)
append_history_file :: proc(data: []rune) { append_history_file :: proc(data: []rune) {
append(&term.history, slice.clone(data)) append(&term.history.data, slice.clone(data))
term.history_pos = nil term.history.maybe_idx = nil
term.written_line = nil term.history.maybe_cached_input = nil
fd, err := os.open(term.history_file, os.O_WRONLY | os.O_APPEND | os.O_CREATE) fd, err := os.open(term.history.file, os.O_WRONLY | os.O_APPEND | os.O_CREATE)
log.assertf(err == nil, "Failed to write to history file")
defer os.close(fd) defer os.close(fd)
if err != nil {
fmt.eprintfln("skal: Couldn't write to history file")
return
}
history_data := strings.concatenate({utf8.runes_to_string(data[:]), "\n"}, context.temp_allocator) history_data := strings.concatenate({utf8.runes_to_string(data[:]), "\n"}, context.temp_allocator)
_, werr := os.write_string(fd, history_data) _, werr := os.write_string(fd, history_data)
log.ensuref(werr == nil, "skal: Couldn't write to history file") log.assertf(werr == nil, "Failed to write to history file")
} }
skip_and_clear :: proc() { clear_input :: proc() {
reset_prompt() clear(&term.input.buffer)
prompt_prefix := get_prompt_prefix() prompt_prefix := get_prompt_prefix()
fmt.printf("%s", prompt_prefix) fmt.printf("%s", prompt_prefix)
term.min_pos = i32(len(prompt_prefix)) term.input.prompt_size = i32(len(prompt_prefix))
term.pos = term.min_pos
} }
run_prompt :: proc() -> string { skip_and_clear :: proc() {
fmt.println()
clear_input()
}
run_prompt :: proc() -> Maybe(string) {
enable_raw_mode() enable_raw_mode()
clear_input()
skip_and_clear()
in_stream := os.stream_from_handle(os.stdin) in_stream := os.stream_from_handle(os.stdin)
for { 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") log.assertf(err == nil, "Couldn't read from stdin")
switch rn { switch rn {
case '\n': case '\n':
fmt.println() fmt.println()
if len(term.input_buffer) == 0 do return "" if len(term.input.buffer) == 0 do return ""
append_history_file(term.input_buffer[:]) append_history_file(term.input.buffer[:])
input := utf8.runes_to_string(term.input_buffer[:], context.temp_allocator) input := utf8.runes_to_string(term.input.buffer[:], context.temp_allocator)
return input return input
case '\f': case '\f':
fmt.println()
return "clear" // This is a terminal history wipe, usually isn't return "clear" // This is a terminal history wipe, usually isn't
case '\u007f': case '\u007f':
backwards() pop_rune_at_cursor()
case '\x1b': case '\x1b':
handle_esc_seq(&in_stream) handle_esc_seq(&in_stream)
@ -270,14 +297,10 @@ run_prompt :: proc() -> string {
case utf8.RUNE_EOF: case utf8.RUNE_EOF:
fallthrough fallthrough
case '\x04': case '\x04':
fmt.println() return nil
skip_and_clear()
case: // Bug: if user moved to earlier character case: // Bug: if user moved to earlier character
append(&term.input_buffer, rn) write_rune(rn)
term.pos += 1
fmt.print(rn)
} }
} }

View file

@ -26,16 +26,23 @@ main :: proc() {
shell.init_shell() shell.init_shell()
cli.init_cli() cli.init_cli()
defer cli.deinit_cli()
for true { 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) cmd_seq, parser_err := parser.parse(input)
log.assertf(parser_err == nil, "Could not parse input") log.assertf(parser_err == nil, "Could not parse input")
stop := shell.execute(&cmd_seq) stop := shell.execute(&cmd_seq)
free_all(context.temp_allocator)
if stop == .Stop do break if stop == .Stop do break
} }
} }

View file

@ -9,6 +9,7 @@ import "core:os"
import "core:log" import "core:log"
import "../parser" import "../parser"
import "../cli"
ShellState :: enum { ShellState :: enum {
Continue, Continue,
@ -16,20 +17,20 @@ ShellState :: enum {
} }
maybe_foreground_pid: Maybe(posix.pid_t) = nil maybe_foreground_pid: Maybe(posix.pid_t) = nil
ctx: runtime.Context
handle_ctrl_c :: proc"c"(sig: posix.Signal) { handle_ctrl_c :: proc"c"(sig: posix.Signal) {
context = runtime.default_context()
foreground_pid, foreground_running := maybe_foreground_pid.? foreground_pid, foreground_running := maybe_foreground_pid.?
if foreground_running { if foreground_running {
posix.kill(foreground_pid, .SIGINT) posix.kill(foreground_pid, .SIGINT)
} else { }
// Clear prompt
} cli.skip_and_clear()
} }
handle_backround_process :: proc"c"(sig: posix.Signal) { handle_backround_process :: proc"c"(sig: posix.Signal) {
context = ctx context = runtime.default_context()
foreground_pid, foreground_running := maybe_foreground_pid.? foreground_pid, foreground_running := maybe_foreground_pid.?
if foreground_running do return if foreground_running do return
@ -49,36 +50,10 @@ handle_backround_process :: proc"c"(sig: posix.Signal) {
} }
init_shell :: proc() { init_shell :: proc() {
ctx = context
posix.signal(.SIGINT, handle_ctrl_c) posix.signal(.SIGINT, handle_ctrl_c)
posix.signal(.SIGCHLD, handle_backround_process) 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) { pipe_command :: proc(pipe_seq: ^parser.PipeSequence, maybe_fd: Maybe(posix.FD), idx: int) {
cmd := pipe_seq.commands[idx] 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 argv[0] = cmd.name
for i := 0; i < len(cmd.args); i += 1 do argv[i+1] = cmd.args[i] for i := 0; i < len(cmd.args); i += 1 do argv[i+1] = cmd.args[i]
_ = posix.execvp(argv[0], raw_data(argv)) _ = posix.execvp(argv[0], raw_data(argv))
fmt.printfln("skal: command not found: %s", argv[0]) fmt.printfln("skal: command not found: %s", argv[0])
os.exit(1) 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) { change_dir :: proc(cmd: ^parser.Command) {
if len(cmd.args) > 0 { if len(cmd.args) > 0 {
ch_status := posix.chdir(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 { } else {
ch_status := posix.chdir(posix.getenv("HOME")) 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 { 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 // if pipe sequence is above 0 it has at least one command
first_cmd := cmd_seq^.pipe_sequences[0].commands[0] first_cmd := cmd_seq^.pipe_sequences[0].commands[0]