wui/index.js

const proc = require('proc');
const term = require('term');
const tui = require('tui');

/**
 * @typedef WinEntry
 * @property {string} id
 * @property {WinLayoutCallback} layout
 * @property {Window} win
 * @property {Widget} [widget]
 * @private
 */

/**
 * @callback WinLayoutCallback
 * @param {WindowSize} size
 * @returns {Window}
 */

/**
 * @callback WuiRunCallback
 * @param {number} key
 * @returns {boolean}
 */

var debug_enabled = false;

/**
 * @exports wui
 */
const wui = {};

const entries = {};

/**
 *
 * @param {string} id
 * @param {WinLayoutCallback} layout
 * @throws {Error} If window with given id exists
 */
wui.add_win = function (id, layout) {
	if (entries[id]) {
		throw new Error('Window exists: ' + id);
	}

	debug('wui', 'add_win', id);

	entries[id] = {
		id: id,
		layout: layout,
		win: layout(tui.get_size(tui.stdscr)),
		widget: undefined,
	};
};

/**
 * Connects a widget to a window. If the window has a connected widget, it is
 * automatically disconnected.
 *
 * @param {string} id
 * @param {Widget} widget
 * @return {Widget} The given widget
 * @throws {Error} If window with given id doesn't exist
 */
wui.connect = function (id, widget, props) {
	props = props || {};

	const entry = entries[id];

	if (!entry) {
		throw new Error('Window does not exist: ' + id);
	}

	if (entry.widget) {
		debug(widget.class.name + '.' + entry.id, 'set_win', undefined);
		entry.widget.set_win();
	}

	debug(widget, 'connect', id);

	entry.widget = widget;
	widget.set_win(entry.win);
	widget.invalidate();

	return widget;
};

/**
 *
 * @param {string} id
 * @throws {Error} If window with given id does not exist
 */
wui.del_win = function (id) {
	const entry = entries[id];

	if (!entry) {
		throw new Error('Window does not exist: ' + id);
	}

	const widget = entry.widget;

	if (widget) {
		debug(widget.class.name + '.' + entry.id, 'set_win', undefined);
		widget.set_win();
	}

	debug('wui', 'del_win', id);

	delete entries[id];
};

/**
 * Disconnect a widget from its window and return the widget
 *
 * @param {string} id
 * @returns {Widget|undefined}
 */
wui.disconnect = function (id) {
	const entry = entries[id];

	if (!entry) {
		throw new Error('Window does not exist: ' + id);
	}

	const widget = entry.widget;

	if (widget) {
		debug(widget.class.name + '.' + entry.id, 'set_win', undefined);
		widget.set_win();
	}

	debug('wui', 'disconnect', id);

	entry.widget = undefined;
	tui.clear(entry.win);

	return widget;
};

/**
 * Get the widget connected to a given window
 *
 * @param {string} id
 * @returns {Widget|undefined}
 */
wui.get_widget = function (id) {
	const entry = entries[id];

	if (!entry) {
		throw new Error('Window does not exist: ' + id);
	}

	return entry.widget;
};

/**
 * Get a window by id
 *
 * @param {string} id
 * @returns {Widget|undefined}
 */
wui.get_win = function (id) {
	const entry = entries[id];

	if (!entry) {
		throw new Error('Window does not exist: ' + id);
	}

	return entry.win;
};

/**
 *
 */
wui.init = function () {
	tui.init();
	tui.curs_set(false);
	tui.clear();

	proc.atexit(tui.end);
};

/**
 *
 */
wui.relayout = function () {
	const size = tui.get_size(tui.stdscr);

	debug('wui', 'relayout', size);

	Object.values(entries).forEach(function (entry) {
		entry.win = entry.layout(size);

		if (entry.widget) {
			entry.widget.set_win(entry.win);
		}
	});
};

/**
 *
 * @param {WuiRunCallback} cb
 */
wui.run = function (cb) {
	while (true) {
		draw();
		refresh();

		const key = tui.getch();

		debug('wui', 'getch', key);

		if (key === tui.KEY_RESIZE) {
			wui.relayout();
			continue;
		}

		if (cb(key) === false) {
			debug('wui', 'exitting');
			break;
		}
	}
};

/**
 * Routes a key through all enabled widgets
 *
 * @param {number} key
 * @returns {boolean}
 */
wui.send_key = function (key) {
	var result = false;

	debug('wui', 'send_key', key);

	Object.values(entries).forEach(function (entry) {
		const widget = entry.widget;

		if (!widget) {
			return;
		}

		if (widget.get_win() !== entry.win) {
			throw new Error('Invalid widget state in window: ' + entry.id);
		}

		if (widget.is_disabled()) {
			debug(widget.class.name + '.' + entry.id, 'skipped (disabled)');
			return;
		}

		const handled = widget.send_key(key);

		debug(widget.class.name + '.' + entry.id, 'send_key', key, handled);

		result |= handled;
	});

	return result;
};

/**
 * @param {boolean} enabled
 */
wui.set_debug_enabled = function (enabled) {
	debug_enabled = enabled;
};

/**
 * @private
 */
function debug(caller) {
	if (!debug_enabled) {
		return;
	}

	const args = Array.prototype.slice.call(arguments).slice(1);

	if (caller.class) {
		args.unshift(caller.class.name + ':');
	} else {
		args.unshift(caller + ':');
	}

	term.println2.apply(null, args);
}

/**
 * @private
 */
function draw() {
	debug('wui', 'draw');

	Object.values(entries).forEach(function (entry) {
		const widget = entry.widget;

		if (!widget) {
			return;
		}

		if (widget.get_win() !== entry.win) {
			throw new Error('Invalid widget state in window: ' + entry.id);
		}

		if (widget._invalid) {
			debug('wui', 'clear', entry.id);
			tui.clear(entry.win);

			debug(widget.class.name + '.' + entry.id, 'draw');
			widget.draw();

			widget._invalid = false;
		}
	});
}

/**
 * @private
 */
function refresh() {
	debug('wui', 'refresh');

	tui.refresh(tui.stdscr);

	Object.values(entries).forEach(function (entry) {
		tui.refresh(entry.win);
	});
}

return wui;