You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

171 lines
4.5 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

class TerminalView {
constructor(element) {
this.el = element;
this.lines = [];
this.renderPending = false;
this.autoScroll = true;
// Track scroll position to determine auto-scroll
const container = this.el.parentElement;
container.addEventListener('scroll', () => {
const atBottom =
container.scrollHeight - container.scrollTop - container.clientHeight < 30;
this.autoScroll = atBottom;
});
}
setContent(lines) {
this.lines = lines;
this.scheduleRender();
}
applyDiff(patches) {
for (const patch of patches) {
this.lines.splice(patch.startLine, patch.deleteCount, ...patch.insertLines);
}
this.scheduleRender();
}
scheduleRender() {
if (this.renderPending) return;
this.renderPending = true;
requestAnimationFrame(() => {
this.render();
this.renderPending = false;
});
}
isQrLine(line) {
// Detect QR code lines: mostly Unicode block characters (▀▄█)
const blockChars = (line.match(/[\u2580-\u2588]/g) || []).length;
const nonSpace = line.replace(/\s/g, '').length;
return nonSpace > 4 && blockChars / nonSpace > 0.3;
}
render() {
const output = [];
let i = 0;
while (i < this.lines.length) {
if (this.isQrLine(this.lines[i])) {
// Collect all consecutive QR lines
const qrBlock = [];
while (i < this.lines.length && this.isQrLine(this.lines[i])) {
qrBlock.push(this.lines[i]);
i++;
}
// Calculate font-size so QR block fits container width without clipping
const maxLen = Math.max(...qrBlock.map(l => l.length));
const containerWidth = (this.el.parentElement?.clientWidth || 375) - 32;
// monospace char width ≈ 0.62 × font-size
const fontSize = Math.max(3, Math.floor(containerWidth / (maxLen * 0.62)));
for (const qline of qrBlock) {
const esc = qline
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
output.push(`<span class="qr-line" style="font-size:${fontSize}px">${esc}</span>`);
}
} else {
output.push(this.highlightLine(this.lines[i]));
i++;
}
}
const html = output.join('\n');
this.el.innerHTML = html;
if (this.autoScroll) {
const container = this.el.parentElement;
container.scrollTop = container.scrollHeight;
}
}
highlightLine(line) {
// Escape HTML entities first
let escaped = line
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Apply highlighting patterns (order matters — more specific first)
// Diff: added lines (starts with +)
if (/^\+[^+]/.test(line)) {
return `<span class="hl-added">${escaped}</span>`;
}
// Diff: removed lines (starts with -)
if (/^-[^-]/.test(line)) {
return `<span class="hl-removed">${escaped}</span>`;
}
// Tool use markers (blue circle)
escaped = escaped.replace(
/(\u23FA|\u25CF|\u2B24)/g,
'<span class="hl-tool">$1</span>'
);
// Thinking markers
escaped = escaped.replace(
/(\u273B)/g,
'<span class="hl-thinking">$1</span>'
);
// Output indent marker
escaped = escaped.replace(
/(\u23BF)/g,
'<span class="hl-output">$1</span>'
);
// Prompt marker
escaped = escaped.replace(
/(\u276F)/g,
'<span class="hl-prompt">$1</span>'
);
// Success markers
escaped = escaped.replace(
/(\u2713|\u2714|passed|PASS)/g,
'<span class="hl-success">$1</span>'
);
// Error markers
escaped = escaped.replace(
/(\u2717|\u2718|error|ERROR|FAIL|failed)/g,
'<span class="hl-error">$1</span>'
);
// Box drawing characters
escaped = escaped.replace(
/([\u2500-\u257F\u250C\u2510\u2514\u2518\u251C\u2524\u252C\u2534\u253C]+)/g,
'<span class="hl-box">$1</span>'
);
// File paths (simplified: word containing / with file extension)
escaped = escaped.replace(
/(?<!\w)((?:\.?\.?\/)?(?:[\w.-]+\/)+[\w.-]+\.[\w]+)/g,
'<span class="hl-path">$1</span>'
);
// Line numbers in context (e.g., " 123 |" or ":42:")
escaped = escaped.replace(
/^(\s*\d+\s*[|:])/,
'<span class="hl-linenum">$1</span>'
);
return escaped;
}
scrollToBottom() {
const container = this.el.parentElement;
container.scrollTop = container.scrollHeight;
this.autoScroll = true;
}
clear() {
this.lines = [];
this.el.innerHTML = '';
}
}
window.TerminalView = TerminalView;