-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmobile-keys.js
More file actions
197 lines (177 loc) · 6.43 KB
/
mobile-keys.js
File metadata and controls
197 lines (177 loc) · 6.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
(function () {
'use strict';
if (!('ontouchstart' in window) && !navigator.maxTouchPoints) return;
// Wait for ttyd to initialize window.term
const waitForTerm = setInterval(() => {
if (!window.term) return;
clearInterval(waitForTerm);
init(window.term);
}, 200);
function init(term) {
const bar = document.createElement('div');
bar.id = 'mobile-keys';
bar.innerHTML = `
<div class="mk-row">
<button data-mod="ctrl" class="mod">Ctrl</button>
<button data-mod="alt" class="mod">Alt</button>
<button data-mod="shift" class="mod">Shift</button>
<button data-key="Escape">Esc</button>
<button data-key="Tab">Tab</button>
<button data-key="Enter">⏎</button>
</div>
<div class="mk-row">
<button data-combo="ctrl-b" class="combo">C-b</button>
<button data-combo="ctrl-c" class="combo">C-c</button>
<button data-combo="ctrl-d" class="combo">C-d</button>
<button data-key="ArrowUp">↑</button>
<button data-key="ArrowDown">↓</button>
<button data-key="ArrowLeft">←</button>
<button data-key="ArrowRight">→</button>
</div>
`;
document.body.appendChild(bar);
const style = document.createElement('style');
const BAR_H = 72; // approx height of two-row toolbar
style.textContent = `
html, body { height: 100%; overflow: hidden !important; margin: 0 !important; }
#mobile-keys {
position: fixed; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column; gap: 3px;
padding: 5px 3px; background: #1a1a1a;
border-top: 1px solid #444; z-index: 9999;
}
.mk-row {
display: flex; gap: 3px; justify-content: center;
}
#mobile-keys button {
flex: 1 1 0; min-width: 0;
background: #333; color: #eee; border: 1px solid #555;
border-radius: 5px; padding: 7px 2px; font-size: 12px;
text-align: center; white-space: nowrap; overflow: hidden;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
user-select: none; -webkit-user-select: none;
}
#mobile-keys button:active { background: #555; }
#mobile-keys button.mod.active {
background: #0969da; border-color: #58a6ff; color: #fff;
}
#mobile-keys button.combo {
background: #2a2a4a; border-color: #6a6aaa; color: #aaf;
}
#mobile-keys button.combo:active { background: #4a4a7a; }
`;
document.head.appendChild(style);
// Dynamically size terminal + toolbar to fit the visible viewport
// (accounts for virtual keyboard, browser chrome, etc.)
const termContainer = document.querySelector('#terminal-container') ||
document.querySelector('.xterm');
function layout() {
const vh = window.visualViewport ? window.visualViewport.height : window.innerHeight;
const vTop = window.visualViewport ? window.visualViewport.offsetTop : 0;
// Position toolbar at the bottom of the visible viewport
bar.style.bottom = (window.innerHeight - vh - vTop) + 'px';
// Constrain the terminal to visible area minus toolbar
const termH = vh - BAR_H;
if (termContainer) {
termContainer.style.height = Math.max(termH, 80) + 'px';
termContainer.style.maxHeight = Math.max(termH, 80) + 'px';
termContainer.style.overflow = 'hidden';
}
document.body.style.height = vh + 'px';
}
let resizeTimer;
function layoutAndRefit() {
layout();
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => term.fit && term.fit(), 100);
}
// Initial layout
layoutAndRefit();
setTimeout(layoutAndRefit, 300);
setTimeout(layoutAndRefit, 800);
// React to virtual keyboard open/close and orientation changes
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', layoutAndRefit);
window.visualViewport.addEventListener('scroll', layoutAndRefit);
}
window.addEventListener('resize', layoutAndRefit);
const mods = { ctrl: false, alt: false, shift: false };
// Find the textarea xterm.js uses for keyboard input
function getTextarea() {
return document.querySelector('.xterm-helper-textarea');
}
function clearMods() {
mods.ctrl = mods.alt = mods.shift = false;
bar.querySelectorAll('.mod').forEach(b => b.classList.remove('active'));
}
function sendKey(key) {
const ta = getTextarea();
if (ta) ta.focus();
const opts = {
key,
bubbles: true,
cancelable: true,
ctrlKey: mods.ctrl,
altKey: mods.alt,
shiftKey: mods.shift,
};
// Map key names to codes and keyCode values
const codeMap = {
Escape: ['Escape', 27],
Tab: ['Tab', 9],
ArrowUp: ['ArrowUp', 38],
ArrowDown: ['ArrowDown', 40],
ArrowLeft: ['ArrowLeft', 37],
ArrowRight: ['ArrowRight', 39],
Enter: ['Enter', 13],
};
if (codeMap[key]) {
opts.code = codeMap[key][0];
opts.keyCode = codeMap[key][1];
}
if (ta) {
ta.dispatchEvent(new KeyboardEvent('keydown', opts));
}
clearMods();
}
bar.addEventListener('click', (e) => {
const btn = e.target.closest('button');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
const mod = btn.dataset.mod;
if (mod) {
mods[mod] = !mods[mod];
btn.classList.toggle('active', mods[mod]);
// Re-focus terminal
const ta = getTextarea();
if (ta) ta.focus();
return;
}
// Combo buttons: send a key with fixed modifiers
const combo = btn.dataset.combo;
if (combo === 'ctrl-b' || combo === 'ctrl-c' || combo === 'ctrl-d') {
const saved = { ...mods };
mods.ctrl = true;
sendKey(combo.slice(-1));
mods.ctrl = saved.ctrl;
mods.alt = saved.alt;
mods.shift = saved.shift;
return;
}
const key = btn.dataset.key;
if (key) sendKey(key);
});
// Intercept real keyboard input when modifiers are active
document.addEventListener('keydown', (e) => {
if (!mods.ctrl && !mods.alt && !mods.shift) return;
// Only intercept single printable keys + Enter
if (e.key.length === 1 || e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
sendKey(e.key);
}
}, true);
}
})();