|
|
|
const $ = require('../lib/chibi')
|
|
|
|
const { encode2B } = require('../utils')
|
|
|
|
|
|
|
|
/**
|
|
|
|
* User input
|
|
|
|
*
|
|
|
|
* --- Rx messages: ---
|
|
|
|
* S - screen content (binary encoding of the entire screen with simple compression)
|
|
|
|
* T - text labels - Title and buttons, \0x01-separated
|
|
|
|
* B - beep
|
|
|
|
* . - heartbeat
|
|
|
|
*
|
|
|
|
* --- Tx messages ---
|
|
|
|
* s - string
|
|
|
|
* b - action button
|
|
|
|
* p - mb press
|
|
|
|
* r - mb release
|
|
|
|
* m - mouse move
|
|
|
|
*/
|
|
|
|
module.exports = function (conn, screen) {
|
|
|
|
// handle for input object
|
|
|
|
let input
|
|
|
|
|
|
|
|
const KEY_NAMES = {
|
|
|
|
0x03: 'Cancel',
|
|
|
|
0x06: 'Help',
|
|
|
|
0x08: 'Backspace',
|
|
|
|
0x09: 'Tab',
|
|
|
|
0x0C: 'Clear',
|
|
|
|
0x0D: 'Enter',
|
|
|
|
0x10: 'Shift',
|
|
|
|
0x11: 'Control',
|
|
|
|
0x12: 'Alt',
|
|
|
|
0x13: 'Pause',
|
|
|
|
0x14: 'CapsLock',
|
|
|
|
0x1B: 'Escape',
|
|
|
|
0x20: ' ',
|
|
|
|
0x21: 'PageUp',
|
|
|
|
0x22: 'PageDown',
|
|
|
|
0x23: 'End',
|
|
|
|
0x24: 'Home',
|
|
|
|
0x25: 'ArrowLeft',
|
|
|
|
0x26: 'ArrowUp',
|
|
|
|
0x27: 'ArrowRight',
|
|
|
|
0x28: 'ArrowDown',
|
|
|
|
0x29: 'Select',
|
|
|
|
0x2A: 'Print',
|
|
|
|
0x2B: 'Execute',
|
|
|
|
0x2C: 'PrintScreen',
|
|
|
|
0x2D: 'Insert',
|
|
|
|
0x2E: 'Delete',
|
|
|
|
0x3A: ':',
|
|
|
|
0x3B: ';',
|
|
|
|
0x3C: '<',
|
|
|
|
0x3D: '=',
|
|
|
|
0x3E: '>',
|
|
|
|
0x3F: '?',
|
|
|
|
0x40: '@',
|
|
|
|
0x5B: 'Meta',
|
|
|
|
0x5C: 'Meta',
|
|
|
|
0x5D: 'ContextMenu',
|
|
|
|
0x6A: 'Numpad*',
|
|
|
|
0x6B: 'Numpad+',
|
|
|
|
0x6D: 'Numpad-',
|
|
|
|
0x6E: 'Numpad.',
|
|
|
|
0x6F: 'Numpad/',
|
|
|
|
0x90: 'NumLock',
|
|
|
|
0x91: 'ScrollLock',
|
|
|
|
0xA0: '^',
|
|
|
|
0xA1: '!',
|
|
|
|
0xA2: '"',
|
|
|
|
0xA3: '#',
|
|
|
|
0xA4: '$',
|
|
|
|
0xA5: '%',
|
|
|
|
0xA6: '&',
|
|
|
|
0xA7: '_',
|
|
|
|
0xA8: '(',
|
|
|
|
0xA9: ')',
|
|
|
|
0xAA: '*',
|
|
|
|
0xAB: '+',
|
|
|
|
0xAC: '|',
|
|
|
|
0xAD: '-',
|
|
|
|
0xAE: '{',
|
|
|
|
0xAF: '}',
|
|
|
|
0xB0: '~',
|
|
|
|
0xBA: ';',
|
|
|
|
0xBB: '=',
|
|
|
|
0xBC: 'Numpad,',
|
|
|
|
0xBD: '-',
|
|
|
|
0xBE: 'Numpad,',
|
|
|
|
0xC0: '`',
|
|
|
|
0xC2: 'Numpad,',
|
|
|
|
0xDB: '[',
|
|
|
|
0xDC: '\\',
|
|
|
|
0xDD: ']',
|
|
|
|
0xDE: "'",
|
|
|
|
0xE0: 'Meta'
|
|
|
|
}
|
|
|
|
// numbers 0-9
|
|
|
|
for (let i = 0x30; i <= 0x39; i++) KEY_NAMES[i] = String.fromCharCode(i)
|
|
|
|
// characters A-Z
|
|
|
|
for (let i = 0x41; i <= 0x5A; i++) KEY_NAMES[i] = String.fromCharCode(i)
|
|
|
|
// function F1-F20
|
|
|
|
for (let i = 0x70; i <= 0x83; i++) KEY_NAMES[i] = `F${i - 0x70 + 1}`
|
|
|
|
// numpad 0-9
|
|
|
|
for (let i = 0x60; i <= 0x69; i++) KEY_NAMES[i] = `Numpad${i - 0x60}`
|
|
|
|
|
|
|
|
let cfg = {
|
|
|
|
np_alt: false, // Application Numpad Mode
|
|
|
|
cu_alt: false, // Application Cursors Mode
|
|
|
|
fn_alt: false, // SS3 function keys mode
|
|
|
|
mt_click: false, // Mouse click tracking
|
|
|
|
mt_move: false, // Mouse move tracking
|
|
|
|
no_keys: false, // Suppress any key / clipboard event intercepting
|
|
|
|
crlf_mode: false, // Enter sends CR LF
|
|
|
|
all_fn: false // Capture also F5, F11 and F12
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Fn alt choice for key message */
|
|
|
|
const fa = (alt, normal) => cfg.fn_alt ? alt : normal
|
|
|
|
|
|
|
|
/** Cursor alt choice for key message */
|
|
|
|
const ca = (alt, normal) => cfg.cu_alt ? alt : normal
|
|
|
|
|
|
|
|
/** Numpad alt choice for key message */
|
|
|
|
const na = (alt, normal) => cfg.np_alt ? alt : normal
|
|
|
|
|
|
|
|
const keymap = {
|
|
|
|
/* eslint-disable key-spacing */
|
|
|
|
'Backspace': '\x08',
|
|
|
|
'Tab': '\x09',
|
|
|
|
'Enter': () => cfg.crlf_mode ? '\x0d\x0a' : '\x0d',
|
|
|
|
'Control+Enter': '\x0a',
|
|
|
|
'Escape': '\x1b',
|
|
|
|
'ArrowUp': () => ca('\x1bOA', '\x1b[A'),
|
|
|
|
'ArrowDown': () => ca('\x1bOB', '\x1b[B'),
|
|
|
|
'ArrowRight': () => ca('\x1bOC', '\x1b[C'),
|
|
|
|
'ArrowLeft': () => ca('\x1bOD', '\x1b[D'),
|
|
|
|
'Home': () => ca('\x1bOH', fa('\x1b[H', '\x1b[1~')),
|
|
|
|
'Insert': '\x1b[2~',
|
|
|
|
'Delete': '\x1b[3~',
|
|
|
|
'End': () => ca('\x1bOF', fa('\x1b[F', '\x1b[4~')),
|
|
|
|
'PageUp': '\x1b[5~',
|
|
|
|
'PageDown': '\x1b[6~',
|
|
|
|
'F1': () => fa('\x1bOP', '\x1b[11~'),
|
|
|
|
'F2': () => fa('\x1bOQ', '\x1b[12~'),
|
|
|
|
'F3': () => fa('\x1bOR', '\x1b[13~'),
|
|
|
|
'F4': () => fa('\x1bOS', '\x1b[14~'),
|
|
|
|
'F5': '\x1b[15~', // note the disconnect
|
|
|
|
'F6': '\x1b[17~',
|
|
|
|
'F7': '\x1b[18~',
|
|
|
|
'F8': '\x1b[19~',
|
|
|
|
'F9': '\x1b[20~',
|
|
|
|
'F10': '\x1b[21~', // note the disconnect
|
|
|
|
'F11': '\x1b[23~',
|
|
|
|
'F12': '\x1b[24~',
|
|
|
|
'Shift+F1': () => fa('\x1bO1;2P', '\x1b[25~'),
|
|
|
|
'Shift+F2': () => fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect
|
|
|
|
'Shift+F3': () => fa('\x1bO1;2R', '\x1b[28~'),
|
|
|
|
'Shift+F4': () => fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect
|
|
|
|
'Shift+F5': () => fa('\x1b[15;2~', '\x1b[31~'),
|
|
|
|
'Shift+F6': () => fa('\x1b[17;2~', '\x1b[32~'),
|
|
|
|
'Shift+F7': () => fa('\x1b[18;2~', '\x1b[33~'),
|
|
|
|
'Shift+F8': () => fa('\x1b[19;2~', '\x1b[34~'),
|
|
|
|
'Shift+F9': () => fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is?
|
|
|
|
'Shift+F10': () => fa('\x1b[21;2~', '\x1b[36~'),
|
|
|
|
'Shift+F11': () => fa('\x1b[22;2~', '\x1b[37~'),
|
|
|
|
'Shift+F12': () => fa('\x1b[23;2~', '\x1b[38~'),
|
|
|
|
'Numpad0': () => na('\x1bOp', '0'),
|
|
|
|
'Numpad1': () => na('\x1bOq', '1'),
|
|
|
|
'Numpad2': () => na('\x1bOr', '2'),
|
|
|
|
'Numpad3': () => na('\x1bOs', '3'),
|
|
|
|
'Numpad4': () => na('\x1bOt', '4'),
|
|
|
|
'Numpad5': () => na('\x1bOu', '5'),
|
|
|
|
'Numpad6': () => na('\x1bOv', '6'),
|
|
|
|
'Numpad7': () => na('\x1bOw', '7'),
|
|
|
|
'Numpad8': () => na('\x1bOx', '8'),
|
|
|
|
'Numpad9': () => na('\x1bOy', '9'),
|
|
|
|
'Numpad*': () => na('\x1bOR', '*'),
|
|
|
|
'Numpad+': () => na('\x1bOl', '+'),
|
|
|
|
'Numpad-': () => na('\x1bOS', '-'),
|
|
|
|
'Numpad.': () => na('\x1bOn', '.'),
|
|
|
|
'Numpad/': () => na('\x1bOQ', '/'),
|
|
|
|
// we don't implement numlock key (should change in numpad_alt mode,
|
|
|
|
// but it's even more useless than the rest and also has the side
|
|
|
|
// effect of changing the user's numlock state)
|
|
|
|
|
|
|
|
// shortcuts
|
|
|
|
'Control+]': '\x1b', // alternate way to enter ESC
|
|
|
|
'Control+\\': '\x1c',
|
|
|
|
'Control+[': '\x1d',
|
|
|
|
'Control+^': '\x1e',
|
|
|
|
'Control+_': '\x1f',
|
|
|
|
|
|
|
|
// extra controls
|
|
|
|
'Control+ArrowLeft': '\x1f[1;5D',
|
|
|
|
'Control+ArrowRight': '\x1f[1;5C',
|
|
|
|
'Control+ArrowUp': '\x1f[1;5A',
|
|
|
|
'Control+ArrowDown': '\x1f[1;5B',
|
|
|
|
'Control+Home': '\x1f[1;5H',
|
|
|
|
'Control+End': '\x1f[1;5F',
|
|
|
|
|
|
|
|
// extra shift controls
|
|
|
|
'Shift+ArrowLeft': '\x1f[1;2D',
|
|
|
|
'Shift+ArrowRight': '\x1f[1;2C',
|
|
|
|
'Shift+ArrowUp': '\x1f[1;2A',
|
|
|
|
'Shift+ArrowDown': '\x1f[1;2B',
|
|
|
|
'Shift+Home': '\x1f[1;2H',
|
|
|
|
'Shift+End': '\x1f[1;2F',
|
|
|
|
|
|
|
|
// macOS text editing commands
|
|
|
|
'Alt+ArrowLeft': '\x1bb', // ⌥← to go back a word (^[b)
|
|
|
|
'Alt+ArrowRight': '\x1bf', // ⌥→ to go forward one word (^[f)
|
|
|
|
'Meta+ArrowLeft': '\x01', // ⌘← to go to the beginning of a line (^A)
|
|
|
|
'Meta+ArrowRight': '\x05', // ⌘→ to go to the end of a line (^E)
|
|
|
|
'Alt+Backspace': '\x17', // ⌥⌫ to delete a word (^W)
|
|
|
|
'Meta+Backspace': '\x15', // ⌘⌫ to delete to the beginning of a line (^U)
|
|
|
|
|
|
|
|
// copy to clipboard
|
|
|
|
'Control+Shift+C' () {
|
|
|
|
screen.copySelectionToClipboard()
|
|
|
|
},
|
|
|
|
'Control+Insert' () {
|
|
|
|
screen.copySelectionToClipboard()
|
|
|
|
},
|
|
|
|
|
|
|
|
// toggle debug mode
|
|
|
|
'Control+F12' () {
|
|
|
|
screen.window.debug ^= 1
|
|
|
|
}
|
|
|
|
/* eslint-enable key-spacing */
|
|
|
|
}
|
|
|
|
|
|
|
|
// ctrl+[A-Z] sent as simple low ASCII codes
|
|
|
|
for (let i = 1; i <= 26; i++) {
|
|
|
|
keymap[`Control+${String.fromCharCode(0x40 + i)}`] = String.fromCharCode(i)
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Send a literal message */
|
|
|
|
function sendString (str) {
|
|
|
|
return conn.send('s' + str)
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Send a button event */
|
|
|
|
function sendButton (n) {
|
|
|
|
conn.send('b' + String.fromCharCode(n))
|
|
|
|
}
|
|
|
|
|
|
|
|
const shouldAcceptEvent = function () {
|
|
|
|
if (cfg.no_keys) return false
|
|
|
|
if (document.activeElement instanceof window.HTMLTextAreaElement) return false
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
const keyBlacklist = [
|
|
|
|
'F5', 'F11', 'F12', 'Shift+F5'
|
|
|
|
]
|
|
|
|
|
|
|
|
let softModifiers = {
|
|
|
|
alt: false,
|
|
|
|
ctrl: false,
|
|
|
|
meta: false,
|
|
|
|
shift: false
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleKeyDown = function (e) {
|
|
|
|
if (!shouldAcceptEvent()) return
|
|
|
|
|
|
|
|
let modifiers = []
|
|
|
|
// sorted alphabetically
|
|
|
|
if (e.altKey || softModifiers.alt) modifiers.push('Alt')
|
|
|
|
if (e.ctrlKey || softModifiers.ctrl) modifiers.push('Control')
|
|
|
|
if (e.metaKey || softModifiers.meta) modifiers.push('Meta')
|
|
|
|
if (e.shiftKey || softModifiers.shift) modifiers.push('Shift')
|
|
|
|
|
|
|
|
let key = KEY_NAMES[e.which] || e.key
|
|
|
|
|
|
|
|
// ignore clipboard events
|
|
|
|
if ((e.ctrlKey || e.metaKey) && key === 'V') return
|
|
|
|
|
|
|
|
let binding = null
|
|
|
|
|
|
|
|
for (let name in keymap) {
|
|
|
|
let itemModifiers = name.split('+')
|
|
|
|
let itemKey = itemModifiers.pop()
|
|
|
|
|
|
|
|
if (itemKey === key && itemModifiers.sort().join() === modifiers.join()) {
|
|
|
|
if (keyBlacklist.includes(name) && !cfg.all_fn) continue
|
|
|
|
binding = keymap[name]
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (binding) {
|
|
|
|
if (binding instanceof Function) binding = binding()
|
|
|
|
e.preventDefault()
|
|
|
|
if (typeof binding === 'string') {
|
|
|
|
sendString(binding)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Bind/rebind key messages */
|
|
|
|
function initKeys ({ allFn }) {
|
|
|
|
// This takes care of text characters typed
|
|
|
|
window.addEventListener('keypress', function (evt) {
|
|
|
|
if (!shouldAcceptEvent()) return
|
|
|
|
if (evt.ctrlKey || evt.metaKey) return
|
|
|
|
|
|
|
|
let str = ''
|
|
|
|
if (evt.key && evt.key.length === 1) str = evt.key
|
|
|
|
else if (evt.which && evt.which !== 229) str = String.fromCodePoint(evt.which)
|
|
|
|
|
|
|
|
if (str.length > 0 && str.charCodeAt(0) >= 32) {
|
|
|
|
// prevent space from scrolling
|
|
|
|
if (evt.which === 32) evt.preventDefault()
|
|
|
|
sendString(str)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
|
|
window.addEventListener('copy', e => {
|
|
|
|
if (!shouldAcceptEvent()) return
|
|
|
|
let selectedText = screen.getSelectedText()
|
|
|
|
if (selectedText) {
|
|
|
|
e.preventDefault()
|
|
|
|
e.clipboardData.setData('text/plain', selectedText)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
window.addEventListener('paste', e => {
|
|
|
|
if (!shouldAcceptEvent()) return
|
|
|
|
e.preventDefault()
|
|
|
|
let string = e.clipboardData.getData('text/plain')
|
|
|
|
if (string.includes('\n') || string.length > 90) {
|
|
|
|
if (!input.termUpload) console.error('input.termUpload is undefined')
|
|
|
|
input.termUpload.setContent(string)
|
|
|
|
input.termUpload.open()
|
|
|
|
} else {
|
|
|
|
// simple string, just paste it
|
|
|
|
if (screen.bracketedPaste) string = `\x1b[200~${string}\x1b[201~`
|
|
|
|
sendString(string)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
cfg.all_fn = allFn
|
|
|
|
}
|
|
|
|
|
|
|
|
// mouse button states
|
|
|
|
let mb1 = 0
|
|
|
|
let mb2 = 0
|
|
|
|
let mb3 = 0
|
|
|
|
|
|
|
|
/** Init the Input module */
|
|
|
|
function init (opts) {
|
|
|
|
initKeys(opts)
|
|
|
|
|
|
|
|
// Button presses
|
|
|
|
$('#action-buttons button').forEach(s => {
|
|
|
|
s.addEventListener('click', function (evt) {
|
|
|
|
sendButton(+this.dataset['n'])
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
// global mouse state tracking - for motion reporting
|
|
|
|
window.addEventListener('mousedown', evt => {
|
|
|
|
if (evt.button === 0) mb1 = 1
|
|
|
|
if (evt.button === 1) mb2 = 1
|
|
|
|
if (evt.button === 2) mb3 = 1
|
|
|
|
})
|
|
|
|
|
|
|
|
window.addEventListener('mouseup', evt => {
|
|
|
|
if (evt.button === 0) mb1 = 0
|
|
|
|
if (evt.button === 1) mb2 = 0
|
|
|
|
if (evt.button === 2) mb3 = 0
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// record modifier keys
|
|
|
|
// bits: Meta, Alt, Shift, Ctrl
|
|
|
|
let modifiers = 0b0000
|
|
|
|
|
|
|
|
window.addEventListener('keydown', e => {
|
|
|
|
if (e.ctrlKey) modifiers |= 1
|
|
|
|
if (e.shiftKey) modifiers |= 2
|
|
|
|
if (e.altKey) modifiers |= 4
|
|
|
|
if (e.metaKey) modifiers |= 8
|
|
|
|
})
|
|
|
|
window.addEventListener('keyup', e => {
|
|
|
|
modifiers = 0
|
|
|
|
if (e.ctrlKey) modifiers |= 1
|
|
|
|
if (e.shiftKey) modifiers |= 2
|
|
|
|
if (e.altKey) modifiers |= 4
|
|
|
|
if (e.metaKey) modifiers |= 8
|
|
|
|
})
|
|
|
|
|
|
|
|
/** Prepare modifiers byte for mouse message */
|
|
|
|
function packModifiersForMouse () {
|
|
|
|
return modifiers
|
|
|
|
}
|
|
|
|
|
|
|
|
input = {
|
|
|
|
/** Init the Input module */
|
|
|
|
init,
|
|
|
|
|
|
|
|
/** Send a literal string message */
|
|
|
|
sendString,
|
|
|
|
|
|
|
|
/** Enable alternate key modes (cursors, numpad, fn) */
|
|
|
|
setAlts: function (cu, np, fn, crlf) {
|
|
|
|
if (cfg.cu_alt !== cu || cfg.np_alt !== np || cfg.fn_alt !== fn || cfg.crlf_mode !== crlf) {
|
|
|
|
cfg.cu_alt = cu
|
|
|
|
cfg.np_alt = np
|
|
|
|
cfg.fn_alt = fn
|
|
|
|
cfg.crlf_mode = crlf
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
setMouseMode (click, move) {
|
|
|
|
cfg.mt_click = click
|
|
|
|
cfg.mt_move = move
|
|
|
|
},
|
|
|
|
|
|
|
|
// Mouse events
|
|
|
|
onMouseMove (x, y) {
|
|
|
|
if (!cfg.mt_move) return
|
|
|
|
const b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0
|
|
|
|
const m = packModifiersForMouse()
|
|
|
|
conn.send('m' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
|
|
|
|
},
|
|
|
|
|
|
|
|
onMouseDown (x, y, b) {
|
|
|
|
if (!cfg.mt_click) return
|
|
|
|
if (b > 3 || b < 1) return
|
|
|
|
const m = packModifiersForMouse()
|
|
|
|
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
|
|
|
|
},
|
|
|
|
|
|
|
|
onMouseUp (x, y, b) {
|
|
|
|
if (!cfg.mt_click) return
|
|
|
|
if (b > 3 || b < 1) return
|
|
|
|
const m = packModifiersForMouse()
|
|
|
|
conn.send('r' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
|
|
|
|
// console.log("B ",b," M ",m);
|
|
|
|
},
|
|
|
|
|
|
|
|
onMouseWheel (x, y, dir) {
|
|
|
|
if (!cfg.mt_click) return
|
|
|
|
// -1 ... btn 4 (away from user)
|
|
|
|
// +1 ... btn 5 (towards user)
|
|
|
|
const m = packModifiersForMouse()
|
|
|
|
const b = (dir < 0 ? 4 : 5)
|
|
|
|
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m))
|
|
|
|
// console.log("B ",b," M ",m);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prevent capturing keys. This is used for text input
|
|
|
|
* modals on the terminal screen
|
|
|
|
*/
|
|
|
|
blockKeys (yes) {
|
|
|
|
cfg.no_keys = yes
|
|
|
|
},
|
|
|
|
|
|
|
|
handleKeyDown,
|
|
|
|
softModifiers
|
|
|
|
}
|
|
|
|
return input
|
|
|
|
}
|