UNPKG

@bitblit/asyncglk

Version:
178 lines (174 loc) 5.88 kB
/* 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; } } } }