export function createInputBar(term, send) {
const bar = document.getElementById('inputBar');
const ribbon = document.getElementById('inputRibbon');
const input = document.getElementById('inputText');
const sendBtn = document.getElementById('inputSend');
if (!bar || !input) return { destroy() {} };
const textarea = term.textarea;
if (textarea) {
textarea.setAttribute('tabindex', '-1');
textarea.style.pointerEvents = 'none';
textarea.style.opacity = '0';
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
}
function parseKey(raw) {
return raw.replace(/\\x([0-9a-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)))
.replace(/\\t/g, '\t')
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r');
}
function show() {
bar.classList.remove('hidden');
resizeTerminal();
}
function hide() {
bar.classList.add('hidden');
input.blur();
resizeTerminal();
}
function computeKeyboardOffset(innerHeight, vvHeight, vvOffsetTop) {
return Math.max(0, innerHeight - vvHeight - vvOffsetTop);
}
function resizeTerminal() {
window.dispatchEvent(new Event('resize'));
}
ribbon.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-key]');
if (!btn) return;
e.preventDefault();
const seq = parseKey(btn.dataset.key);
send(seq);
input.focus();
});
ribbon.addEventListener('mousedown', (e) => {
if (e.target.closest('button')) e.preventDefault();
});
function sendAndExecute() {
const text = input.value;
if (text) send(text);
send('\r');
input.value = '';
}
function sendWithoutEnter() {
const text = input.value;
if (text) send(text);
input.value = '';
input.focus();
}
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
sendAndExecute();
}
});
sendBtn.addEventListener('click', (e) => {
e.preventDefault();
sendWithoutEnter();
input.focus();
});
const overlay = document.getElementById('touchOverlay');
function activateInput() {
show();
setTimeout(() => input.focus(), 50);
}
if (window.visualViewport) {
const vv = window.visualViewport;
let lastHeight = vv.height;
const onViewportChange = () => {
const h = vv.height;
if (h > lastHeight + 50 && !bar.classList.contains('hidden')) {
hide();
}
lastHeight = h;
};
vv.addEventListener('resize', onViewportChange);
vv.addEventListener('scroll', onViewportChange);
}
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.preventDefault();
hide();
}
});
async function uploadFile(file) {
const form = new FormData();
form.append('file', file);
const res = await fetch('/api/upload', { method: 'POST', body: form });
if (!res.ok) throw new Error(await res.text());
const { path } = await res.json();
send(path);
}
const uploadBtn = document.getElementById('uploadBtn');
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '*/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
if (uploadBtn) {
uploadBtn.addEventListener('click', (e) => {
e.preventDefault();
fileInput.click();
});
uploadBtn.addEventListener('mousedown', (e) => e.preventDefault());
}
fileInput.addEventListener('change', async () => {
const file = fileInput.files?.[0];
if (!file) return;
try {
await uploadFile(file);
} catch (err) {
console.error('Upload failed:', err);
}
fileInput.value = '';
});
const recBtn = document.getElementById('recBtn');
let mediaRecorder = null;
let chunks = [];
let recStream = null;
function recCleanup() {
if (recStream) {
recStream.getTracks().forEach((t) => t.stop());
recStream = null;
}
mediaRecorder = null;
if (recBtn) recBtn.classList.remove('recording');
}
function recFlashError() {
if (!recBtn) return;
recBtn.classList.add('rec-error');
setTimeout(() => recBtn.classList.remove('rec-error'), 1500);
}
function extForMime(mime) {
if (!mime) return 'webm';
if (mime.includes('mp4')) return 'm4a';
if (mime.includes('ogg')) return 'ogg';
if (mime.includes('webm')) return 'webm';
return 'webm';
}
function pickMimeType() {
const prefs = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
for (const t of prefs) {
if (MediaRecorder.isTypeSupported(t)) return t;
}
return ''; }
async function startRecording() {
try {
recStream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (err) {
console.error('Microphone access failed:', err);
recCleanup();
recFlashError();
return;
}
chunks = [];
const mimeType = pickMimeType();
try {
mediaRecorder = mimeType
? new MediaRecorder(recStream, { mimeType })
: new MediaRecorder(recStream);
} catch (err) {
console.error('MediaRecorder init failed:', err);
recCleanup();
recFlashError();
return;
}
mediaRecorder.addEventListener('dataavailable', (e) => {
if (e.data && e.data.size > 0) chunks.push(e.data);
});
mediaRecorder.addEventListener('stop', async () => {
const type = mediaRecorder?.mimeType || 'audio/webm';
const ext = extForMime(type);
const blob = new Blob(chunks, { type });
chunks = [];
recCleanup();
if (blob.size === 0) return;
const file = new File([blob], 'recording-' + Date.now() + '.' + ext, { type });
try {
await uploadFile(file);
} catch (err) {
console.error('Upload failed:', err);
recFlashError();
}
});
mediaRecorder.start();
if (recBtn) recBtn.classList.add('recording');
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop(); } else {
recCleanup();
}
}
if (recBtn) {
const recSupported =
!!navigator.mediaDevices?.getUserMedia && typeof MediaRecorder !== 'undefined';
if (!recSupported) {
recBtn.style.display = 'none';
} else {
recBtn.addEventListener('click', (e) => {
e.preventDefault();
if (mediaRecorder && mediaRecorder.state === 'recording') {
stopRecording();
} else {
startRecording();
}
});
recBtn.addEventListener('mousedown', (e) => e.preventDefault());
}
}
return {
_computeKeyboardOffset: computeKeyboardOffset,
show: activateInput,
hide,
destroy() {
if (textarea) {
textarea.removeAttribute('tabindex');
textarea.style.pointerEvents = '';
}
}
};
}