From 495ac9f0e17215b337d7d88186cf968bf80ab93c Mon Sep 17 00:00:00 2001 From: Marc Gurevitx Date: Fri, 19 Jul 2024 22:55:07 +0300 Subject: [PATCH 1/5] Add non-standard kbGet() and kbEcho() (unixes) --- MiniScript-cpp/demo/tetris.ms | 324 +++++++++++++++++++++++++ MiniScript-cpp/src/ShellIntrinsics.cpp | 84 +++++++ 2 files changed, 408 insertions(+) create mode 100644 MiniScript-cpp/demo/tetris.ms diff --git a/MiniScript-cpp/demo/tetris.ms b/MiniScript-cpp/demo/tetris.ms new file mode 100644 index 0000000..b19adba --- /dev/null +++ b/MiniScript-cpp/demo/tetris.ms @@ -0,0 +1,324 @@ +// This demo uses non-standard `kbGet()` and `kbEcho()` + +import "vt" + +delay = 0.5 +delaySub = 0.01 +fieldHeight = 20 +fieldWidth = 10 +fieldLeft = 20 +fieldColor = vt.color.black +kUp = char(27) + "[A" +kDown = char(27) + "[B" +kRight = char(27) + "[C" +kLeft = char(27) + "[D" +anchorRow = fieldHeight + 2 +gameOverFG = vt.color.white +gameOverBG = vt.color.teal + +cell = {} +cell.empty = null +cell.I = vt.color.aqua +cell.O = vt.color.yellow +cell.T = vt.color.purple +cell.L = vt.color.orange +cell.J = vt.color.navy +cell.S = vt.color.lime +cell.Z = vt.color.red +_cell = cell + +field = [] +shape = null + +initField = function + globals.field = [] + for r in range(0, fieldHeight - 1, 1) + field.push [] + for c in range(0, fieldWidth - 1, 1) + field[-1].push _cell.empty + end for + end for +end function + +drawField = function + for r in range(0, fieldHeight - 1, 1) + print vt.cursor.goto(r + 1, fieldLeft - 1) + vt.backColor(fieldColor) + "|", "" + for c in range(0, fieldWidth - 1, 1) + print vt.backColor(field[r][c]) + " ", "" + end for + print vt.backColor(fieldColor) + "|" + end for + print vt.cursor.goto(fieldHeight + 1, fieldLeft - 1) + vt.backColor(fieldColor) + "+" + "-" * fieldWidth * 2 + "+" + print vt.cursor.goto(anchorRow, 1) +end function + +drawGameOver = function + print vt.cursor.goto(10, fieldLeft) + vt.textColor(gameOverFG) + vt.backColor(gameOverBG) + " G A M E " + print vt.cursor.goto(11, fieldLeft) + vt.textColor(gameOverFG) + vt.backColor(gameOverBG) + " O V E R " + print vt.cursor.goto(anchorRow, 1) +end function + +initShape = function + f = [@initI, + @initO, + @initT, + @initL, + @initJ, + @initS, + @initZ][rnd * 7] + s = f + for cell in s.cells + if field[cell.row][cell.col] != _cell.empty then return null + end for + return s +end function + +initI = function + s = {} + s.cells = [] + s.cells.push {"row": 0, "col": 3, "color": _cell.I} + s.cells.push {"row": 0, "col": 4, "color": _cell.I} + s.cells.push {"row": 0, "col": 5, "color": _cell.I} + s.cells.push {"row": 0, "col": 6, "color": _cell.I} + s.centerRow = 0 + s.centerCol = 4 + return s +end function + +initO = function + s = {} + s.cells = [] + s.cells.push {"row": 0, "col": 4, "color": _cell.O} + s.cells.push {"row": 0, "col": 5, "color": _cell.O} + s.cells.push {"row": 1, "col": 5, "color": _cell.O} + s.cells.push {"row": 1, "col": 4, "color": _cell.O} + s.centerRow = 0.5 + s.centerCol = 4.5 + return s +end function + +initT = function + s = {} + s.cells = [] + s.cells.push {"row": 0, "col": 3, "color": _cell.T} + s.cells.push {"row": 0, "col": 4, "color": _cell.T} + s.cells.push {"row": 0, "col": 5, "color": _cell.T} + s.cells.push {"row": 1, "col": 4, "color": _cell.T} + s.centerRow = 0 + s.centerCol = 4 + return s +end function + +initL = function + s = {} + s.cells = [] + s.cells.push {"row": 1, "col": 4, "color": _cell.L} + s.cells.push {"row": 0, "col": 4, "color": _cell.L} + s.cells.push {"row": 0, "col": 5, "color": _cell.L} + s.cells.push {"row": 0, "col": 6, "color": _cell.L} + s.centerRow = 0.5 + s.centerCol = 4.5 + return s +end function + +initJ = function + s = {} + s.cells = [] + s.cells.push {"row": 0, "col": 3, "color": _cell.J} + s.cells.push {"row": 0, "col": 4, "color": _cell.J} + s.cells.push {"row": 0, "col": 5, "color": _cell.J} + s.cells.push {"row": 1, "col": 5, "color": _cell.J} + s.centerRow = 0.5 + s.centerCol = 4.5 + return s +end function + +initS = function + s = {} + s.cells = [] + s.cells.push {"row": 1, "col": 3, "color": _cell.S} + s.cells.push {"row": 1, "col": 4, "color": _cell.S} + s.cells.push {"row": 0, "col": 4, "color": _cell.S} + s.cells.push {"row": 0, "col": 5, "color": _cell.S} + s.centerRow = 0.5 + s.centerCol = 4.5 + return s +end function + +initZ = function + s = {} + s.cells = [] + s.cells.push {"row": 0, "col": 4, "color": _cell.Z} + s.cells.push {"row": 0, "col": 5, "color": _cell.Z} + s.cells.push {"row": 1, "col": 5, "color": _cell.Z} + s.cells.push {"row": 1, "col": 6, "color": _cell.Z} + s.centerRow = 0.5 + s.centerCol = 4.5 + return s +end function + +drawShape = function(s) + for cell in s.cells + print vt.cursor.goto(cell.row + 1, fieldLeft + cell.col * 2) + vt.backColor(cell.color) + " " + end for + print vt.cursor.goto(anchorRow, 1) +end function + +eraseShape = function(s) + for cell in s.cells + print vt.cursor.goto(cell.row + 1, fieldLeft + cell.col * 2) + vt.backColor(fieldColor) + " " + end for + print vt.cursor.goto(anchorRow, 1) +end function + +absorbShape = function(s) + for cell in s.cells + field[cell.row][cell.col] = cell.color + end for +end function + +cloneShape = function(s) + s2 = {} + s2.cells = [] + for cell in s.cells + s2.cells.push cell + {} + end for + s2.centerRow = s.centerRow + s2.centerCol = s.centerCol + return s2 +end function + +moveDown = function(s) + s2 = cloneShape(s) + for cell in s2.cells + cell.row += 1 + if cell.row >= fieldHeight then return null + if field[cell.row][cell.col] != _cell.empty then return null + end for + s2.centerRow += 1 + return s2 +end function + +moveLeft = function(s) + s2 = cloneShape(s) + for cell in s2.cells + cell.col -= 1 + if cell.col < 0 then return null + if field[cell.row][cell.col] != _cell.empty then return null + end for + s2.centerCol -= 1 + return s2 +end function + +moveRight = function(s) + s2 = cloneShape(s) + for cell in s2.cells + cell.col += 1 + if cell.col >= fieldWidth then return null + if field[cell.row][cell.col] != _cell.empty then return null + end for + s2.centerCol += 1 + return s2 +end function + +fall = function(s) + s2 = cloneShape(s) + while true + moved = moveDown(s2) + if moved == null then return s2 + s2 = moved + end while +end function + +rotate = function(s) + s2 = cloneShape(s) + crossedLeftP = false + crossedRightP = false + for cell in s2.cells + r = cell.row + c = cell.col + cell.row = s2.centerRow + (c - s2.centerCol) + cell.col = s2.centerCol - (r - s2.centerRow) + if cell.col < 0 then crossedLeftP = true + if cell.col >= fieldWidth then crossedRightP = true + if cell.row >= fieldHeight then return null + if field[cell.row][cell.col] != _cell.empty then return null + end for + if crossedLeftP then s2 = moveRight(s2) + if crossedRightP then s2 = moveLeft(s2) + return s2 +end function + +deleteRows = function + redrawP = false + for i in range(0, fieldHeight - 1, 1) + nNulls = 0 + for cell in field[i] + if cell == _cell.empty then nNulls += 1 + end for + if nNulls == 0 then + emptyRow = [] + for j in range(0, fieldWidth - 1, 1) + emptyRow.push _cell.empty + end for + outer.field = [emptyRow] + field[:i] + field[i + 1:] + outer.delay -= delaySub + redrawP = true + end if + end for + if redrawP then drawField +end function + + +initField +drawField +kbEcho false +t = time +while true + yield + + if shape == null then + shape = initShape + if shape == null then + drawGameOver + kbEcho true + exit + end if + drawShape shape + end if + + s = shape + moved = null + k = kbGet + k3 = k[:3] + if k3 == kUp then + moved = rotate(s) + else if k3 == kDown then + moved = fall(s) + else if k3 == kLeft then + moved = moveLeft(s) + else if k3 == kRight then + moved = moveRight(s) + end if + + if moved != null then s = moved + + if time > t then + t = time + delay + moved = moveDown(s) + if moved == null then + absorbShape s + deleteRows + shape = null + s = null + else + s = moved + end if + end if + + if s != shape then + eraseShape shape + drawShape s + shape = s + end if +end while diff --git a/MiniScript-cpp/src/ShellIntrinsics.cpp b/MiniScript-cpp/src/ShellIntrinsics.cpp index 7e2c133..752d7af 100644 --- a/MiniScript-cpp/src/ShellIntrinsics.cpp +++ b/MiniScript-cpp/src/ShellIntrinsics.cpp @@ -6,6 +6,8 @@ // Copyright © 2021 Joe Strout. All rights reserved. // + + #include "ShellIntrinsics.h" #include #include @@ -52,6 +54,7 @@ #include #include // for readdir #include // for basename and dirname + #include // for tcgetattr and tcsetattr #include // for stat #include // for realpath #if defined(__APPLE__) || defined(__FreeBSD__) @@ -62,6 +65,7 @@ #define PATHSEP '/' #include #include + #include #endif extern "C" { @@ -1288,6 +1292,79 @@ static IntrinsicResult intrinsic_exec(Context *context, IntrinsicResult partialR } #endif +// kbGet: Like MiniMicro's `key.get` but in a console. +// Never blocks, returns `null` if no keys were pressed. +static IntrinsicResult intrinsic_kbGet(Context *context, IntrinsicResult partialResult) { +#if WINDOWS + + // ... + return IntrinsicResult::Null; + +#else + + // Our steps: + // * For a short time make terminal "raw" to process keys without waiting for [Return] + // * Check if there's input and read the data (1 or more bytes depending on the type of the key) + // * Immediately restore terminal back to normal mode + // * Return the collected bytes (if any) as a string, or null if no key was pressed + + struct termios ttystate; + if (tcgetattr(STDIN_FILENO, &ttystate) < 0) return IntrinsicResult::Null; + ttystate.c_lflag &= ~ICANON; + ttystate.c_cc[VMIN] = 1; + if (tcsetattr(STDIN_FILENO, TCSANOW, &ttystate) < 0) return IntrinsicResult::Null; + ValueList bytes; + while (true) { + fd_set fds; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + if (select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv) < 0) break; + if (!FD_ISSET(STDIN_FILENO, &fds)) break; + char c; + if (read(STDIN_FILENO, &c, 1) < 1) break; + Value v(c); + bytes.Add(v); + } + if (tcgetattr(STDIN_FILENO, &ttystate) < 0) return IntrinsicResult::Null; + ttystate.c_lflag |= ICANON; + if (tcsetattr(STDIN_FILENO, TCSANOW, &ttystate) < 0) return IntrinsicResult::Null; + int nBytes = bytes.Count(); + if (nBytes == 0) return IntrinsicResult::Null; + char *buf = (char *)malloc(nBytes); + if (buf == nullptr) return IntrinsicResult::Null; + for (int i=0; iGetVar("on").BoolValue(); +#if WINDOWS + + // ... + return IntrinsicResult::Null; + +#else + struct termios ttystate; + if (tcgetattr(STDIN_FILENO, &ttystate) < 0) return IntrinsicResult::Null; + if (on) { + ttystate.c_lflag |= ECHO; + } else { + ttystate.c_lflag &= ~ECHO; + } + tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); + return IntrinsicResult::Null; +#endif +} + static bool disallowAssignment(ValueDict& dict, Value key, Value value) { return true; } @@ -1649,6 +1726,13 @@ void AddShellIntrinsics() { f = Intrinsic::Create("RawData"); f->code = &intrinsic_RawData; + f = Intrinsic::Create("kbGet"); + f->code = &intrinsic_kbGet; + + f = Intrinsic::Create("kbEcho"); + f->AddParam("on"); + f->code = &intrinsic_kbEcho; + // RawData methods From b6651bd2c7f440366773ee7b2992ecb74e3b6517 Mon Sep 17 00:00:00 2001 From: Marc Gurevitx Date: Thu, 25 Jul 2024 00:37:49 +0300 Subject: [PATCH 2/5] Porting (Linux) --- CMakeLists.txt | 2 + MiniScript-cpp/README-key.md | 63 +++++++ MiniScript-cpp/demo/tetris.ms | 45 ++--- MiniScript-cpp/src/Key.cpp | 248 +++++++++++++++++++++++++ MiniScript-cpp/src/Key.h | 58 ++++++ MiniScript-cpp/src/ShellIntrinsics.cpp | 236 +++++++++++++++-------- 6 files changed, 549 insertions(+), 103 deletions(-) create mode 100644 MiniScript-cpp/README-key.md create mode 100644 MiniScript-cpp/src/Key.cpp create mode 100644 MiniScript-cpp/src/Key.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d00a90..da635a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ set(MINISCRIPT_HEADERS set(MINICMD_HEADERS MiniScript-cpp/src/DateTimeUtils.h + MiniScript-cpp/src/Key.h MiniScript-cpp/src/OstreamSupport.h MiniScript-cpp/src/ShellExec.h MiniScript-cpp/src/ShellIntrinsics.h @@ -92,6 +93,7 @@ endif() add_executable(minicmd MiniScript-cpp/src/main.cpp MiniScript-cpp/src/DateTimeUtils.cpp + MiniScript-cpp/src/Key.cpp MiniScript-cpp/src/OstreamSupport.cpp MiniScript-cpp/src/ShellIntrinsics.cpp MiniScript-cpp/src/ShellExec.cpp diff --git a/MiniScript-cpp/README-key.md b/MiniScript-cpp/README-key.md new file mode 100644 index 0000000..fd8f7e4 --- /dev/null +++ b/MiniScript-cpp/README-key.md @@ -0,0 +1,63 @@ +# Key module + +`key` module is a port of a [module](https://miniscript.org/wiki/Key) by the same name from Mini Micro that adds keyboard functions for immediate input in the console. + +| API | Description | +|---|---| +| `key.available` | compatible | +| `key.get` | compatible | +| `key.put(keyChar)` | compatible | +| `key.clear` | compatible | +| `key.pressed(keyName="space")` | not ported | +| `key.keyNames` | not ported | +| `key.axis(axis="Horizontal")` | not ported | +| `key._putInFront(keyChar)` | (non-standard) same as `key.put` but instead of adding its arg at the end of the input buffer, inserts it at the beginning | +| `key._echo` | (non-standard, only unixes) property that controls whether typed characters are echoed in the terminal | +| `key._scanMap` | (non-standard) property that controls how scan codes and escape sequences are mapped to the values that `key.get` is expected to return | + +There's a small demo that demonstrates the use of `key.available` and `key.get`: `demo/tetris.ms`. + + +## Implementation of the input buffer + +The functions of this module maintain their shared internal buffer where key presses are stored. + +It's implemented as a `SimpleVector` of entries where each entry is structure of two fields: + +| `c` | character code point of a regular (symbol) key | +| `scanCode` | a code of a special key | + +Only one of these fields is non-zero at any times. + +This data type allows registering key presses on unixes where only code points are used, and Windows where key presses generate either a code point or a sequence of two integers: `0` and a scan code. + + +## Scan map + +Terminals vary in how they report special keys' presses. + +For example this is what \[Arrow up\] becomes in the Linux terminal: `char(27) + "[A"` (3 ASCII characters). The same key press on Windows produces `0` followed by scan code `72`. Finally, on Mini Micro it's `char(19)`. + +*Scan maps* is an internal mechanism of the `key` module that converts all various values into the same values that are returned by Mini Micro's `key.get` and hence should ensure portability of scripts. + +Scan maps are stored in a `key._scanMap` property and are MiniScript maps where the keys are either `number` type (in case you're mapping a scan code) or `string` type (in case of a sequence of characters), and the values are what you want to be returned by `key.get`. + +So, to overcome the above \[Arrow up\] problem one could define `key._scanMap` as + +```c +key._scanMap = { + char(27) + "[A": char(19), + 72: char(19), +} +``` + +There is already a predefined scan map in the `key` module that covers certain special keys (including arrow keys) for each platform. + + +## Scan map optimization + +In games, handling the user input is tipically a part of a game loop. + +So, to make `key.get` a bit faster and avoid converting strings into code points on each frame, the scan map gets populated with optimized keys. + +This optimization happens on assignment to the `_scanMap` property via the `*AssignOverride` trick. diff --git a/MiniScript-cpp/demo/tetris.ms b/MiniScript-cpp/demo/tetris.ms index b19adba..21977cd 100644 --- a/MiniScript-cpp/demo/tetris.ms +++ b/MiniScript-cpp/demo/tetris.ms @@ -1,4 +1,5 @@ -// This demo uses non-standard `kbGet()` and `kbEcho()` +// TETRIS +// This demo uses `key.available()` and `key.get()` import "vt" @@ -8,10 +9,10 @@ fieldHeight = 20 fieldWidth = 10 fieldLeft = 20 fieldColor = vt.color.black -kUp = char(27) + "[A" -kDown = char(27) + "[B" -kRight = char(27) + "[C" -kLeft = char(27) + "[D" +kUp = char(19) +kDown = char(20) +kRight = char(18) +kLeft = char(17) anchorRow = fieldHeight + 2 gameOverFG = vt.color.white gameOverBG = vt.color.teal @@ -20,9 +21,9 @@ cell = {} cell.empty = null cell.I = vt.color.aqua cell.O = vt.color.yellow -cell.T = vt.color.purple +cell.T = vt.color.fuchsia cell.L = vt.color.orange -cell.J = vt.color.navy +cell.J = vt.color.blue cell.S = vt.color.lime cell.Z = vt.color.red _cell = cell @@ -272,7 +273,7 @@ end function initField drawField -kbEcho false +key._echo = false t = time while true yield @@ -281,7 +282,7 @@ while true shape = initShape if shape == null then drawGameOver - kbEcho true + key._echo = true exit end if drawShape shape @@ -289,19 +290,19 @@ while true s = shape moved = null - k = kbGet - k3 = k[:3] - if k3 == kUp then - moved = rotate(s) - else if k3 == kDown then - moved = fall(s) - else if k3 == kLeft then - moved = moveLeft(s) - else if k3 == kRight then - moved = moveRight(s) - end if - - if moved != null then s = moved + while key.available + k = key.get + if k == kUp then + moved = rotate(s) + else if k == kDown then + moved = fall(s) + else if k == kLeft then + moved = moveLeft(s) + else if k == kRight then + moved = moveRight(s) + end if + if moved != null then s = moved + end while if time > t then t = time + delay diff --git a/MiniScript-cpp/src/Key.cpp b/MiniScript-cpp/src/Key.cpp new file mode 100644 index 0000000..3799a8b --- /dev/null +++ b/MiniScript-cpp/src/Key.cpp @@ -0,0 +1,248 @@ +#include "Key.h" +#include "UnicodeUtil.h" + +#if _WIN32 || _WIN64 + #define WINDOWS 1 + + // ... + +#else + #include + #include + #include +#endif + +namespace MiniScript { + + +SimpleVector inputBuffer; + +ValueDict& KeyDefaultScanMap() { + static ValueDict scanMap; + if (scanMap.Count() == 0) { + #if WINDOWS + + // ... + + #else + scanMap.SetValue("\x7F", "\x08"); // backspace + scanMap.SetValue("\x1B[3~", "\x7F"); // delete + scanMap.SetValue("\x1B[A", "\x13"); // up + scanMap.SetValue("\x1B[B", "\x14"); // down + scanMap.SetValue("\x1B[C", "\x12"); // right + scanMap.SetValue("\x1B[D", "\x11"); // left + scanMap.SetValue("\x1B[H", "\x01"); // home + scanMap.SetValue("\x1B[F", "\x05"); // end + #endif + } + return scanMap; +} + +void KeyOptimizeScanMap(ValueDict& scanMap) { + for (ValueDictIterator kv = scanMap.GetIterator(); !kv.Done(); kv.Next()) { + Value k = kv.Key(); + Value v = kv.Value(); + if (k.type == ValueType::Number) { + ValueList optimizedKey; + optimizedKey.Add(Value::zero); + optimizedKey.Add(k); + scanMap.SetValue(optimizedKey, v); + } else if (k.type == ValueType::String) { + String kStr(k.ToString()); + if (kStr.Length() == 0) continue; + String first(kStr.Substring(0, 1)); + String rest(kStr.Substring(1)); + ValueList optimizedKey; + optimizedKey.Add(UTF8Decode((unsigned char *)first.c_str())); + optimizedKey.Add(Value::zero); + if (rest.Length() == 0) { + scanMap.SetValue(optimizedKey, v); + } else { + Value subV = scanMap.Lookup(optimizedKey, Value::null); + if (subV.IsNull() || subV.type != ValueType::Map) { + ValueDict d; + scanMap.SetValue(optimizedKey, d); + } + ValueDict sub = scanMap.Lookup(optimizedKey, Value::null).GetDict(); + sub.SetValue(rest, v); + KeyOptimizeScanMap(sub); + } + } + } +} + +// Helper function to read all available characters from STDIN into the global input buffer. +void slurpStdin() { + struct InputBufferEntry e = {0, 0}; + + #if WINDOWS + + // ... + + #else + + // Make terminal reads non-blocking + struct termios ttystate, backUp; + if (tcgetattr(STDIN_FILENO, &ttystate) < 0) return; + memcpy(&backUp, &ttystate, sizeof(ttystate)); + ttystate.c_lflag &= ~ICANON; + ttystate.c_cc[VMIN] = 0; + ttystate.c_cc[VTIME] = 0; + if (tcsetattr(STDIN_FILENO, TCSANOW, &ttystate) < 0) return; + + unsigned char buf[5] = {0, 0, 0, 0, 0}; + unsigned char *p = buf; + while (true) { + + // Check if there are some key presses + fd_set fds; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + if (select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv) < 0) break; + if (!FD_ISSET(STDIN_FILENO, &fds)) { + + // Nothing's left to slurp ; save to the input buffer a current unsaved character + if (p > buf) { + e.c = UTF8Decode(buf); + inputBuffer.push_back(e); + } + break; + } + + // Read one byte from STDIN + if (read(STDIN_FILENO, p, 1) < 1) break; + if (p > buf && !IsUTF8IntraChar(*p)) { + + // A new character has begun under `p`, save the previous one to the input buffer and continue slurping + e.c = UTF8Decode(buf); + inputBuffer.push_back(e); + buf[0] = *p; + buf[1] = 0; + buf[2] = 0; + buf[3] = 0; + buf[4] = 0; + p = buf; + } + p++; + } + + // Restore terminal + tcsetattr(STDIN_FILENO, TCSANOW, &backUp); + + #endif +} + +Value KeyAvailable() { + slurpStdin(); + return Value::Truth(inputBuffer.size() > 0); +} + +Value KeyGet(ValueDict& scanMap) { + slurpStdin(); + if (inputBuffer.size() == 0) return Value::null; + struct InputBufferEntry e = inputBuffer[0]; + struct InputBufferEntry initialE = e; + inputBuffer.deleteIdx(0); + int nScanned = 0; + while (true) { + ValueList optimizedKey; + optimizedKey.Add(e.c); + optimizedKey.Add(e.scanCode); + Value foundVal = scanMap.Lookup(optimizedKey, Value::null); + if (foundVal.IsNull()) break; + if (foundVal.type == ValueType::String) { + for (int i=0; i=0; i--) { + struct InputBufferEntry e = {0, 0}; + String character = s.Substring(i, 1); + e.c = UTF8Decode((unsigned char *)character.c_str()); + inputBuffer.insert(e, 0); + } + } else { + for (int i=0; i +#include +#include "MiniscriptTypes.h" +#include "SimpleVector.h" + +namespace MiniScript { + + +// InputBufferEntry: Elements of the input buffer. +struct InputBufferEntry { + wchar_t c; // inputted character (if a regular key, otherwise 0) + wchar_t scanCode; // scan code (if a special key, otherwise 0) +}; + +// inputBuffer: Global input buffer to store key presses. +extern SimpleVector inputBuffer; + +// KeyDefaultScanMap: Returns a platform specific dictionary that maps keys' scan codes to the return values of `key.get()`. +ValueDict& KeyDefaultScanMap(); + +// KeyOptimizeScanMap: Adds optimized keys to the scan map for faster `key.get()`s. +void KeyOptimizeScanMap(ValueDict& scanMap); + +// KeyAvailable: Checks whether there is a keypress in the input buffer. +Value KeyAvailable(); + +// KeyGet: Pulls the next key out of the input buffer. +// (This function immediately returns Value::null if the buffer is empty, but the caller intrinsic will still wait if necessary +// to comply with the specification of `key.get()`). +Value KeyGet(ValueDict& scanMap); + +// KeyPutCodepoint: Enqueues a single character by code point into the keyboard buffer. +void KeyPutCodepoint(long codepoint, bool inFront=false); + +// KeyPutString: Enqueues a string into the keyboard buffer. +void KeyPutString(String s, bool inFront=false); + +// KeyClear: Clears the input buffer. +void KeyClear(); + +// KeyGetEcho: Returns whether terminal echo is on. +bool KeyGetEcho(); + +// KeySetEcho: Sets terminal echo on or off. +void KeySetEcho(bool on); + + +} + +#endif /* KEY_MODULE_H */ diff --git a/MiniScript-cpp/src/ShellIntrinsics.cpp b/MiniScript-cpp/src/ShellIntrinsics.cpp index 752d7af..1815e49 100644 --- a/MiniScript-cpp/src/ShellIntrinsics.cpp +++ b/MiniScript-cpp/src/ShellIntrinsics.cpp @@ -24,6 +24,7 @@ #include "whereami/whereami.h" #include "DateTimeUtils.h" #include "ShellExec.h" +#include "Key.h" #include #include @@ -54,7 +55,6 @@ #include #include // for readdir #include // for basename and dirname - #include // for tcgetattr and tcsetattr #include // for stat #include // for realpath #if defined(__APPLE__) || defined(__FreeBSD__) @@ -65,7 +65,6 @@ #define PATHSEP '/' #include #include - #include #endif extern "C" { @@ -150,6 +149,7 @@ Intrinsic *i_fread = nullptr; Intrinsic *i_freadLine = nullptr; Intrinsic *i_fposition = nullptr; Intrinsic *i_feof = nullptr; + Intrinsic *i_rawDataLen = nullptr; Intrinsic *i_rawDataResize = nullptr; Intrinsic *i_rawDataByte = nullptr; @@ -171,6 +171,16 @@ Intrinsic *i_rawDataSetDouble = nullptr; Intrinsic *i_rawDataUtf8 = nullptr; Intrinsic *i_rawDataSetUtf8 = nullptr; +Intrinsic *i_keyAvailable = nullptr; +Intrinsic *i_keyGet = nullptr; +Intrinsic *i_keyPut = nullptr; +Intrinsic *i_keyClear = nullptr; +Intrinsic *i_keyPressed = nullptr; +Intrinsic *i_keyKeyNames = nullptr; +Intrinsic *i_keyAxis = nullptr; +Intrinsic *i_keyPutInFront = nullptr; +Intrinsic *i_keyEcho = nullptr; + // Copy a file. Return 0 on success, or some value < 0 on error. static int UnixishCopyFile(const char* source, const char* destination) { #if WINDOWS @@ -251,6 +261,7 @@ static String ExpandVariables(String path) { static ValueDict& FileHandleClass(); static ValueDict& RawDataType(); +static ValueDict& KeyModule(); static IntrinsicResult intrinsic_input(Context *context, IntrinsicResult partialResult) { Value prompt = context->GetVar("prompt"); @@ -1130,6 +1141,69 @@ static IntrinsicResult intrinsic_rawDataSetUtf8(Context *context, IntrinsicResul return IntrinsicResult(nBytes); } + +static IntrinsicResult intrinsic_keyAvailable(Context *context, IntrinsicResult partialResult) { + return IntrinsicResult(KeyAvailable()); +} + +static IntrinsicResult intrinsic_keyGet(Context *context, IntrinsicResult partialResult) { + if (!KeyAvailable().BoolValue()) return IntrinsicResult(Value::null, false); + ValueDict keyModule = KeyModule(); + Value scanMapV = keyModule.Lookup("_scanMap", Value::null); + if (scanMapV.type != ValueType::Map) keyModule.ApplyAssignOverride("_scanMap", KeyDefaultScanMap()); + ValueDict scanMap = keyModule.Lookup("_scanMap", Value::null).GetDict(); + return IntrinsicResult(KeyGet(scanMap)); +} + +static IntrinsicResult intrinsic_keyPut(Context *context, IntrinsicResult partialResult) { + Value keyChar = context->GetVar("keyChar"); + if (keyChar.type == ValueType::Number) { + KeyPutCodepoint(keyChar.UIntValue()); + } else if (keyChar.type == ValueType::String) { + KeyPutString(keyChar.ToString()); + } else { + TypeException("string or number required for keyChar").raise(); + } + return IntrinsicResult::Null; +} + +static IntrinsicResult intrinsic_keyPutInFront(Context *context, IntrinsicResult partialResult) { + Value keyChar = context->GetVar("keyChar"); + if (keyChar.type == ValueType::Number) { + KeyPutCodepoint(keyChar.UIntValue(), true); + } else if (keyChar.type == ValueType::String) { + KeyPutString(keyChar.ToString(), true); + } else { + TypeException("string or number required for keyChar").raise(); + } + return IntrinsicResult::Null; +} + +static IntrinsicResult intrinsic_keyClear(Context *context, IntrinsicResult partialResult) { + KeyClear(); + return IntrinsicResult::Null; +} + +static IntrinsicResult intrinsic_keyPressed(Context *context, IntrinsicResult partialResult) { + RuntimeException("`key.pressed` is not implemented").raise(); + return IntrinsicResult::Null; +} + +static IntrinsicResult intrinsic_keyKeyNames(Context *context, IntrinsicResult partialResult) { + RuntimeException("`key.keyNames` is not implemented").raise(); + return IntrinsicResult::Null; +} + +static IntrinsicResult intrinsic_keyAxis(Context *context, IntrinsicResult partialResult) { + RuntimeException("`key.axis` is not implemented").raise(); + return IntrinsicResult::Null; +} + +static IntrinsicResult intrinsic_keyEcho(Context *context, IntrinsicResult partialResult) { + return IntrinsicResult(KeyGetEcho()); +} + + #if WINDOWS // timeout : The time to wait in milliseconds before killing the child process. bool CreateChildProcess(const String& cmd, String& out, String& err, DWORD& returnCode, DWORD timeout) { @@ -1292,79 +1366,6 @@ static IntrinsicResult intrinsic_exec(Context *context, IntrinsicResult partialR } #endif -// kbGet: Like MiniMicro's `key.get` but in a console. -// Never blocks, returns `null` if no keys were pressed. -static IntrinsicResult intrinsic_kbGet(Context *context, IntrinsicResult partialResult) { -#if WINDOWS - - // ... - return IntrinsicResult::Null; - -#else - - // Our steps: - // * For a short time make terminal "raw" to process keys without waiting for [Return] - // * Check if there's input and read the data (1 or more bytes depending on the type of the key) - // * Immediately restore terminal back to normal mode - // * Return the collected bytes (if any) as a string, or null if no key was pressed - - struct termios ttystate; - if (tcgetattr(STDIN_FILENO, &ttystate) < 0) return IntrinsicResult::Null; - ttystate.c_lflag &= ~ICANON; - ttystate.c_cc[VMIN] = 1; - if (tcsetattr(STDIN_FILENO, TCSANOW, &ttystate) < 0) return IntrinsicResult::Null; - ValueList bytes; - while (true) { - fd_set fds; - FD_ZERO(&fds); - FD_SET(STDIN_FILENO, &fds); - struct timeval tv; - tv.tv_sec = 0; - tv.tv_usec = 0; - if (select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv) < 0) break; - if (!FD_ISSET(STDIN_FILENO, &fds)) break; - char c; - if (read(STDIN_FILENO, &c, 1) < 1) break; - Value v(c); - bytes.Add(v); - } - if (tcgetattr(STDIN_FILENO, &ttystate) < 0) return IntrinsicResult::Null; - ttystate.c_lflag |= ICANON; - if (tcsetattr(STDIN_FILENO, TCSANOW, &ttystate) < 0) return IntrinsicResult::Null; - int nBytes = bytes.Count(); - if (nBytes == 0) return IntrinsicResult::Null; - char *buf = (char *)malloc(nBytes); - if (buf == nullptr) return IntrinsicResult::Null; - for (int i=0; iGetVar("on").BoolValue(); -#if WINDOWS - - // ... - return IntrinsicResult::Null; - -#else - struct termios ttystate; - if (tcgetattr(STDIN_FILENO, &ttystate) < 0) return IntrinsicResult::Null; - if (on) { - ttystate.c_lflag |= ECHO; - } else { - ttystate.c_lflag &= ~ECHO; - } - tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); - return IntrinsicResult::Null; -#endif -} - static bool disallowAssignment(ValueDict& dict, Value key, Value value) { return true; } @@ -1413,6 +1414,47 @@ static ValueDict& FileHandleClass() { return result; } + +static bool assignKey(ValueDict& dict, Value key, Value value) { + if (key.ToString() == "_scanMap") { + if (value.type != ValueType::Map) return true; // silently fail because of wrong type. + ValueDict scanMap = value.GetDict(); + KeyOptimizeScanMap(scanMap); + dict.SetValue("_scanMap", value); + return true; + } + if (key.ToString() == "_echo") { + KeySetEcho(value.BoolValue()); + return true; + } + return false; // allow standard assignment to also apply. +} + +static ValueDict& KeyModule() { + static ValueDict keyModule; + + if (keyModule.Count() == 0) { + keyModule.SetValue("available", i_keyAvailable->GetFunc()); + keyModule.SetValue("get", i_keyGet->GetFunc()); + keyModule.SetValue("put", i_keyPut->GetFunc()); + keyModule.SetValue("clear", i_keyClear->GetFunc()); + keyModule.SetValue("pressed", i_keyPressed->GetFunc()); + keyModule.SetValue("keyNames", i_keyKeyNames->GetFunc()); + keyModule.SetValue("axis", i_keyAxis->GetFunc()); + keyModule.SetValue("_putInFront", i_keyPutInFront->GetFunc()); + keyModule.SetValue("_echo", i_keyEcho->GetFunc()); + keyModule.SetAssignOverride(assignKey); + keyModule.ApplyAssignOverride("_scanMap", KeyDefaultScanMap()); + } + + return keyModule; +} + +static IntrinsicResult intrinsic_Key(Context *context, IntrinsicResult partialResult) { + return IntrinsicResult(KeyModule()); +} + + static ValueDict& RawDataType() { static ValueDict result; if (result.Count() == 0) { @@ -1726,12 +1768,8 @@ void AddShellIntrinsics() { f = Intrinsic::Create("RawData"); f->code = &intrinsic_RawData; - f = Intrinsic::Create("kbGet"); - f->code = &intrinsic_kbGet; - - f = Intrinsic::Create("kbEcho"); - f->AddParam("on"); - f->code = &intrinsic_kbEcho; + f = Intrinsic::Create("key"); + f->code = &intrinsic_Key; // RawData methods @@ -1845,4 +1883,40 @@ void AddShellIntrinsics() { // END RawData methods + + // key.* methods + + i_keyAvailable = Intrinsic::Create(""); + i_keyAvailable->code = &intrinsic_keyAvailable; + + i_keyGet = Intrinsic::Create(""); + i_keyGet->code = &intrinsic_keyGet; + + i_keyPut = Intrinsic::Create(""); + i_keyPut->AddParam("keyChar"); + i_keyPut->code = &intrinsic_keyPut; + + i_keyClear = Intrinsic::Create(""); + i_keyClear->code = &intrinsic_keyClear; + + i_keyPressed = Intrinsic::Create(""); + i_keyPressed->AddParam("keyName", "space"); + i_keyPressed->code = &intrinsic_keyPressed; + + i_keyKeyNames = Intrinsic::Create(""); + i_keyKeyNames->code = &intrinsic_keyKeyNames; + + i_keyAxis = Intrinsic::Create(""); + i_keyAxis->AddParam("axis", "Horizontal"); + i_keyAxis->code = &intrinsic_keyAxis; + + i_keyPutInFront = Intrinsic::Create(""); + i_keyPutInFront->AddParam("keyChar"); + i_keyPutInFront->code = &intrinsic_keyPutInFront; + + i_keyEcho = Intrinsic::Create(""); + i_keyEcho->code = &intrinsic_keyEcho; + + // END key.* methods + } From a176d31d5b2619e0f6369b7d4a65f8f404e60d78 Mon Sep 17 00:00:00 2001 From: Marc Gurevitx Date: Thu, 25 Jul 2024 14:54:37 +0300 Subject: [PATCH 3/5] key module (Windows) --- MiniScript-cpp/README-key.md | 30 +++++++++++++++----------- MiniScript-cpp/src/Key.cpp | 42 ++++++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/MiniScript-cpp/README-key.md b/MiniScript-cpp/README-key.md index fd8f7e4..70c0c3a 100644 --- a/MiniScript-cpp/README-key.md +++ b/MiniScript-cpp/README-key.md @@ -8,28 +8,32 @@ | `key.get` | compatible | | `key.put(keyChar)` | compatible | | `key.clear` | compatible | -| `key.pressed(keyName="space")` | not ported | -| `key.keyNames` | not ported | -| `key.axis(axis="Horizontal")` | not ported | -| `key._putInFront(keyChar)` | (non-standard) same as `key.put` but instead of adding its arg at the end of the input buffer, inserts it at the beginning | -| `key._echo` | (non-standard, only unixes) property that controls whether typed characters are echoed in the terminal | -| `key._scanMap` | (non-standard) property that controls how scan codes and escape sequences are mapped to the values that `key.get` is expected to return | +| `key.pressed(keyName="space")` | (not ported) | +| `key.keyNames` | (not ported) | +| `key.axis(axis="Horizontal")` | (not ported) | +| `key._putInFront(keyChar)` | (non-standard) same as `key.put` but inserts its arg at the beginning of the input buffer | +| `key._echo` | (non-standard, only unixes) boolean property that controls whether typed characters are echoed in the terminal | +| `key._scanMap` | (non-standard) property that controls how scan codes and escape sequences are mapped to the values that `key.get` is expected to return (`map` type) | -There's a small demo that demonstrates the use of `key.available` and `key.get`: `demo/tetris.ms`. +There's a small script that demonstrates the use of `key.available`, `key.get` and `key._echo`: `demo/tetris.ms`. ## Implementation of the input buffer -The functions of this module maintain their shared internal buffer where key presses are stored. +Functions of this module maintain a shared internal buffer where key presses are stored. It's implemented as a `SimpleVector` of entries where each entry is structure of two fields: +| Field | Description | +|---|---| | `c` | character code point of a regular (symbol) key | | `scanCode` | a code of a special key | Only one of these fields is non-zero at any times. -This data type allows registering key presses on unixes where only code points are used, and Windows where key presses generate either a code point or a sequence of two integers: `0` and a scan code. +This data type was chosen to register key presses on various systems: +- On unixes only code points are used +- On Windows key presses may generate either a code point or a sequence of two integers: `0` and a scan code. ## Scan map @@ -38,9 +42,9 @@ Terminals vary in how they report special keys' presses. For example this is what \[Arrow up\] becomes in the Linux terminal: `char(27) + "[A"` (3 ASCII characters). The same key press on Windows produces `0` followed by scan code `72`. Finally, on Mini Micro it's `char(19)`. -*Scan maps* is an internal mechanism of the `key` module that converts all various values into the same values that are returned by Mini Micro's `key.get` and hence should ensure portability of scripts. +*Scan maps* is a mechanism of the `key` module that converts all various values into the same values that are returned by Mini Micro's `key.get` and hence facilitates portability of scripts. -Scan maps are stored in a `key._scanMap` property and are MiniScript maps where the keys are either `number` type (in case you're mapping a scan code) or `string` type (in case of a sequence of characters), and the values are what you want to be returned by `key.get`. +Scan maps are MiniScript `map`s (stored as a `key._scanMap` property) where the keys are either `number` type (in case you're mapping a scan code) or `string` type (in case of a sequence of characters), and the values are what you want to be returned by `key.get`. So, to overcome the above \[Arrow up\] problem one could define `key._scanMap` as @@ -56,8 +60,8 @@ There is already a predefined scan map in the `key` module that covers certain s ## Scan map optimization -In games, handling the user input is tipically a part of a game loop. +In games, handling the user input is tipically a part of a game loop and thus needs to be fast. -So, to make `key.get` a bit faster and avoid converting strings into code points on each frame, the scan map gets populated with optimized keys. +To avoid converting strings into code points on each frame inside `key.get`, the scan map gets populated with optimized keys. This optimization happens on assignment to the `_scanMap` property via the `*AssignOverride` trick. diff --git a/MiniScript-cpp/src/Key.cpp b/MiniScript-cpp/src/Key.cpp index 3799a8b..1f73463 100644 --- a/MiniScript-cpp/src/Key.cpp +++ b/MiniScript-cpp/src/Key.cpp @@ -3,9 +3,7 @@ #if _WIN32 || _WIN64 #define WINDOWS 1 - - // ... - + #include #else #include #include @@ -21,9 +19,13 @@ ValueDict& KeyDefaultScanMap() { static ValueDict scanMap; if (scanMap.Count() == 0) { #if WINDOWS - - // ... - + scanMap.SetValue(83, "\x7F"); // delete + scanMap.SetValue(72, "\x13"); // up + scanMap.SetValue(80, "\x14"); // down + scanMap.SetValue(77, "\x12"); // right + scanMap.SetValue(75, "\x11"); // left + scanMap.SetValue(71, "\x01"); // home + scanMap.SetValue(79, "\x05"); // end #else scanMap.SetValue("\x7F", "\x08"); // backspace scanMap.SetValue("\x1B[3~", "\x7F"); // delete @@ -73,11 +75,17 @@ void KeyOptimizeScanMap(ValueDict& scanMap) { // Helper function to read all available characters from STDIN into the global input buffer. void slurpStdin() { - struct InputBufferEntry e = {0, 0}; - #if WINDOWS - // ... + while (_kbhit()) { + struct InputBufferEntry e = {0, 0}; + e.c = _getwch(); + if (e.c == 0 || e.c == 0xE0) { + e.c = 0; + e.scanCode = _getwch(); + } + inputBuffer.push_back(e); + } #else @@ -106,6 +114,7 @@ void slurpStdin() { // Nothing's left to slurp ; save to the input buffer a current unsaved character if (p > buf) { + struct InputBufferEntry e = {0, 0}; e.c = UTF8Decode(buf); inputBuffer.push_back(e); } @@ -117,6 +126,7 @@ void slurpStdin() { if (p > buf && !IsUTF8IntraChar(*p)) { // A new character has begun under `p`, save the previous one to the input buffer and continue slurping + struct InputBufferEntry e = {0, 0}; e.c = UTF8Decode(buf); inputBuffer.push_back(e); buf[0] = *p; @@ -166,13 +176,14 @@ Value KeyGet(ValueDict& scanMap) { break; // malformed scan map } } + String s; if (initialE.c == 0) { - Value v(initialE.scanCode); - return v; + s = "<" + String::Format(initialE.scanCode, "%d") + ">"; + } else { + unsigned char buf[5] = {0, 0, 0, 0, 0}; + long nBytes = UTF8Encode(initialE.c, buf); + s = String((char *)buf, nBytes); } - unsigned char buf[5] = {0, 0, 0, 0, 0}; - long nBytes = UTF8Encode(initialE.c, buf); - String s((char *)buf, nBytes); Value v(s); return v; } @@ -213,7 +224,6 @@ void KeyClear() { bool KeyGetEcho() { #if WINDOWS - // ... return false; #else @@ -228,7 +238,7 @@ bool KeyGetEcho() { void KeySetEcho(bool on) { #if WINDOWS - // ... + // no op #else From f41482295ae371584de622a36c2ebe7f87388d0c Mon Sep 17 00:00:00 2001 From: Marc Gurevitx Date: Thu, 25 Jul 2024 16:01:19 +0300 Subject: [PATCH 4/5] Remove mesterious new lines --- MiniScript-cpp/src/ShellIntrinsics.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/MiniScript-cpp/src/ShellIntrinsics.cpp b/MiniScript-cpp/src/ShellIntrinsics.cpp index 1815e49..e82a988 100644 --- a/MiniScript-cpp/src/ShellIntrinsics.cpp +++ b/MiniScript-cpp/src/ShellIntrinsics.cpp @@ -6,8 +6,6 @@ // Copyright © 2021 Joe Strout. All rights reserved. // - - #include "ShellIntrinsics.h" #include #include From 989d687af43af4e8f2a494bcda1dcf375be4a395 Mon Sep 17 00:00:00 2001 From: Marc Gurevitx Date: Sun, 28 Jul 2024 10:07:02 +0300 Subject: [PATCH 5/5] Eliminating a bug in Tetris (freezing shapes) --- MiniScript-cpp/demo/tetris.ms | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MiniScript-cpp/demo/tetris.ms b/MiniScript-cpp/demo/tetris.ms index 21977cd..575e9e4 100644 --- a/MiniScript-cpp/demo/tetris.ms +++ b/MiniScript-cpp/demo/tetris.ms @@ -251,7 +251,6 @@ rotate = function(s) end function deleteRows = function - redrawP = false for i in range(0, fieldHeight - 1, 1) nNulls = 0 for cell in field[i] @@ -264,10 +263,8 @@ deleteRows = function end for outer.field = [emptyRow] + field[:i] + field[i + 1:] outer.delay -= delaySub - redrawP = true end if end for - if redrawP then drawField end function @@ -310,6 +307,7 @@ while true if moved == null then absorbShape s deleteRows + drawField shape = null s = null else