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); } }