/* Atlus — Custom on-screen keyboard for terminal */ (function () { 'use strict'; const MODES = { keys: 'Keys', fn: 'Fn', nav: 'Nav', sym: 'Sym', }; const QUICK_KEYS = ['$', '#', '>', '-', '_', '.', '/', '|', '~']; const QWERTY_ROWS = [ ['1','2','3','4','5','6','7','8','9','0'], ['q','w','e','r','t','y','u','i','o','p'], ['a','s','d','f','g','h','j','k','l'], ['z','x','c','v','b','n','m'], ]; const SYM_ROWS = [ ['!','@','#','$','%','^','&','*','(',')'], ['-','_','=','+','[',']','{','}','\\','|'], [';',':','\'','"',',','.','/','?','`','~'], ['<','>','©','®','…','§','±','×'], ]; const FN_KEYS = ['F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12']; const NAV_KEYS = [ {label: '↑', seq: '\x1b[A'}, {label: '↓', seq: '\x1b[B'}, {label: '←', seq: '\x1b[D'}, {label: '→', seq: '\x1b[C'}, {label: 'Home', seq: '\x1b[H'}, {label: 'End', seq: '\x1b[F'}, {label: 'PgUp', seq: '\x1b[5~'}, {label: 'PgDn', seq: '\x1b[6~'}, {label: 'Del', seq: '\x1b[3~'}, {label: 'Ins', seq: '\x1b[2~'}, ]; class OnScreenKeyboard { constructor() { this.el = null; this.terminal = null; // set by terminal app this.mode = 'keys'; this.shift = false; this.ctrl = false; this.alt = false; // States: null=off, 'armed'=one-shot, 'locked'=persistent this.ctrlState = null; this.altState = null; this.shiftState = null; this.visible = true; } create() { this.el = document.createElement('div'); this.el.className = 'osk'; this._render(); return this.el; } toggle() { this.visible = !this.visible; if (this.el) this.el.classList.toggle('hidden', !this.visible); } setTerminal(term) { this.terminal = term; } _send(data) { if (!this.terminal) return; let out = data; // Apply modifiers if (this.ctrlState && data.length === 1) { const code = data.toLowerCase().charCodeAt(0); if (code >= 97 && code <= 122) { out = String.fromCharCode(code - 96); } } else if (this.altState && data.length === 1) { out = '\x1b' + data; } if (this.shiftState && data.length === 1 && data >= 'a' && data <= 'z') { out = data.toUpperCase(); } this.terminal.input(out); // Clear armed (one-shot) modifiers if (this.ctrlState === 'armed') { this.ctrlState = null; } if (this.altState === 'armed') { this.altState = null; } if (this.shiftState === 'armed') { this.shiftState = null; } this._updateModifiers(); } _toggleMod(mod) { const key = mod + 'State'; if (this[key] === null) { this[key] = 'armed'; } else if (this[key] === 'armed') { this[key] = 'locked'; } else { this[key] = null; } this._updateModifiers(); } _updateModifiers() { if (!this.el) return; this.el.querySelectorAll('.osk-key.mod').forEach(key => { const mod = key.dataset.mod; if (!mod) return; const state = this[mod + 'State']; key.classList.toggle('armed', state === 'armed'); key.classList.toggle('locked', state === 'locked'); }); } _render() { this.el.innerHTML = ''; // Quick row const quick = document.createElement('div'); quick.className = 'osk-quick'; QUICK_KEYS.forEach(k => { const btn = this._makeKey(k, () => this._send(k)); quick.appendChild(btn); }); this.el.appendChild(quick); // Mode tabs const modes = document.createElement('div'); modes.className = 'osk-modes'; for (const [id, label] of Object.entries(MODES)) { const tab = document.createElement('button'); tab.className = 'osk-mode-tab' + (id === this.mode ? ' active' : ''); tab.textContent = label; tab.addEventListener('click', () => { this.mode = id; this._render(); }); modes.appendChild(tab); } this.el.appendChild(modes); // Mode content if (this.mode === 'keys') this._renderQwerty(); else if (this.mode === 'fn') this._renderFn(); else if (this.mode === 'nav') this._renderNav(); else if (this.mode === 'sym') this._renderSym(); } _makeKey(label, handler, className = '') { const btn = document.createElement('button'); btn.className = 'osk-key ' + className; btn.textContent = label; let touchFired = false; btn.addEventListener('touchstart', (e) => { e.preventDefault(); touchFired = true; handler(); }); btn.addEventListener('click', (e) => { e.preventDefault(); if (touchFired) { touchFired = false; return; } handler(); }); return btn; } _renderQwerty() { // Modifier row const modRow = document.createElement('div'); modRow.className = 'osk-row'; modRow.appendChild(this._makeKey('Esc', () => this._send('\x1b'), 'mod')); modRow.appendChild(this._makeKey('Tab', () => this._send('\t'), 'mod')); const ctrlKey = this._makeKey('Ctrl', () => this._toggleMod('ctrl'), 'mod'); ctrlKey.dataset.mod = 'ctrl'; modRow.appendChild(ctrlKey); const altKey = this._makeKey('Alt', () => this._toggleMod('alt'), 'mod'); altKey.dataset.mod = 'alt'; modRow.appendChild(altKey); this.el.appendChild(modRow); // QWERTY rows QWERTY_ROWS.forEach((row, i) => { const rowEl = document.createElement('div'); rowEl.className = 'osk-row'; if (i === 3) { // Shift key const shiftKey = this._makeKey('Shift', () => this._toggleMod('shift'), 'mod wide'); shiftKey.dataset.mod = 'shift'; rowEl.appendChild(shiftKey); } row.forEach(k => { rowEl.appendChild(this._makeKey(k, () => this._send(k))); }); if (i === 3) { rowEl.appendChild(this._makeKey('⌫', () => this._send('\x7f'), 'wide')); } this.el.appendChild(rowEl); }); // Space row const spaceRow = document.createElement('div'); spaceRow.className = 'osk-row'; spaceRow.appendChild(this._makeKey('Paste', () => this._paste(), 'mod')); spaceRow.appendChild(this._makeKey('Space', () => this._send(' '), 'space')); spaceRow.appendChild(this._makeKey('Enter', () => this._send('\r'), 'wide')); this.el.appendChild(spaceRow); this._updateModifiers(); } _renderSym() { SYM_ROWS.forEach(row => { const rowEl = document.createElement('div'); rowEl.className = 'osk-row'; row.forEach(k => { rowEl.appendChild(this._makeKey(k, () => this._send(k))); }); this.el.appendChild(rowEl); }); } _renderFn() { const scroll = document.createElement('div'); scroll.className = 'osk-fn-scroll'; FN_KEYS.forEach((k, i) => { const seq = `\x1bO${String.fromCharCode(80 + i)}`; // F1-F4 use \x1bOP-S, F5+ use \x1b[15~, etc. const seqs = [ '\x1bOP','\x1bOQ','\x1bOR','\x1bOS', '\x1b[15~','\x1b[17~','\x1b[18~','\x1b[19~', '\x1b[20~','\x1b[21~','\x1b[23~','\x1b[24~', ]; scroll.appendChild(this._makeKey(k, () => this._send(seqs[i]))); }); this.el.appendChild(scroll); // Common ctrl combos const combos = document.createElement('div'); combos.className = 'osk-row'; const ctrlKeys = [ {label: 'Ctrl+C', seq: '\x03'}, {label: 'Ctrl+D', seq: '\x04'}, {label: 'Ctrl+Z', seq: '\x1a'}, {label: 'Ctrl+A', seq: '\x01'}, {label: 'Ctrl+L', seq: '\x0c'}, {label: 'Ctrl+R', seq: '\x12'}, ]; ctrlKeys.forEach(k => { combos.appendChild(this._makeKey(k.label, () => this._send(k.seq), 'mod')); }); this.el.appendChild(combos); } _renderNav() { const nav = document.createElement('div'); nav.className = 'osk-nav'; NAV_KEYS.forEach(k => { nav.appendChild(this._makeKey(k.label, () => this._send(k.seq))); }); this.el.appendChild(nav); } async _paste() { try { const text = await navigator.clipboard.readText(); if (text && this.terminal) { this.terminal.input(text); } } catch (e) { // Clipboard access denied } } } window.Atlus.keyboard = new OnScreenKeyboard(); })();