const jt = require('joshi_tui.so');
const term = require('term');
/**
* @typedef {object} DrawMode
* @property {number} attrs Text attribute flags
* @property {number} pair A color pair number
*/
/**
* @typedef {object} Window
* @property {string} is_a Always contains 'Window'
* @property {Uint8Array} handle Opaque handle to window
* @property {WindowBorder} [border] Optional border definition
* @property {WindowSize} [size] Cached window size (can be missing)
* @property {DrawMode} draw_mode Window draw mode
*/
/**
* @typedef {object} WindowBorder
* @param {string} lt
* @param {string} t
* @param {string} rt
* @param {string} r
* @param {string} rb
* @param {string} b
* @param {string} lb
* @param {string} l
* @see {@link module:tui.DEFAULT_BORDER}
*/
/**
* @typedef {object} WindowPadding
* @property {number} left
* @property {number} top
* @property {number} right
* @property {number} bottom
*/
/**
* @typedef {object} WindowPos
* @property {number} row
* @property {number} col
*/
/**
* @typedef {object} WindowSize
* @property {number} rows
* @property {number} cols
*/
/**
* Default draw mode to use when clearing window or painting borders
*
* @see {@link module:tui.set_default_draw_mode}
* @private
*/
var default_draw_mode;
/**
* Cursor position (relative to {@link module:tui.stdscr}
*
* @type {WindowPos}
* @private
*/
const cursor = {
row: 1,
col: 1,
};
/**
* Next color id to use when calling {@link module:tui.add_colors}
* @module tui
* @private
*/
var next_color = 8;
/**
* Next color pair id to use when calling {@link module:tui.add_draw_modes}
* @private
*/
var next_pair = 1;
/**
* @enum {number}
* @exports tui
* @readonly
*/
const tui = {
/** Bitmask to extract attributes */
A_ATTRIBUTES: 0xffffff00,
/** Bitmask to extract a character */
A_CHARTEXT: 0x000000ff,
/** Bitmask to extract a color (?) */
A_COLOR: 0x0000ff00,
/** Normal display */
A_NORMAL: 0x00000000,
/** Best hightlighting mode of terminal */
A_STANDOUT: 0x00010000,
/** Underlining */
A_UNDERLINE: 0x00020000,
/** Reverse video */
A_REVERSE: 0x00040000,
/** Blinking */
A_BLINK: 0x00080000,
/** Half bright */
A_DIM: 0x00100000,
/** Extra bright or bold */
A_BOLD: 0x00200000,
/** Alternate character set */
A_ALTCHARSET: 0x00400000,
/** Invisible or blank mode */
A_INVIS: 0x00800000,
/** Protected mode */
A_PROTECT: 0x01000000,
A_HORIZONTAL: 0x02000000,
A_LEFT: 0x04000000,
A_LOW: 0x08000000,
A_RIGHT: 0x10000000,
A_TOP: 0x20000000,
A_VERTICAL: 0x40000000,
COLOR_BLACK: 0,
COLOR_RED: 1,
COLOR_GREEN: 2,
COLOR_YELLOW: 3,
COLOR_BLUE: 4,
COLOR_MAGENTA: 5,
COLOR_CYAN: 6,
COLOR_WHITE: 7,
/**
* @type {object}
*/
DEFAULT_BORDER: {
lt: '┌',
t: '─',
rt: '┐',
r: '│',
rb: '┘',
b: '─',
lb: '└',
l: '│',
},
KEY_CTRL_A: 1,
KEY_CTRL_B: 2,
KEY_CTRL_C: 3,
KEY_CTRL_D: 4,
KEY_CTRL_E: 5,
KEY_CTRL_F: 6,
KEY_CTRL_G: 7,
KEY_CTRL_H: 8,
KEY_TAB: 9,
KEY_ENTER: 10,
KEY_CTRL_K: 11,
KEY_CTRL_L: 12,
KEY_CTRL_M: 13,
KEY_CTRL_N: 14,
KEY_CTRL_O: 15,
KEY_CTRL_P: 16,
KEY_CTRL_Q: 17,
KEY_CTRL_R: 18,
KEY_CTRL_S: 19,
KEY_CTRL_T: 20,
KEY_CTRL_U: 21,
KEY_CTRL_V: 22,
KEY_CTRL_W: 23,
KEY_CTRL_X: 24,
KEY_CTRL_Y: 25,
KEY_CTRL_Z: 26,
KEY_ESC: 27,
KEY_INSERT: 0513,
KEY_DEL: 0512,
KEY_BACKSPACE: 0407,
KEY_HOME: 0406,
KEY_END: 0550,
KEY_NEXT_PAGE: 0522,
KEY_PREV_PAGE: 0523,
KEY_RESIZE: 0632,
KEY_DOWN: 0402,
KEY_UP: 0403,
KEY_LEFT: 0404,
KEY_RIGHT: 0405,
KEY_F1: 0411,
KEY_F2: 0412,
KEY_F3: 0413,
KEY_F4: 0414,
KEY_F5: 0415,
KEY_F6: 0416,
KEY_F7: 0417,
KEY_F8: 0420,
KEY_F9: 0421,
KEY_F10: 0422,
KEY_F11: 0423,
KEY_F12: 0424,
/** @deprecated use {@link module:tui.KEY_NEXT_PAGE} instead */
KEY_NPAGE: 0522,
/** @deprecated use {@link module:tui.KEY_PREV_PAGE} instead */
KEY_PPAGE: 0523,
/**
* @type {boolean}
*/
can_change_color: undefined,
/**
* @type {boolean}
*/
has_colors: undefined,
max_color_pairs: undefined,
max_colors: undefined,
/**
* The default window, which represents the whole screen
* @type {Window}
*/
stdscr: undefined,
};
/**
* Define multiple colors to be used by draw modes
*
* @example
* const colors = tui.add_colors({
* WHITE: 'fff',
* BLACK: '000',
* RED: 'ff0000',
* });
*
* @param {object} definitions
* An object containing properties where keys represent names and values are
* strings containing hex values for red, green and blue (as in CSS).
*
* @returns {object}
* An object with the same keys as the {definitions} param but values are
* replaced by color ids.
*
* @see {@link module:tui.add_draw_modes}
*/
tui.add_colors = function (definitions) {
if (!tui.has_colors) {
throw new Error('Terminal does not support colors');
}
if (!tui.can_change_color) {
throw new Error('Terminal does not support colors redefinition');
}
return Object.entries(definitions).reduce(function (map, entry) {
const name = entry[0];
const rgb = entry[1];
if (rgb.length === 3) {
rgb = rgb[0] + rgb[0] + rgb[1] + rgb[1] + rgb[2] + rgb[2];
}
if (rgb.length !== 6) {
throw new Error('Invalid color: ' + name);
}
const r = (1000 * parseInt(rgb.substring(0, 2), 16)) / 255;
const g = (1000 * parseInt(rgb.substring(2, 4), 16)) / 255;
const b = (1000 * parseInt(rgb.substring(4, 6), 16)) / 255;
if (isNaN(r) || isNaN(g) || isNaN(b)) {
throw new Error('Invalid color: ' + name);
}
const color = next_color;
jt.init_color(color, r, g, b);
next_color++;
map[name] = color;
return map;
}, {});
return color;
};
/**
* Define multiple draw modes
*
* @example
* const modes = tui.add_draw_modes({
* DEFAULT: [tui.A_NORMAL, color.WHITE, color.BLACK],
* ERROR: [tui.A_BOLD, color.RED, color.BLACK],
* });
*
* @param {object} definitions
* An object containing properties where keys represent names and values are
* tuples (arrays) of [attrs, fg, bg].
*
* @returns {object}
* An object with the same keys as the {definitions} param but values are
* replaced by draw mode ids
*/
tui.add_draw_modes = function (definitions) {
return Object.entries(definitions).reduce(function (map, entry) {
const name = entry[0];
const attrs = entry[1][0];
const fg = entry[1][1];
const bg = entry[1][2];
if (isNaN(attrs) || isNaN(fg) || isNaN(bg)) {
throw new Error('Invalid draw mode: ' + name);
}
const pair = next_pair;
jt.init_pair(pair, fg, bg);
next_pair++;
map[name] = {
attrs: attrs,
pair: pair,
};
return map;
}, {});
};
/**
* @param {number} row Row coordinate (starts at 1)
* @param {number} col Column coordinate (starts at 1)
* @returns {void}
*/
tui.curs_move = function (row, col) {
const win = tui.stdscr;
jt.wmove(win.handle, row - 1, col - 1);
cursor.row = row;
cursor.col = col;
};
/**
* Erase whole window
*
* @param {Window} [win={@link module:tui.stdscr}]
* @see {@link module:tui.clear}
* @returns {void}
*/
tui.clear = function (win) {
win = win || tui.stdscr;
const wsize = tui.get_size(win);
var line = '';
for (var i = 0; i < wsize.cols; i++) {
line += ' ';
}
const saved_draw_mode = tui.get_draw_mode(win);
tui.set_draw_mode(win, default_draw_mode);
for (var row = 1; row <= wsize.rows; row++) {
tui.print(win, row, 1, line);
}
draw_border(win);
};
/**
* Get cursor position
*
* @returns {WindowPos}
*/
tui.curs_pos = function () {
return cursor;
};
/**
* @param {boolean} visible
* @returns {void}
*/
tui.curs_set = function (visible) {
// we don't support "very visible" cursor (value 2) for the moment
jt.curs_set(visible ? 1 : 0);
};
/**
*
* @returns {void}
*/
tui.end = function () {
jt.endwin();
tui.stdscr = undefined;
};
/**
* @returns {DrawMode}
*/
tui.get_draw_mode = function (win) {
win = win || tui.stdscr;
return win.draw_mode;
};
/**
* Get the human readable name of a key code
*
* @param {number} key
* @returns {string} Something like 'Ctrl-D', 'Space', or 'A'
*/
tui.get_key_name = function (key) {
key = Number(key);
if (isNaN(key)) {
throw new Error('Invalid key code: ' + key);
}
// Lookup table
const entry = Object.entries(tui).find(function (entry) {
return entry[0].startsWith('KEY_') && entry[1] === key;
});
if (entry) {
var name = entry[0].substring(4);
// Change case
name = name[0] + name.substring(1).toLocaleLowerCase();
// Uppercase each letter after underscore
for (var i = 1; i < name.length; i++) {
if (name[i] === '_') {
name =
name.substring(0, i) +
' ' +
name[i + 1].toLocaleUpperCase() +
name.substring(i + 2);
}
}
return name;
}
return '<' + key + '>';
};
/**
* Get the size of a window, i.e., number or row and columns.
*
* @param {Window} [win={@link module:tui.stdscr}]
* @returns {WindowSize} The window size
*/
tui.get_size = function (win) {
win = win || tui.stdscr;
if (win.size) {
return win.size;
}
const ret = jt.getmaxyx(win.handle);
if (win.border) {
ret.y -= 2;
ret.x -= 2;
}
return {
rows: ret.y,
cols: ret.x,
};
};
/**
* @returns {int} A key code
*/
tui.getch = function () {
return jt.wgetch();
};
/**
* @returns {void}
*/
tui.init = function () {
if (tui.stdscr) {
throw new Error('Screen has been already initialized');
}
const info = jt.initscr();
const wattr = jt.wattr_get(info.win, null);
tui.stdscr = {
is_a: 'Window',
handle: info.win,
draw_mode: {
attrs: wattr.attrs,
pair: wattr.pair,
},
};
tui.can_change_color = info.can_change_color;
tui.has_colors = info.has_colors;
tui.max_color_pairs = info.max_color_pairs;
tui.max_colors = info.max_colors;
tui.set_default_draw_mode(tui.stdscr.draw_mode);
};
/**
* @param {Window} [win={@link module:tui.stdscr}]
* @param {number} row
* @param {number} col
* @param {...*} items Items to print
* @returns {void}
*/
tui.print = function () {
var win = arguments[0];
var row = arguments[1];
var col = arguments[2];
var first_item_index = 3;
if (win.is_a !== 'Window') {
col = row;
row = win;
win = tui.stdscr;
first_item_index = 2;
}
check_pos(win, row, col);
const wsize = tui.get_size(win);
const cols_avail = wsize.cols - col + 1;
var str = '';
for (var i = first_item_index; i < arguments.length; i++) {
if (i > first_item_index) {
str += ' ';
}
str += term
.to_string(arguments[i])
.replace('\t', ' ')
.replace('\n', '↩');
if (str.length >= cols_avail) {
break;
}
}
// Clip string
if (str.length > cols_avail) {
str = str.substring(0, cols_avail);
}
const y = win.border ? row : row - 1;
const x = win.border ? col : col - 1;
if (!win.border && row === wsize.rows && col + str.length > wsize.cols) {
jt.wmove(win.handle, y, x);
jt.waddstr(win.handle, str.substring(0, str.length - 1));
jt.winsstr(win.handle, str.substring(str.length - 1));
} else {
jt.wmove(win.handle, y, x);
jt.waddstr(win.handle, str);
}
restore_cursor();
};
/**
* @param {Window} [win={@link module:tui.stdscr}]
* @returns {void}
*/
tui.refresh = function (win) {
win = win || tui.stdscr;
jt.wrefresh(win.handle);
};
/**
* Set the colors used to clear the screen and draw borders
*
* @param {DrawMode} draw_mode
* @see {@link module:tui.clear}
*/
tui.set_default_draw_mode = function (draw_mode) {
default_draw_mode = draw_mode;
};
/**
* @param {DrawMode} mode
* @returns {void}
*/
tui.set_draw_mode = function (win, mode) {
if (mode === undefined) {
mode = win;
win = tui.stdscr;
}
jt.wattr_set(win.handle, mode.attrs, mode.pair, null);
win.draw_mode = mode;
};
/**
* Set the timeout (in milliseconds) for the {@link module:tui.getch} function.
*
* @param {number|null} delay
* Number of milliseconds to wait for a key press before timeout. A delay of 0
* configures getch() for non-blocking mode, whereas a value of null makes
* getch() block until a key is pressed.
*/
tui.timeout = function (delay) {
const win = tui.stdscr;
if (delay !== null && (isNaN(delay) || Number(delay) < 0)) {
throw new Error('Invalid delay: ' + delay);
}
jt.wtimeout(win.handle, delay === null ? -1 : Number(delay));
};
/**
* Delete an existing {Window}
*
* @param {Window} win
* @returns {void}
*/
tui.win_del = function (win) {
if (win === tui.stdscr) {
throw new Error('Window stdscr cannot be deleted');
}
jt.delwin(win.handle);
};
/**
* Move an existing {Window}
*
* @param {Window} win
* @returns {void}
*/
tui.win_move = function (win, row, col) {
if (win === tui.stdscr) {
throw new Error('Window stdscr cannot be moved');
}
jt.mvwin(win.handle, row - 1, col - 1);
};
/**
* Create a new {Window} with given coordinates and size
*
* @param {number} row
* @param {number} col
* @param {number} rows
* @param {number} cols
*
* @param {WindowBorder|boolean} [border=false]
* An explicit {WindowBorder} object, or {true} to use
* {@link module:tui.DEFAULT_BORDER}
*
* @returns {Window}
*/
tui.win_new = function (row, col, rows, cols, border) {
if (border === true) {
border = tui.DEFAULT_BORDER;
}
const handle = jt.newwin(rows, cols, row - 1, col - 1);
const wattr = jt.wattr_get(handle, null);
const win = {
is_a: 'Window',
handle: handle,
border: border,
draw_mode: {
attrs: wattr.attrs,
pair: wattr.pair,
},
size: {
rows: border ? rows - 2 : rows,
cols: border ? cols - 2 : cols,
},
};
tui.clear(win);
draw_border(win);
return win;
};
function check_pos(win, row, col) {
const wsize = tui.get_size(win);
if (row < 1 || row > wsize.rows || col < 1 || col > wsize.cols) {
throw new Error(
'Invalid position (' +
row +
',' +
col +
') for window ' +
'of size [' +
wsize.rows +
'x' +
wsize.cols +
']'
);
}
}
/**
* Draw a border around a window
*
* @private
* @param {Window} win
* @returns {void}
*/
function draw_border(win) {
if (!win.border) {
return;
}
const border = win.border;
const maxyx = jt.getmaxyx(win.handle);
const maxy = maxyx.y - 1;
const maxx = maxyx.x - 1;
const saved_draw_mode = tui.get_draw_mode(win);
tui.set_draw_mode(win, default_draw_mode);
jt.wmove(win.handle, 0, 0);
jt.waddstr(win.handle, border.lt);
jt.wmove(win.handle, 0, maxx);
jt.waddstr(win.handle, border.rt);
jt.wmove(win.handle, maxy, maxx);
jt.winsstr(win.handle, border.rb);
jt.wmove(win.handle, maxy, 0);
jt.waddstr(win.handle, border.lb);
jt.wmove(win.handle, 0, 1);
for (var i = 0; i < maxx - 1; i++) {
jt.waddstr(win.handle, border.t);
}
jt.wmove(win.handle, maxy, 1);
for (var i = 0; i < maxx - 1; i++) {
jt.waddstr(win.handle, border.b);
}
for (var y = 1; y <= maxy - 1; y++) {
jt.wmove(win.handle, y, 0);
jt.waddstr(win.handle, border.l);
jt.wmove(win.handle, y, maxx);
jt.waddstr(win.handle, border.r);
}
tui.set_draw_mode(win, saved_draw_mode);
restore_cursor();
}
function restore_cursor() {
jt.wmove(tui.stdscr.handle, cursor.row - 1, cursor.col - 1);
}
return tui;