mirror of
https://github.com/He4eT/cheap-glkote.git
synced 2026-05-05 00:47:28 +00:00
Dirty initial commit
This commit is contained in:
commit
eab87d32a9
13 changed files with 7039 additions and 0 deletions
106
src/cheapGlkOte.js
Normal file
106
src/cheapGlkOte.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
const GlkOte = require('./glkOte/glkote-term')
|
||||
|
||||
class CheapGlkOte extends GlkOte {
|
||||
constructor(handlers) {
|
||||
super()
|
||||
|
||||
this.window = null
|
||||
this.current_input_type = null
|
||||
|
||||
this.handlers = handlers
|
||||
}
|
||||
|
||||
sendFn(message) {
|
||||
this.send_response(
|
||||
this.current_input_type,
|
||||
this.window,
|
||||
message)
|
||||
this.current_input_type = null
|
||||
}
|
||||
|
||||
init(iface) {
|
||||
/* Only one window can be opened */
|
||||
const glk_window_open = iface.Glk.glk_window_open
|
||||
iface.Glk.glk_window_open = (splitwin, ...args) =>
|
||||
splitwin
|
||||
? null
|
||||
: glk_window_open(splitwin, ...args)
|
||||
|
||||
this.handlers.onInit()
|
||||
super.init(iface)
|
||||
}
|
||||
|
||||
update_inputs(data) {
|
||||
if (!data.length) return null
|
||||
|
||||
const {type} = data[0]
|
||||
if (['char', 'line'].includes(type)) {
|
||||
this.current_input_type = type
|
||||
this.handlers.onUpdateInputs(type)
|
||||
}
|
||||
}
|
||||
|
||||
accept_specialinput(data) {
|
||||
if (data.type === 'fileref_prompt') {
|
||||
const callback = ref =>
|
||||
this.send_response(
|
||||
'specialresponse', null, 'fileref_prompt', ref)
|
||||
|
||||
this.interface.Dialog.open(
|
||||
data.filemode !== 'read',
|
||||
data.filetype,
|
||||
data.gameid,
|
||||
callback)
|
||||
} else {
|
||||
this.error(
|
||||
'Request for unknown special input type: ' + data.type)
|
||||
}
|
||||
}
|
||||
|
||||
update_content(messages) {
|
||||
const filtered =
|
||||
messages.filter(content =>
|
||||
content.id === this.window.id)[0]
|
||||
|
||||
this.handlers.onUpdateContent(filtered)
|
||||
}
|
||||
|
||||
exit() {
|
||||
this.handlers.onExit()
|
||||
super.exit()
|
||||
}
|
||||
|
||||
cancel_inputs(data) {
|
||||
if (data.length === 0) {
|
||||
this.current_input_type = null
|
||||
this.handlers.onUpdateInputs(null)
|
||||
}
|
||||
}
|
||||
|
||||
disable(disable) {
|
||||
this.disabled = disable
|
||||
this.handlers.onDisable(disable)
|
||||
}
|
||||
|
||||
update_windows(data) {
|
||||
data.forEach(win => {
|
||||
if (win.type === 'buffer') {
|
||||
this.window = win
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
log(msg) {
|
||||
console.log(`[log]: ${msg}`)
|
||||
}
|
||||
|
||||
warning(msg) {
|
||||
console.warn(`[warning]: ${msg}`)
|
||||
}
|
||||
|
||||
error(message) {
|
||||
console.error(`[error]: ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CheapGlkOte
|
||||
36
src/fakeDialog.js
Normal file
36
src/fakeDialog.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
class FakeDialog {
|
||||
constructor(handlers) {
|
||||
this.streaming = false
|
||||
this.handlers = handlers
|
||||
this.path = 'fake/path'
|
||||
}
|
||||
|
||||
file_ref_exists = ref => false
|
||||
|
||||
file_construct_ref(filename, usage, gameid) {
|
||||
return {
|
||||
filename: [this.path, filename].join('/'),
|
||||
usage: usage || ''
|
||||
}
|
||||
}
|
||||
|
||||
file_read(dirent, israw) {
|
||||
console.log('fake_file_read', dirent, israw)
|
||||
return 'content'
|
||||
}
|
||||
|
||||
file_write(dirent, content, israw) {
|
||||
if (content.length === 0) return (void null)
|
||||
console.log('fake_file_write', dirent, israw, content.length)
|
||||
}
|
||||
|
||||
open(tosave, usage, gameid, callback) {
|
||||
this.handlers.onFileNameRequest(tosave, usage, callback)
|
||||
}
|
||||
|
||||
log(message) {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FakeDialog
|
||||
6396
src/glkOte/glkapi.js
Normal file
6396
src/glkOte/glkapi.js
Normal file
File diff suppressed because it is too large
Load diff
145
src/glkOte/glkote-term.js
Normal file
145
src/glkOte/glkote-term.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
class GlkOte {
|
||||
constructor() {
|
||||
this.current_metrics = null
|
||||
this.disabled = false
|
||||
this.generation = 0
|
||||
this.interface = null
|
||||
this.version = require('../../package.json').version
|
||||
}
|
||||
|
||||
measure_window() {
|
||||
return {
|
||||
buffercharheight: 1,
|
||||
buffercharwidth: 1,
|
||||
buffermarginx: 0,
|
||||
buffermarginy: 0,
|
||||
graphicsmarginx: 0,
|
||||
graphicsmarginy: 0,
|
||||
gridcharheight: 1,
|
||||
gridcharwidth: 1,
|
||||
gridmarginx: 0,
|
||||
gridmarginy: 0,
|
||||
height: 0,
|
||||
inspacingx: 0,
|
||||
inspacingy: 0,
|
||||
outspacingx: 0,
|
||||
outspacingy: 0,
|
||||
width: 0,
|
||||
}
|
||||
}
|
||||
|
||||
getinterface() {
|
||||
return this.interface
|
||||
}
|
||||
|
||||
init(iface) {
|
||||
if (!iface) {
|
||||
this.error('No game interface object has been provided.')
|
||||
}
|
||||
if (!iface.accept) {
|
||||
this.error('The game interface object must have an accept() function.')
|
||||
}
|
||||
|
||||
this.interface = iface
|
||||
this.current_metrics = this.measure_window()
|
||||
|
||||
this.send_response('init', null, this.current_metrics)
|
||||
}
|
||||
|
||||
update(data) {
|
||||
if (data.type === 'error') {
|
||||
this.error(data.message)
|
||||
}
|
||||
if (data.type === 'pass') {
|
||||
return
|
||||
}
|
||||
if (data.type !== 'update' && data.type !== 'exit') {
|
||||
this.log(`Ignoring unknown message type: ${data.type}`)
|
||||
return
|
||||
}
|
||||
if (data.gen === this.generation) {
|
||||
this.log(`Ignoring repeated generation number: ${data.gen}`)
|
||||
return
|
||||
}
|
||||
if (data.gen < this.generation) {
|
||||
this.log(
|
||||
`Ignoring out-of-order generation number: got ${data.gen}, currently at ${this.generation}`
|
||||
)
|
||||
return
|
||||
}
|
||||
this.generation = data.gen
|
||||
|
||||
if (this.disabled) {
|
||||
this.disable(false)
|
||||
}
|
||||
|
||||
/* Handle the update */
|
||||
if (data.input != null) {
|
||||
this.cancel_inputs(data.input)
|
||||
}
|
||||
if (data.windows != null) {
|
||||
this.update_windows(data.windows)
|
||||
}
|
||||
if (data.content != null && data.content.length) {
|
||||
this.update_content(data.content)
|
||||
}
|
||||
if (data.input != null) {
|
||||
this.update_inputs(data.input)
|
||||
}
|
||||
|
||||
/* Disable everything if requested */
|
||||
this.disabled = false
|
||||
if (data.disabled || data.specialinput) {
|
||||
this.disable(true)
|
||||
}
|
||||
|
||||
if (data.specialinput != null) {
|
||||
this.accept_specialinput(data.specialinput)
|
||||
}
|
||||
|
||||
/* Detach all handlers and exit */
|
||||
if (data.type === 'exit') {
|
||||
this.exit()
|
||||
}
|
||||
}
|
||||
|
||||
send_response(type, win, val, val2) {
|
||||
const res = {
|
||||
type: type,
|
||||
gen: this.generation,
|
||||
}
|
||||
|
||||
if (win) {
|
||||
res.window = win.id
|
||||
}
|
||||
|
||||
if (type === 'init' || type === 'arrange') {
|
||||
res.metrics = val
|
||||
}
|
||||
|
||||
if (type === 'init') {
|
||||
res.support = this.support()
|
||||
}
|
||||
|
||||
if (type === 'char') {
|
||||
res.value = val
|
||||
}
|
||||
|
||||
if (type === 'line') {
|
||||
res.value = val
|
||||
}
|
||||
|
||||
if (type === 'specialresponse') {
|
||||
res.response = val
|
||||
res.value = val2
|
||||
}
|
||||
|
||||
this.interface.accept(res)
|
||||
}
|
||||
|
||||
support() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GlkOte
|
||||
33
src/index.js
Normal file
33
src/index.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
const FakeDialog = require('./fakeDialog')
|
||||
const CheapGlkOte = require('./cheapGlkOte')
|
||||
|
||||
const noop = () => void null
|
||||
|
||||
const noopHandlers = [
|
||||
'onInit',
|
||||
'onUpdateContent',
|
||||
'onDisable',
|
||||
'onUpdateInputs',
|
||||
'onFileNameRequest',
|
||||
'onExit',
|
||||
'setSend'
|
||||
].reduce((acc, x) => (acc[x] = noop, acc), {})
|
||||
|
||||
module.exports = handlers_ => {
|
||||
const handlers =
|
||||
Object.assign({}, noopHandlers, handlers_)
|
||||
|
||||
const Dialog = new FakeDialog(handlers)
|
||||
const GlkOte = new CheapGlkOte(handlers)
|
||||
|
||||
const sendFn = GlkOte.sendFn.bind(GlkOte)
|
||||
|
||||
return {
|
||||
sendFn,
|
||||
glkInterface: {
|
||||
Dialog,
|
||||
GlkOte,
|
||||
Glk: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/stdio.js
Normal file
133
src/stdio.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
const readline = require('readline')
|
||||
const MuteStream = require('mute-stream')
|
||||
const ansiEscapes = require('ansi-escapes')
|
||||
|
||||
const stdin = process.stdin
|
||||
const stdout = new MuteStream()
|
||||
stdout.pipe(process.stdout)
|
||||
const rl = readline.createInterface({
|
||||
input: stdin,
|
||||
output: stdout,
|
||||
prompt: ''
|
||||
})
|
||||
|
||||
let send = _ => _
|
||||
|
||||
const setSend = fn => {
|
||||
send = fn
|
||||
}
|
||||
|
||||
const onInit = () => {
|
||||
if (stdin.isTTY) {
|
||||
stdin.setRawMode(true)
|
||||
}
|
||||
readline.emitKeypressEvents(stdin)
|
||||
rl.resume()
|
||||
}
|
||||
|
||||
const onUpdateContent = messages =>
|
||||
messages.text.forEach(({append, content}) => {
|
||||
if (!append) {
|
||||
stdout.write('\n')
|
||||
}
|
||||
|
||||
if (content) {
|
||||
content.forEach(x => {
|
||||
if (x.text === '>') return null
|
||||
|
||||
if (x.style === 'input') {
|
||||
if (stdout.isTTY) {
|
||||
stdout.write(ansiEscapes.eraseLines(2))
|
||||
stdout.write('> ')
|
||||
}
|
||||
}
|
||||
|
||||
stdout.write(
|
||||
typeof x === 'string'
|
||||
? x
|
||||
: x.text)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const onUpdateInputs = type => {
|
||||
type
|
||||
? attach_handlers(type)
|
||||
: detach_handlers()
|
||||
}
|
||||
|
||||
const onExit = () => {
|
||||
detach_handlers()
|
||||
rl.close()
|
||||
stdout.write('\n')
|
||||
}
|
||||
|
||||
const onDisable = disable =>
|
||||
disable
|
||||
? detach_handlers()
|
||||
: attach_handlers()
|
||||
|
||||
const onFileNameRequest = (tosave, usage, callback) => {
|
||||
stdout.write('\n')
|
||||
rl.question(
|
||||
'Please enter a file name (without an extension): ',
|
||||
filename => callback(filename
|
||||
? {filename, usage}
|
||||
: null))
|
||||
}
|
||||
|
||||
const handle_char_input = (str, key) => {
|
||||
const key_replacements = {
|
||||
'\x7F': 'delete',
|
||||
'\t': 'tab',
|
||||
}
|
||||
|
||||
detach_handlers()
|
||||
|
||||
// Make sure this char isn't being remembered for the next line input
|
||||
rl._line_buffer = null
|
||||
rl.line = ''
|
||||
|
||||
// Process special keys
|
||||
const res =
|
||||
key_replacements[str] ||
|
||||
str ||
|
||||
key.name.replace(/f(\d+)/, 'func$1')
|
||||
|
||||
send(res)
|
||||
}
|
||||
|
||||
const attach_handlers = type => {
|
||||
if (type === 'char') {
|
||||
stdout.mute()
|
||||
stdin.on('keypress', handle_char_input)
|
||||
}
|
||||
if (type === 'line') {
|
||||
rl.on('line', handle_line_input)
|
||||
}
|
||||
}
|
||||
|
||||
const detach_handlers = () => {
|
||||
stdin.removeListener('keypress', handle_char_input)
|
||||
rl.removeListener('line', handle_line_input)
|
||||
stdout.unmute()
|
||||
}
|
||||
|
||||
const handle_line_input = line => {
|
||||
if (stdout.isTTY) {
|
||||
stdout.write(ansiEscapes.eraseLines(1))
|
||||
}
|
||||
detach_handlers()
|
||||
|
||||
send(line)
|
||||
}
|
||||
|
||||
module.exports.handlers = {
|
||||
onInit,
|
||||
onUpdateContent,
|
||||
onDisable,
|
||||
onUpdateInputs,
|
||||
onFileNameRequest,
|
||||
onExit,
|
||||
setSend
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue