DZMW5PRBTJV4XFWSDSYNR4AWK5LIRBHW5TMN5E4N633AOT5KMLCAC
# Paled TODO
## August 25, 2020
[ ] Finally fix up and down arrow problems
[ ] Cd builtin command
[ ] Tokenizer - paths can start with "./", "../", "~/", "~", or "/" on macOS, Linux, and Windows
or ".\", "..\", "~\", "~", "C:", "\", or "C:\" (or "C:/") for Windows only
[ ] getenv & setenv
[ ] dhist builtin
## August 22, 2020
[x] write_strings
[x] write_at and print_at
[~] ncure example program in Odin
[x] More parsing - Arguments
## Overall
[ ] Ncure - Linux
[x] General manipulation
[~] Batch commands
[x] Enable/disable echo
[~] Colors
[ ] 256 Colors
[ ] 32-bit Colors
[~] Input
[x] ASCII
[ ] UTF8
[ ] Directory hashing
[x] tools/wrappers folders
[ ] Read paths.pconfig files
[ ] Reconstruct hashtable on directory changes
[ ] Command Input
[ ] Line Editor
[~] Backspace
[~] Delete
[ ] Left and Right Keys
[ ] Mouse Input?
[ ] Command history
[~] Lexer
[x] Char
[x] String
[x] Ident
[x] Keyword
[ ] Path?
[x] Others: (, ), {, }, ., ;
[ ] Parser
[x] Builtins
[ ] Argument handling
[ ] Math expressions: +, -, /, *, (), %, ^
[ ] Paths
[ ] Subcommand mode
[ ] Pipes/IPC
[ ] Execution
[x] Builtins
[ ] Paled Lib
[ ] Subcommand
[ ] Pipes/IPC
[ ] Argument handling
[ ] Paths
[ ] Builtins
[ ] cd
[ ] hash
[x] exit
[~] help
[x] clear
[ ] debug
[ ] lib - load and call into libraries from the command line.
[ ] dhist
[ ] Tools
[ ] List
[ ] Copy
[ ] Make
[ ] Move
[ ] Remove
[ ] Rename
[ ] Trash
[ ] Wrappers
[ ] Odin
[ ] Man
[ ] Vi
[ ] Ssh
[ ] Finger
[ ] Pijul
[ ] Go
[ ] Gcc
[ ] Builtin File Management
[ ] Copy file(s)
[ ] Paste file(s) into current dir or specific dir
[ ] Cut file(s)
[ ] Clear copied file(s)
[ ] Directory history (back and forward)
[ ] Directory stack (push and pop directories)
[ ] Search files/dirs, ability to go to containing directory quickly
[ ] Change file/dir properties
[ ] 'list' command sortby and filtering options?
[ ] Project directories (named dirs) - start with '~'
[ ] Launching default program for a specified file
[ ] Text files
[ ] Source code files - editor or compiler
[ ] Image files
[ ] Video files
[ ] directories?
[ ] .desktop, executable, appimage, dmg, msi, etc.
[ ] Launch default program for a function
[ ] A way to specify default programs for these functions
[ ] editor
[ ] shell
[ ] shell script
[ ] pager
[ ] webbrowser, gopher browser, gemini browser
[ ] file browser
[ ] terminal
[ ] terminal multiplexer
* max history to 400-450 by default ?
* Possible memory leak - hit just an enter many many times, and eventually the memory jumped - could it be ncure?
package main
import "core:runtime"
import "tokenizer"
import "ncure"
BuiltinProc :: #type proc(self: ^Builtin, globalData: ^GlobalData);
CallProc :: #type proc(self: ^Call, globalData: ^GlobalData);
Parser :: struct {
builtins: ^map[string]BuiltinProc,
builtinCalls: ^map[string]CallProc,
statements: [dynamic]Statement,
currentTokenIndex: int,
}
Statement :: union {
Builtin,
Call,
ParserError,
}
ParserError :: union {
ParserError_Internal,
ParserError_UnknownIdent,
ParserError_UnknownCall,
ParserError_NoParams,
ParserError_UnexpectedToken,
ParserError_Unimplemented,
}
/*ParserErrorBase :: struct {
tokens: []tokenizer.Token,
}*/
ParserError_Internal :: struct {
tokens: []tokenizer.Token,
internal_location: runtime.Source_Code_Location,
}
ParserError_UnknownIdent :: struct {
tokens: []tokenizer.Token,
}
ParserError_UnknownCall :: struct {
tokens: []tokenizer.Token,
}
ParserError_NoParams :: struct {
tokens: []tokenizer.Token,
}
ParserError_UnexpectedToken :: struct {
tokens: []tokenizer.Token,
}
ParserError_Unimplemented :: struct {}
Builtin :: struct {
p: BuiltinProc,
rest: []tokenizer.Token,
}
make_builtin :: proc(builtin: ^Builtin, p: BuiltinProc, rest: []tokenizer.Token) {
builtin.p = p;
builtin.rest = rest;
}
Call :: struct {
p: CallProc,
command: []tokenizer.Token,
subcommand: []tokenizer.Token,
hasSubcommand: bool,
help: bool, // True is no parentheses - should show help of command
params: [dynamic]Parameter, // TODO
}
make_call :: proc(call: ^Call, command: []tokenizer.Token, subcommand: []tokenizer.Token = nil) {
call.p = defaultCallProc;
call.command = command;
call.subcommand = nil;
if len(subcommand) > 0 {
call.hasSubcommand = true;
call.subcommand = subcommand;
}
call.help = true;
call.params = make([dynamic]Parameter);
}
make_builtinCall :: proc(call: ^Call, p: CallProc) {
make_call(call, nil, nil);
call.p = p;
}
destroy_call :: proc(call: ^Call) {
delete(call.params);
}
Parameter :: struct {
value: Value,
name: ^tokenizer.Token,
}
makeParser :: proc(parser: ^Parser, builtins: ^map[string]BuiltinProc, builtinCalls: ^map[string]CallProc) {
parser.builtins = builtins;
parser.builtinCalls = builtinCalls;
parser.statements = make([dynamic]Statement);
}
destroyParser :: proc(parser: ^Parser) {
for i in 0..<len(parser.statements) {
statement := &parser.statements[i];
#partial switch v in statement {
case Call: {
destroy_call(transmute(^Call) statement);
}
}
}
delete(parser.statements);
}
Value :: struct {
using _: ^tokenizer.Token,
boolean: bool,
}
currentToken :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer) -> (^tokenizer.Token, int) {
if parser.currentTokenIndex >= len(tok.tokens) do return nil, parser.currentTokenIndex;
return &tok.tokens[parser.currentTokenIndex], parser.currentTokenIndex;
}
nextToken :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer) -> (^tokenizer.Token, int) {
parser.currentTokenIndex += 1;
return currentToken(parser, tok);
}
hasNextToken :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer) -> bool {
token, i := peekNextToken(parser, tok);
return i < len(tok.tokens) && token.type != tokenizer.TokenType.End;
}
hasNextTokenIf :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer, type: tokenizer.TokenType) -> bool {
token, i := peekNextToken(parser, tok);
return i < len(tok.tokens) && token.type != tokenizer.TokenType.End && token.type == type;
}
nextTokenIf :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer, type: tokenizer.TokenType) -> (^tokenizer.Token, int, bool) {
index := parser.currentTokenIndex + 1;
token := &tok.tokens[index];
if token.type == type {
parser.currentTokenIndex = index;
return token, index, true;
}
return nil, -1, false;
}
peekNextToken :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer) -> (^tokenizer.Token, int) {
return &tok.tokens[parser.currentTokenIndex + 1], parser.currentTokenIndex + 1;
}
incrementTokenIndex :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer) {
parser.currentTokenIndex += 1;
}
parseInput :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer) -> ParserError {
using tokenizer;
parser.currentTokenIndex = 0;
for {
current_token, current_i := currentToken(parser, tok);
#partial switch (current_token.type) {
case TokenType.Keyword: {
statement := parseKeywordStatement(parser, tok);
if err, isError := statement.(ParserError); isError {
return err;
}
ncure.println(statement);
append(&parser.statements, statement);
}
case TokenType.Identifier: {
statement := parseIdentifierStatement(parser, tok);
if err, isError := statement.(ParserError); isError {
return err;
}
append(&parser.statements, statement);
}
case: {
// TODO: Cleanup
error: ParserError = ParserError_UnexpectedToken{tok.tokens[current_i:current_i + 1]};
return error;
}
}
current_token, current_i = currentToken(parser, tok);
if current_i >= len(tok.tokens) || current_token.type == TokenType.End {
break;
}
}
return nil;
}
// TODO
parseKeywordStatement :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer) -> (Statement) {
using tokenizer;
keyword, startTokenIndex := currentToken(parser, tok);
nextToken, nextTokenIndex := nextToken(parser, tok);
#partial switch nextToken.type {
case TokenType.LeftParen: {
incrementTokenIndex(parser, tok);
p, ok := parser.builtinCalls[keyword.str];
if ok {
call: Call;
make_builtinCall(&call, p);
return call;
} else {
// Check if regular builtin
p, ok := parser.builtins[keyword.str];
if ok {
semicolonToken_index := findStatementEnd(parser, tok);
builtin: Builtin;
make_builtin(&builtin, p, tok.tokens[parser.currentTokenIndex:semicolonToken_index]);
parser.currentTokenIndex = semicolonToken_index + 1; // TODO
return builtin;
} else {
// ERROR
// error: ParserError = ParserError_Internal{tok.tokens[startTokenIndex:parser.currentTokenIndex], #location};
// return error;
}
}
}
case: {
/*p, ok := parser.builtins[keyword.str];
if ok {
semicolonToken_index := findStatementEnd(parser, tok);
builtin: Builtin;
make_builtin(&builtin, p, tok.tokens[parser.currentTokenIndex:semicolonToken_index]);
parser.currentTokenIndex = semicolonToken_index + 1;
return builtin;
} else {
}*/
// TODO: Cleanup
error: ParserError = ParserError_Unimplemented{};
return error;
}
}
unreachable();
}
// Check ident in builtins
// Check left paren
// Check Subcommand
// Check ident in builtin Calls
// Otherwise, check in command hashmaps
// Check in current directory last (maybe...)
parseIdentifierStatement :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer) -> (Statement) {
using tokenizer;
ident, identIndex := currentToken(parser, tok);
nextToken, nextTokenIndex := nextToken(parser, tok);
// Check if in builtins
p, is_builtin := parser.builtins[ident.str];
if is_builtin {
semicolonToken_index := findStatementEnd(parser, tok);
builtin: Builtin;
make_builtin(&builtin, p, tok.tokens[parser.currentTokenIndex:semicolonToken_index]);
parser.currentTokenIndex = semicolonToken_index + 1;
return builtin;
}
#partial switch nextToken.type {
case TokenType.Dot: {
return parseSubcommand(parser, tok, identIndex);
}
case TokenType.LeftParen: {
return parseCall(parser, tok, identIndex);
}
case TokenType.End, TokenType.Semicolon: {
return parseHelpCall(parser, tok, identIndex);
}
case: {
// TODO: Cleanup
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
}
unreachable();
}
parseSubcommand :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer, identIndex: int) -> Statement {
using tokenizer;
subcommand, subcommandIndex, hasSubcommand := nextTokenIf(parser, tok, TokenType.Identifier);
if !hasSubcommand {
// TODO: Cleanup
error: ParserError = ParserError_UnexpectedToken{tok.tokens[subcommandIndex:subcommandIndex + 1]};
return error;
}
nextToken, nextTokenIndex := nextToken(parser, tok);
#partial switch nextToken.type {
case TokenType.LeftParen: {
return parseCall(parser, tok, identIndex, true, subcommandIndex);
}
case TokenType.End, TokenType.Semicolon: {
incrementTokenIndex(parser, tok);
return parseHelpCall(parser, tok, identIndex, true, subcommandIndex);
}
case: {
// TODO: Cleanup
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
}
unreachable();
}
parseHelpCall :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer, identIndex: int, hasSubcommand := false, subcommandIndex: int = 0) -> Statement {
call: Call;
if hasSubcommand do make_call(&call, tok.tokens[identIndex:identIndex + 1], tok.tokens[subcommandIndex:subcommandIndex + 1]);
else do make_call(&call, tok.tokens[identIndex:identIndex + 1]);
call.help = true;
return call;
}
parseCall :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer, identIndex: int, hasSubcommand := false, subcommandIndex: int = 0) -> Statement {
using tokenizer;
call: Call;
if hasSubcommand do make_call(&call, tok.tokens[identIndex:identIndex + 1], tok.tokens[subcommandIndex:subcommandIndex + 1]);
else do make_call(&call, tok.tokens[identIndex:identIndex + 1]);
call.help = false;
error := parseParameters(parser, tok, &call.params);
if error != nil {
// TODO: Cleanup stuff
return error;
}
return call;
}
// FirstParameter := (ident = value) | value
// OtherParameters := ',' ((ident = value) | value)
// value := Number | String | Char | Boolean
parseParameters :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer, params: ^[dynamic]Parameter) -> ParserError {
using tokenizer;
param: Parameter;
hasName := false;
hasValue := false;
outer: for {
nextToken, nextTokenIndex := nextToken(parser, tok);
#partial switch nextToken.type {
case TokenType.Identifier: {
// Check if there's an equals, if not, then expression, otherwise a named parameter
equalsToken, equalsTokenIndex, hasEquals := nextTokenIf(parser, tok, TokenType.Equal);
if hasEquals && !hasName {
param.name = nextToken;
hasName = true;
} else {
// Identifier Value
// TODO: Not currently allowed atm
return ParserError_UnexpectedToken {tok.tokens[nextTokenIndex + 1:nextTokenIndex + 2]};
}
}
case TokenType.Number: {
if hasValue {
// TODO: Cleanup
// TODO: Expressions?
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
param.value = Value { nextToken, false };
hasValue = true;
}
case TokenType.String: {
if hasValue {
// TODO: Cleanup
// TODO: Expressions?
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
param.value = Value { nextToken, false };
hasValue = true;
}
case TokenType.Char: {
if hasValue {
// TODO: Cleanup
// TODO: Expressions?
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
param.value = Value { nextToken, false };
hasValue = true;
}
case TokenType.Keyword: {
if hasValue {
// TODO: Cleanup
// TODO: Expressions?
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
if nextToken.str == "true" || nextToken.str == "false" {
param.value = Value { nextToken, true };
hasValue = true;
}
}
case TokenType.Comma: {
// End of current parameter. Append it to list
if !hasValue {
// TODO: Cleanup
// TODO: No parameter value error
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
append(params, param);
param.name = nil;
param.value = Value { nil, false };
hasName = false;
hasValue = false;
}
case TokenType.RightParen: {
if hasName && !hasValue {
// TODO: Cleanup
// TODO: No parameter value error
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
} else if hasValue {
append(params, param);
// ncure.println("Test");
param.name = nil;
param.value = Value { nil, false };
hasName = false;
hasValue = false;
}
nextTokenIf(parser, tok, TokenType.Semicolon);
incrementTokenIndex(parser, tok);
break outer;
}
case TokenType.Semicolon: {
if hasName && !hasValue {
// TODO: Cleanup
// TODO: No parameter value error
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
} else {
// TODO: Cleanup
// TODO: Error, no right parentheses
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
}
case TokenType.End: {
if hasName && !hasValue {
// TODO: Cleanup
// TODO: No parameter value error
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
} else {
// TODO: Cleanup
// TODO: Error, no right parentheses
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
}
case: {
// TODO: Cleanup
error: ParserError = ParserError_UnexpectedToken{tok.tokens[nextTokenIndex:nextTokenIndex + 1]};
return error;
}
}
}
return nil;
}
findStatementEnd :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer) -> int {
using tokenizer;
offset := parser.currentTokenIndex;
for token, i in tok.tokens[parser.currentTokenIndex:] {
if token.type == TokenType.Semicolon || token.type == TokenType.End {
return offset + i;
}
}
unreachable();
}
findToken :: proc(parser: ^Parser, tok: ^tokenizer.Tokenizer, type: tokenizer.TokenType) -> (int, bool) {
using tokenizer;
offset := parser.currentTokenIndex;
for token, i in tok.tokens[parser.currentTokenIndex:] {
if token.type == type {
return offset + i, true;
}
}
return len(tok.tokens) - 1, false;
}
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);
}
}
package main
import "linux"
import "ncure"
import "core:strings"
import "core:path"
defaultCallProc :: proc(self: ^Call, globalData: ^GlobalData) {
// ncure.printf("%s\n", self^);
// Check if command is in hashtable
// TODO: command is a slice that can be of multiple tokens
commandPath, isHashed := globalData.path_hash[self.command[0].str];
if !isHashed {
commandPath = self.command[0].str;
// Check that file exists, is a file, and is executable
exists := path.is_file(commandPath);
ncure.println(commandPath);
if !exists {
ncure.write_string(ncure.ForegroundColor.Red, "Error: File doesn't exist");
ncure.newLine();
return;
}
}
amt := 1;
if self.hasSubcommand || !self.help {
amt += 1;
}
args := make([dynamic]cstring, amt, len(self.params) + amt);
args[0] = strings.clone_to_cstring(commandPath);
if self.hasSubcommand do args[1] = strings.clone_to_cstring(self.subcommand[0].str); // TODO
else if !self.help do args[1] = strings.clone_to_cstring("default");
defer delete(args);
builder := strings.make_builder();
for i in 0..<len(self.params) {
param := &self.params[i];
if param.name != nil {
strings.write_string(&builder, param.name.str);
strings.write_byte(&builder, '=');
append(&args, strings.clone_to_cstring(strings.to_string(builder)));
strings.reset_builder(&builder);
}
valueString, _ := strings.replace_all(param.value.str, "\"", "");
strings.write_string(&builder, valueString); // TODO
append(&args, strings.clone_to_cstring(strings.to_string(builder)));
strings.reset_builder(&builder);
}
append(&args, nil);
commandPath_cstr := strings.clone_to_cstring(commandPath);
defer delete(commandPath_cstr);
executeProgram(globalData, commandPath_cstr, &args[0]);
}
executeProgram :: proc(globalData: ^GlobalData, path: cstring, args: ^cstring) {
pid, wpid: linux.pid_t;
status: int;
pid = linux.fork();
if pid == 0 {
// Child process
// Set terminal stop and interactive attention signals to default
linux.signal(linux.SIGINT, linux.SIG_DFL);
linux.signal(linux.SIGTSTP, linux.SIG_DFL);
// Ignore SIGHUP to allow program to continue
// running even if the controlling terminal/paled closes.
// linux.signal(linux.SIGHUP, linux.SIG_IGN);
result := linux._unix_execvp(path, args);
if result == -1 {
// TODO: Error
}
} else if pid < 0 {
// TODO: Forking error
} else {
// Parent - Paled
for {
wpid = linux.waitpid(pid, &status, linux.WUNTRACED);
if linux.WIFEXITED(status) || linux.WIFSIGNALED(status) do break;
}
}
}
package main
import "core:os"
import "core:container"
import "tokenizer"
import "ncure"
import "linux"
// -- Builtins --
printVersionAndCopyright :: proc() {
ncure.write_line(ncure.ForegroundColor.Blue, "Paled (odin) V0.5");
ncure.write_line("Copyright (c) 2020 Christian Lee Seibold. MIT Licensed.");
ncure.newLine();
}
BuiltinVersion :: proc(self: ^Builtin, globalData: ^GlobalData) {
ncure.batch_start();
defer ncure.batch_end();
printVersionAndCopyright();
}
BuiltinHelp :: proc(self: ^Builtin, globalData: ^GlobalData) {
ncure.batch_start();
defer ncure.batch_end();
// Version and Copyright
printVersionAndCopyright();
// Builtins
ncure.write_line(ncure.ForegroundColor.Blue, "Builtins:");
ncure.write_line("* tools - prints out list of all programs in tools directory");
ncure.write_line("* cd - changes current directory");
ncure.write_line("* motd - prints the message of the day");
ncure.write_line("* getenv - prints the value of the given environment variable");
ncure.write_line("* dhist - print the directory history");
ncure.newLine();
ncure.write_line("* sh");
ncure.write_line("* builtins");
ncure.write_line("* clear");
ncure.write_line("* exit");
ncure.newLine();
// Syntax
ncure.write_line(ncure.ForegroundColor.Blue, "Syntax:");
ncure.write_line("The syntax for calling programs/binaries is much like C");
ncure.write_line("and other programming languages:");
ncure.write_line(ncure.ForegroundColor.Cyan, "> list(\".\")");
ncure.newLine();
ncure.write_line("Some programs support subcommands. The syntax for");
ncure.write_line("calling a subcommand is:");
ncure.write_line(ncure.ForegroundColor.Cyan, "> list.dirs(\".\")");
ncure.newLine();
ncure.write_line("You can see the documentation, including a list of");
ncure.write_line("subcommands, for a program by typing the name without");
ncure.write_line("parentheses:");
ncure.write_line(ncure.ForegroundColor.Cyan, "> list");
ncure.newLine();
ncure.write_line("Named Parameters and Default Arguments are also");
ncure.write_line("supported. Notice that list's documentation shows the");
ncure.write_line("first parameter defaults to \".\" - this parameter is");
ncure.write_line("optional.");
ncure.write_line(ncure.ForegroundColor.Cyan, "> list(detail = true)");
ncure.newLine();
ncure.write_line("Lastly, Builtins do not need to use parentheses:");
ncure.write_line(ncure.ForegroundColor.Cyan, "> cd ~");
}
BuiltinDebug :: proc(self: ^Builtin, globalData: ^GlobalData) {
ncure.batch_start();
defer ncure.batch_end();
ncure.printf("History Count: %d", container.queue_len(globalData.commandHistory));
ncure.newLine();
}
BuiltinExit :: proc(self: ^Builtin, globalData: ^GlobalData) {
ncure.enableEcho();
ncure.showCursor();
os.exit(0);
}
BuiltinCd :: proc(self: ^Builtin, globalData: ^GlobalData) {
linux.setenv("OLDPWD", globalData.current, true);
ncure.println(self.rest);
globalData.current = os.get_current_directory();
linux.setenv("PWD", globalData.current, true);
}
BuiltinGetenv :: proc(self: ^Builtin, globaldata: ^GlobalData) {
if self.rest[0].type == tokenizer.TokenType.Identifier {
result, ok := linux.secure_getenv(self.rest[0].str);
if ok {
ncure.println(result);
}
}
}
BuiltinClear :: proc(self: ^Builtin, globalData: ^GlobalData) {
ncure.batch_start();
defer ncure.batch_end();
ncure.clearScreen();
ncure.setCursor_topleft();
}
BuiltinUnimplemented :: proc(self: ^Builtin, globalData: ^GlobalData) {
ncure.batch_start();
defer ncure.batch_end();
ncure.write_string("Unimplemented.");
ncure.newLine();
}
// -- Builtin Calls --
package main
import "core:strings"
import "core:unicode/utf8"
peek_byte :: proc(b: ^strings.Builder) -> (r: byte) {
if len(b.buf) == 0 {
return 0;
}
r = b.buf[len(b.buf) - 1];
return;
}
peek_rune :: proc(b: ^strings.Builder) -> (r: rune, width: int) {
r, width = utf8.decode_last_rune(b.buf[:]);
return;
}