@bitblit/asyncglk
Version:
A Typescript Glk library
227 lines (223 loc) • 7.19 kB
JavaScript
/*
Generic GlkOte implementation
=============================
Copyright (c) 2024 Dannii Willis
MIT licenced
https://github.com/curiousdannii/asyncglk
*/
import { Blorb } from '../../blorb/blorb.js';
import * as Constants from '../../common/constants.js';
import * as protocol from '../../common/protocol.js';
import { filetype_to_extension } from '../../dialog/common/common.js';
export class GlkOteBase {
classname = 'GlkOte';
version = Constants.PACKAGE_VERSION;
accept_func = () => { };
autorestoring = false;
Blorb;
current_metrics = Object.assign({}, Constants.DEFAULT_METRICS);
Dialog;
disabled = false;
generation = 0;
is_inited = false;
options = {};
timer = null;
waiting_for_update = false;
async init(options) {
if (!options) {
return this.error('no options provided');
}
if (!options.accept) {
return this.error('an accept function was not given to GlkOte');
}
this.options = options;
this.accept_func = options.accept;
if (options.Blorb) {
this.Blorb = options.Blorb;
}
if (options.Dialog) {
this.Dialog = options.Dialog;
}
this.is_inited = true;
// Send an init event, which if is received by GlkApi, will then result in VM.start() being called
this.send_event({ type: 'init' });
}
error(error) {
throw (typeof error === 'string' ? new Error(error) : error);
}
extevent(value) {
this.send_event({
type: 'external',
value,
});
}
getdomcontext() {
throw new Error('getdomcontext is not applicable to this GlkOte library');
}
getdomid(name) {
throw new Error('getdomid is not applicable to this GlkOte library');
}
getinterface() {
return this.options;
}
getlibrary(name) {
switch (name) {
case 'Blorb': return this.Blorb;
case 'Dialog': return this.Dialog;
default: return null;
}
}
inited() {
return this.is_inited;
}
log(msg) {
console.log(msg);
}
setdomcontext(val) {
throw new Error('setdomcontext is not applicable to this GlkOte library');
}
update(data) {
try {
this.autorestoring = false;
this.waiting_for_update = false;
if (data.type === 'error') {
return this.error(data.message);
}
if (data.type === 'pass') {
return;
}
if (data.type === 'retry') {
setTimeout(() => this.send_event({ type: 'refresh' }), 2000);
return;
}
if (data.type !== 'update') {
return this.error(`Unknown update type: ${data.type}`);
}
if (data.gen === this.generation) {
this.log(`Ignoring repeated generation number: ${data.gen}`);
return;
}
this.generation = data.gen;
if (this.disabled) {
this.disable(false);
}
// Now handle all the state updates
if (data.input) {
this.cancel_inputs(data.input);
}
if (data.windows) {
this.update_windows(data.windows);
}
if (data.content) {
this.update_content(data.content);
}
if (data.input) {
this.update_inputs(data.input);
}
if (data.timer !== undefined) {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
if (data.timer) {
this.timer = setInterval(() => this.ontimer(), data.timer);
}
}
if (data.specialinput) {
this.handle_specialinput(data.specialinput);
}
// Disable everything if requested
this.disabled = false;
if (data.disable || data.specialinput) {
this.disable(true);
}
if (data.autorestore) {
this.autorestoring = true;
this.autorestore(data.autorestore);
}
// Page background colour
if (typeof data.page_margin_bg !== 'undefined' && this.options.set_body_to_page_bg) {
this.set_page_bg(data.page_margin_bg);
}
if (data.disable) {
this.exit();
}
}
catch (err) {
this.error(err);
}
}
warning(msg) {
console.warn(msg);
}
// AsyncGlk specific implementation methods
autorestore(data) { }
capabilities() {
return ['timer'];
}
exit() { }
handle_specialinput(data) {
if (data.type === 'fileref_prompt') {
const replyfunc = (ref) => this.send_event({
type: 'specialresponse',
response: 'fileref_prompt',
value: ref,
});
try {
if (!this.Dialog) {
setTimeout(() => replyfunc(null), 0);
}
else {
if (this.Dialog.async) {
this.Dialog.prompt(filetype_to_extension(data.filetype), data.filemode !== 'read').then(filename => {
replyfunc(filename ? { filename } : null);
});
}
else {
this.Dialog.open(data.filemode !== 'read', data.filetype, data.gameid, replyfunc);
}
}
}
catch (ex) {
this.log(`Unable to open file dialog: ${ex}`);
/* Return a failure. But we don't want to call send_response before
glkote_update has finished, so we defer the reply slightly. */
setTimeout(() => replyfunc(null), 0);
}
}
else {
this.error(`Request for unknown special input type: ${data.type}`);
}
}
ontimer() {
if (!this.disabled && this.timer) {
this.send_event({ type: 'timer' });
}
}
save_allstate() { }
send_event(ev) {
if (this.disabled && ev.type !== 'specialresponse') {
return;
}
if (this.waiting_for_update) {
console.log('Trying to send event when waiting for input', ev);
return;
}
this.waiting_for_update = true;
ev.gen = this.generation;
switch (ev.type) {
case 'arrange':
ev.metrics = this.current_metrics;
break;
case 'init':
ev.metrics = this.current_metrics;
ev.support = this.capabilities();
break;
case 'specialresponse':
ev.response = 'fileref_prompt';
break;
}
this.accept_func(ev);
}
set_page_bg(colour) { }
}