Terminal UI console for Node 20+ server apps with:
- Dynamic title row with (optional) uptime, CPU/memory usage, AFK state, mouse mode, and custom fragments
- AFK detector with configurable inactivity timeout to disable rendering while the terminal is idle
- Buffer viewport with auto-scroll on new logs when at live bottom
- Scrollback navigation (PageUp/PageDown, Home/End with empty input, mouse wheel)
- Bottom input anchored to terminal bottom with multiline soft-wrap
- ANSI-decolor-aware line measurement for sizing/wrapping
- UTC timestamps on log lines with runtime seconds to hundredths
console.loginterception (console._logkeeps original)- Prefix history navigation (
type prefix, then ArrowUp/ArrowDown) - Retained output buffer capped at 5000 log entries by default
- Command loader with grouped help system from one or more command folders (
commandsDirsupports a string or ordered array) - Persistent title/wrap/timestamps config in
console_config.json - Persistent AFK timeout config (
variabled.afkTimeoutSeconds) inconsole_config.json - Persistent input history in
console_history.json(last 1000 entries)
Click the preview image to open the demo video.
- Install
- Run demo
- Dummy C heartbeat server
- Built-in commands
- Integration
- Game Server Embedding
- External Process Embedding (
embeddingExecutablemode) - RageMP embedding example (
ragemp-server.exe) - Module Consumers (CJS and ESM)
- Command Loader
- Persistent Config
- Persistent History
- First Start Behavior
- Title API
- Notes
- Scroll Controls
npm installnpm starthelp- Show loaded built-instoggle title- Toggle title row and persist statetoggle wrap- Toggle word-wrap and persist statetoggle timestamps- Toggle UTC log timestamps and persist statetoggle mouse- Toggle app mouse mode (wheel scroll) vs native terminal selection/copy/paste modetoggle afk- Toggle AFK timeout detection on/off and persist stateget [filter]- List storedvariabled.*values (optionally filtered)set <variable> <value>- Set and persistvariabled.<variable>unset <variable>- Remove and persistvariabled.<variable>cls- Clear output buffermandelbrot [options]- Draw Mandelbrot sized to visible buffer viewport (--coloravailable)r <server side code>- Evaluate codeexit- Exit console
Commands are plain (no slash prefix).
const { createConsole } = require("./index");
const ui = createConsole({
titleEnabled: true,
wordWrapEnabled: true,
prompt: "> "
}).start();
// Optional: expose API globally for your server runtime
global.tty = ui.tty;
// Logs written through console.log go to the buffer with UTC timestamps
console.log("server started");Individual title segments can be suppressed with createConsole flags:
const ui = createConsole({
hideTitleName: true, // remove the title name (e.g. "node-server")
hideTitleUptime: true, // remove "up: …"
hideTitleCPU: true, // remove "CPU: …"
hideTitleMem: true, // remove "Mem: …"
hideTitleAfk: true, // remove "state: …"
hideTitleMouse: true // remove "mouse: …"
}).start();All flags default to false (every segment shown). The spinner is always present.
For embedded usage (for example in a game server), create one Console instance and assign its public context to your host object.
// Assuming your game server runtime is `mp` and you want to expose the console API on `mp.tty`:
const { Console } = require("tty-console");
const consoleRuntime = new Console({
configPath: "./console_config.json",
historyPath: "./console_history.json",
commandsDir: "./commands",
exitOnStop: false
});
// Expose runtime API on host object.
mp.tty = consoleRuntime.tty;
// Optional: run commands programmatically.
await mp.tty.parseCommand("help");
mp.tty.writeLog("game server initialized");Notes:
new Console(...)is safe in embedded/headless mode and does not auto-start the TUI.- Call
.start()only if you actually want the terminal UI to render. exitOnStop: falseavoids terminating your host process whenstop()is called.
This mode is different from the in-process embedding above.
- In-process embedding: your host process imports this package and controls
Consoledirectly. embeddingExecutablemode:node index.jsbecomes a launcher that starts another executable and bridges terminal I/O to it.
Use this when your real host runtime is an external executable (for example a game server binary) and you still want this terminal UI as a front-end.
Set embeddingExecutable in console_config.json:
{
"embeddingExecutable": "ragemp-server.exe"
}When this value is present and non-empty, running node index.js spawns that executable instead of starting a standalone local console session.
Inside the spawned process, EMBEDDED_NATIVE_LOG is already set, so the launcher path is skipped to prevent recursive self-spawning.
Startup sequence:
index.jsloadsconsole_config.jsonand resolvesembeddingExecutable.- It creates
<executable-name>_stdout.lognext to the executable. - It spawns the executable as a child process.
- Child
stdoutis piped into that log file. - Child
stderrstays attached to the terminal (inherit). - Launcher
stdinis forwarded in raw mode to childstdin(including VT/mouse escape sequences). - The embedded console runtime tails the log file and renders new output continuously.
This means node index.js is the parent launcher process, while the configured executable is the process that actually hosts your real server/runtime behavior.
- Keyboard and mouse input path: terminal -> launcher stdin -> child stdin.
- Output path for normal logs: child stdout ->
<executable-name>_stdout.log-> console tail reader -> UI buffer. - Error stream path: child stderr -> terminal directly.
The launcher handles stdin backpressure by pausing terminal reads when child stdin buffers fill, then resuming on drain.
- No auto-restart: if the embedded executable exits or crashes, launcher mode exits too.
- No reconnect flow: restart
node index.jsto start again. - Built-in commands (
help,toggle,exit, etc.) are console-side commands, not a command tunnel to the child process protocol. - No prompt-detection protocol is implemented for the child process.
- Launcher log cleanup is best-effort on shutdown (with retry on common Windows file lock errors).
EMBEDDED_NATIVE_LOG: path to the created stdout log file.TTY_LAUNCHER_MARKER: debug marker used only when internal debug mode is enabled.
- Executable not found: confirm
embeddingExecutableis correct and resolvable from your environment. - No live output: check whether
<executable-name>_stdout.logis created and growing. - Unexpected immediate exit: the launcher exits when the child process closes.
- Input feels blocked: this may be temporary stdin backpressure handling while child stdin drains.
Minimal config:
{
"embeddingExecutable": "ragemp-server.exe"
}Run:
node index.jsExpected behavior:
node index.jslaunchesragemp-server.exe.ragemp-server_stdout.logis created next to the executable.- RageMP stdout is captured into that log and rendered live in the console UI.
- Terminal input is forwarded to the child process.
- When
ragemp-server.exeexits, launcher mode exits as well.
If you need direct API control (new Console(...), mp.tty, parseCommand, etc.), use the in-process embedding model from the Game Server Embedding section instead.
CommonJS:
const { Console, createConsole } = require("tty-console");ESM:
import ttyConsole from "tty-console";
const { Console, createConsole } = ttyConsole;At startup the console scans commands/ for .js files and registers them as commands.
- Filename defines command name (
toggle.js->toggle) - Module must export
cmd - Optional
helptext is aggregated and displayed byhelp - Command handlers can be sync or async
commandsDir accepts a string (single directory) or an array of strings (multiple directories):
// Single directory (default behavior, backward compatible)
const ui = createConsole({ commandsDir: "./commands" });
// Multiple directories — later entries override earlier ones on name collision
const { Console } = require("tty-console");
const path = require("path");
const ui = new Console({
commandsDir: [
path.join(__dirname, "node_modules/tty-console/commands"), // loaded first
path.join(__dirname, "commands") // loaded second, overrides duplicates
]
});Important: when commandsDir is an array, folders are processed in the exact order they appear. If two folders provide the same command name, the later folder in the array overwrites the earlier one.
Non-existent directories in the array are silently skipped. If none of the listed directories exist, the help text shows "No commands directory found."
Command handler signature:
module.exports = {
cmd: (ctx, args, rawLine) => {
return "ok";
},
help: "example - demo"
};See commands/README.md for full details.
console_config.json stores toggle states:
{
"titleEnabled": true,
"wordWrapEnabled": true,
"timestampsEnabled": true,
"mouseCaptureEnabled": true,
"afkEnabled": true,
"variabled": {
"title": "Console",
"afkTimeoutSeconds": "300"
},
"embeddingExecutable": "ragemp-server.exe"
}variabled.afkTimeoutSeconds is measured in seconds. Activity includes terminal keyboard and mouse interaction.
afkEnabled controls whether AFK timeout detection is active.
embeddingExecutable enables launcher mode. If empty or missing, index.js starts in normal local console mode.
The file is loaded on startup and saved by toggle title, toggle wrap, toggle timestamps, toggle mouse, toggle afk, set, and unset.
console_history.json stores the submitted input history used by ArrowUp/ArrowDown and prefix matching.
- Stores the newest 1000 non-empty submitted lines
- Loaded on startup and saved on each submitted command
- Restored automatically after restart
Neither persistence file is required ahead of time.
console_config.jsonis auto-created on first start if missingconsole_history.jsonis auto-created on first start if missing
Modern callback fragments:
ui.tty.addConsoleTitle(() => `players: ${global.mp.players.length}`);Compatibility helpers exposed through ui.tty:
consoleTitleHeader(array)consoleTitleFooter(array)addConsoleTitle(fragmentOrExpression)getConsoleTitle()
addConsoleTitle accepts:
- Function fragments (recommended)
- String expressions (evaluated with
with (globalThis)) for migration
- Buffer rendering currently uses decolored text for deterministic wrap math.
- Input is single logical command line with multiline visual wrapping.
- Viewport is always recalculated from available rows:
rows - titleHeight - inputHeight. - While scrolled up in history, newly printed logs do not move your current viewport.
PageUp: scroll up by one page minus one line.PageDown: scroll down by one page minus one line.Home: jump to the top of the retained log when the input is empty.End: jump to the live bottom when the input is empty.Home/Endwith non-empty input keep their original cursor start/end behavior.- Mouse wheel up/down scrolls through history in small line steps while app mouse mode is enabled.
- Press
Esc, click the title line, or runtoggle mouseto switch between app mouse mode and native terminal selection mode. - In native selection mode, terminal text selection/copy/paste works as expected, but app mouse wheel scrolling is disabled until switched back.