@bitblit/asyncglk
Version:
A Typescript Glk library
178 lines (174 loc) • 5.88 kB
JavaScript
/*
Cheap console GlkOte implementation
===================================
Copyright (c) 2022 Dannii Willis
MIT licenced
https://github.com/curiousdannii/asyncglk
*/
import MuteStream from 'mute-stream';
import * as readline from 'readline';
import * as TTY from 'tty';
import { get_stdio } from './stdio.js';
import * as GlkOte from '../common/glkote.js';
import * as protocol from '../../common/protocol.js';
// Ansi escapes
// From https://github.com/sindresorhus/ansi-escapes/blob/main/index.js
const isTerminalApp = process.env.TERM_PROGRAM === 'Apple_Terminal';
const ESC = '\u001B[';
const cursorRestorePosition = isTerminalApp ? '\u001B8' : ESC + 'u';
const cursorSavePosition = isTerminalApp ? '\u001B7' : ESC + 's';
const eraseEndLine = ESC + 'K';
const scrollDown = ESC + 'T';
const KEY_REPLACEMENTS = {
'\x7F': 'delete',
'\t': 'tab',
};
export default class CheapGlkOte extends GlkOte.GlkOteBase {
current_input_type = null;
handle_char_input_callback = (str, key) => this.handle_char_input(str, key);
handle_line_input_callback = (line) => this.handle_line_input(line);
rl;
stdin;
stdout;
window = null;
constructor() {
super();
const cheap_stdio = get_stdio();
this.rl = cheap_stdio.rl;
this.stdin = cheap_stdio.stdin;
this.stdout = cheap_stdio.stdout;
}
async init(options) {
if (!options) {
throw new Error('no options provided');
}
// TODO: handle output from all buffer windows
// Wrap glk_window_open so that only one window can be opened
if (options.Glk) {
const old_glk_window_open = options.Glk.glk_window_open.bind(options.Glk);
options.Glk.glk_window_open = function (splitwin, method, size, wintype, rock) {
if (splitwin) {
return null;
}
return old_glk_window_open(splitwin, method, size, wintype, rock);
};
}
this.rl.resume();
if (process.stdout.isTTY) {
this.current_metrics.height = process.stdout.rows;
this.current_metrics.width = process.stdout.columns;
}
// Note that this must be called last as it will result in VM.start() being called
super.init(options);
}
attach_handlers() {
if (this.current_input_type === 'char') {
this.stdout.mute();
this.stdin.on('keypress', this.handle_char_input_callback);
}
if (this.current_input_type === 'line') {
this.rl.on('line', this.handle_line_input_callback);
}
}
cancel_inputs(windows) {
if (windows.length === 0) {
this.current_input_type = null;
this.detach_handlers();
}
}
detach_handlers() {
this.stdin.removeListener('keypress', this.handle_char_input_callback);
this.rl.removeListener('line', this.handle_line_input_callback);
this.stdout.unmute();
}
disable(disable) {
this.disabled = disable;
if (disable) {
this.detach_handlers();
}
else {
this.attach_handlers();
}
}
exit() {
this.detach_handlers();
this.rl.close();
this.stdout.write('\n');
super.exit();
}
handle_char_input(str, key) {
if (this.current_input_type === 'char') {
this.current_input_type = null;
this.detach_handlers();
// Make sure this char isn't being remembered for the next line input
this.rl._line_buffer = null;
this.rl.line = '';
// Process special keys
const res = KEY_REPLACEMENTS[str] || str || key.name.replace(/f(\d+)/, 'func$1');
this.send_event({
type: 'char',
window: this.window.id,
value: res,
});
}
}
handle_line_input(line) {
if (this.current_input_type === 'line') {
if (this.stdout.isTTY) {
this.stdout.write(scrollDown + cursorRestorePosition + eraseEndLine);
}
this.current_input_type = null;
this.detach_handlers();
this.send_event({
type: 'line',
window: this.window.id,
value: line,
});
}
}
save_allstate() {
throw new Error('save_allstate not yet implemented');
}
update_content(content) {
const window_content = content.filter(content => content.id === this.window.id)[0];
for (const line of window_content?.text || []) {
if (!line.append) {
this.stdout.write('\n');
}
const content = line.content;
if (content) {
for (let i = 0; i < content.length; i++) {
const run = content[i];
if (typeof run === 'string') {
i++;
this.stdout.write(content[i]);
}
else if ('text' in run) {
this.stdout.write(run.text);
}
}
}
}
}
update_inputs(windows) {
if (windows.length) {
if (windows[0].type === 'char') {
this.current_input_type = 'char';
}
if (windows[0].type === 'line') {
if (this.stdout.isTTY) {
this.stdout.write(cursorSavePosition);
}
this.current_input_type = 'line';
}
this.attach_handlers();
}
}
update_windows(windows) {
for (const win of windows) {
if (win.type === 'buffer') {
this.window = win;
}
}
}
}