package main
import "core:fmt"
import "core:os"
import "core:path"
import "core:strings"
import "core:unicode/utf8"
// import "core:hash"
import "core:container"
import "core:runtime"
import "linux"
import "ncure"
import "tokenizer"
VERSION :: "0.5";
COPYRIGHT_YEAR :: "2020";
GlobalData :: struct {
running: bool,
executablePath: string,
executableDir: string,
username: string,
homeDirectory: string,
toolsFolder: string,
wrappersFolder: string,
path_hash: map[string]string,
shell_vars: map[string]string, // TODO
directoryHistory: [dynamic]string,
// commandHistory: [dynamic]string, // TODO: Use a queue
commandHistory: container.Queue(string),
history_index: int,
current: string,
}
init_globalData :: proc(globalData: ^GlobalData) {
globalData.path_hash = make(map[string]string);
globalData.shell_vars = make(map[string]string); // TODO
globalData.directoryHistory = make([dynamic]string);
// globalData.commandHistory = make([dynamic]string);
container.queue_init(&globalData.commandHistory, 0, 5);
globalData.history_index = 0;
}
add_history :: proc(globalData: ^GlobalData, s: string) {
if container.queue_len(globalData.commandHistory) >= 200 {
for i in 0..<(container.queue_len(globalData.commandHistory) - 200) {
container.queue_pop_front(&globalData.commandHistory);
}
}
container.queue_push(&globalData.commandHistory, s);
}
terminate :: proc "c" (signum: int) {
context = runtime.default_context();
// TODO: Determine if should wait for something to finish or not
// TODO: Wait for child processes to finish? Or deattach them
// TODO: Save important state here
ncure.batch_end();
ncure.newLine();
ncure.enableEcho();
os.exit(0);
}
foo :: cast(^u8) cast(uintptr) 35251;
setupSignals :: proc() {
// str := (cast(^u8) uintptr(35251));
// blah := cast(cstring) str;
// Ignore Ctrl+C Interactive Attention Signal
// and Ctrl+Z Terminal Stop Signal
linux.signal(linux.SIGINT, linux.SIG_IGN);
linux.signal(linux.SIGTSTP, linux.SIG_IGN);
linux.signal(linux.SIGQUIT, linux.sighandler_t(terminate));
linux.signal(linux.SIGTERM, linux.sighandler_t(terminate));
linux.signal(linux.SIGABRT, linux.sighandler_t(terminate));
linux.signal(linux.SIGALRM, linux.sighandler_t(terminate));
linux.signal(linux.SIGVTALRM, linux.sighandler_t(terminate));
linux.signal(linux.SIGXCPU, linux.sighandler_t(terminate));
linux.signal(linux.SIGXFSZ, linux.sighandler_t(terminate));
// TODO: Handle SIGCONT signal? This signal is sent when
// the process is restarted from being suspended/paused by SIGSTOP or SIGTSTP
// TODO: Handle SIGFPE so that an erroneous arithmetic operation doesn't terminate the shell?
}
main :: proc() {
setupSignals();
globalData: GlobalData;
init_globalData(&globalData);
setupUserInfo(&globalData);
setupDefaultDirectories(&globalData);
setupEnvironmentVariables(&globalData);
globalData.current = os.get_current_directory();
linux.setenv("PWD", globalData.current, true);
// TODO: Set OLDPWD for previous working directory
ncure.disableEcho(false);
defer ncure.enableEcho();
ncure.batch_start();
{
defer ncure.batch_end();
ncure.clearScreen();
ncure.setCursor_topleft();
// ncure.println(typeid_of(type_of(foo)));
printPrompt(&globalData);
}
// NOTE: Only used for tokenizer
keywords: tokenizer.Set = transmute(tokenizer.Set) map[string]bool {
"true" = true,
"false" = true,
"sh" = true,
"bash" = true,
"nil" = true,
};
builtins := map[string]BuiltinProc {
"cd" = BuiltinCd,
"clear" = BuiltinClear,
"help" = BuiltinHelp,
"version" = BuiltinVersion,
"exit" = BuiltinExit,
"debug" = BuiltinDebug,
"getenv" = BuiltinGetenv,
"hash" = BuiltinUnimplemented,
"tools" = BuiltinUnimplemented,
"wrappers" = BuiltinUnimplemented,
"motd" = BuiltinUnimplemented,
"dhist" = BuiltinUnimplemented,
"sh" = BuiltinUnimplemented,
"bash" = BuiltinUnimplemented,
"mksh" = BuiltinUnimplemented,
};
builtinCalls := map[string]CallProc {
};
running := true;
first := true;
input := strings.make_builder();
defer strings.destroy_builder(&input);
for running {
strings.reset_builder(&input);
if !first do printPrompt(&globalData);
else do first = false;
cliInput(&input, &globalData);
inputString := strings.to_string(input);
fmt.println("");
if len(inputString) == 0 do continue;
tok := tokenizer.makeTokenizer(inputString, &keywords);
tokenizer.tokenize(&tok);
defer tokenizer.destroyTokenizer(&tok);
// tokenizer.printTokens(&tok);
parser: Parser;
makeParser(&parser, &builtins, &builtinCalls);
defer destroyParser(&parser);
error := parseInput(&parser, &tok);
if error != nil {
#partial switch v in error {
case ParserError_UnexpectedToken: {
token := error.(ParserError_UnexpectedToken).tokens[0];
ncure.printf(ncure.ForegroundColor.Red, "Parsing Error: Unexpected %s '%s'", token.type, token.str);
ncure.newLine();
}
}
continue;
}
ncure.newLine();
// Runs statements
for statement in parser.statements {
#partial switch v in statement { // TODO
case Builtin: {
builtin := statement.(Builtin);
builtin->p(&globalData);
}
case Call: {
call := statement.(Call);
call->p(&globalData);
}
}
}
ncure.newLine();
/*fmt.printf("\n");
fmt.println(inputString);*/
// Add command to history
// append(&globalData.commandHistory, strings.clone(strings.to_string(input)));
container.queue_push(&globalData.commandHistory, strings.clone(strings.to_string(input)));
}
ncure.write_rune('\n');
}
printPrompt :: proc(globalData: ^GlobalData) {
ncure.setColor(ncure.ForegroundColor.Green);
ncure.write_string(globalData.username);
ncure.write_string(": ");
ncure.write_string(globalData.current);
ncure.write_string("|> ");
ncure.resetColors();
}
cliInput :: proc(input: ^strings.Builder, globalData: ^GlobalData) {
strings.reset_builder(input);
data: byte;
for {
data = ncure.getch();
// ncure.batch_start();
// defer ncure.batch_end();
if ncure.Input(data) == ncure.Input.CTRL_C {
globalData.running = false;
strings.reset_builder(input);
break;
} else if ncure.Input(data) == ncure.Input.BACKSPACE {
if len(input.buf) <= 0 do continue;
strings.pop_rune(input);
ncure.backspace();
continue;
} else if ncure.Input(data) == ncure.Input.ENTER {
globalData.history_index = 0;
break;
} else if ncure.Input(data) == ncure.Input.CTRL_BACKSPACE {
if len(input.buf) <= 0 do continue;
// Search for whitespace before cursor
last_whitespace_index := strings.last_index(string(input.buf[:]), " ");
rune_count := strings.rune_count(string(input.buf[:]));
if last_whitespace_index == -1{
strings.reset_builder(input);
ncure.backspace(rune_count);
continue;
}
num_to_delete := rune_count - last_whitespace_index;
ncure.backspace(num_to_delete);
for i in 0..<num_to_delete {
strings.pop_rune(input);
}
continue;
} else if ncure.Input(data) == ncure.Input.CTRL_L {
ncure.clearScreen();
ncure.setCursor_topleft();
printPrompt(globalData);
ncure.write_string(string(input.buf[:]));
continue;
} else if ncure.isSpecial(data) {
data = ncure.getch();
handleHistory :: proc(input: ^strings.Builder, using globalData: ^GlobalData) {
old_rune_count := strings.rune_count(string(input.buf[:]));
if history_index > 0 && history_index <= container.queue_len(commandHistory) {
strings.reset_builder(input);
hist_str := container.queue_get(commandHistory, container.queue_len(commandHistory) - history_index);
strings.write_string(input, hist_str);
ncure.backspace(old_rune_count - 1);
ncure.write_string(hist_str);
} else if history_index <= 0 { // TODO: Buggy
strings.reset_builder(input);
ncure.backspace(old_rune_count);
}
}
if ncure.Input(data) == ncure.Input.UP {
if globalData.history_index < container.queue_len(globalData.commandHistory) do globalData.history_index += 1;
handleHistory(input, globalData);
} else if ncure.Input(data) == ncure.Input.DOWN {
ncure.write_string("test down");
if globalData.history_index != 0 do globalData.history_index -= 1;
handleHistory(input, globalData);
}
} else if data >= 32 && data <= 126 {
ncure.write_byte(data);
strings.write_byte(input, data);
}
}
}
setupUserInfo :: proc(globalData: ^GlobalData) {
username, username_exists := os.getenv("USER");
homeDir, homeDir_exists := os.getenv("HOME");
if !homeDir_exists {
uid := linux.getuid();
passwd := linux.getpwuid(uid);
globalData.homeDirectory = string(passwd.pw_dir);
globalData.username = string(passwd.pw_name);
} else {
globalData.homeDirectory = string(homeDir);
globalData.username = string(username);
}
}
setupDefaultDirectories :: proc(globalData: ^GlobalData) {
globalData.executablePath = linux.get_executable_path();
globalData.executableDir = path.dir(globalData.executablePath);
builder := strings.make_builder(0, len(globalData.executableDir));
defer strings.destroy_builder(&builder);
strings.write_string(&builder, globalData.executableDir);
append_to_path(&builder, "tools");
globalData.toolsFolder = strings.clone(strings.to_string(builder));
strings.reset_builder(&builder);
strings.write_string(&builder, globalData.executableDir);
append_to_path(&builder, "wrappers");
globalData.wrappersFolder = strings.clone(strings.to_string(builder));
hashDirectoryFiles(globalData, globalData.toolsFolder);
hashDirectoryFiles(globalData, globalData.wrappersFolder);
}
handlePathsConfigFile :: proc(globalData: ^GlobalData, path: string) {
contents, ok := os.read_entire_file(path);
}
setupEnvironmentVariables :: proc(globalData: ^GlobalData) {
linux.setenv("USER", globalData.username, true);
linux.setenv("USERNAME", globalData.username, true);
linux.setenv("HOME", globalData.homeDirectory, true);
linux.setenv("SHELL", "paled", true);
// TODO: LOGNAME?
}
hashDirectoryFiles :: proc(globalData: ^GlobalData, directory: string) {
dp: ^linux.DIR;
dp_error: os.Errno;
dirp: ^linux.dirent;
dirp_error: os.Errno;
dp, dp_error = linux.opendir(directory);
if (dp == nil) {
fmt.printf("Error opening directory '%s': %s\n", directory, dp_error);
ncure.enableEcho();
os.exit(1); // TODO
}
defer linux.closedir(dp);
path_builder := strings.make_builder();
defer strings.destroy_builder(&path_builder);
for {
defer strings.reset_builder(&path_builder);
dirp, dirp_error = linux.readdir(dp);
if dirp == nil && dirp_error == os.ERROR_NONE do break;
else if dirp == nil do continue; // TODO: Print error?
d_name_length := len(cstring(&dirp.d_name[0]));
d_name_str := string(dirp.d_name[:d_name_length]);
strings.write_string(&path_builder, directory);
append_to_path(&path_builder, d_name_str);
path := strings.to_string(path_builder);
fileInfo, info_err := os.stat(path);
if info_err != os.ERROR_NONE {
fmt.printf("Error stating file '%s': %s\n", path, info_err);
continue;
}
if os.S_ISREG(fileInfo.mode) || os.S_ISLNK(fileInfo.mode) {
copy_path := strings.clone(path);
globalData.path_hash[strings.clone(d_name_str)] = copy_path; // TODO
ncure.write_string(ncure.ForegroundColor.Red, ":");
// fmt.println(d_name_str);
}
}
// fmt.println(globalData.path_hash);
}
append_to_path :: proc(builder: ^strings.Builder, sarr: ..string) {
for s in sarr {
if !os.is_path_separator(rune(peek_byte(builder))) do strings.write_rune(builder, linux.get_path_separator());
// if peek_byte(builder) != '/' do strings.write_rune(builder, '/');
strings.write_string(builder, s);
}
}