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..70c0c3a --- /dev/null +++ b/MiniScript-cpp/README-key.md @@ -0,0 +1,67 @@ +# 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 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 script that demonstrates the use of `key.available`, `key.get` and `key._echo`: `demo/tetris.ms`. + + +## Implementation of the input buffer + +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 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 + +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 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 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 + +```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 and thus needs to be fast. + +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/demo/tetris.ms b/MiniScript-cpp/demo/tetris.ms new file mode 100644 index 0000000..575e9e4 --- /dev/null +++ b/MiniScript-cpp/demo/tetris.ms @@ -0,0 +1,323 @@ +// TETRIS +// This demo uses `key.available()` and `key.get()` + +import "vt" + +delay = 0.5 +delaySub = 0.01 +fieldHeight = 20 +fieldWidth = 10 +fieldLeft = 20 +fieldColor = vt.color.black +kUp = char(19) +kDown = char(20) +kRight = char(18) +kLeft = char(17) +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.fuchsia +cell.L = vt.color.orange +cell.J = vt.color.blue +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 + 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 + end if + end for +end function + + +initField +drawField +key._echo = false +t = time +while true + yield + + if shape == null then + shape = initShape + if shape == null then + drawGameOver + key._echo = true + exit + end if + drawShape shape + end if + + s = shape + moved = null + 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 + moved = moveDown(s) + if moved == null then + absorbShape s + deleteRows + drawField + 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/Key.cpp b/MiniScript-cpp/src/Key.cpp new file mode 100644 index 0000000..1f73463 --- /dev/null +++ b/MiniScript-cpp/src/Key.cpp @@ -0,0 +1,258 @@ +#include "Key.h" +#include "UnicodeUtil.h" + +#if _WIN32 || _WIN64 + #define WINDOWS 1 + #include +#else + #include + #include + #include +#endif + +namespace MiniScript { + + +SimpleVector inputBuffer; + +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 + 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() { + #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 + + // 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) { + struct InputBufferEntry e = {0, 0}; + 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 + struct InputBufferEntry e = {0, 0}; + 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"; + } else { + unsigned char buf[5] = {0, 0, 0, 0, 0}; + long nBytes = UTF8Encode(initialE.c, buf); + s = String((char *)buf, nBytes); + } + Value v(s); + return v; +} + +void KeyPutCodepoint(long codepoint, bool inFront) { + struct InputBufferEntry e = {0, 0}; + e.c = codepoint; + if (inFront) { + inputBuffer.insert(e, 0); + } else { + inputBuffer.push_back(e); + } +} + +void KeyPutString(String s, bool inFront) { + if (inFront) { + for (int i=s.Length()-1; 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 b5dd942..73c3e11 100644 --- a/MiniScript-cpp/src/ShellIntrinsics.cpp +++ b/MiniScript-cpp/src/ShellIntrinsics.cpp @@ -22,6 +22,7 @@ #include "whereami/whereami.h" #include "DateTimeUtils.h" #include "ShellExec.h" +#include "Key.h" #include #include @@ -145,6 +146,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; @@ -166,6 +168,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 @@ -246,6 +258,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 +1143,196 @@ 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) { + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = nullptr; + + HANDLE hChildStd_OUT_Rd = nullptr; + HANDLE hChildStd_OUT_Wr = nullptr; + HANDLE hChildStd_ERR_Rd = nullptr; + HANDLE hChildStd_ERR_Wr = nullptr; + + // Create a pipe for the child process's STDOUT. + if (!CreatePipe(&hChildStd_OUT_Rd, &hChildStd_OUT_Wr, &saAttr, 0)) + return false; + + // Ensure the read handle to the pipe for STDOUT is not inherited. + SetHandleInformation(hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0); + + // Create a pipe for the child process's STDERR. + if (!CreatePipe(&hChildStd_ERR_Rd, &hChildStd_ERR_Wr, &saAttr, 0)) + return false; + + // Ensure the read handle to the pipe for STDERR is not inherited. + SetHandleInformation(hChildStd_ERR_Rd, HANDLE_FLAG_INHERIT, 0); + + STARTUPINFO siStartInfo; + ZeroMemory(&siStartInfo, sizeof(STARTUPINFO)); + siStartInfo.cb = sizeof(STARTUPINFO); + siStartInfo.hStdError = hChildStd_ERR_Wr; + siStartInfo.hStdOutput = hChildStd_OUT_Wr; + siStartInfo.dwFlags |= STARTF_USESTDHANDLES; + + PROCESS_INFORMATION piProcInfo; + ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION)); + + // Start the child process. + if (!CreateProcessA(nullptr, + (LPSTR)cmd.c_str(), // command line + nullptr, // process security attributes + nullptr, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + nullptr, // use parent's environment + nullptr, // use parent's current directory + &siStartInfo, // STARTUPINFO pointer + &piProcInfo)) // receives PROCESS_INFORMATION + { + return false; + } + + // Close handles to the stdin and stdout pipes no longer needed by the child process. + // If they are not explicitly closed, there is no way to recognize that the child process has completed. + CloseHandle(hChildStd_OUT_Wr); + CloseHandle(hChildStd_ERR_Wr); + + // Read output from the child process's pipe for STDOUT + // and print to the parent process's STDOUT. + DWORD dwRead; + CHAR chBuf[4096]; + bool bSuccess = FALSE; + + for (;;) { + bSuccess = ReadFile(hChildStd_OUT_Rd, chBuf, 4096, &dwRead, nullptr); + if (!bSuccess || dwRead == 0) break; + + String outputStr(chBuf, dwRead); + out += outputStr; + } + + // Read from STDERR + for (;;) { + bSuccess = ReadFile(hChildStd_ERR_Rd, chBuf, 4096, &dwRead, nullptr); + if (!bSuccess || dwRead == 0) break; + + String errorStr(chBuf, dwRead); + err += errorStr; + } + + // Wait until child process exits or timeout + DWORD waitResult = WaitForSingleObject(piProcInfo.hProcess, timeout); + if (waitResult == WAIT_TIMEOUT) { + // If the process is still running after the timeout, terminate it + TerminateProcess(piProcInfo.hProcess, 1); // Use 1 or another number to indicate forced termination + + err += "Timed out"; + returnCode = 124 << 8; // (124 is status code used by `timeout` command) + } + + // Regardless of the outcome, try to get the exit code + if (!GetExitCodeProcess(piProcInfo.hProcess, &returnCode)) { + returnCode = (DWORD)-1; // Use -1 or another value to indicate that getting the exit code failed + } + + // Close handles to the child process and its primary thread. + CloseHandle(piProcInfo.hProcess); + CloseHandle(piProcInfo.hThread); + + // Close the remaining pipe handles. + CloseHandle(hChildStd_OUT_Rd); + CloseHandle(hChildStd_ERR_Rd); + + return true; +} + +static IntrinsicResult intrinsic_exec(Context* context, IntrinsicResult partialResult) { + String cmd = "cmd /k " + context->GetVar("cmd").ToString(); + String out; + String err; + DWORD returnCode; + + double timeoutSecs = context->GetVar("timeout").DoubleValue(); + double timeoutMs = (timeoutSecs == 0) ? INFINITE : (timeoutSecs * 1000); + + if (!CreateChildProcess(cmd, out, err, returnCode, timeoutMs)) { + Error("Failed to create child process."); + } + + // Build our result map. + ValueDict result; + result.SetValue("output", Value(out)); + result.SetValue("errors", Value(err)); + result.SetValue("status", Value(returnCode)); + return IntrinsicResult(result); +} +#else + static IntrinsicResult intrinsic_exec(Context *context, IntrinsicResult partialResult) { double now = context->vm->RunTime(); if (partialResult.Done()) { @@ -1210,6 +1413,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) { @@ -1523,6 +1767,9 @@ void AddShellIntrinsics() { f = Intrinsic::Create("RawData"); f->code = &intrinsic_RawData; + f = Intrinsic::Create("key"); + f->code = &intrinsic_Key; + // RawData methods @@ -1635,4 +1882,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 + }