Use css for wrapping text

This commit is contained in:
Jared Miller 2026-01-31 11:43:56 -05:00
parent 31340fe0a8
commit ab61445bcc
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 72 additions and 11 deletions

View file

@ -10,7 +10,6 @@
--columns: 1; --columns: 1;
--font-size: 12px; --font-size: 12px;
--terminal-height: 400px; --terminal-height: 400px;
--wrap-mode: pre-wrap;
} }
* { * {
@ -243,12 +242,28 @@
font-family: "SF Mono", Monaco, "Cascadia Code", "Courier New", monospace; font-family: "SF Mono", Monaco, "Cascadia Code", "Courier New", monospace;
font-size: var(--font-size); font-size: var(--font-size);
line-height: 1.3; line-height: 1.3;
white-space: var(--wrap-mode);
word-wrap: normal;
overflow-wrap: normal;
color: #e0e0e0; 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 { .prompt {
background: #2a2a2a; background: #2a2a2a;
border: 2px solid #ff9800; border: 2px solid #ff9800;
@ -1020,7 +1035,7 @@
columns: 1, // 1, 2, 3, or 'fit' columns: 1, // 1, 2, 3, or 'fit'
fontSize: 12, // px value fontSize: 12, // px value
terminalHeight: 400, // 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"> title="Scroll to bottom">
</button> </button>
<div class="terminal" id="output-${s.id}">${s.output}</div> <div class="terminal ${state.settings.wrapText ? 'wrap-mode' : 'scroll-mode'}" id="output-${s.id}">${s.output}</div>
</div> </div>
</div> </div>
`).join(''); `).join('');
// Initialize scroll handlers and button visibility // Initialize scroll handlers, button visibility, and mark rule lines
sessionsToRender.forEach(s => { sessionsToRender.forEach(s => {
initSessionOutputUI(s.id); initSessionOutputUI(s.id);
updateScrollButton(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 // Only mutate DOM when expanded to reduce work
if (session.expanded) { if (session.expanded) {
$output.innerHTML = session.output; $output.innerHTML = session.output;
// Mark box-drawing lines to prevent wrapping
markRuleLines($output);
} }
session.outputRenderedLength = session.output.length; session.outputRenderedLength = session.output.length;
@ -1979,8 +2031,16 @@
// Apply terminal height // Apply terminal height
root.style.setProperty('--terminal-height', `${state.settings.terminalHeight}px`); root.style.setProperty('--terminal-height', `${state.settings.terminalHeight}px`);
// Apply wrap mode // Apply wrap mode to all terminal elements
root.style.setProperty('--wrap-mode', state.settings.wrapText ? 'pre-wrap' : 'pre'); 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() { function updateSettingsUI() {

View file

@ -458,8 +458,9 @@ const server = Bun.serve<SessionData>({
// Initialize in-memory session state // Initialize in-memory session state
sessionStates.set(session.id, createDefaultSessionState()); sessionStates.set(session.id, createDefaultSessionState());
// Create terminal emulator with default size (will be resized later) // Create terminal emulator wide enough to avoid premature wrapping
const termSession = createTerminal(80, 24); // Browser CSS handles responsive display; we just need ANSI processing
const termSession = createTerminal(300, 50);
sessionTerminals.set(session.id, termSession); sessionTerminals.set(session.id, termSession);
console.debug( console.debug(