From da6105d46c0399dc966e45e596e7f736df7bb15d Mon Sep 17 00:00:00 2001 From: Thakee Nathees Date: Fri, 27 May 2022 18:20:46 +0530 Subject: [PATCH] term library added --- src/core/compiler.c | 31 +- src/libs/ext_term.c | 259 ++++++++ src/libs/libs.c | 6 + src/libs/thirdparty/term/LICENSE | 21 + src/libs/thirdparty/term/term.h | 1012 ++++++++++++++++++++++++++++++ tests/examples/snake.pk | 124 ++++ tests/examples/tui/_init.pk | 321 ++++++++++ tests/lang/controlflow.pk | 1 + tests/modules/io.File.pk | 1 + 9 files changed, 1754 insertions(+), 22 deletions(-) create mode 100644 src/libs/ext_term.c create mode 100644 src/libs/thirdparty/term/LICENSE create mode 100644 src/libs/thirdparty/term/term.h create mode 100644 tests/examples/snake.pk create mode 100644 tests/examples/tui/_init.pk diff --git a/src/core/compiler.c b/src/core/compiler.c index 07cfe5a..1933e28 100644 --- a/src/core/compiler.c +++ b/src/core/compiler.c @@ -485,12 +485,6 @@ struct Compiler { // such assignments. bool can_define; - // This value will set to true for parsing a temproary expression - // (ie. if ...) and the value of the expression will be popped out of - // the stack onece we're done, if it's a defenition, that value should be - // duplicated on the stack to prevent it. - bool expr_temp; - // This value will be true after parsing a call expression, for every other // Expressions it'll be false. This is **ONLY** to be used when compiling a // return statement to check if the last parsed expression is a call to @@ -1896,13 +1890,10 @@ static void exprInterpolation(Compiler* compiler) { static void exprFunction(Compiler* compiler) { bool can_define = compiler->can_define; - bool expr_temp = compiler->expr_temp; compiler->can_define = true; - compiler->expr_temp = false; compileFunction(compiler, FUNC_LITERAL); compiler->can_define = can_define; - compiler->expr_temp = expr_temp; } static void exprName(Compiler* compiler) { @@ -1994,10 +1985,6 @@ static void exprName(Compiler* compiler) { ASSERT(compiler->parser.has_errors || (compiler->func->stack_size - 1) == index, OOPS); - if (compiler->expr_temp) { - emitOpcode(compiler, OP_DUP); - } - } else { // The assigned value or the result of the operator will be at the top of // the stack by now. Store it. @@ -3087,10 +3074,10 @@ static void compileIfStatement(Compiler* compiler, bool elif) { skipNewLines(compiler); - bool expr_temp = compiler->expr_temp; - compiler->expr_temp = true; + bool can_define = compiler->can_define; + compiler->can_define = false; compileExpression(compiler); //< Condition. - compiler->expr_temp = expr_temp; + compiler->can_define = can_define; emitOpcode(compiler, OP_JUMP_IF_NOT); int ifpatch = emitShort(compiler, 0xffff); //< Will be patched. @@ -3140,10 +3127,10 @@ static void compileWhileStatement(Compiler* compiler) { loop.depth = compiler->scope_depth; compiler->loop = &loop; - bool expr_temp = compiler->expr_temp; - compiler->expr_temp = true; + bool can_define = compiler->can_define; + compiler->can_define = false; compileExpression(compiler); //< Condition. - compiler->expr_temp = expr_temp; + compiler->can_define = can_define; emitOpcode(compiler, OP_JUMP_IF_NOT); int whilepatch = emitShort(compiler, 0xffff); //< Will be patched. @@ -3176,10 +3163,10 @@ static void compileForStatement(Compiler* compiler) { // Compile and store sequence. compilerAddVariable(compiler, "@Sequence", 9, iter_line); // Sequence - bool expr_temp = compiler->expr_temp; - compiler->expr_temp = true; + bool can_define = compiler->can_define; + compiler->can_define = false; compileExpression(compiler); - compiler->expr_temp = expr_temp; + compiler->can_define = can_define; // Add iterator to locals. It's an increasing integer indicating that the // current loop is nth starting from 0. diff --git a/src/libs/ext_term.c b/src/libs/ext_term.c new file mode 100644 index 0000000..7971b6f --- /dev/null +++ b/src/libs/ext_term.c @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2020-2022 Thakee Nathees + * Copyright (c) 2021-2022 Pocketlang Contributors + * Distributed Under The MIT License + */ + +#ifndef PK_AMALGAMATED +#include "libs.h" +#endif + +#define TERM_IMPLEMENT +#include "thirdparty/term/term.h" //<< AMALG_INLINE >> +#undef TERM_IMPLEMENT + +// A reference to the event class, to check is instance of. +static PkHandle* _cls_term_event = NULL; + +static void _setSlotVector(PKVM* vm, int slot, int tmp, double x, double y) { + + if (!pkImportModule(vm, "types", slot)) return; + if (!pkGetAttribute(vm, slot, "Vector", slot)) return; + if (!pkNewInstance(vm, slot, slot, 0, 0)) return; + + pkSetSlotNumber(vm, tmp, x); + if (!pkSetAttribute(vm, slot, "x", tmp)) return; + pkSetSlotNumber(vm, tmp, y); + if (!pkSetAttribute(vm, slot, "y", tmp)) return; +} + +void* _termEventNew(PKVM* vm) { + term_Event* event = pkRealloc(vm, NULL, sizeof(term_Event)); + event->type = TERM_ET_UNKNOWN; + return event; +} + +void _termEventDelete(PKVM* vm, void* event) { + pkRealloc(vm, event, 0); +} + +void _termEventGetter(PKVM* vm) { + const char* name; + if (!pkValidateSlotString(vm, 1, &name, NULL)) return; + + term_Event* event = pkGetSelf(vm); + + if (strcmp(name, "type") == 0) { + pkSetSlotNumber(vm, 0, (double)event->type); + + } else if (strcmp(name, "keycode") == 0) { + pkSetSlotNumber(vm, 0, (double)event->key.code); + + } else if (strcmp(name, "ascii") == 0) { + pkSetSlotNumber(vm, 0, (double)event->key.ascii); + + } else if (strcmp(name, "modifiers") == 0) { + if (event->type == TERM_ET_KEY_DOWN) { + pkSetSlotNumber(vm, 0, (double)event->key.modifiers); + } else { + pkSetSlotNumber(vm, 0, (double)event->mouse.modifiers); + } + + } else if (strcmp(name, "button") == 0) { + pkSetSlotNumber(vm, 0, (double)event->mouse.button); + + } else if (strcmp(name, "pos") == 0) { + pkReserveSlots(vm, 2); + _setSlotVector(vm, 0, 1, event->mouse.pos.x, event->mouse.pos.y); + + } else if (strcmp(name, "scroll") == 0) { + pkSetSlotBool(vm, 0, event->mouse.scroll); + } + +} + +void _registerEnums(PKVM* vm, PkHandle* term) { + pkReserveSlots(vm, 1); + pkSetSlotHandle(vm, 0, term); + + pkSetSlotNumber(vm, 1, TERM_KEY_UNKNOWN); pkSetAttribute(vm, 0, "KEY_UNKNOWN", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_0); pkSetAttribute(vm, 0, "KEY_0", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_1); pkSetAttribute(vm, 0, "KEY_1", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_2); pkSetAttribute(vm, 0, "KEY_2", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_3); pkSetAttribute(vm, 0, "KEY_3", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_4); pkSetAttribute(vm, 0, "KEY_4", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_5); pkSetAttribute(vm, 0, "KEY_5", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_6); pkSetAttribute(vm, 0, "KEY_6", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_7); pkSetAttribute(vm, 0, "KEY_7", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_8); pkSetAttribute(vm, 0, "KEY_8", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_9); pkSetAttribute(vm, 0, "KEY_9", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_A); pkSetAttribute(vm, 0, "KEY_A", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_B); pkSetAttribute(vm, 0, "KEY_B", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_C); pkSetAttribute(vm, 0, "KEY_C", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_D); pkSetAttribute(vm, 0, "KEY_D", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_E); pkSetAttribute(vm, 0, "KEY_E", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F); pkSetAttribute(vm, 0, "KEY_F", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_G); pkSetAttribute(vm, 0, "KEY_G", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_H); pkSetAttribute(vm, 0, "KEY_H", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_I); pkSetAttribute(vm, 0, "KEY_I", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_J); pkSetAttribute(vm, 0, "KEY_J", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_K); pkSetAttribute(vm, 0, "KEY_K", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_L); pkSetAttribute(vm, 0, "KEY_L", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_M); pkSetAttribute(vm, 0, "KEY_M", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_N); pkSetAttribute(vm, 0, "KEY_N", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_O); pkSetAttribute(vm, 0, "KEY_O", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_P); pkSetAttribute(vm, 0, "KEY_P", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_Q); pkSetAttribute(vm, 0, "KEY_Q", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_R); pkSetAttribute(vm, 0, "KEY_R", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_S); pkSetAttribute(vm, 0, "KEY_S", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_T); pkSetAttribute(vm, 0, "KEY_T", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_U); pkSetAttribute(vm, 0, "KEY_U", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_V); pkSetAttribute(vm, 0, "KEY_V", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_W); pkSetAttribute(vm, 0, "KEY_W", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_X); pkSetAttribute(vm, 0, "KEY_X", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_Y); pkSetAttribute(vm, 0, "KEY_Y", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_Z); pkSetAttribute(vm, 0, "KEY_Z", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_ESC); pkSetAttribute(vm, 0, "KEY_ESC", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_ENTER); pkSetAttribute(vm, 0, "KEY_ENTER", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_SPACE); pkSetAttribute(vm, 0, "KEY_SPACE", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_HOME); pkSetAttribute(vm, 0, "KEY_HOME", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_END); pkSetAttribute(vm, 0, "KEY_END", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_PAGEUP); pkSetAttribute(vm, 0, "KEY_PAGEUP", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_PAGEDOWN); pkSetAttribute(vm, 0, "KEY_PAGEDOWN", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_LEFT); pkSetAttribute(vm, 0, "KEY_LEFT", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_UP); pkSetAttribute(vm, 0, "KEY_UP", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_RIGHT); pkSetAttribute(vm, 0, "KEY_RIGHT", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_DOWN); pkSetAttribute(vm, 0, "KEY_DOWN", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_INSERT); pkSetAttribute(vm, 0, "KEY_INSERT", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_DELETE); pkSetAttribute(vm, 0, "KEY_DELETE", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_BACKSPACE); pkSetAttribute(vm, 0, "KEY_BACKSPACE", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_TAB); pkSetAttribute(vm, 0, "KEY_TAB", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F1); pkSetAttribute(vm, 0, "KEY_F1", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F2); pkSetAttribute(vm, 0, "KEY_F2", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F3); pkSetAttribute(vm, 0, "KEY_F3", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F4); pkSetAttribute(vm, 0, "KEY_F4", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F5); pkSetAttribute(vm, 0, "KEY_F5", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F6); pkSetAttribute(vm, 0, "KEY_F6", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F7); pkSetAttribute(vm, 0, "KEY_F7", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F8); pkSetAttribute(vm, 0, "KEY_F8", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F9); pkSetAttribute(vm, 0, "KEY_F9", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F10); pkSetAttribute(vm, 0, "KEY_F10", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F11); pkSetAttribute(vm, 0, "KEY_F11", 1); + pkSetSlotNumber(vm, 1, TERM_KEY_F12); pkSetAttribute(vm, 0, "KEY_F12", 1); + + pkSetSlotNumber(vm, 1, TERM_MB_UNKNOWN); pkSetAttribute(vm, 0, "BUTTON_UNKNOWN", 1); + pkSetSlotNumber(vm, 1, TERM_MB_LEFT); pkSetAttribute(vm, 0, "BUTTON_LEFT", 1); + pkSetSlotNumber(vm, 1, TERM_MB_MIDDLE); pkSetAttribute(vm, 0, "BUTTON_MIDDLE", 1); + pkSetSlotNumber(vm, 1, TERM_MB_RIGHT); pkSetAttribute(vm, 0, "BUTTON_RIGHT", 1); + + pkSetSlotNumber(vm, 1, TERM_MD_NONE); pkSetAttribute(vm, 0, "MD_NONE", 1); + pkSetSlotNumber(vm, 1, TERM_MD_CTRL); pkSetAttribute(vm, 0, "MD_CTRL", 1); + pkSetSlotNumber(vm, 1, TERM_MD_ALT); pkSetAttribute(vm, 0, "MD_ALT", 1); + pkSetSlotNumber(vm, 1, TERM_MD_SHIFT); pkSetAttribute(vm, 0, "MD_SHIFT", 1); + + pkSetSlotNumber(vm, 1, TERM_ET_UNKNOWN); pkSetAttribute(vm, 0, "EVENT_UNKNOWN", 1); + pkSetSlotNumber(vm, 1, TERM_ET_KEY_DOWN); pkSetAttribute(vm, 0, "EVENT_KEY_DOWN", 1); + pkSetSlotNumber(vm, 1, TERM_ET_RESIZE); pkSetAttribute(vm, 0, "EVENT_RESIZE", 1); + pkSetSlotNumber(vm, 1, TERM_ET_DOUBLE_CLICK); pkSetAttribute(vm, 0, "EVENT_DOUBLE_CLICK", 1); + pkSetSlotNumber(vm, 1, TERM_ET_MOUSE_DOWN); pkSetAttribute(vm, 0, "EVENT_MOUSE_DOWN", 1); + pkSetSlotNumber(vm, 1, TERM_ET_MOUSE_UP); pkSetAttribute(vm, 0, "EVENT_MOUSE_UP", 1); + pkSetSlotNumber(vm, 1, TERM_ET_MOUSE_MOVE); pkSetAttribute(vm, 0, "EVENT_MOUSE_MOVE", 1); + pkSetSlotNumber(vm, 1, TERM_ET_MOUSE_DRAG); pkSetAttribute(vm, 0, "EVENT_MOUSE_DRAG", 1); + pkSetSlotNumber(vm, 1, TERM_ET_MOUSE_SCROLL); pkSetAttribute(vm, 0, "EVENT_MOUSE_SCROLL", 1); + +} + +void _termInit(PKVM* vm) { + bool capture_events; + if (!pkValidateSlotBool(vm, 1, &capture_events)) return; + term_init(capture_events); +} + +void _termCleanup(PKVM* vm) { + term_cleanup(); +} + +void _termIsatty(PKVM* vm) { + pkSetSlotBool(vm, 0, term_isatty()); +} + +void _termNewScreenBuffer(PKVM* vm) { + term_new_screen_buffer(); +} + +void _termRestoreScreenBuffer(PKVM* vm) { + term_restore_screen_buffer(); +} + +void _termGetSize(PKVM* vm) { + pkReserveSlots(vm, 2); + term_Vec size = term_getsize(); + _setSlotVector(vm, 0, 1, size.x, size.y); +} + +void _termGetPosition(PKVM* vm) { + pkReserveSlots(vm, 2); + term_Vec pos = term_getposition(); + _setSlotVector(vm, 0, 1, pos.x, pos.y); +} + +void _termSetPosition(PKVM* vm) { + double x, y; + + int argc = pkGetArgc(vm); + if (!pkCheckArgcRange(vm, argc, 1, 2)) return; + + if (argc == 1) { + pkReserveSlots(vm, 3); + if (!pkGetAttribute(vm, 1, "x", 2)) return; + if (!pkValidateSlotNumber(vm, 2, &x)) return; + + if (!pkGetAttribute(vm, 1, "y", 2)) return; + if (!pkValidateSlotNumber(vm, 2, &y)) return; + } else { + if (!pkValidateSlotNumber(vm, 1, &x)) return; + if (!pkValidateSlotNumber(vm, 2, &y)) return; + } + + term_Vec pos = term_vec((int)x, (int)y); + term_setposition(pos); +} + +void _termReadEvent(PKVM* vm) { + pkReserveSlots(vm, 3); + pkSetSlotHandle(vm, 2, _cls_term_event); + if (!pkValidateSlotInstanceOf(vm, 1, 2)) return; + + term_Event* event = pkGetSlotNativeInstance(vm, 1); + pkSetSlotBool(vm, 0, term_read_event(event)); +} + +/*****************************************************************************/ +/* MODULE REGISTER */ +/*****************************************************************************/ + +void registerModuleTerm(PKVM* vm) { + PkHandle* term = pkNewModule(vm, "term"); + + _registerEnums(vm, term); + pkModuleAddFunction(vm, term, "init", _termInit, 1); + pkModuleAddFunction(vm, term, "cleanup", _termCleanup, 0); + pkModuleAddFunction(vm, term, "isatty", _termIsatty, 0); + pkModuleAddFunction(vm, term, "new_screen_buffer", _termNewScreenBuffer, 0); + pkModuleAddFunction(vm, term, "restore_screen_buffer", _termRestoreScreenBuffer, 0); + pkModuleAddFunction(vm, term, "getsize", _termGetSize, 0); + pkModuleAddFunction(vm, term, "getposition", _termGetPosition, 0); + pkModuleAddFunction(vm, term, "setposition", _termSetPosition, -1); + pkModuleAddFunction(vm, term, "read_event", _termReadEvent, 1); + + _cls_term_event = pkNewClass(vm, "Event", NULL, term, _termEventNew, _termEventDelete); + pkClassAddMethod(vm, _cls_term_event, "@getter", _termEventGetter, 1); + + pkRegisterModule(vm, term); + pkReleaseHandle(vm, term); +} + +void cleanupModuleTerm(PKVM* vm) { + if (_cls_term_event) pkReleaseHandle(vm, _cls_term_event); +} + diff --git a/src/libs/libs.c b/src/libs/libs.c index b22501f..c73ba89 100644 --- a/src/libs/libs.c +++ b/src/libs/libs.c @@ -17,6 +17,9 @@ void registerModuleIO(PKVM* vm); void registerModulePath(PKVM* vm); void registerModuleDummy(PKVM* vm); +void registerModuleTerm(PKVM* vm); +void cleanupModuleTerm(PKVM* vm); + // Registers the modules. void registerLibs(PKVM* vm) { registerModuleMath(vm); @@ -25,8 +28,11 @@ void registerLibs(PKVM* vm) { registerModuleIO(vm); registerModulePath(vm); registerModuleDummy(vm); + + registerModuleTerm(vm); } // Cleanup the modules. void cleanupLibs(PKVM* vm) { + cleanupModuleTerm(vm); } diff --git a/src/libs/thirdparty/term/LICENSE b/src/libs/thirdparty/term/LICENSE new file mode 100644 index 0000000..5ac7fe5 --- /dev/null +++ b/src/libs/thirdparty/term/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Thakee Nathees + +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. diff --git a/src/libs/thirdparty/term/term.h b/src/libs/thirdparty/term/term.h new file mode 100644 index 0000000..4672c3d --- /dev/null +++ b/src/libs/thirdparty/term/term.h @@ -0,0 +1,1012 @@ +/*****************************************************************************/ +/* PUBLIC API */ +/*****************************************************************************/ + +/* + * Note that at this point *nix stdout are buffered and windows aren't. + * + * *nix systems doesn't support double click at this point, but it's in my + * TODO. Contributions are wellcome. + * + * Window resize events can be enabled by setting NO_WINDOW_RESIZE_EVENT + * macro value to 1 and recompile, however you have to provide a event + * callback for resize events in *nix systems. + * + */ + +#include + +/* + * A generic Vector type to pass size, position data around. + */ +typedef struct { + int x; + int y; +} term_Vec; + +/* A macro function to create a vector. */ +#define term_vec(x, y) (term_Vec) { (x), (y) } + + +/* + * List of keycodes. Note that not all possible keys are listed + * here however they can be retrieved by from the key event's + * ascii value. + */ +typedef enum { + + TERM_KEY_UNKNOWN = 0, + + TERM_KEY_0 = '0', + TERM_KEY_1 = '1', + TERM_KEY_2 = '2', + TERM_KEY_3 = '3', + TERM_KEY_4 = '4', + TERM_KEY_5 = '5', + TERM_KEY_6 = '6', + TERM_KEY_7 = '7', + TERM_KEY_8 = '8', + TERM_KEY_9 = '9', + + TERM_KEY_A = 'A', + TERM_KEY_B = 'B', + TERM_KEY_C = 'C', + TERM_KEY_D = 'D', + TERM_KEY_E = 'E', + TERM_KEY_F = 'F', + TERM_KEY_G = 'G', + TERM_KEY_H = 'H', + TERM_KEY_I = 'I', + TERM_KEY_J = 'J', + TERM_KEY_K = 'K', + TERM_KEY_L = 'L', + TERM_KEY_M = 'M', + TERM_KEY_N = 'N', + TERM_KEY_O = 'O', + TERM_KEY_P = 'P', + TERM_KEY_Q = 'Q', + TERM_KEY_R = 'R', + TERM_KEY_S = 'S', + TERM_KEY_T = 'T', + TERM_KEY_U = 'U', + TERM_KEY_V = 'V', + TERM_KEY_W = 'W', + TERM_KEY_X = 'X', + TERM_KEY_Y = 'Y', + TERM_KEY_Z = 'Z', + + TERM_KEY_ESC, + TERM_KEY_ENTER, + TERM_KEY_SPACE, + TERM_KEY_HOME, + TERM_KEY_END, + TERM_KEY_PAGEUP, + TERM_KEY_PAGEDOWN, + TERM_KEY_LEFT, + TERM_KEY_UP, + TERM_KEY_RIGHT, + TERM_KEY_DOWN, + TERM_KEY_INSERT, + TERM_KEY_DELETE, + TERM_KEY_BACKSPACE, + TERM_KEY_TAB, + + TERM_KEY_F1, + TERM_KEY_F2, + TERM_KEY_F3, + TERM_KEY_F4, + TERM_KEY_F5, + TERM_KEY_F6, + TERM_KEY_F7, + TERM_KEY_F8, + TERM_KEY_F9, + TERM_KEY_F10, + TERM_KEY_F11, + TERM_KEY_F12, + +} term_KeyCode; + + +/* + * Event type. + */ +typedef enum { + TERM_ET_UNKNOWN = 0, + TERM_ET_KEY_DOWN, + TERM_ET_DOUBLE_CLICK, + TERM_ET_MOUSE_DOWN, + TERM_ET_MOUSE_UP, + TERM_ET_MOUSE_MOVE, + TERM_ET_MOUSE_DRAG, + TERM_ET_MOUSE_SCROLL, + TERM_ET_RESIZE, +} term_EventType; + + +/* + * Key event modifier flags, that contain the ctrl, alt, shift key state. + */ +typedef enum { + TERM_MD_NONE = 0x0, + TERM_MD_CTRL = (1 << 1), + TERM_MD_ALT = (1 << 2), + TERM_MD_SHIFT = (1 << 3), +} term_Modifiers; + + +/* + * Mouse button event's button index. + */ +typedef enum { + TERM_MB_UNKNOWN = 0, + TERM_MB_LEFT = 1, + TERM_MB_MIDDLE = 2, + TERM_MB_RIGHT = 3, +} term_MouseBtn; + + +/* + * Key event. + */ +typedef struct { + term_KeyCode code; + char ascii; + term_Modifiers modifiers; +} term_EventKey; + + +/* + * Mouse event. + */ +typedef struct { + term_MouseBtn button; + term_Vec pos; + bool scroll; /* If true down otherwise up. */ + term_Modifiers modifiers; +} term_EventMouse; + +/* + * Event. + */ +typedef struct { + term_EventType type; + union { + term_EventKey key; + term_EventMouse mouse; + term_Vec resize; + }; +} term_Event; + + +/* Returns true if both stdin and stdout are tty like device. */ +bool term_isatty(); + + +/* + * Initialize the terminal. On windows it'll enable the virtual terminal + * processing, *nix it'll enter non-canonical mode, and won't echo the + * characters that are inputted. + * + * @param capture_events: If true it'll enable input events processing. + */ +void term_init(bool capture_events); + + +/* Cleans up all the internals. */ +void term_cleanup(void); + + +/* + * Reads an event. You should initialize terminal with capture_events + * to enable reading events. + * + * @param event: an event pointer that'll updated after reading an event. + * + * @return If an event has been read it'll return true. + */ +bool term_read_event(term_Event* event); + + +/* Create an alternative screen buffer. */ +void term_new_screen_buffer(); + + +/* + * Restore the screen buffer after switching to an alternative buffer with + * term_new_screen_buffer(). This will also clean entier screen and place + * the cursor at (0, 0). + */ +void term_restore_screen_buffer(); + + +/* Returns the screen size. */ +term_Vec term_getsize(); + + +/* Returns the cursor position in a zero based index coordinate. */ +term_Vec term_getposition(); + + +/* Sets the cursor position in a zero based index coordinate. */ +void term_setposition(term_Vec pos); + + +/*****************************************************************************/ +/* INTERNAL HEADERS AND MACROS */ +/*****************************************************************************/ + +#ifdef TERM_IMPLEMENT + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #define TERM_SYS_WIN +#else + #define TERM_SYS_NIX +#endif + +/* Platform specific includes */ +#if defined(TERM_SYS_WIN) + #include +#elif defined(TERM_SYS_NIX) + #include + #include + #include +#endif + +#if defined(_MSC_VER) || (defined(TERM_SYS_WIN) && defined(__TINYC__)) + #include + #define read _read + #define fileno _fileno + #define isatty _isatty +#else + #include +#endif + +/* + * In older version of windows some terminal attributes are not defined + * So I'm defining everyting here if they're not already. + * + * Note that I have no idea which macros are not defined and not, I'm just + * re-defining every this looks suspicious. + */ +#ifdef TERM_SYS_WIN + + #ifndef ENABLE_VIRTUALINAL_PROCESSING + #define ENABLE_VIRTUALINAL_PROCESSING 0x0004 + #endif + + #ifndef ENABLE_EXTENDED_FLAGS + #define ENABLE_EXTENDED_FLAGS 0x0080 + #endif + + #ifndef FROM_LEFT_1ST_BUTTON_PRESSED + #define FROM_LEFT_1ST_BUTTON_PRESSED 0x0001 + #endif + + #ifndef RIGHTMOST_BUTTON_PRESSED + #define RIGHTMOST_BUTTON_PRESSED 0x0002 + #endif + + #ifndef FROM_LEFT_2ND_BUTTON_PRESSED + #define FROM_LEFT_2ND_BUTTON_PRESSED 0x0004 + #endif + + #ifndef MOUSE_MOVED + #define MOUSE_MOVED 0x0001 + #endif + + #ifndef DOUBLE_CLICK + #define DOUBLE_CLICK 0x0002 + #endif + + #ifndef MOUSE_WHEELED + #define MOUSE_WHEELED 0x0004 + #endif + + #ifndef LEFT_ALT_PRESSED + #define LEFT_ALT_PRESSED 0x0002 + #endif + + #ifndef RIGHT_ALT_PRESSED + #define RIGHT_ALT_PRESSED 0x0001 + #endif + + #ifndef RIGHT_CTRL_PRESSED + #define RIGHT_CTRL_PRESSED 0x0004 + #endif + + #ifndef LEFT_CTRL_PRESSED + #define LEFT_CTRL_PRESSED 0x0008 + #endif + + #ifndef SHIFT_PRESSED + #define SHIFT_PRESSED 0x0010 + #endif + +#endif /* TERM_SYS_WIN */ + +/* Input read buffer size. */ +#define INPUT_BUFF_SZ 256 + + +/* Returns predicate (a <= c <= b). */ +#define BETWEEN(a, c, b) ((a) <= (c) && (c) <= (b)) + +/*****************************************************************************/ +/* TYPEDEFINES AND DECLARATIONS */ +/*****************************************************************************/ + + +#define _veceq(v1, v2) (((v1.x) == (v2).x) && ((v1).y == (v2).y)) + +typedef struct { + +#if defined(TERM_SYS_WIN) + DWORD outmode, inmode; /* Backup modes. */ + HANDLE h_stdout, h_stdin; /* Handles. */ + +#elif defined(TERM_SYS_NIX) + struct termios tios; /* Backup modes. */ + uint8_t buff[INPUT_BUFF_SZ]; /* Input buffer. */ + int32_t buffc; /* Buffer element count. */ +#endif + + term_Vec screensize; + term_Vec mousepos; + + bool capture_events; + bool initialized; + +} term_Ctx; + + +static term_Ctx _ctx; + +static void _init(); +static void _cleanup(); +static term_Vec _getsize(); + +#ifdef TERM_SYS_NIX +static void _handle_resize(int sig); +#endif + +static bool _read_event(term_Event* event); + + +/*****************************************************************************/ +/* IMPLEMENTATIONS */ +/*****************************************************************************/ + + +bool term_isatty() { + return (!!isatty(fileno(stdout))) && (!!isatty(fileno(stdin))); +} + + +void term_init(bool capture_events) { + memset(&_ctx, 0, sizeof(term_Ctx)); + _ctx.capture_events = capture_events; + + _init(); + + _ctx.screensize = _getsize(); + _ctx.initialized = true; + atexit(term_cleanup); +} + + +void term_cleanup(void) { + assert(_ctx.initialized); + _cleanup(); +} + + +bool term_read_event(term_Event* event) { + return _read_event(event); +} + + +#if defined(TERM_SYS_WIN) +static void _init() { + _ctx.h_stdout = GetStdHandle(STD_OUTPUT_HANDLE); + GetConsoleMode(_ctx.h_stdout, &_ctx.outmode); + + _ctx.h_stdin = GetStdHandle(STD_INPUT_HANDLE); + GetConsoleMode(_ctx.h_stdin, &_ctx.inmode); + + DWORD outmode = (_ctx.outmode | ENABLE_VIRTUALINAL_PROCESSING); + SetConsoleMode(_ctx.h_stdout, outmode); + + if (_ctx.capture_events) { + DWORD inmode = ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT; + SetConsoleMode(_ctx.h_stdin, inmode); + } +} + + +static void _cleanup() { + SetConsoleMode(_ctx.h_stdout, _ctx.outmode); + SetConsoleMode(_ctx.h_stdin, _ctx.inmode); +} + + +#elif defined(TERM_SYS_NIX) +static void _init() { + tcgetattr(fileno(stdin), &_ctx.tios); + + struct termios raw = _ctx.tios; + + /* + * ECHO : It won't print character as we type. + * ICANON : Disable canonical mode, inputs will + * be byte by byte instead of line by line. + * ISIG : Disable Ctrl+C, Ctrl+Z + * IXON : Disable Ctrl+S, Ctrl+Q + * IEXTEN : Disable Ctrl+V + * ICRNL : Fix Ctrl+M. It won't convert '\r' into '\n' anymore. + * OPOST : It won't convert '\n' into '\r\n' anymore. + * BRKINT : Disable break condition that'll send a SIGINT. + */ + raw.c_lflag &= ~(ECHO | ICANON); + if (_ctx.capture_events) { + /*raw.c_oflag &= ~(OPOST);*/ + raw.c_iflag &= ~(IXON | ICRNL | BRKINT); + raw.c_lflag &= ~(ISIG | IEXTEN); + } + + /* + * VMIN : Minimum number of bytes should be read before return from read(). + * VTIME : Maximum amount of time to be wait before read() returns. + * 1 unit is 100 of a second (ie. vtime = n => 1/100 s) + * + * However on windows running WSL, VTIME won't work, it'll wait till an + * input is read. + */ + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; + + tcsetattr(fileno(stdin), TCSAFLUSH, &raw); + + /* Enable mouse events. */ + if (_ctx.capture_events) { + fprintf(stdout, "\x1b[?1003h\x1b[?1006h"); + fflush(stdout); + } + + /* Handle resize events. */ + signal(SIGWINCH, _handle_resize); + +} + + +static void _cleanup() { + + /* Disable mouse events. */ + if (_ctx.capture_events) { + fprintf(stdout, "\x1b[?1003l\x1b[?1006l\x1b[?25h"); + } + + tcsetattr(fileno(stdin), TCSAFLUSH, &_ctx.tios); +} + + +void _handle_resize(int sig) { + term_Vec newsize = _getsize(); + if (!_veceq(_ctx.screensize, newsize)) { + return; + } + _ctx.screensize = newsize; + +#if 0 /* TODO: This event should be dispatch or sent to some kind of an event queue. */ + term_Event event; + memset(&event, 0, sizeof(term_Event)); + event.type = TERM_ET_RESIZE; + event.resize = _ctx.screensize; + + #error "Dispatch the event with a callback or something." + your_callback(&event); +#endif +} + +#endif /* TERM_SYS_NIX */ + + +void term_new_screen_buffer() { + fprintf(stdout, "\x1b[?1049h"); +} + + +void term_restore_screen_buffer() { + fprintf(stdout, "\x1b[H\x1b[J"); /* Clear screen and go to (0, 0). */ + fprintf(stdout, "\x1b[?1049l"); +} + + +term_Vec term_getposition() { + term_Vec pos; + + #if defined(TERM_SYS_WIN) + CONSOLE_SCREEN_BUFFER_INFO binfo; + GetConsoleScreenBufferInfo(_ctx.h_stdout, &binfo); + pos.x = binfo.dwCursorPosition.X; + pos.y = binfo.dwCursorPosition.Y; + return pos; + + #elif defined(TERM_SYS_NIX) + + struct termios tio; + if (tcgetattr(fileno(stdin), &tio) != 0) { + assert(false && "tcgetattr(stdin) failed."); + } + + tcsetattr(fileno(stdin), TCSANOW, &tio); + assert(((tio.c_lflag & (ICANON | ECHO)) == 0) + && "Did you forget to call term_init()"); + + + /* + * Request cursor position. stdin will be in the for of ESC[n;mR + * here where n is the row and m is the column. (1 based). + */ + fprintf(stdout, "\x1b[6n"); + + if (getchar() != '\x1b' || getchar() != '[') { + assert(false && "getchar() failed in getposition()"); + } + + pos.x = 0; pos.y = 0; + int* p = &pos.y; + + while (true) { + int c = getchar(); + + if (c == EOF) { + assert(false && "getchar() failed in getposition()"); + } + + if (c == ';') p = &pos.x; + if (c == 'R') break; + + + if (BETWEEN('0', c, '9')) { + *p = *p * 10 + (c - '0'); + } + } + + /* Since column, row numbers are 1 based substract 1 for 0 based. */ + pos.x--; pos.y--; + + #endif /* TERM_SYS_NIX */ + + return pos; +} + + +void term_setposition(term_Vec pos) { + fprintf(stdout, "\x1b[%i;%iH", pos.y + 1, pos.x + 1); +} + + +static term_Vec _getsize() { + term_Vec size; + +#if defined(TERM_SYS_WIN) + CONSOLE_SCREEN_BUFFER_INFO binfo; + GetConsoleScreenBufferInfo(_ctx.h_stdout, &binfo); + size.x = binfo.srWindow.Right - binfo.srWindow.Left + 1; + size.y = binfo.srWindow.Bottom - binfo.srWindow.Top + 1; + +#elif defined(TERM_SYS_NIX) + struct winsize wsize; + ioctl(fileno(stdout), TIOCGWINSZ, &wsize); + size.x = wsize.ws_col; + size.y = wsize.ws_row; +#endif + + return size; +} + + +term_Vec term_getsize() { + return _ctx.screensize; +} + + +/*****************************************************************************/ +/* INPUT PROCESSING */ +/*****************************************************************************/ + +#if defined(TERM_SYS_WIN) + + +/* + * Convert windows virtual keys to term_KeyCodes. Returns false if the key + * should be ignored as an event (ex: shift, ctrl, ...). + */ +static bool _toTermKeyCode(WORD vk, term_KeyCode * kc) { + + if (0x30 <= vk && vk <= 0x39) { *kc = (term_KeyCode)TERM_KEY_0 + (vk - 0x30); return true; } + if (0x60 <= vk && vk <= 0x69) { *kc = (term_KeyCode)TERM_KEY_0 + (vk - 0x60); return true; } + if (0x41 <= vk && vk <= 0x5a) { *kc = (term_KeyCode)TERM_KEY_A + (vk - 0x41); return true; } + if (0x70 <= vk && vk <= 0x7b) { *kc = (term_KeyCode)TERM_KEY_F1 + (vk - 0x70); return true; } + + /* + * Note that shift, ctrl, alt keys are returned as TERM_KEY_UNKNOWN + *since *nix systems don't support them. + */ + switch (vk) { + case VK_BACK: *kc = TERM_KEY_BACKSPACE; break; + case VK_TAB: *kc = TERM_KEY_TAB; break; + case VK_RETURN: *kc = TERM_KEY_ENTER; break; + case VK_ESCAPE: *kc = TERM_KEY_ESC; break; + case VK_SPACE: *kc = TERM_KEY_SPACE; break; + case VK_PRIOR: *kc = TERM_KEY_PAGEUP; break; + case VK_NEXT: *kc = TERM_KEY_PAGEDOWN; break; + case VK_END: *kc = TERM_KEY_END; break; + case VK_HOME: *kc = TERM_KEY_HOME; break; + case VK_LEFT: *kc = TERM_KEY_LEFT; break; + case VK_RIGHT: *kc = TERM_KEY_RIGHT; break; + case VK_UP: *kc = TERM_KEY_UP; break; + case VK_DOWN: *kc = TERM_KEY_DOWN; break; + case VK_INSERT: *kc = TERM_KEY_INSERT; break; + case VK_DELETE: *kc = TERM_KEY_DELETE; break; + + case VK_SHIFT: + case VK_CONTROL: + case VK_MENU: + case VK_PAUSE: + case VK_CAPITAL: /* Capslock. */ + return false; + } + + return true; +} + + +static bool _read_event(term_Event* event) { + memset(event, 0, sizeof(term_Event)); + event->type = TERM_ET_UNKNOWN; + + DWORD count; + if (!GetNumberOfConsoleInputEvents(_ctx.h_stdin, &count)) { + /* TODO: error handle api ("GetNumberOfConsoleInputEvents() failed."). */ + return false; + } + + if (count == 0) return false; + + INPUT_RECORD ir; + if (!ReadConsoleInput(_ctx.h_stdin, &ir, 1, &count)) { + /* TODO: error handle api ("ReadConsoleInput() failed."). */ + return false; + } + + switch (ir.EventType) { + case KEY_EVENT: { + KEY_EVENT_RECORD* ker = &ir.Event.KeyEvent; + + /* Key up event not available in *nix systems. So we're ignoring here as well. */ + if (!ker->bKeyDown) return false; + + if (!_toTermKeyCode(ker->wVirtualKeyCode, &event->key.code)) return false; + + event->type = TERM_ET_KEY_DOWN; + event->key.ascii = ker->uChar.AsciiChar; + + if ((ker->dwControlKeyState & LEFT_ALT_PRESSED) || (ker->dwControlKeyState & RIGHT_ALT_PRESSED)) + event->key.modifiers |= TERM_MD_ALT; + if ((ker->dwControlKeyState & LEFT_CTRL_PRESSED) || (ker->dwControlKeyState & RIGHT_CTRL_PRESSED)) + event->key.modifiers |= TERM_MD_CTRL; + if (ker->dwControlKeyState & SHIFT_PRESSED) + event->key.modifiers |= TERM_MD_SHIFT; + + } break; + + case MOUSE_EVENT: { + + MOUSE_EVENT_RECORD* mer = &ir.Event.MouseEvent; + + static DWORD last_state = 0; /* Last state to compare if a new button pressed. */ + bool pressed = mer->dwButtonState; /* If any state is on pressed will be true. */ + + /* Xor will give != 0 if any button changed. */ + DWORD change = last_state ^ mer->dwButtonState; + + if (change != 0) { + /* + * What if the mouse doesn't have the middle button and right + * button is the second button?. + */ + if (change & FROM_LEFT_1ST_BUTTON_PRESSED) { + pressed = mer->dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED; + event->mouse.button = TERM_MB_LEFT; + + } else if (change & RIGHTMOST_BUTTON_PRESSED) { + pressed = mer->dwButtonState & RIGHTMOST_BUTTON_PRESSED; + event->mouse.button = TERM_MB_RIGHT; + + } else if (change & FROM_LEFT_2ND_BUTTON_PRESSED) { + pressed = mer->dwButtonState & FROM_LEFT_2ND_BUTTON_PRESSED; + event->mouse.button = TERM_MB_MIDDLE; + } + } + last_state = mer->dwButtonState; + + event->mouse.pos.x = mer->dwMousePosition.X; + event->mouse.pos.y = mer->dwMousePosition.Y; + + if (mer->dwEventFlags == 0) { + event->type = (pressed) ? TERM_ET_MOUSE_DOWN : TERM_ET_MOUSE_UP; + + } else if (mer->dwEventFlags & MOUSE_MOVED) { + if (_veceq(_ctx.mousepos, event->mouse.pos)) { + return false; + } + event->type = (pressed) ? TERM_ET_MOUSE_DRAG : TERM_ET_MOUSE_MOVE; + + } else if (mer->dwEventFlags & MOUSE_WHEELED) { + event->type = TERM_ET_MOUSE_SCROLL; + event->mouse.scroll = (mer->dwButtonState & 0xFF000000) ? true : false; + + } else if (mer->dwEventFlags & DOUBLE_CLICK) { + event->type = TERM_ET_DOUBLE_CLICK; + } + + _ctx.mousepos.x = event->mouse.pos.x; + _ctx.mousepos.y = event->mouse.pos.y; + + if ((mer->dwControlKeyState & LEFT_ALT_PRESSED) || (mer->dwControlKeyState & RIGHT_ALT_PRESSED)) + event->mouse.modifiers |= TERM_MD_ALT; + if ((mer->dwControlKeyState & LEFT_CTRL_PRESSED) || (mer->dwControlKeyState & RIGHT_CTRL_PRESSED)) + event->mouse.modifiers |= TERM_MD_CTRL; + if (mer->dwControlKeyState & SHIFT_PRESSED) + event->mouse.modifiers |= TERM_MD_SHIFT; + + } break; + + case WINDOW_BUFFER_SIZE_EVENT: { + WINDOW_BUFFER_SIZE_RECORD* wbs = &ir.Event.WindowBufferSizeEvent; + event->type = TERM_ET_RESIZE; + term_Vec newsize = term_vec(wbs->dwSize.X, wbs->dwSize.Y); + if (_ctx.screensize.x == newsize.x && _ctx.screensize.y == newsize.y) return false; + _ctx.screensize = newsize; + event->resize = _ctx.screensize; + } break; + + /* Not handling as it's not available in *nix. */ + case MENU_EVENT: + case FOCUS_EVENT: + return false; + } + + return event->type != TERM_ET_UNKNOWN; +} + +#elif defined(TERM_SYS_NIX) + +/* Returns the length of an escape sequence. */ +static int _escape_length(const char* buff, uint32_t size) { + int length = 0; + + while (length < size) { + char c = buff[length++]; + + if (BETWEEN('a', c, 'z') || BETWEEN('A', c, 'Z') || (c == '~')) { + if (c == 'O' && length < size) { + c = buff[length]; + if (BETWEEN('A', c, 'D') || BETWEEN('P', c, 'S') || c == 'F' || c == 'H') { + return length + 1; + } + } + + return length; + + } else if (c == '\x1b') { + return length; + } + } + + return length; +} + + +static void _key_event(char c, term_Event* event) { + event->type = TERM_ET_KEY_DOWN; + event->key.ascii = c; + + /* Note: Ctrl+M and both reads as '\r'. */ + if (c == '\r') { event->key.code = TERM_KEY_ENTER; return; } + if (c == 127) { event->key.code = TERM_KEY_BACKSPACE; return; } + if (c == 9) { event->key.code = TERM_KEY_TAB; return; } + if (c == 32) { event->key.code = TERM_KEY_SPACE; return; } + + event->key.code = (term_KeyCode) c; + + /* Ctrl + key */ + if (1 <= c && c <= 26) { + event->key.modifiers |= TERM_MD_CTRL; + event->key.code = (term_KeyCode)('A' + (c - 1)); + + } else if (BETWEEN('a', c, 'z') || BETWEEN('A', c, 'Z') || BETWEEN('0', c, '9')) { + event->key.code = (term_KeyCode) toupper(c); + if (BETWEEN('A', c, 'Z')) event->key.modifiers |= TERM_MD_SHIFT; + } +} + + +/* Reference: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking */ +static void _mouse_event(const char* buff, uint32_t count, term_Event * event) { + + /* buff = cb ; cx ; cy m|M */ + + const char* c = buff; + if (!(BETWEEN('0', *c, '9'))) return; + + int cb = 0, cx = 0, cy = 0; + char m; + + while (BETWEEN('0', *c, '9')) cb = cb * 10 + (*c++ - '0'); + if (*c++ != ';') return; + while (BETWEEN('0', *c, '9')) cx = cx * 10 + (*c++ - '0'); + if (*c++ != ';') return; + while (BETWEEN('0', *c, '9')) cy = cy * 10 + (*c++ - '0'); + + m = *c++; + if (m != 'm' && m != 'M') return; + + /* + * low two bits = button information. + * next three bits = modifiers. + * next bits = event type. + */ + int low = cb & 0b11; + int high = (cb & 0b11100) >> 2; + int type = cb >> 5; + + /* Note that the modifiers won't work/incorrect in WSL. */ + if (high & 0b001) event->mouse.modifiers |= TERM_MD_SHIFT; + if (high & 0b010) event->mouse.modifiers |= TERM_MD_ALT; + if (high & 0b100) event->mouse.modifiers |= TERM_MD_CTRL; + + event->mouse.pos.x = cx - 1; + event->mouse.pos.y = cy - 1; + + switch (type) { + case 0: { + event->type = (m == 'M') ? TERM_ET_MOUSE_DOWN : TERM_ET_MOUSE_UP; + event->mouse.button = (term_MouseBtn)(low + 1); + } break; + + case 1: { + if (low == 0b11) { /* Mouse move. */ + event->type = TERM_ET_MOUSE_MOVE; + + } else { /* Drag. */ + event->type = TERM_ET_MOUSE_DRAG; + event->mouse.button = (term_MouseBtn)(low + 1); + } + } break; + + case 2: { /* Scroll. */ + event->type = TERM_ET_MOUSE_SCROLL; + + if (low == 0) event->mouse.scroll = false; + else if (low == 1) event->mouse.scroll = true; + + } break; + } +} + + +void _parse_escape_sequence(const char* buff, uint32_t count, term_Event* event) { + assert(buff[0] == '\x1b'); + + if (count == 1) { + event->type = TERM_ET_KEY_DOWN; + event->key.ascii = *buff; + event->key.code = TERM_KEY_ESC; + return; + } + + if (count == 2) { + _key_event(buff[1], event); + event->key.modifiers |= TERM_MD_ALT; + return; + } + + + #define _SET_KEY(keycode) \ + do { \ + event->type = TERM_ET_KEY_DOWN; \ + event->key.code = keycode; \ + } while (false) + + #define _MATCH(str) \ + ((count >= strlen(str) + 1) && (strncmp(buff + 1, (str), strlen(str)) == 0)) + + if (_MATCH("[<")) _mouse_event(buff + 3, count - 3, event); + + else if (_MATCH("[A") || _MATCH("OA")) _SET_KEY(TERM_KEY_UP); + else if (_MATCH("[B") || _MATCH("OB")) _SET_KEY(TERM_KEY_DOWN); + else if (_MATCH("[C") || _MATCH("OC")) _SET_KEY(TERM_KEY_RIGHT); + else if (_MATCH("[D") || _MATCH("OD")) _SET_KEY(TERM_KEY_LEFT); + else if (_MATCH("[5~") || _MATCH("[[5~")) _SET_KEY(TERM_KEY_PAGEUP); + else if (_MATCH("[6~") || _MATCH("[[6~")) _SET_KEY(TERM_KEY_PAGEDOWN); + + else if (_MATCH("[H") || _MATCH("OH") || _MATCH("[1~") || _MATCH("[[7~")) _SET_KEY(TERM_KEY_HOME); + else if (_MATCH("[F") || _MATCH("OF") || _MATCH("[4~") || _MATCH("[[8~")) _SET_KEY(TERM_KEY_END); + + else if (_MATCH("[2~")) _SET_KEY(TERM_KEY_INSERT); + else if (_MATCH("[3~")) _SET_KEY(TERM_KEY_DELETE); + + else if (_MATCH("OP") || _MATCH("[11~")) _SET_KEY(TERM_KEY_F1); + else if (_MATCH("OQ") || _MATCH("[12~")) _SET_KEY(TERM_KEY_F2); + else if (_MATCH("OR") || _MATCH("[13~")) _SET_KEY(TERM_KEY_F3); + else if (_MATCH("OS") || _MATCH("[14~")) _SET_KEY(TERM_KEY_F4); + + else if (_MATCH("[15~")) _SET_KEY(TERM_KEY_F5); + else if (_MATCH("[17~")) _SET_KEY(TERM_KEY_F6); + else if (_MATCH("[18~")) _SET_KEY(TERM_KEY_F7); + else if (_MATCH("[19~")) _SET_KEY(TERM_KEY_F8); + else if (_MATCH("[20~")) _SET_KEY(TERM_KEY_F9); + else if (_MATCH("[21~")) _SET_KEY(TERM_KEY_F10); + else if (_MATCH("[23~")) _SET_KEY(TERM_KEY_F11); + else if (_MATCH("[24~")) _SET_KEY(TERM_KEY_F12); + + #undef _MATCH + #undef _SET_KEY +} + + +static void _buff_shift(uint32_t length) { + assert(_ctx.buff != NULL); + if (length < _ctx.buffc) { + memmove(_ctx.buff, _ctx.buff + length, _ctx.buffc - length); + _ctx.buffc -= length; + } else { + _ctx.buffc = 0; + } +} + + +static bool _read_event(term_Event* event) { + + memset(event, 0, sizeof(term_Event)); + event->type = TERM_ET_UNKNOWN; + + int count = read(fileno(stdin), _ctx.buff + _ctx.buffc, INPUT_BUFF_SZ - _ctx.buffc); + if (count == 0) return false; + + _ctx.buffc += count; + + int event_length = 1; /* Num of character for the event in the buffer. */ + + if (*_ctx.buff == '\x1b') { + event_length = _escape_length(_ctx.buff + 1, _ctx.buffc - 1) + 1; + _parse_escape_sequence(_ctx.buff, event_length, event); + if (event->type == TERM_ET_MOUSE_MOVE) { + if (_veceq(_ctx.mousepos, event->mouse.pos)) { + _buff_shift(event_length); + return false; + } + _ctx.mousepos = event->mouse.pos; + } + + } else { + _key_event(_ctx.buff[0], event); + } + + _buff_shift(event_length); + + return event->type != TERM_ET_UNKNOWN; +} + +#endif /* TERM_SYS_NIX */ + +#endif /* TERM_IMPLEMENT */ diff --git a/tests/examples/snake.pk b/tests/examples/snake.pk new file mode 100644 index 0000000..fd281ab --- /dev/null +++ b/tests/examples/snake.pk @@ -0,0 +1,124 @@ + +import tui, term, math +from types import Vector + +## This is a pocketlang implementation of: +## https://www.youtube.com/watch?v=xGmXxpIj6vs&t=279s&ab_channel=ChrisDeLeonofHomeTeamGameDev + +## x, y velocity of snake. +vx = 0; vy = 0 + +## position (at the middle). +px = 0; py = 0 + +## tile size. +tx = 0; ty = 0 + +## apple position. +ax = 15; ay = 15 + +## tail, trail +trail = [] +tail = 15 + +## score +score = 0 + +def init() + size = tui.get_size() + tx = size.x + ty = size.y + + px = tx / 2; py = ty / 2 +end + +def frame() + px += vx + py += vy + if px < 0 then px = tx - 1 end + if px > tx - 1 then px = 0 end + if py < 0 then py = ty - 1 end + if py > ty - 1 then py = 0 end + + tui.clear() + tui.string('press Q to quit | score = $score') + + for i in trail + tui.set_position(i.x, i.y) + tui.start_bg(0, 0xff, 0) + tui.string(' ') + tui.end_bg() + + if i.x == px and i.y == py + tail = 5 + end + end + + tui.set_position(ax, ay) + tui.start_bg(0xff, 0, 0) + tui.string(' ') + tui.end_bg() + + trail.append(Vector(px, py)) + while trail.length > tail + trail.pop(0) + end + + if px == ax and py == ay + tail += 1 + score += 1 + ax = math.rand() % tx + ay = math.rand() % ty + end + tui.flush() +end + +def handle(event) + if event.type == term.EVENT_KEY_DOWN + if event.keycode == term.KEY_Q + tui.stop() + end + + ## The "Switch Statement" alternative. + { + term.KEY_LEFT: fn + vx = -1; vy = 0 + end, + + term.KEY_UP: fn + vx = 0; vy = -1 + end, + + term.KEY_DOWN: fn + vx = 0; vy = 1 + end, + + term.KEY_RIGHT: fn + vx = 1; vy = 0 + end, + + term.KEY_T: fn + math.sin(12) + end, + + }.get(event.keycode, fn end)() + + end +end + + +def main() + config = tui.Config() + config.capture_events = true + config.hide_cursor = true + config.new_buffer = true + config.fps = 20 + config.init_fn = init + config.event_fn = handle + config.frame_fn = frame + tui.run(config) +end + + +main() + diff --git a/tests/examples/tui/_init.pk b/tests/examples/tui/_init.pk new file mode 100644 index 0000000..5acc2e7 --- /dev/null +++ b/tests/examples/tui/_init.pk @@ -0,0 +1,321 @@ + +import term, io +from time import clock, sleep +from types import ByteBuffer + +def write(msg) + io.write(io.stdout, msg) +end + +## The configuration that should be used to create tui context +## and run. +class Config + def _init() + self.title = null + self.fps = 60 + self.hide_cursor = false + self.capture_events = false + self.new_buffer = false + + self.init_fn = null + self.event_fn = null + self.frame_fn = null + self.cleanup_fn = null + end +end + +## Interlan context contains draw buffer and other data. +class _Context + def _init() + self.buff = ByteBuffer() + self.clear() + end + + ## Called to reset all the internal for reusing the context. + def clear() + self.buff.clear() + self.done = false ## If true the main loop ends. + end +end + +## Internal global context. +_ctx = _Context() + +## ------------------------------------------------------------------------- +## FUNCTIONS +## ------------------------------------------------------------------------- + +## Flush all the buffered bytes in the context's buffer. +def flush() + write(_ctx.buff.string()) + io.flush() + _ctx.buff.clear() +end + +## Stops the main loop and return from run(). +def stop() + _ctx.done = true +end + +def set_title(title) + write("\x1b]0;$title\x07") +end + +def hide_cursor() + write("\x1b[?25l") +end + +def show_cursor() + write("\x1b[?25h") +end + +## It'll write escape sequence to move the cursor but not actually move. +## for this to make effect, one should flush the buffer. +def set_position(x, y) + _ctx.buff.write("\x1b[${y + 1};${x + 1}H") +end + +def get_size() + return term.getsize() +end + +## +## Hex ASCII DEC-Line-Drawing +## ------------------------------ +## 0x6a j ┘ +## 0x6b k ┐ +## 0x6c l ┌ +## 0x6d m └ +## 0x6e n ┼ +## 0x71 q ─ +## 0x74 t ├ +## 0x75 u ┤ +## 0x76 v ┴ +## 0x77 w ┬ +## 0x78 x │ +## +def box_char(c) + _ctx.buff.write("\x1b(0$c\x1b(B") +end + +## FIXME: Implement str * int multiplicatin to support +## +## "\x1b(0${ c * n }\x1b(B" +## +## and define box_char_len() or refactor the above to take argument n. + +## Write a string to the context buffer. +def string(s) + _ctx.buff.write(str(s)) +end + +## Clear the screen and move cursor to (0, 0). +def clear() + _ctx.buff.write("\x1b[H\x1b[J") +end + +## Clear everythin from cursor to EOL. +def clear_eol() + _ctx.buff.write("\x1b[K") +end + +## Clear everythin from cursor to EOF. +def clear_eof() + _ctx.buff.write("\x1b[J") +end + +## Reset all the stylings. +def reset() + _ctx.buff.write("\x1b[0m") +end + +def _is_valid_color(r, g, b) + return r is Number and g is Number and b is Number + return r.isbyte() and g.isbyte() and b.isbyte() +end + +## Set the forground color. +def start_color(r, g, b) + assert(_is_valid_color(r, g, b)) + _ctx.buff.write("\x1b[38;2;$r;$g;${b}m") +end + +## End the forground color. +def end_color() + _ctx.buff.write("\x1b[39m") +end + +## Start the background color. +def start_bg(r, g, b) + assert(_is_valid_color(r, g, b)) + _ctx.buff.write("\x1b[48;2;$r;$g;${b}m") +end + +## End the background color. +def end_bg() + _ctx.buff.write("\x1b[49m") +end + +def start_bold() + _ctx.buff.write("\x1b[1m") +end + +def end_bold() + _ctx.buff.write("\x1b[22m") +end + +def start_dim() + _ctx.buff.write("\x1b[2m") +end + +def end_dim() + _ctx.buff.write("\x1b[22m") +end + +def start_italic() + _ctx.buff.write("\x1b[3m") +end + +def end_italic() + _ctx.buff.write("\x1b[23m") +end + +def start_underline() + _ctx.buff.write("\x1b[4m") +end + +def end_underline() + _ctx.buff.write("\x1b[24m") +end + +def start_hidden() + _ctx.buff.write("\x1b[8m") +end + +def end_hidden() + _ctx.buff.write("\x1b[28m") +end + +def start_strikethrough() + _ctx.buff.write("\x1b[9m") +end + +def end_strikethrough() + _ctx.buff.write("\x1b[29m") +end + +def start_color_black() + _ctx.buff.write("\x1b[30m") +end + +def end_color_black() + _ctx.buff.write("\x1b[40m") +end + +def start_color_red() + _ctx.buff.write("\x1b[31m") +end + +def end_color_red() + _ctx.buff.write("\x1b[41m") +end + +def start_color_green() + _ctx.buff.write("\x1b[32m") +end + +def end_color_green() + _ctx.buff.write("\x1b[42m") +end + +def start_color_yellow() + _ctx.buff.write("\x1b[33m") +end + +def end_color_yellow() + _ctx.buff.write("\x1b[43m") +end + +def start_color_blue() + _ctx.buff.write("\x1b[34m") +end + +def end_color_blue() + _ctx.buff.write("\x1b[44m") +end + +def start_color_magenta() + _ctx.buff.write("\x1b[35m") +end + +def end_color_magenta() + _ctx.buff.write("\x1b[45m") +end + +def start_color_cyan() + _ctx.buff.write("\x1b[36m") +end + +def end_color_cyan() + _ctx.buff.write("\x1b[46m") +end + +def start_color_white() + _ctx.buff.write("\x1b[37m") +end + +def end_color_white() + _ctx.buff.write("\x1b[47m") +end + +def start_color_default() + _ctx.buff.write("\x1b[39m") +end + +def end_color_default() + _ctx.buff.write("\x1b[49m") +end + + +## ------------------------------------------------------------------------- +## TUI MAIN LOOP +## ------------------------------------------------------------------------- + +def run(config) + term.init(config.capture_events) + + if config.new_buffer then term.new_screen_buffer() end + if config.hide_cursor then hide_cursor() end + if config.init_fn then config.init_fn() end + + event = term.Event() + + ## The main loop. + while not _ctx.done + start = clock() + + ## Dispatch events. + while term.read_event(event) + if config.event_fn then config.event_fn(event) end + if _ctx.done then break end + end + + ## Run frame function + if config.frame_fn then config.frame_fn() end + + ## Sleep to sync FPS. + et = clock() - start ## Elapsed time. + ft = 1 / config.fps ## Frame time. + + ## TODO: set title to print fps if said in config. + if ft > et then sleep((ft - et) * 1000) end + end + + ## Cleanup. + if config.cleanup_fn then config.cleanup_fn() end + if config.hide_cursor then show_cursor() end + if config.new_buffer then term.restore_screen_buffer() end + term.cleanup() + _ctx.clear() + +end diff --git a/tests/lang/controlflow.pk b/tests/lang/controlflow.pk index 2115b96..4d10dc2 100644 --- a/tests/lang/controlflow.pk +++ b/tests/lang/controlflow.pk @@ -72,6 +72,7 @@ assert(val == 'bar') def test_in_local() + x = false if x = fn y = false; return not y end () assert(x == true) end diff --git a/tests/modules/io.File.pk b/tests/modules/io.File.pk index 8814509..6e04ee0 100644 --- a/tests/modules/io.File.pk +++ b/tests/modules/io.File.pk @@ -16,6 +16,7 @@ def read_file() 'line4 : qux\n', ] + line = '' while line = f.getline() assert(line in LINES) end