Add initial CLI
This commit is contained in:
parent
5a77cc3bdf
commit
dd9831f1b5
4 changed files with 216 additions and 15 deletions
210
cli/cli.odin
Normal file
210
cli/cli.odin
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import "core:io"
|
||||||
|
import "core:os"
|
||||||
|
import "core:log"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:mem"
|
||||||
|
import "core:slice"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:sys/posix"
|
||||||
|
import "core:path/filepath"
|
||||||
|
import "core:unicode/utf8"
|
||||||
|
|
||||||
|
INPUT_MAX :: 4096
|
||||||
|
HISTORY_MAX :: 2048
|
||||||
|
HISTORY_FILE :: "skal.history"
|
||||||
|
|
||||||
|
Term :: struct {
|
||||||
|
input_buffer: [dynamic]byte,
|
||||||
|
pos: u32,
|
||||||
|
min_pos: u32,
|
||||||
|
history: [HISTORY_MAX]string,
|
||||||
|
orig_mode: posix.termios
|
||||||
|
}
|
||||||
|
|
||||||
|
term := Term{pos = 0, min_pos = 0}
|
||||||
|
|
||||||
|
init_cli :: proc () {
|
||||||
|
mem_err: mem.Allocator_Error
|
||||||
|
term.input_buffer, mem_err = make([dynamic]byte, context.allocator) //todo deinit
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
history_content_it := string(history_content)
|
||||||
|
for line in strings.split_lines_iterator(&history_content_it) {
|
||||||
|
fmt.printfln(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit_cli :: proc () {
|
||||||
|
delete(term.input_buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_raw_mode :: proc "c" () {
|
||||||
|
posix.tcsetattr(posix.STDIN_FILENO, .TCSANOW, &term.orig_mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_raw_mode :: proc() {
|
||||||
|
status := posix.tcgetattr(posix.STDIN_FILENO, &term.orig_mode)
|
||||||
|
log.assertf(status == .OK, "Couldn't enable terminal in raw mode")
|
||||||
|
|
||||||
|
posix.atexit(disable_raw_mode)
|
||||||
|
|
||||||
|
raw := term.orig_mode
|
||||||
|
raw.c_lflag -= {.ECHO, .ICANON}
|
||||||
|
status = posix.tcsetattr(posix.STDIN_FILENO, .TCSANOW, &raw)
|
||||||
|
log.assertf(status == .OK, "Couldn't enable terminal in raw mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
get_prompt_prefix :: proc() -> string {
|
||||||
|
dir := os.get_current_directory(context.temp_allocator)
|
||||||
|
home := string(posix.getenv("HOME"))
|
||||||
|
|
||||||
|
if strings.contains(dir, home) { // Bug: might replace if later directories mirror the home path
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
get_input :: proc() -> string {
|
||||||
|
return string(term.input_buffer[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
reset_prompt :: proc() {
|
||||||
|
clear(&term.input_buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
handle_esc_seq :: proc(in_stream: ^io.Stream) {
|
||||||
|
next_ch, _, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
handle_nav :: proc(in_stream: ^io.Stream) {
|
||||||
|
UP :: 'A'
|
||||||
|
DOWN :: 'B'
|
||||||
|
RIGHT :: 'C'
|
||||||
|
LEFT :: 'D'
|
||||||
|
|
||||||
|
ch, _, err := io.read_rune(in_stream^)
|
||||||
|
log.assertf(err == nil, "Couldn't read from stdin")
|
||||||
|
|
||||||
|
switch ch {
|
||||||
|
case UP:
|
||||||
|
case DOWN:
|
||||||
|
case RIGHT:
|
||||||
|
if u32(len(term.input_buffer)) + term.min_pos > term.pos {
|
||||||
|
fmt.print("\x1b[C")
|
||||||
|
term.pos += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case LEFT:
|
||||||
|
if term.min_pos < term.pos {
|
||||||
|
fmt.print("\x1b[D")
|
||||||
|
term.pos -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
fmt.print(DELETE_AND_REVERSE)
|
||||||
|
term.pos -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skip_and_clear :: proc() {
|
||||||
|
reset_prompt()
|
||||||
|
prompt_prefix := get_prompt_prefix()
|
||||||
|
fmt.printf("%s", prompt_prefix)
|
||||||
|
|
||||||
|
term.min_pos = u32(len(prompt_prefix))
|
||||||
|
term.pos = term.min_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
run_prompt :: proc() -> string {
|
||||||
|
enable_raw_mode()
|
||||||
|
|
||||||
|
skip_and_clear()
|
||||||
|
|
||||||
|
in_stream := os.stream_from_handle(os.stdin)
|
||||||
|
|
||||||
|
for {
|
||||||
|
ch, size, err := io.read_rune(in_stream)
|
||||||
|
log.assertf(err == nil, "Couldn't read from stdin")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ch == '\n':
|
||||||
|
fmt.println()
|
||||||
|
return get_input()
|
||||||
|
|
||||||
|
case ch == '\f':
|
||||||
|
fmt.println()
|
||||||
|
return "clear"
|
||||||
|
|
||||||
|
case ch == '\u007f':
|
||||||
|
backwards()
|
||||||
|
|
||||||
|
case ch == '\x1b':
|
||||||
|
handle_esc_seq(&in_stream)
|
||||||
|
|
||||||
|
|
||||||
|
case ch == utf8.RUNE_EOF:
|
||||||
|
fallthrough
|
||||||
|
case ch == '\x04':
|
||||||
|
fmt.println()
|
||||||
|
skip_and_clear()
|
||||||
|
|
||||||
|
case:
|
||||||
|
bytes, _ := utf8.encode_rune(ch)
|
||||||
|
append(&term.input_buffer, ..bytes[:size])
|
||||||
|
term.pos += u32(size)
|
||||||
|
|
||||||
|
fmt.print(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
14
main.odin
14
main.odin
|
|
@ -7,8 +7,7 @@ import "core:mem"
|
||||||
|
|
||||||
import "parser"
|
import "parser"
|
||||||
import "shell"
|
import "shell"
|
||||||
|
import "cli"
|
||||||
INPUT_MAX :: 4096
|
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
track: mem.Tracking_Allocator
|
track: mem.Tracking_Allocator
|
||||||
|
|
@ -25,18 +24,11 @@ main :: proc() {
|
||||||
mem.tracking_allocator_destroy(&track)
|
mem.tracking_allocator_destroy(&track)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf: [INPUT_MAX]byte
|
|
||||||
shell.init_shell()
|
shell.init_shell()
|
||||||
|
cli.init_cli()
|
||||||
|
|
||||||
for true {
|
for true {
|
||||||
prompt := shell.get_prompt()
|
input := cli.run_prompt()
|
||||||
|
|
||||||
fmt.print(prompt)
|
|
||||||
|
|
||||||
buf_len, err := os.read(os.stdin, buf[:])
|
|
||||||
log.assertf(err == nil, "Error reading input")
|
|
||||||
|
|
||||||
input := string(buf[:buf_len])
|
|
||||||
|
|
||||||
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")
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ ParseError :: enum {
|
||||||
SequenceType :: enum {
|
SequenceType :: enum {
|
||||||
Head,
|
Head,
|
||||||
Or,
|
Or,
|
||||||
And
|
And,
|
||||||
}
|
}
|
||||||
|
|
||||||
Command :: struct {
|
Command :: struct {
|
||||||
|
|
@ -214,7 +214,7 @@ parse :: proc(input: string) -> (cmd_seq: CommandSequence, err: ParseError) {
|
||||||
cur_pipe_seq = cur_pipe_seq,
|
cur_pipe_seq = cur_pipe_seq,
|
||||||
cur_cmd = cur_cmd,
|
cur_cmd = cur_cmd,
|
||||||
tokens = c_tokens,
|
tokens = c_tokens,
|
||||||
token_idx = 1
|
token_idx = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
for parser_state.token_idx < len(parser_state.tokens) {
|
for parser_state.token_idx < len(parser_state.tokens) {
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,12 @@ import "core:strings"
|
||||||
import "core:sys/posix"
|
import "core:sys/posix"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:mem"
|
|
||||||
|
|
||||||
import "../parser"
|
import "../parser"
|
||||||
|
|
||||||
ShellState :: enum {
|
ShellState :: enum {
|
||||||
Continue,
|
Continue,
|
||||||
Stop
|
Stop,
|
||||||
}
|
}
|
||||||
|
|
||||||
maybe_foreground_pid: Maybe(posix.pid_t) = nil
|
maybe_foreground_pid: Maybe(posix.pid_t) = nil
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue