7GVCZB5QR3Z6OK6WNJUKLSCUZDR6HRM43GCNLLCXHWA27RL3UDNAC
package tokenizer
import "core:fmt"
import "core:strings"
import "core:unicode/utf8"
Set :: distinct map[string]bool;
// TODO: Use an arena allocator
Tokenizer :: struct {
source: string,
keywords: ^Set,
ident_allowWhitespace: bool,
tokenContext: TokenizerContext,
line, col: int,
newLine: bool, // TODO
backslashEscape: bool, // TODO
quoted: bool,
currentIndex: int,
currentRune: rune,
currentRuneSize: int,
tokenStart: int,
tokens: [dynamic]Token,
}
TokenizerContext :: enum u8 {
None,
Number,
Identifier,
QuotedIdentifier,
Char,
String,
RawString,
}
TokenType :: enum u16 {
// 0 - 255 are the ascii characters
Null = 0,
HorizontalTab = 9,
ESC = 27,
Exclamation = 33,
Hash = 35,
DollarSign = 36,
Percent = 37,
Ampersand = 38,
LeftParen = 40,
RightParen = 41,
Astrisk = 42,
Plus = 43,
Comma = 44,
Dash = 45,
Dot = 46,
Slash = 47,
Colon = 58,
Semicolon = 59,
LessThan = 60,
Equal = 61,
GreaterThan = 62,
QuestionMark = 63,
At = 64,
LeftBracket = 91,
Backslash = 92,
RightBracket = 93,
Hat = 94, // ^
Unerscore = 95,
Backtick = 96, // `
LeftCurlyBracket = 123,
VerticalBar = 124, // |
RightCurlyBracket = 125,
Whitespace = 256,
Identifier,
QuotedIdentifier,
Keyword,
Number,
String,
RawString,
Char,
End,
}
Token :: struct {
type: TokenType,
str: string,
line, col: int,
}
makeToken :: proc(type: TokenType, str: string, line, col: int) -> Token {
token: Token;
token.type = type;
token.str = str;
token.line = line;
token.col = col - len(str) + 1;
return token;
}
makeTokenizer :: proc(source: string, keywords: ^Set, ident_allowWhitespace: bool = false) -> Tokenizer {
tokenizer: Tokenizer;
tokenizer.tokenContext = TokenizerContext.None;
tokenizer.source = source;
tokenizer.line = 1;
tokenizer.col = 0;
tokenizer.tokens = make([dynamic]Token, 0);
tokenizer.keywords = keywords;
tokenizer.ident_allowWhitespace = ident_allowWhitespace;
return tokenizer;
}
destroyTokenizer :: proc(tok: ^Tokenizer) {
delete(tok.tokens);
}
is_newline :: proc(r: rune) -> bool {
switch r {
case '\n', '\r': return true;
case: return false;
}
}
// -----
tokenize :: proc(tok: ^Tokenizer) {
newLine: bool = false;
for r, i in tok.source {
tok.currentIndex = i;
tok.currentRune = r;
tok.currentRuneSize = utf8.rune_size(r);
if is_newline(r) && !newLine {
tok.line += 1;
tok.col = 0;
newLine = true;
continue;
} else {
tok.col += 1;
newLine = false;
}
switch tok.tokenContext {
case .None: handleNone(tok);
case .Number: handleNumber(tok);
case .Identifier: handleIdentifier(tok);
case .QuotedIdentifier: handleIdentifier(tok, true);
case .Char: handleChar(tok);
case .String: handleString(tok, false);
case .RawString: handleString(tok, true);
}
}
// End of file/input
tok.currentIndex += 1;
tok.currentRune = '\x00';
tok.currentRuneSize = 1;
switch tok.tokenContext {
case .None: handleNone(tok);
case .Number: handleNumber(tok);
case .Identifier: handleIdentifier(tok);
case .QuotedIdentifier: handleIdentifier(tok, true);
case .Char: handleChar(tok);
case .String: handleString(tok, false);
case .RawString: handleString(tok, true);
}
// End token
endToken := makeToken(TokenType.End, tok.source[tok.currentIndex:], tok.line, tok.col);
append(&tok.tokens, endToken);
}
printTokens :: proc(tok: ^Tokenizer) {
for token in tok.tokens {
fmt.println(token);
if token.type == TokenType.Semicolon {
fmt.println("");
}
}
}
handleNone :: proc(using tok: ^Tokenizer) {
// Skip Whitespace
if strings.is_space(currentRune) do return;
switch currentRune {
case 'a'..'z', 'A'..'Z': {
tokenStart = currentIndex;
if quoted do tokenContext = TokenizerContext.QuotedIdentifier;
else do tokenContext = TokenizerContext.Identifier;
}
case '(': {
token := makeToken(TokenType.LeftParen, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case ')': {
token := makeToken(TokenType.RightParen, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '{': {
token := makeToken(TokenType.LeftCurlyBracket, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '}': {
token := makeToken(TokenType.RightCurlyBracket, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case ':': {
token := makeToken(TokenType.Colon, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case ';': {
token := makeToken(TokenType.Semicolon, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case ',': {
token := makeToken(TokenType.Comma, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '"': {
tokenStart = currentIndex;
tokenContext = TokenizerContext.String;
}
case '`': {
tokenStart = currentIndex;
tokenContext = TokenizerContext.RawString;
}
case '\'': {
tokenStart = currentIndex;
tokenContext = TokenizerContext.Char;
}
case '0'..'9': {
tokenStart = currentIndex;
tokenContext = TokenizerContext.Number;
}
case '-': // TODO
case '+': {
token := makeToken(TokenType.Plus, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '*': {
token := makeToken(TokenType.Astrisk, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '/': {
token := makeToken(TokenType.Slash, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '\\': {
token := makeToken(TokenType.Backslash, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '^': {
token := makeToken(TokenType.Hat, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '.': {
token := makeToken(TokenType.Dot, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '=': {
token := makeToken(TokenType.Equal, source[currentIndex:currentIndex + 1], line, col);
append(&tokens, token);
}
case '$': {
quoted = true;
return;
}
case: {
}
}
if quoted do quoted = false;
}
handleIdentifier :: proc(using tok: ^Tokenizer, quotedIdentifier: bool = false) {
// Allow whitespace in identifiers
if tok.ident_allowWhitespace && strings.is_space(currentRune) do return;
switch(currentRune) {
case 'a'..'z', 'A'..'Z', '0'..'9', '_', '-': {
return;
}
case: {
type: TokenType = TokenType.Identifier;
if quotedIdentifier do type = TokenType.QuotedIdentifier;
str := source[tokenStart:currentIndex];
if tok.keywords[str] {
type = TokenType.Keyword;
}
token := makeToken(type, str, line, col);
append(&tokens, token);
tokenContext = TokenizerContext.None;
handleNone(tok);
}
}
}
handleString :: proc(using tok: ^Tokenizer, raw: bool = false) {
// Allow whitespace in strings
if strings.is_space(currentRune) do return;
if currentRune == '"' && !raw {
token := makeToken(TokenType.String, source[tokenStart:currentIndex + 1], line, col);
append(&tokens, token);
tokenContext = TokenizerContext.None;
} else if currentRune == '`' && raw {
token := makeToken(TokenType.RawString, source[tokenStart:currentIndex + 1], line, col);
append(&tokens, token);
tokenContext = TokenizerContext.None;
}
}
// TODO: Error on more than one character in char literal
handleChar :: proc(using tok: ^Tokenizer) {
if currentRune == '\'' {
token := makeToken(TokenType.Char, source[tokenStart:currentIndex + 1], line, col);
append(&tokens, token);
tokenContext = TokenizerContext.None;
}
}
handleNumber :: proc(using tok: ^Tokenizer) {
switch currentRune {
case '0'..'9', '.': {
return;
}
case: { // Note: Whitespace *not* allowed
token := makeToken(TokenType.Number, source[tokenStart:currentIndex], line, col);
append(&tokens, token);
tokenContext = TokenizerContext.None;
handleNone(tok);
}
}
}
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 ncure
package ncure
import "core:strings"
import "core:strconv"
import "core:os"
import "core:fmt"
import "core:mem"
import "../linux"
ESC :: "\e";
SEQUENCE_START :: "\e[";
NEWLINE :: "\n";
CLEAR :: "\e[2J";
CLEAR_DOWN :: "\e[J";
CLEAR_UP :: "\e[1J";
CLEAR_LINE :: "\e[2K";
CLEAR_LINE_RIGHT :: "\e[K";
CLEAR_LINE_LEFT :: "\e[1K";
TOP_LEFT :: "\e[1;1H";
GET_CURSOR :: "\e[6n";
HIDE_CURSOR :: "\e[?25l";
SHOW_CURSOR :: "\e[?25h";
SAVE_CURSOR :: "\e7";
RESTORE_CURSOR :: "\e8";
MOVE_UP :: "\e[1A";
MOVE_DOWN :: "\e[1B";
MOVE_LEFT :: "\e[1D";
MOVE_RIGHT :: "\e[1C";
BatchInfo :: struct {
builder: strings.Builder,
cursor: CursorPos, // Current cursor position at latest ncure call. NOTE: Doesn't necessarily work properly atm.
cursor_start: CursorPos, // Cursor position at the start of a batch
savedCursor: bool,
savedCursorPos: CursorPos, // NOTE: Doesn't necessarily work atm.
termSize: TermSize,
}
@private
_batch := false;
@private
_batchInfo: ^BatchInfo = nil; // TODO: Switch to a stack thing so we can have nested Batches
@private
_createBatchInfo :: proc(batchInfo: ^BatchInfo) {
batchInfo.builder = strings.make_builder();
batchInfo.cursor = getCursor();
batchInfo.cursor_start = batchInfo.cursor;
batchInfo.termSize = getTermSize();
batchInfo.savedCursor = false;
_batchInfo = batchInfo;
}
@private
_destroyBatchInfo :: proc(batchInfo: ^BatchInfo) {
strings.destroy_builder(&batchInfo.builder);
}
batch_start :: proc() -> ^BatchInfo { // TODO
batchInfo: ^BatchInfo = cast(^BatchInfo) mem.alloc(size_of(BatchInfo));
_createBatchInfo(batchInfo);
_batch = true;
return batchInfo;
}
batch_end :: proc() {
if _batch != true do return;
os.write_string(os.stdout, strings.to_string(_batchInfo.builder));
_destroyBatchInfo(_batchInfo);
_batch = false;
}
// TODO: This isn't thread-safe at all
batch :: proc(p: #type proc(batchInfo: ^BatchInfo, args: ..any), args: ..any) {
// State for Batch Build: builder, cursor, and termsize
batchInfo: BatchInfo;
_createBatchInfo(&batchInfo);
defer _destroyBatchInfo(&batchInfo);
_batch = true;
p(&batchInfo, ..args);
os.write_string(os.stdout, strings.to_string(batchInfo.builder));
_batch = false;
}
getTermSize :: proc() -> (termSize: TermSize) {
w: linux.winsize;
if _, err := linux.ioctl(os.stdout, linux.TIOCGWINSZ, &w); err != os.ERROR_NONE {
// Error
}
termSize.width = int(w.ws_col);
termSize.height = int(w.ws_row);
return termSize;
}
getCursor :: proc() -> CursorPos {
if _batch do return _batchInfo.cursor;
cursor: CursorPos;
// Disable Echo, send request, then switch terminal
// back to previous settings
prev, _ := disableEcho(false);
os.write_string(os.stdout, GET_CURSOR);
if set_error := linux.tcsetattr(os.stdin, linux.TCSANOW, &prev); set_error != os.ERROR_NONE {
fmt.println("Error setting terminal info: %s\n", set_error);
}
// Get response
response := strings.make_builder();
defer strings.destroy_builder(&response);
data: byte;
for {
data = getch();
strings.write_byte(&response, data);
if data == 'R' do break;
}
// Parse response
response_str := strings.to_string(response);
arg1_start: int;
arg1_end: int;
arg2_start: int;
arg2_end: int;
for c, i in response_str {
if c == '[' do arg1_start = i + 1;
if c == ';' {
arg1_end = i;
arg2_start = i + 1;
}
if c == 'R' {
arg2_end = i;
}
}
arg1 := response_str[arg1_start:arg1_end];
arg2 := response_str[arg2_start:arg2_end];
cursor.y = strconv.atoi(arg1);
cursor.x = strconv.atoi(arg2);
return cursor;
}
getCursor_topleft :: proc() -> CursorPos {
return CursorPos {1, 1};
}
getCursor_topright :: proc(termSize: ^TermSize = nil) -> CursorPos {
new_ts: TermSize;
if _batch {
new_ts = _batchInfo.termSize;
} else {
new_ts = getTermSize();
if termSize != nil do termSize^ = new_ts;
}
return CursorPos {new_ts.width, 1};
}
getCursor_bottomleft :: proc(termSize: ^TermSize = nil) -> CursorPos {
new_ts: TermSize;
if _batch {
new_ts = _batchInfo.termSize;
} else {
new_ts = getTermSize();
if termSize != nil do termSize^ = new_ts;
}
return CursorPos {1, new_ts.height};
}
getCursor_bottomright :: proc(termSize: ^TermSize = nil) -> CursorPos {
new_ts: TermSize;
if _batch {
new_ts = _batchInfo.termSize;
} else {
new_ts = getTermSize();
if termSize != nil do termSize^ = new_ts;
}
return CursorPos {new_ts.width, new_ts.height};
}
hideCursor :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, HIDE_CURSOR);
} else {
os.write_string(os.stdout, HIDE_CURSOR);
}
}
showCursor :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, SHOW_CURSOR);
} else {
os.write_string(os.stdout, SHOW_CURSOR);
}
}
saveCursor :: proc(overwrite := false) {
if !overwrite {
assert(!_batchInfo.savedCursor, "A cursor has already been saved without being restored.");
}
if _batch {
strings.write_string(&_batchInfo.builder, SAVE_CURSOR);
// Set savedCursor so that subsequent commands know when a saved cursor will be overridden
_batchInfo.savedCursor = true;
_batchInfo.savedCursorPos = _batchInfo.cursor;
} else {
os.write_string(os.stdout, SAVE_CURSOR);
}
}
restoreCursor :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, RESTORE_CURSOR);
// Set savedCursor so that subsequent commands know when a saved cursor is being overridden
_batchInfo.savedCursor = false;
_batchInfo.cursor = _batchInfo.savedCursorPos;
} else {
os.write_string(os.stdout, RESTORE_CURSOR);
}
}
// TODO: Add option to do something like this in the batching stuff??
save_restore :: proc(cursor: CursorPos, f: #type proc()) {
saveCursor();
setCursor(cursor);
f();
restoreCursor();
}
getSequence_set :: proc(x, y: int, b: ^strings.Builder = nil) -> string {
if x == 1 && y == 1 {
if b != nil {
strings.write_string(b, TOP_LEFT);
return strings.to_string(b^);
}
return strings.clone(TOP_LEFT);
}
buf: [129]byte;
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
if y == 1 do strings.write_string(builder, "1;");
else {
strings.write_string(builder, strconv.itoa(buf[:], y));
strings.write_rune(builder, ';');
}
if x == 1 do strings.write_string(builder, "1H");
else {
strings.write_string(builder, strconv.itoa(buf[:], x));
strings.write_rune(builder, 'H');
}
return strings.to_string(builder^);
}
getSequence_moveup :: proc(amt: int, b: ^strings.Builder = nil) -> string {
if amt == 1 {
if b != nil {
strings.write_string(b, MOVE_UP);
return strings.to_string(b^);
}
return strings.clone(MOVE_UP);
}
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
buf: [129]byte;
strings.write_string(builder, strconv.itoa(buf[:], amt));
strings.write_rune(builder, 'A');
return strings.to_string(builder^);
}
getSequence_movedown :: proc(amt: int, b: ^strings.Builder = nil) -> string {
if amt == 1 {
if b != nil {
strings.write_string(b, MOVE_DOWN);
return strings.to_string(b^);
}
return strings.clone(MOVE_DOWN);
}
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
buf: [129]byte;
strings.write_string(builder, strconv.itoa(buf[:], amt));
strings.write_rune(builder, 'B');
return strings.to_string(builder^);
}
getSequence_moveleft :: proc(amt: int, b: ^strings.Builder = nil) -> string {
if amt == 1 {
if b != nil {
strings.write_string(b, MOVE_LEFT);
return strings.to_string(b^);
}
return strings.clone(MOVE_LEFT);
}
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
buf: [129]byte;
strings.write_string(builder, strconv.itoa(buf[:], amt));
strings.write_rune(builder, 'D');
return strings.to_string(builder^);
}
getSequence_moveright :: proc(amt: int, b: ^strings.Builder = nil) -> string {
if amt == 1 {
if b != nil {
strings.write_string(b, MOVE_RIGHT);
return strings.to_string(b^);
}
return strings.clone(MOVE_RIGHT);
}
builder_new: strings.Builder;
builder: ^strings.Builder = b;
if b == nil {
// Create new builder for this sequence only if not
// being added to a pre-existing builder.
builder_new = strings.make_builder();
builder = &builder_new;
}
strings.write_string(builder, SEQUENCE_START);
buf: [129]byte;
strings.write_string(builder, strconv.itoa(buf[:], amt));
strings.write_rune(builder, 'C');
return strings.to_string(builder^);
}
setCursor_xy :: proc(x, y: int, cursor: ^CursorPos = nil, savePrev := false) {
str: string;
defer delete(str);
if savePrev {
saveCursor();
}
if _batch {
str := getSequence_set(x, y, &_batchInfo.builder);
_batchInfo.cursor.x = x;
_batchInfo.cursor.y = y;
} else {
str := getSequence_set(x, y);
defer delete(str);
os.write_string(os.stdout, str);
}
if cursor != nil {
cursor.x = x;
cursor.y = y;
}
}
setCursor_cursor :: proc(cursor: CursorPos, savePrev := false) {
setCursor_xy(x = cursor.x, y = cursor.y, savePrev = savePrev);
}
setCursor :: proc{setCursor_xy, setCursor_cursor};
setCursor_topleft :: proc(cursor: ^CursorPos = nil, savePrev := false) {
if savePrev {
saveCursor();
}
if _batch {
strings.write_string(&_batchInfo.builder, TOP_LEFT);
_batchInfo.cursor.x = 1;
_batchInfo.cursor.y = 1;
} else {
os.write_string(os.stdout, TOP_LEFT);
}
if cursor != nil {
cursor.x = 1;
cursor.y = 1;
}
}
setCursor_topright :: proc(termSize: ^TermSize = nil, cursor: ^CursorPos = nil, savePrev := false) {
if savePrev {
saveCursor();
}
c := getCursor_topright(termSize);
setCursor(c);
if cursor != nil do cursor^ = c;
}
setCursor_bottomleft :: proc(termSize: ^TermSize = nil, cursor: ^CursorPos = nil, savePrev := false) {
if savePrev {
saveCursor();
}
c := getCursor_bottomleft(termSize);
setCursor(c);
if cursor != nil do cursor^ = c;
}
setCursor_bottomright :: proc(termSize: ^TermSize = nil, cursor: ^CursorPos = nil, savePrev := false) {
if savePrev {
saveCursor();
}
c := getCursor_bottomright(termSize);
setCursor(c);
if cursor != nil do cursor^ = c;
}
// TODO: Add optional cursor argument to be set
moveCursor_up :: proc(amt: int = 1) {
if _batch {
str := getSequence_moveup(amt, &_batchInfo.builder);
_batchInfo.cursor.y -= amt;
} else {
str := getSequence_moveup(amt);
defer delete(str);
os.write_string(os.stdout, str);
}
}
moveCursor_down :: proc(amt: int = 1) {
if _batch {
str := getSequence_movedown(amt, &_batchInfo.builder);
_batchInfo.cursor.y += amt;
} else {
str := getSequence_movedown(amt);
defer delete(str);
os.write_string(os.stdout, str);
}
}
moveCursor_left :: proc(amt: int = 1) {
if _batch {
str := getSequence_moveleft(amt, &_batchInfo.builder);
_batchInfo.cursor.x -= amt;
} else {
str := getSequence_moveleft(amt);
defer delete(str);
os.write_string(os.stdout, str);
}
}
moveCursor_right :: proc(amt: int = 1) {
if _batch {
str := getSequence_moveright(amt, &_batchInfo.builder);
_batchInfo.cursor.x += amt;
} else {
str := getSequence_moveright(amt);
defer delete(str);
os.write_string(os.stdout, str);
}
}
moveCursor_start :: proc() {
if _batch {
strings.write_byte(&_batchInfo.builder, '\r');
_batchInfo.cursor.x = 1;
} else {
os.write_byte(os.stdout, '\r');
}
}
moveCursor_end :: proc(termSize: ^TermSize = nil) {
new_ts: TermSize;
moveCursor_start();
if _batch {
new_ts = _batchInfo.termSize;
getSequence_moveright(new_ts.width, &_batchInfo.builder);
_batchInfo.cursor.x = new_ts.width;
} else {
new_ts = getTermSize();
if termSize != nil do termSize^ = new_ts;
str := getSequence_moveright(new_ts.width);
os.write_string(os.stdout, str);
}
}
// TODO: The write and print functions don't change the cursor position correctly
// due to needing to scan the string for escape sequences, new lines, \b,
// non-printable characters, and combinational utf-8 characters
write_string_nocolor :: proc(s: string) {
if _batch {
strings.write_string(&_batchInfo.builder, s);
_batchInfo.cursor.x += len(s); // TODO: This would not work with \b, non-printable chars, and escape sequences within the string
} else {
os.write_string(os.stdout, s);
}
}
write_string_at_nocolor :: proc(cursor: CursorPos, s: string) {
saveCursor();
setCursor(cursor);
write_string_nocolor(s);
restoreCursor();
}
write_string_color :: proc(fg: ForegroundColor, s: string) {
setColor(fg);
if _batch {
strings.write_string(&_batchInfo.builder, s);
_batchInfo.cursor.x += len(s); // TODO: This would not work with \b, non-printable chars, and escape sequences within the string
} else {
os.write_string(os.stdout, s);
}
resetColors();
}
write_string_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, s: string) {
saveCursor();
setCursor(cursor);
write_string_color(fg, s);
restoreCursor();
}
write_string :: proc{write_string_nocolor, write_string_color, write_string_at_nocolor, write_string_at_color};
// TODO: write_strings functions with ..string arg, but doesn't use print/printf/println
write_strings_nocolor :: proc(args: ..string) {
for s in args {
write_string(s);
}
}
write_strings_at_nocolor :: proc(cursor: CursorPos, args: ..string) {
saveCursor();
setCursor(cursor);
write_strings_nocolor(..args);
restoreCursor();
}
write_strings_color :: proc(fg: ForegroundColor, args: ..string) {
for s in args {
write_string(fg, s);
}
}
write_strings_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, args: ..string) {
saveCursor();
setCursor(cursor);
write_strings_color(fg, ..args);
restoreCursor();
}
write_strings :: proc{write_strings_nocolor, write_strings_color, write_strings_at_nocolor, write_strings_at_color};
write_line_nocolor :: proc(s: string) {
if _batch {
strings.write_string(&_batchInfo.builder, s);
} else {
os.write_string(os.stdout, s);
}
newLine();
}
write_line_at_nocolor :: proc(cursor: CursorPos, s: string) {
saveCursor();
setCursor(cursor);
write_line_nocolor(s);
restoreCursor();
}
write_line_color :: proc(fg: ForegroundColor, s: string) {
setColor(fg);
if _batch {
strings.write_string(&_batchInfo.builder, s);
} else {
os.write_string(os.stdout, s);
}
resetColors();
newLine();
}
write_line_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, s: string) {
saveCursor();
setCursor(cursor);
write_line_color(fg, s);
restoreCursor();
}
write_line :: proc{write_line_nocolor, write_line_color, write_line_at_nocolor, write_line_at_color};
write_byte_current :: proc(b: byte) {
if _batch {
strings.write_byte(&_batchInfo.builder, b);
_batchInfo.cursor.x += 1;
} else {
os.write_byte(os.stdout, b);
}
}
write_byte_at :: proc(cursor: CursorPos, b: byte) {
saveCursor();
setCursor(cursor);
write_byte_current(b);
restoreCursor();
}
write_byte :: proc{write_byte_current, write_byte_at};
write_rune_current :: proc(r: rune) {
if _batch {
strings.write_rune(&_batchInfo.builder, r);
_batchInfo.cursor.x += 1; // TODO: non-printable/combinational rune
} else {
os.write_rune(os.stdout, r);
}
}
write_rune_at :: proc(cursor: CursorPos, r: rune) {
saveCursor();
setCursor(cursor);
write_rune_current(r);
restoreCursor();
}
write_rune :: proc{write_rune_current, write_rune_at};
// TODO: Not sure how to handle separator
print_nocolor :: proc(args: ..any, sep := " ") {
if _batch {
fmt.sbprint(&_batchInfo.builder, ..args);
} else {
fmt.print(..args);
}
}
print_at_nocolor :: proc(cursor: CursorPos, args: ..any, sep := " ") {
saveCursor();
setCursor(cursor);
print_nocolor(..args);
restoreCursor();
}
print_color :: proc(fg: ForegroundColor, args: ..any, sep := " ") {
setColor(fg);
if _batch {
fmt.sbprint(&_batchInfo.builder, ..args);
} else {
fmt.print(..args);
}
resetColors();
}
print_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, args: ..any, sep := " ") {
saveCursor();
setCursor(cursor);
print_color(fg, ..args);
restoreCursor();
}
print :: proc{print_nocolor, print_color, print_at_nocolor, print_at_color};
println_nocolor :: proc(args: ..any, sep := " ") {
if _batch {
fmt.sbprintln(&_batchInfo.builder, ..args);
_batchInfo.cursor.y += 1; // For the last newline
} else {
fmt.println(..args);
}
}
println_at_nocolor :: proc(cursor: CursorPos, args: ..any, sep := " ") {
saveCursor();
setCursor(cursor);
println_nocolor(..args);
restoreCursor();
}
println_color :: proc(fg: ForegroundColor, args: ..any, sep := " ") {
setColor(fg);
if _batch {
fmt.sbprintln(&_batchInfo.builder, ..args);
_batchInfo.cursor.y += 1; // For the last newline
} else {
fmt.println(..args);
}
resetColors();
}
println_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, args: ..any, sep := " ") {
saveCursor();
setCursor(cursor);
println_color(fg, ..args);
restoreCursor();
}
println :: proc{println_nocolor, println_color, println_at_nocolor, println_at_color};
printf_nocolor :: proc(format: string, args: ..any) {
if _batch {
fmt.sbprintf(&_batchInfo.builder, format, ..args);
} else {
fmt.printf(format, ..args);
}
}
printf_at_nocolor :: proc(cursor: CursorPos, format: string, args: ..any) {
saveCursor();
setCursor(cursor);
printf_nocolor(format, ..args);
restoreCursor();
}
printf_color :: proc(fg: ForegroundColor, format: string, args: ..any) {
setColor(fg);
if _batch {
fmt.sbprintf(&_batchInfo.builder, format, ..args);
} else {
fmt.printf(format, ..args);
}
resetColors();
}
printf_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, format: string, args: ..any) {
saveCursor();
setCursor(cursor);
printf_color(fg, format, ..args);
restoreCursor();
}
printf :: proc{printf_nocolor, printf_color, printf_at_nocolor, printf_at_color};
newLine :: proc(amt: int = 1) {
if _batch {
for i in 0..<amt {
strings.write_string(&_batchInfo.builder, NEWLINE);
}
_batchInfo.cursor.x = 1;
_batchInfo.cursor.y += amt;
} else {
for i in 0..<amt {
os.write_string(os.stdout, NEWLINE);
}
}
}
clearScreen :: proc() {
if _batch {
// Clearing the screen with erase everything before it.
// Therefore, we can reset everything that was already in
// the string builder
strings.reset_builder(&_batchInfo.builder);
strings.write_string(&_batchInfo.builder, CLEAR);
} else {
os.write_string(os.stdout, CLEAR);
}
}
clearLine :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, CLEAR_LINE);
} else {
os.write_string(os.stdout, CLEAR_LINE);
}
}
clearLine_right :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, CLEAR_LINE_RIGHT);
} else {
os.write_string(os.stdout, CLEAR_LINE_RIGHT);
}
}
clearLine_left :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, CLEAR_LINE_LEFT);
} else {
os.write_string(os.stdout, CLEAR_LINE_LEFT);
}
}
backspace :: proc(amt := 1, clear := true) {
if _batch {
// TODO: This doesn't handle escape sequences, non-printable characters, or combinational characters
// TODO: Problem - doing a backspace after a backspace that has added escape sequences will result
// in the deletion of some of the previous backspace, potentially.
/*for i in 0..<min(amt, strings.builder_len(_batchInfo.builder)) {
strings.pop_rune(&_batchInfo.builder);
}*/
// If trying to backspace more than what was buffered, then
// just add new escape sequences to the buffer to do this.
// diff := amt - strings.builder_len(_batchInfo.builder);
diff := amt;
if (diff > 0) {
moveCursor_left(diff);
if clear do clearLine_right();
else {
for i in 0..<diff {
os.write_string(os.stdout, " ");
}
moveCursor_left(diff);
}
}
} else {
moveCursor_left(amt);
if clear do clearLine_right();
else {
for i in 0..<amt {
os.write_string(os.stdout, " ");
}
moveCursor_left(amt);
}
}
}
package ncure
// TODO: Implement 256 colors, and perhaps true 24-bit colors
TermSize :: struct {
width: int,
height: int,
}
CursorPos :: [2]int;
ForegroundColor :: enum u8 {
Red = 31,
BrightRed = 91,
Green = 32,
BrightGreen = 92,
Blue = 34,
BrightBlue = 94,
Yellow = 33,
BrightYellow = 93,
Cyan = 36,
BrightCyan = 96,
Magenta = 35,
BrightMagenta = 96,
White = 37,
BrightWhite = 97,
Black = 30,
Grey = 90,
}
BackgroundColor :: enum u8 {
Red = 41,
BrightRed = 101,
Green = 42,
BrightGreen = 102,
Blue = 44,
BrightBlue = 104,
Yellow = 43,
BrightYellow = 103,
Cyan = 46,
BrightCyan = 106,
Magenta = 45,
BrightMagenta = 105,
White = 47,
BrightWhite = 107,
Black = 40,
Grey = 100,
}
package ncure
Input :: enum u8 {
CTRL_C = 3,
CTRL_L = 12,
CTRL_O = 15,
CTRL_X = 24,
ESC = 27,
SPECIAL1 = -32, // TODO
SPECIAL2 = 224
}
isSpecial :: proc(c: byte) -> bool {
if c == SPECIAL1 || c == SPECIAL2 {
return true;
}
return false;
}
package ncure
import "core:fmt"
import "core:os"
import "../linux"
Input :: enum u8 {
CTRL_C = 3,
CTRL_D = 4,
END_INPUT = 4,
CTRL_BACKSPACE = 8,
ENTER = 10,
CTRL_L = 12,
CTRL_O = 15,
CTRL_X = 24,
CTRL_Z = 26,
ESC = 27,
SPECIAL1 = 27,
SPECIAL2 = 91,
LEFT = 68,
RIGHT = 67,
UP = 65,
DOWN = 66,
DELETE1 = 51,
DELETE2 = 126,
END = 70,
HOME = 72,
BACKSPACE = 127
}
isSpecial :: proc(c: byte) -> bool {
if Input(c) == Input.SPECIAL1 {
next := getch();
if Input(next) == Input.SPECIAL2 {
return true;
} else {
// TODO
}
}
return false;
}
disableEcho :: proc(nonblocking := false) -> (prev: linux.termios, current: linux.termios) {
if get_error := linux.tcgetattr(os.stdin, &prev); get_error != os.ERROR_NONE {
// Error
fmt.println("Error getting terminal info: %s\n", get_error);
}
current = prev;
current.c_lflag &= ~linux.ICANON;
current.c_lflag &= ~linux.ECHO;
if nonblocking do current.c_cc[linux.VMIN] = 0;
else do current.c_cc[linux.VMIN] = 1;
current.c_cc[linux.VTIME] = 0;
if set_error := linux.tcsetattr(os.stdin, linux.TCSANOW, ¤t); set_error != os.ERROR_NONE {
fmt.println("Error setting terminal info: %s\n", set_error);
}
return prev, current;
}
enableEcho :: proc() {
term: linux.termios;
if get_error := linux.tcgetattr(os.stdin, &term); get_error != os.ERROR_NONE {
// Error
fmt.println("Error getting terminal info: %s\n", get_error);
}
term.c_lflag |= linux.ICANON;
term.c_lflag |= linux.ECHO;
if set_error := linux.tcsetattr(os.stdin, linux.TCSANOW, &term); set_error != os.ERROR_NONE {
fmt.println("Error setting terminal info: %s\n", set_error);
}
}
enableBlocking :: proc() {
term: linux.termios;
if get_error := linux.tcgetattr(os.stdin, &term); get_error != os.ERROR_NONE {
// Error
fmt.println("Error getting terminal info: %s\n", get_error);
}
term.c_cc[linux.VMIN] = 1;
term.c_cc[linux.VTIME] = 0;
if set_error := linux.tcsetattr(os.stdin, linux.TCSANOW, &term); set_error != os.ERROR_NONE {
fmt.println("Error setting terminal info: %s\n", set_error);
}
}
disableBlocking :: proc() {
term: linux.termios;
if get_error := linux.tcgetattr(os.stdin, &term); get_error != os.ERROR_NONE {
// Error
fmt.println("Error getting terminal info: %s\n", get_error);
}
term.c_cc[linux.VMIN] = 0;
term.c_cc[linux.VTIME] = 0;
if set_error := linux.tcsetattr(os.stdin, linux.TCSANOW, &term); set_error != os.ERROR_NONE {
fmt.println("Error setting terminal info: %s\n", set_error);
}
}
getch :: proc() -> (byte) {
data: [1]byte;
if bytes_read, _ := os.read(os.stdin, data[:]); bytes_read < 0 {
fmt.println("Error reading Input");
}
return data[0];
}
package main
import ncure ".."
import "core:strconv"
import "core:time"
main :: proc() {
ncure.disableEcho(false);
defer ncure.enableEcho();
itoa_buf: [129]byte;
termSize := ncure.getTermSize();
ncure.batch_start();
{
ncure.clearScreen();
ncure.setCursor_topleft();
ncure.write_strings(ncure.ForegroundColor.Magenta, "Current Terminal Size: (", strconv.itoa(itoa_buf[:], termSize.width), ", ", strconv.itoa(itoa_buf[:], termSize.height), ")");
ncure.setCursor_topright();
str_topRight := "Hello!";
ncure.moveCursor_left(len(str_topRight));
ncure.write_string(str_topRight);
ncure.setCursor(5, 4);
ncure.write_string(ncure.ForegroundColor.Cyan, "Set cursor to (5, 4)");
ncure.moveCursor_down();
ncure.moveCursor_right(2);
ncure.write_string(ncure.ForegroundColor.Red, "Gone down one and right two!");
ncure.moveCursor_up(2);
ncure.write_string(ncure.ForegroundColor.Red, "Gone up two lines!");
ncure.moveCursor_down(3);
ncure.moveCursor_start();
ncure.write_string(ncure.ForegroundColor.Green, "Down 3 and Back at start!");
ncure.moveCursor_down();
}
ncure.batch_end();
pos := ncure.getCursor();
ncure.batch_start();
{
ncure.write_strings(ncure.ForegroundColor.Blue, "Cursor pos at start of this text: (", strconv.itoa(itoa_buf[:], pos.x), ", ", strconv.itoa(itoa_buf[:], pos.y), ")");
ncure.newLine();
ncure.moveCursor_end();
ncure.write_string("Cursor moved to end of line. Blahhhhh");
ncure.moveCursor_left(8);
ncure.clearLine_right();
ncure.newLine();
ncure.write_rune('x');
ncure.newLine();
}
ncure.batch_end();
pos = ncure.getCursor();
ncure.batch_start();
{
ncure.setCursor_bottomleft();
ncure.write_string("Testing bottom left");
ncure.setCursor_bottomright();
str_bottomRight := "Testing bottom right";
ncure.moveCursor_left(len(str_bottomRight));
ncure.write_string(str_bottomRight);
ncure.setCursor(pos);
ncure.write_string(ncure.ForegroundColor.Green, "Going back to saved cursor position");
ncure.newLine();
}
ncure.batch_end();
// Progress bar test
termSize = ncure.getTermSize();
division := 10;
ncure.batch_start();
{
ncure.hideCursor();
ncure.moveCursor_right((termSize.width / division) + 1);
ncure.write_byte('|');
ncure.moveCursor_start();
ncure.write_byte('|');
}
ncure.batch_end();
for i in 0..<(termSize.width / division) {
ncure.write_string(ncure.ForegroundColor.Cyan, "=");
time.sleep(1 * time.Second);
}
ncure.newLine();
// Progress bar test 2
// with clearLine and write_byte_at
ncure.moveCursor_right((termSize.width / division) + 1);
rightPos := ncure.getCursor();
startPos := ncure.CursorPos { 1, rightPos.y };
ncure.batch_start();
{
ncure.write_byte('|');
ncure.moveCursor_start();
ncure.write_byte('|');
}
ncure.batch_end();
for i in 0..<(termSize.width / division) {
ncure.batch_start();
ncure.clearLine();
// Redraw bounds
ncure.write_byte_at(rightPos, '|');
ncure.write_byte_at(startPos, '|');
ncure.write_string(ncure.ForegroundColor.Cyan, "=");
ncure.batch_end();
time.sleep(1 * time.Second);
}
ncure.newLine();
ncure.showCursor();
}
package ncure
package ncure
import "core:os"
import "core:strings"
import "core:strconv"
RESET_COLORS :: "\e[0m"; // TODO: \e[39;49m
setColor_foreground :: proc(fg: ForegroundColor) {
new_builder: strings.Builder;
b: ^strings.Builder;
if _batch {
b = &_batchInfo.builder;
} else {
new_builder = strings.make_builder(0, len(SEQUENCE_START));
b = &new_builder;
}
strings.write_string(b, SEQUENCE_START); // ESC[
buf: [129]byte;
strings.write_string(b, strconv.itoa(buf[:], int(fg)));
strings.write_rune(b, 'm');
if !_batch {
os.write_string(os.stdout, strings.to_string(b^));
strings.destroy_builder(b);
}
}
setColor_background :: proc(bg: BackgroundColor) {
new_builder: strings.Builder;
b: ^strings.Builder;
if _batch {
b = &_batchInfo.builder;
} else {
new_builder = strings.make_builder(0, len(SEQUENCE_START));
b = &new_builder;
}
strings.write_string(b, SEQUENCE_START); // ESC[
buf: [129]byte;
strings.write_string(b, strconv.itoa(buf[:], int(bg)));
strings.write_rune(b, 'm');
if !_batch {
os.write_string(os.stdout, strings.to_string(b^));
strings.destroy_builder(b);
}
}
setColor_fg_bg :: proc(fg: ForegroundColor, bg: BackgroundColor) {
new_builder: strings.Builder;
b: ^strings.Builder;
if _batch {
b = &_batchInfo.builder;
} else {
new_builder = strings.make_builder(0, len(SEQUENCE_START));
b = &new_builder;
}
strings.write_string(b, SEQUENCE_START); // ESC[
buf: [129]byte;
strings.write_string(b, strconv.itoa(buf[:], int(fg)));
strings.write_rune(b, ';');
strings.write_string(b, strconv.itoa(buf[:], int(bg)));
strings.write_rune(b, 'm');
if !_batch {
os.write_string(os.stdout, strings.to_string(b^));
strings.destroy_builder(b);
}
}
setColor :: proc{setColor_foreground, setColor_background, setColor_fg_bg};
resetColors :: proc() {
if _batch {
strings.write_string(&_batchInfo.builder, RESET_COLORS);
} else {
os.write_string(os.stdout, RESET_COLORS);
}
}
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 linux
import "core:c"
import "core:strings"
import "core:os"
import "core:fmt"
foreign import libc "system:c"
pid_t :: #type u32;
uid_t :: #type u32;
gid_t :: #type u32;
passwd :: struct {
pw_name: cstring,
pw_passwd: cstring,
pw_uid: uid_t,
pw_gid: gid_t,
pw_gecos: cstring,
pw_dir: cstring,
pw_shell: cstring,
}
DIR :: opaque [0]byte;
ino_t :: distinct c.ulong;
off_t :: distinct c.long;
// NOTE: The only fields in the dirent structure that are mandated by POSIX.1 are d_name and d_ino. The other fields are unstandardized, and not present on all systems.
// See Notes in `man 3 readdir` for more info about structure and size of structure.
dirent :: struct { // TODO: Make this a raw version and another struct that's easier to use.
d_ino: ino_t, // Inode number of the file
d_off: off_t,
d_reclen: c.ushort, // Size in bytes of the returned record
d_type: c.uchar, // File type
d_name: [256]c.char, // Name of file, null-terminated, can exceede 256 chars (in which case d_reclen exceedes the size of this struct)
}
// -- termios stuff --
cc_t :: distinct c.uchar;
speed_t :: distinct c.uint;
tcflag_t :: distinct c.uint;
NCCS :: 32;
termios :: struct {
c_iflag: tcflag_t, // Input modes
c_oflag: tcflag_t, // Output modes
c_cflag: tcflag_t, // Control modes
c_lflag: tcflag_t, // Local modes
c_line: cc_t,
c_cc: [NCCS]cc_t, // Special characters
c_ispeed: speed_t, // Input speed
c_ospeed: speed_t // Output speed
}
/* c_cc characters */
VINTR :: 0;
VQUIT :: 1;
VERASE :: 2;
VKILL :: 3;
VEOF :: 4;
VTIME :: 5;
VMIN :: 6;
VSWTC :: 7;
VSTART :: 8;
VSTOP :: 9;
VSUSP :: 10;
VEOL :: 11;
VREPRINT :: 12;
VDISCARD :: 13;
VWERASE :: 14;
VLNEXT :: 15;
VEOL2 :: 16;
/* c_iflag bits */
IGNBRK: tcflag_t : 0000001;
BRKINT: tcflag_t : 0000002;
IGNPAR: tcflag_t : 0000004;
PARMRK: tcflag_t : 0000010;
INPCK: tcflag_t : 0000020;
ISTRIP: tcflag_t : 0000040;
INLCR: tcflag_t : 0000100;
IGNCR: tcflag_t : 0000200;
ICRNL: tcflag_t : 0000400;
IUCLC: tcflag_t : 0001000;
IXON: tcflag_t : 0002000;
IXANY: tcflag_t : 0004000;
IXOFF: tcflag_t : 0010000;
IMAXBEL :: 0020000;
IUTF8 :: 0040000;
/* c_oflag bits */
OPOST :: 0000001;
OLCUC :: 0000002;
ONLCR :: 0000004;
OCRNL :: 0000010;
ONOCR :: 0000020;
ONLRET :: 0000040;
OFILL :: 0000100;
OFDEL :: 0000200;
/*#if defined __USE_MISC || defined __USE_XOPEN
# define NLDLY 0000400
# define NL0 0000000
# define NL1 0000400
# define CRDLY 0003000
# define CR0 0000000
# define CR1 0001000
# define CR2 0002000
# define CR3 0003000
# define TABDLY 0014000
# define TAB0 0000000
# define TAB1 0004000
# define TAB2 0010000
# define TAB3 0014000
# define BSDLY 0020000
# define BS0 0000000
# define BS1 0020000
# define FFDLY 0100000
# define FF0 0000000
# define FF1 0100000
#endif*/
VTDLY :: 0040000;
VT0 :: 0000000;
VT1 :: 0040000;
/*#ifdef __USE_MISC
# define XTABS 0014000
#endif*/
/* c_cflag bit meaning */
/*#ifdef __USE_MISC
# define CBAUD 0010017
#endif*/
B0 :: 0000000; /* hang up */
B50 :: 0000001;
B75 :: 0000002;
B110 :: 0000003;
B134 :: 0000004;
B150 :: 0000005;
B200 :: 0000006;
B300 :: 0000007;
B600 :: 0000010;
B1200 :: 0000011;
B1800 :: 0000012;
B2400 :: 0000013;
B4800 :: 0000014;
B9600 :: 0000015;
B19200 :: 0000016;
B38400 :: 0000017;
// #ifdef __USE_MISC
// # define EXTA B19200
// # define EXTB B38400
// #endif
CSIZE :: 0000060;
CS5 :: 0000000;
CS6 :: 0000020;
CS7 :: 0000040;
CS8 :: 0000060;
CSTOPB :: 0000100;
CREAD :: 0000200;
PARENB :: 0000400;
PARODD :: 0001000;
HUPCL :: 0002000;
CLOCAL :: 0004000;
// #ifdef __USE_MISC
// # define CBAUDEX 0010000
// #endif
B57600 :: 0010001;
B115200 :: 0010002;
B230400 :: 0010003;
B460800 :: 0010004;
B500000 :: 0010005;
B576000 :: 0010006;
B921600 :: 0010007;
B1000000 :: 0010010;
B1152000 :: 0010011;
B1500000 :: 0010012;
B2000000 :: 0010013;
B2500000 :: 0010014;
B3000000 :: 0010015;
B3500000 :: 0010016;
B4000000 :: 0010017;
__MAX_BAUD :: B4000000;
// #ifdef __USE_MISC
// # define CIBAUD 002003600000 /* input baud rate (not used) */
// # define CMSPAR 010000000000 /* mark or space (stick) parity */
// # define CRTSCTS 020000000000 /* flow control */
// #endif
/* c_lflag bits */
ISIG :: 0000001;
ICANON: tcflag_t : 0000002;
// #if defined __USE_MISC || (defined __USE_XOPEN && !defined __USE_XOPEN2K)
// # define XCASE 0000004
// #endif
ECHO: tcflag_t : 0000010;
ECHOE :: 0000020;
ECHOK :: 0000040;
ECHONL :: 0000100;
NOFLSH :: 0000200;
TOSTOP :: 0000400;
/*#ifdef __USE_MISC
# define ECHOCTL 0001000
# define ECHOPRT 0002000
# define ECHOKE 0004000
# define FLUSHO 0010000
# define PENDIN 0040000
#endif*/
IEXTEN :: 0100000;
/*#ifdef __USE_MISC
# define EXTPROC 0200000
#endif*/
TCSANOW :: 0;
TCSADRAIN :: 1;
TCSAFLUSH :: 2;
// -- ioctl --
winsize :: struct {
ws_row: c.ushort,
ws_col: c.ushort,
ws_xpixel: c.ushort,
ws_ypixel: c.ushort,
}
TIOCGWINSZ :: 21523;
// wait & waitpid Options
WNOHANG :: 1; // Return immediately if no child has exited
WUNTRACED :: 2; // Also return if a child has stopped (but not traced via ptrace).
WCONTINUED :: 8; // Also return if a stopped child has been resumed by delivery of SIGCONT
WIFEXITED :: inline proc(status: int) -> bool {
return (((status) & 0x7f) == 0);
}
WIFSIGNALED :: inline proc(status: int) -> bool {
return (((byte) (((status) & 0x7f) + 1) >> 1) > 0);
}
// Signal Handling
sighandler_t :: #type proc "c" (signum: c.int);
SIG_IGN : uintptr : 1;
SIG_DFL : uintptr : 0;
SIG_ERR : uintptr : ~uintptr(0); // -1
/* ISO C99 signals. */
SIGINT :: 2; /* Interactive attention signal. */
SIGILL :: 4; /* Illegal instruction. */
SIGABRT :: 6; /* Abnormal termination. */
SIGFPE :: 8; /* Erroneous arithmetic operation. */
SIGSEGV :: 11; /* Invalid access to storage. */
SIGTERM :: 15; /* Termination request. */
/* Historical signals specified by POSIX. */
SIGHUP :: 1; /* Hangup. */
SIGQUIT :: 3; /* Quit. */
SIGTRAP :: 5; /* Trace/breakpoint trap. */
SIGKILL :: 9; /* Killed. */
// SIGBUS :: 10; /* Bus error. */
// SIGSYS :: 12; /* Bad system call. */
SIGPIPE :: 13; /* Broken pipe. */
SIGALRM :: 14; /* Alarm clock. */
/* New(er) POSIX signals (1003.1-2008, 1003.1-2013). */
// SIGURG :: 16; /* Urgent data is available at a socket. */
// SIGSTOP :: 17; /* Stop, unblockable. */
// SIGTSTP :: 18; /* Keyboard stop. */
// SIGCONT :: 19; /* Continue. */
SIGCHLD_ISO :: 20; /* Child terminated or stopped. */
SIGTTIN :: 21; /* Background read from control terminal. */
SIGTTOU :: 22; /* Background write to control terminal. */
SIGPOLL_ISO :: 23; /* Pollable event occurred (System V). */
SIGXCPU :: 24; /* CPU time limit exceeded. */
SIGXFSZ :: 25; /* File size limit exceeded. */
SIGVTALRM :: 26; /* Virtual timer expired. */
SIGPROF :: 27; /* Profiling timer expired. */
// SIGUSR1 :: 30; /* User-defined signal 1. */
// SIGUSR2 :: 31; /* User-defined signal 2. */
/* Nonstandard signals found in all modern POSIX systems
(including both BSD and Linux). */
SIGWINCH :: 28; /* Window size change (4.3 BSD, Sun). */
/* Archaic names for compatibility. */
SIGIO :: SIGPOLL_ISO; /* I/O now possible (4.2 BSD). */
SIGIOT :: SIGABRT; /* IOT instruction, abort() on a PDP-11. */
SIGCLD :: SIGCHLD_ISO; /* Old System V name */
// Signal Adjustment for Linux
SIGSTKFLT :: 16; /* Stack fault (obsolete). */
SIGPWR :: 30; /* Power failure imminent. */
SIGBUS :: 7;
SIGUSR1 :: 10;
SIGUSR2 :: 12;
SIGCHLD :: 17;
SIGCONT :: 18;
SIGSTOP :: 19;
SIGTSTP :: 20; /* terminal stop - Ctrl+Z */
SIGURG :: 23;
SIGPOLL :: 29;
SIGSYS :: 31;
foreign libc {
@(link_name="getuid") getuid :: proc() -> uid_t ---;
@(link_name="getpwnam") _unix_getpwnam :: proc(name: cstring) -> ^passwd ---;
@(link_name="getpwuid") getpwuid :: proc(uid: uid_t) -> ^passwd ---;
@(link_name="readlink") _unix_readlink :: proc(pathname: cstring, buf: cstring, bufsiz: c.size_t) -> c.ssize_t ---;
@(link_name="opendir") _unix_opendir :: proc(name: cstring) -> ^DIR ---; // TODO: Returns ^DIR (which is defined as __dirstream in dirent.h)
@(link_name="readdir") _unix_readdir :: proc(dirp: ^DIR) -> ^dirent ---;
@(link_name="closedir") _unix_closedir :: proc(dirp: ^DIR) -> c.int ---;
@(link_name="setenv") _unix_setenv :: proc(name: cstring, value: cstring, overwrite: c.int) -> c.int ---;
@(link_name="unsetenv") _unix_unsetenv :: proc(name: cstring) -> c.int ---;
@(link_name="secure_getenv") _unix_secure_getenv :: proc(name: cstring) -> cstring ---; // NOTE: GNU-specific
@(link_name="tcgetattr") _unix_tcgetattr :: proc(fd: os.Handle, termios_p: ^termios) -> c.int ---;
@(link_name="tcsetattr") _unix_tcsetattr :: proc(fd: os.Handle, optional_actions: c.int, termios_p: ^termios) -> c.int ---;
@(link_name="ioctl") _unix_ioctl :: proc(fd: os.Handle, request: c.ulong, argp: rawptr) -> c.int ---;
@(link_name="fork") fork :: proc() -> pid_t ---;
@(link_name="execvp") _unix_execvp :: proc(file: cstring, argv: ^cstring) -> c.int ---;
@(link_name="waitpid") _unix_waitpid :: proc(pid: pid_t, wstatus: ^c.int, options: c.int) -> pid_t ---;
@(link_name="signal") _unix_signal :: proc(signum: c.int, handler: c.uintptr_t) -> c.uintptr_t ---;
}
getpwnam :: proc(name: string) -> ^passwd {
cstr := strings.clone_to_cstring(name);
defer delete(cstr);
return _unix_getpwnam(cstr);
}
readlink :: proc(pathname: string, buf: []u8) -> (int, os.Errno) {
cstr_pathname := strings.clone_to_cstring(pathname);
defer delete(cstr_pathname);
bytes_written := _unix_readlink(cstr_pathname, cstring(#no_bounds_check &buf[0]), c.size_t(len(buf)));
if bytes_written == -1 {
return -1, os.Errno(os.get_last_error());
}
return int(bytes_written), os.ERROR_NONE;
}
opendir :: proc(name: string) -> (^DIR, os.Errno) {
cstr := strings.clone_to_cstring(name);
defer delete(cstr);
result := _unix_opendir(cstr);
if result == nil {
return nil, os.Errno(os.get_last_error());
}
return result, os.ERROR_NONE;
}
readdir :: proc(dirp: ^DIR) -> (^dirent, os.Errno) {
previous := os.Errno(os.get_last_error());
result := _unix_readdir(dirp);
err := os.Errno(os.get_last_error());
if result == nil && previous != err { // If nil and errno changed, err occured
return nil, err;
} else if result == nil { // If errno not changed, end of directory stream
return nil, os.ERROR_NONE;
}
return result, os.ERROR_NONE;
}
closedir :: proc(dirp: ^DIR) -> os.Errno {
result := _unix_closedir(dirp);
if result == 0 {
return os.ERROR_NONE;
} else {
return os.Errno(os.get_last_error());
}
}
setenv :: proc(name: string, value: string, overwrite: bool) -> os.Errno {
name_str := strings.clone_to_cstring(name);
defer delete(name_str);
value_str := strings.clone_to_cstring(value);
defer delete(value_str);
result := _unix_setenv(name_str, value_str, overwrite ? 1 : 0);
if result == -1 {
return os.Errno(os.get_last_error());
}
return os.ERROR_NONE;
}
unsetenv :: proc(name: string) -> os.Errno {
name_str := strings.clone_to_cstring(name);
defer delete(name_str);
result := _unix_unsetenv(name_str);
if result == -1 {
return os.Errno(os.get_last_error());
}
return os.ERROR_NONE;
}
// NOTE: GNU-specific
secure_getenv :: proc(name: string) -> (string, bool) {
path_str := strings.clone_to_cstring(name);
defer delete(path_str);
cstr := _unix_secure_getenv(path_str);
if cstr == nil {
return "", false;
}
return string(cstr), true;
}
tcgetattr :: proc(fd: os.Handle, termios_p: ^termios) -> os.Errno {
result := _unix_tcgetattr(fd, termios_p);
if result == -1 {
return os.Errno(os.get_last_error());
}
return os.ERROR_NONE;
}
tcsetattr :: proc(fd: os.Handle, optional_actions: int, termios_p: ^termios) -> os.Errno {
result := _unix_tcsetattr(fd, c.int(optional_actions), termios_p);
if result == -1 {
return os.Errno(os.get_last_error());
}
return os.ERROR_NONE;
}
ioctl :: proc(fd: os.Handle, request: u64, argp: rawptr) -> (int, os.Errno) {
result := _unix_ioctl(fd, c.ulong(request), argp);
if result == -1 {
return -1, os.Errno(os.get_last_error());
}
return int(result), os.ERROR_NONE;
}
/*execvp :: proc(file: string, args: []string) -> os.Errno {
file_str := strings.clone_to_cstring(file);
defer delete(file_str);
result := _unix_execvp(file_str, );
if result == -1 {
return os.Errno(os.get_last_error());
}
return os.ERROR_NONE;
}*/
waitpid :: proc(pid: pid_t, wstatus: ^int, options: int) -> pid_t {
return _unix_waitpid(pid, cast(^c.int) wstatus, c.int(options));
}
signal_handler :: proc(signum: int, handler: sighandler_t) -> (uintptr, os.Errno) {
result := _unix_signal(c.int(signum), c.uintptr_t(uintptr(rawptr(handler))));
if c.uintptr_t(result) == c.uintptr_t(SIG_ERR) {
return uintptr(result), os.Errno(os.get_last_error());
}
return uintptr(result), os.ERROR_NONE;
}
signal_int :: proc(signum: int, handler: uintptr) -> (uintptr, os.Errno) {
result := _unix_signal(c.int(signum), c.uintptr_t(handler));
if result == SIG_ERR {
return uintptr(result), os.Errno(os.get_last_error());
}
return uintptr(result), os.ERROR_NONE;
}
signal :: proc{signal_handler, signal_int};
// TODO: Doesn't work on any BSDs or MacOS
// TODO: Look at 'realpath'
get_executable_path :: proc() -> string {
pathname :: "/proc/self/exe";
page_size := os.get_page_size();
buf := make([dynamic]u8, page_size);
// Credit for this loop technique: Tetralux
for {
bytes_written, error := readlink(pathname, buf[:]);
if error == os.ERROR_NONE {
resize(&buf, bytes_written);
return string(buf[:]);
}
if error != os.ERANGE {
return "";
}
resize(&buf, len(buf)+page_size);
}
unreachable();
}
get_path_separator :: proc() -> rune {
return '/';
}
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;
}
#!/bin/bash
mkdir tools
mkdir wrappers
odin build .
@echo off
mkdir tools
mkdir wrappers
odin build .
# Paled
Paled is a new experimental shell written in Odin. Find out more info [here](https://paled.handmade.network)
License: MIT
Copyright: Christian Seibold. 2019-2020.
Website: https://paled.handmade.network
The MIT License (MIT)
Copyright (c) 2019-2020 Christian Lee Seibold
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.