chore(tui): replace koffi with Windows VT input helper

closes #4480
This commit is contained in:
Mario Zechner
2026-05-21 00:21:21 +02:00
parent 385a11bf53
commit 4868222e34
11 changed files with 176 additions and 70 deletions

View File

@@ -516,9 +516,6 @@
"get-east-asian-width": "1.6.0",
"marked": "15.0.12"
},
"optionalDependencies": {
"koffi": "2.16.2"
},
"engines": {
"node": ">=22.19.0"
}
@@ -1369,17 +1366,6 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/koffi": {
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz",
"integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==",
"license": "MIT",
"optional": true,
"hasInstallScript": true,
"funding": {
"url": "https://liberapay.com/Koromix"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",

View File

@@ -2,6 +2,10 @@
## [Unreleased]
### Changed
- Replaced the optional `koffi` dependency for Windows VT input with a tiny vendored native helper, reducing install size while preserving Shift+Tab handling ([#4480](https://github.com/earendil-works/pi/issues/4480)).
## [0.75.4] - 2026-05-20
### Changed

View File

@@ -0,0 +1,53 @@
#include <windows.h>
#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT
#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200
#endif
#define NAPI_AUTO_LENGTH ((unsigned long long)-1)
typedef void* napi_env;
typedef void* napi_value;
typedef void* napi_callback_info;
typedef napi_value (__cdecl *napi_callback)(napi_env, napi_callback_info);
typedef int (__cdecl *napi_create_function_fn)(napi_env, const char*, unsigned long long, napi_callback, void*, napi_value*);
typedef int (__cdecl *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value);
typedef int (__cdecl *napi_get_boolean_fn)(napi_env, int, napi_value*);
static void* node_symbol(const char* name) {
HMODULE module = GetModuleHandleA(0);
void* proc = module ? (void*)GetProcAddress(module, name) : 0;
if (proc) return proc;
module = GetModuleHandleA("node.dll");
return module ? (void*)GetProcAddress(module, name) : 0;
}
static napi_value __cdecl enable_virtual_terminal_input(napi_env env, napi_callback_info info) {
(void)info;
HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode = 0;
int enabled = handle != INVALID_HANDLE_VALUE &&
GetConsoleMode(handle, &mode) &&
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_INPUT);
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
napi_value result = 0;
if (napi_get_boolean) napi_get_boolean(env, enabled, &result);
return result;
}
__declspec(dllexport) napi_value __cdecl napi_register_module_v1(napi_env env, napi_value exports) {
napi_create_function_fn napi_create_function = (napi_create_function_fn)node_symbol("napi_create_function");
napi_set_named_property_fn napi_set_named_property = (napi_set_named_property_fn)node_symbol("napi_set_named_property");
napi_value fn = 0;
if (napi_create_function &&
napi_set_named_property &&
napi_create_function(env, "enableVirtualTerminalInput", NAPI_AUTO_LENGTH, enable_virtual_terminal_input, 0, &fn) == 0) {
napi_set_named_property(env, exports, "enableVirtualTerminalInput", fn);
}
return exports;
}

View File

@@ -12,6 +12,7 @@
},
"files": [
"dist/**/*",
"native/win32/prebuilds/**/*.node",
"README.md"
],
"keywords": [
@@ -38,9 +39,6 @@
"get-east-asian-width": "1.6.0",
"marked": "15.0.12"
},
"optionalDependencies": {
"koffi": "2.16.2"
},
"devDependencies": {
"@xterm/headless": "5.5.0",
"@xterm/xterm": "5.5.0",

View File

@@ -1,6 +1,7 @@
import * as fs from "node:fs";
import { createRequire } from "node:module";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { setKittyProtocolActive } from "./keys.ts";
import { StdinBuffer } from "./stdin-buffer.ts";
@@ -210,23 +211,30 @@ export class ProcessTerminal implements Terminal {
private enableWindowsVTInput(): void {
if (process.platform !== "win32") return;
try {
// Dynamic require to avoid bundling koffi's 74MB of cross-platform
// native binaries into every compiled binary. Koffi is only needed
// on Windows for VT input support.
const koffi = cjsRequire("koffi");
const k32 = koffi.load("kernel32.dll");
const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
const arch = process.arch;
if (arch !== "x64" && arch !== "arm64") return;
const STD_INPUT_HANDLE = -10;
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
const handle = GetStdHandle(STD_INPUT_HANDLE);
const mode = new Uint32Array(1);
GetConsoleMode(handle, mode);
SetConsoleMode(handle, mode[0]! | ENABLE_VIRTUAL_TERMINAL_INPUT);
// Dynamic require so non-Windows and bundled/browser paths never load the
// native helper. In the npm package native/ is next to dist/; in compiled
// binary archives native/ is copied next to the executable.
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const nativePath = path.join("native", "win32", "prebuilds", `win32-${arch}`, "win32-console-mode.node");
const candidates = [
path.join(moduleDir, "..", nativePath),
path.join(moduleDir, nativePath),
path.join(path.dirname(process.execPath), nativePath),
];
for (const modulePath of candidates) {
try {
const helper = cjsRequire(modulePath) as { enableVirtualTerminalInput?: () => boolean };
helper.enableVirtualTerminalInput?.();
return;
} catch {
// Try the next possible packaging location.
}
}
} catch {
// koffi not available — Shift+Tab won't be distinguishable from Tab
// Native helper not available — Shift+Tab won't be distinguishable from Tab.
}
}