diff --git a/public/index.html b/public/index.html index ff0fa3e..e3aa9e9 100644 --- a/public/index.html +++ b/public/index.html @@ -10,7 +10,6 @@ --columns: 1; --font-size: 12px; --terminal-height: 400px; - --wrap-mode: pre-wrap; } * { @@ -243,12 +242,28 @@ font-family: "SF Mono", Monaco, "Cascadia Code", "Courier New", monospace; font-size: var(--font-size); line-height: 1.3; - white-space: var(--wrap-mode); - word-wrap: normal; - overflow-wrap: normal; color: #e0e0e0; } + /* Default: wrap mode - text wraps to fit container */ + .terminal.wrap-mode { + white-space: break-spaces; + word-break: break-word; + overflow-wrap: anywhere; + } + + /* Scroll mode - horizontal scroll, no wrapping */ + .terminal.scroll-mode { + white-space: pre; + overflow-x: auto; + } + + /* Box-drawing lines (detected via JS) should never wrap */ + .terminal .rule-line { + white-space: pre; + display: block; + } + .prompt { background: #2a2a2a; border: 2px solid #ff9800; @@ -1020,7 +1035,7 @@ columns: 1, // 1, 2, 3, or 'fit' fontSize: 12, // px value terminalHeight: 400, // px value - wrapText: false, // true = pre-wrap, false = pre (horizontal scroll) + wrapText: true, // true = wrap (mobile-friendly), false = scroll }, }; @@ -1458,15 +1473,50 @@ title="Scroll to bottom"> ↓ -
${s.output}
+
${s.output}
`).join(''); - // Initialize scroll handlers and button visibility + // Initialize scroll handlers, button visibility, and mark rule lines sessionsToRender.forEach(s => { initSessionOutputUI(s.id); updateScrollButton(s.id); + // Mark box-drawing lines on initial render + if (s.expanded) { + const $output = document.getElementById(`output-${s.id}`); + if ($output) markRuleLines($output); + } + }); + } + + // Box-drawing character range: U+2500 to U+257F + const BOX_DRAWING_REGEX = /[\u2500-\u257F]/g; + + /** + * Check if a line is primarily box-drawing characters (>= 60%) + * Used to prevent wrapping on separator/border lines + */ + function isRuleLine(text) { + if (!text || text.length < 3) return false; + const visibleChars = text.replace(/\s/g, ''); + if (visibleChars.length < 3) return false; + const boxChars = (visibleChars.match(BOX_DRAWING_REGEX) || []).length; + return boxChars / visibleChars.length >= 0.6; + } + + /** + * Process terminal output to mark box-drawing lines as rule-lines + * This prevents wrapping on separator/border lines + */ + function markRuleLines($terminal) { + // xterm serializeAsHTML outputs content in divs (one per row) + // Each div may contain spans for styling + const rows = $terminal.querySelectorAll(':scope > div'); + rows.forEach(row => { + if (isRuleLine(row.textContent)) { + row.classList.add('rule-line'); + } }); } @@ -1483,6 +1533,8 @@ // Only mutate DOM when expanded to reduce work if (session.expanded) { $output.innerHTML = session.output; + // Mark box-drawing lines to prevent wrapping + markRuleLines($output); } session.outputRenderedLength = session.output.length; @@ -1979,8 +2031,16 @@ // Apply terminal height root.style.setProperty('--terminal-height', `${state.settings.terminalHeight}px`); - // Apply wrap mode - root.style.setProperty('--wrap-mode', state.settings.wrapText ? 'pre-wrap' : 'pre'); + // Apply wrap mode to all terminal elements + document.querySelectorAll('.terminal').forEach(el => { + if (state.settings.wrapText) { + el.classList.add('wrap-mode'); + el.classList.remove('scroll-mode'); + } else { + el.classList.add('scroll-mode'); + el.classList.remove('wrap-mode'); + } + }); } function updateSettingsUI() { diff --git a/src/server.ts b/src/server.ts index 6f6b495..71f7485 100644 --- a/src/server.ts +++ b/src/server.ts @@ -458,8 +458,9 @@ const server = Bun.serve({ // Initialize in-memory session state sessionStates.set(session.id, createDefaultSessionState()); - // Create terminal emulator with default size (will be resized later) - const termSession = createTerminal(80, 24); + // Create terminal emulator wide enough to avoid premature wrapping + // Browser CSS handles responsive display; we just need ANSI processing + const termSession = createTerminal(300, 50); sessionTerminals.set(session.id, termSession); console.debug(