Skip to content

Commit 935c0b6

Browse files
committed
feat: use ghostty native scrollback
1 parent 9791146 commit 935c0b6

6 files changed

Lines changed: 777 additions & 137 deletions

File tree

demo/index.html

Lines changed: 57 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,17 @@
9090
border-radius: 0 0 10px 10px;
9191
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
9292
overflow: hidden;
93-
flex: 1;
9493
display: flex;
9594
flex-direction: column;
96-
min-height: 600px;
95+
height: 400px;
9796
}
9897

9998
#terminal-container {
10099
flex: 1;
101100
padding: 10px;
102101
background: #1e1e1e;
103102
position: relative;
103+
overflow: hidden; /* Prevent browser scrolling */
104104
}
105105

106106
.info-box {
@@ -281,21 +281,19 @@ <h1>🖥️ Ghostty Terminal</h1>
281281
</div>
282282

283283
<div class="feature-panel">
284-
<h3>🎯 xterm.js API Features</h3>
284+
<h3>📜 Scrolling Features</h3>
285285
<div class="button-grid">
286-
<button class="test-button" id="btn-paste">📋 Test paste()</button>
287-
<button class="test-button" id="btn-blur">👁️ Test blur()</button>
288-
<button class="test-button" id="btn-input">⌨️ Test input()</button>
289-
<button class="test-button" id="btn-select">🔤 Test select()</button>
290-
<button class="test-button" id="btn-selectLines">📄 Test selectLines()</button>
291-
<button class="test-button" id="btn-getSelectionPos">📍 Get Selection Position</button>
292-
<button class="test-button" id="btn-setTitle">🏷️ Set Title (OSC)</button>
293-
<button class="test-button" id="btn-customHandler">🔧 Toggle Custom Handler</button>
286+
<button class="test-button" id="btn-scrollUp">⬆️ Scroll Up 5 Lines</button>
287+
<button class="test-button" id="btn-scrollDown">⬇️ Scroll Down 5 Lines</button>
288+
<button class="test-button" id="btn-scrollPageUp">📄⬆️ Scroll Page Up</button>
289+
<button class="test-button" id="btn-scrollPageDown">📄⬇️ Scroll Page Down</button>
290+
<button class="test-button" id="btn-scrollToTop">🔝 Scroll to Top</button>
291+
<button class="test-button" id="btn-scrollToBottom">⬇️ Scroll to Bottom</button>
292+
<button class="test-button" id="btn-scrollToLine">🎯 Scroll to Line 10</button>
293+
<button class="test-button" id="btn-generateContent">📝 Generate Scrollback</button>
294294
</div>
295295
<div>
296-
<strong style="display: block; margin-bottom: 8px"
297-
>Event Log (onKey, onTitleChange):</strong
298-
>
296+
<strong style="display: block; margin-bottom: 8px">Event Log (onScroll):</strong>
299297
<div class="event-log" id="event-log">
300298
<div style="opacity: 0.5; text-align: center">Events will appear here...</div>
301299
</div>
@@ -340,9 +338,7 @@ <h3>🎯 xterm.js API Features</h3>
340338
updateStatus('connecting', 'Connecting to server...');
341339

342340
// Include terminal size in WebSocket URL
343-
console.log(`Terminal dimensions at connect time: ${term.cols}x${term.rows}`);
344341
const wsUrlWithSize = `${WS_URL}?cols=${term.cols}&rows=${term.rows}`;
345-
console.log('Connecting to WebSocket:', wsUrlWithSize);
346342

347343
// Show WebSocket URL in status bar
348344
document.getElementById('ws-url').textContent = WS_URL;
@@ -351,8 +347,6 @@ <h3>🎯 xterm.js API Features</h3>
351347

352348
ws.onopen = () => {
353349
updateStatus('connected', 'Connected (PTY mode)');
354-
console.log('WebSocket connected - PTY mode active');
355-
console.log('Terminal onData handler:', term.onData ? 'registered' : 'NOT REGISTERED');
356350

357351
// Focus the terminal
358352
term.focus();
@@ -361,18 +355,9 @@ <h3>🎯 xterm.js API Features</h3>
361355
ws.onmessage = (event) => {
362356
// PTY server sends raw text, not JSON
363357
const data = event.data;
364-
console.log(
365-
'WebSocket received:',
366-
typeof data,
367-
'length:',
368-
data?.length,
369-
'data:',
370-
JSON.stringify(data?.substring(0, 50))
371-
);
372358

373359
// If it's a string (raw PTY output), write directly to terminal
374360
if (typeof data === 'string') {
375-
console.log('Writing to terminal:', JSON.stringify(data.substring(0, 50)));
376361
term.write(data);
377362
}
378363
// Otherwise try JSON parsing for file-browser-server compatibility
@@ -394,7 +379,6 @@ <h3>🎯 xterm.js API Features</h3>
394379

395380
ws.onclose = () => {
396381
updateStatus('disconnected', 'Disconnected');
397-
console.log('WebSocket closed');
398382
term?.write('\r\n\x1b[1;33mConnection closed.\x1b[0m\r\n');
399383
};
400384
}
@@ -517,22 +501,10 @@ <h3>🎯 xterm.js API Features</h3>
517501
}
518502

519503
function handleInput(data) {
520-
console.log(
521-
'handleInput called, data:',
522-
data,
523-
'isPtyMode:',
524-
isPtyMode,
525-
'ws state:',
526-
ws?.readyState
527-
);
528-
529504
// PTY mode: send every keystroke directly to shell
530505
if (isPtyMode) {
531506
if (ws && ws.readyState === WebSocket.OPEN) {
532-
console.log('Sending to shell:', JSON.stringify(data));
533507
ws.send(data);
534-
} else {
535-
console.error('WebSocket not open, state:', ws?.readyState);
536508
}
537509
return;
538510
}
@@ -629,8 +601,6 @@ <h3>🎯 xterm.js API Features</h3>
629601
// Event Logging & Handlers
630602
// =========================================================================
631603

632-
let customHandlerEnabled = false;
633-
634604
function logEvent(type, data) {
635605
const eventLog = document.getElementById('event-log');
636606
const eventDiv = document.createElement('div');
@@ -655,104 +625,75 @@ <h3>🎯 xterm.js API Features</h3>
655625
}
656626

657627
function setupEventHandlers() {
658-
// Event: onKey - fires on every keypress
659-
term.onKey((e) => {
660-
logEvent('onKey', `key="${e.key}" ctrl=${e.domEvent.ctrlKey} alt=${e.domEvent.altKey}`);
628+
// Scrolling event handlers
629+
term.onScroll((position) => {
630+
logEvent('onScroll', `viewportY=${position}`);
661631
});
662632

663-
// Event: onTitleChange - fires when terminal title changes via OSC sequences
664-
term.onTitleChange((title) => {
665-
logEvent('onTitleChange', title);
666-
document.title = `${title} - Ghostty Terminal`;
633+
document.getElementById('btn-clear-log').addEventListener('click', () => {
634+
document.getElementById('event-log').innerHTML =
635+
'<div style="opacity: 0.5; text-align: center;">Log cleared</div>';
667636
});
668637

669-
// Button handlers
670-
document.getElementById('btn-paste').addEventListener('click', () => {
671-
term.paste('Pasted text from paste() method!\n');
672-
logEvent('Action', 'Called paste()');
638+
// Scrolling button handlers
639+
document.getElementById('btn-scrollUp').addEventListener('click', () => {
640+
const scrollbackLen = term.getScrollbackLength();
641+
term.scrollLines(-5);
642+
logEvent('Action', `Called scrollLines(-5)`);
673643
});
674644

675-
document.getElementById('btn-blur').addEventListener('click', () => {
676-
term.blur();
677-
logEvent('Action', 'Called blur() - terminal lost focus');
645+
document.getElementById('btn-scrollDown').addEventListener('click', () => {
646+
term.scrollLines(5);
647+
logEvent('Action', 'Called scrollLines(5) - scroll down');
678648
});
679649

680-
document.getElementById('btn-input').addEventListener('click', () => {
681-
term.input('echo "Input from input() method"\n', true);
682-
logEvent('Action', 'Called input() with wasUserInput=true');
650+
document.getElementById('btn-scrollPageUp').addEventListener('click', () => {
651+
term.scrollPages(-1);
652+
logEvent('Action', 'Called scrollPages(-1) - page up');
683653
});
684654

685-
document.getElementById('btn-select').addEventListener('click', () => {
686-
// Select 20 characters starting at column 0, row 0
687-
// This selects from the current cursor position backwards 20 chars
688-
const cursor = term.wasmTerm.getCursor();
689-
const startRow = Math.max(0, cursor.y - 1);
690-
term.select(0, startRow, 30);
691-
logEvent(
692-
'Action',
693-
`Called select(0, ${startRow}, 30) - selects 30 chars from row ${startRow}`
694-
);
655+
document.getElementById('btn-scrollPageDown').addEventListener('click', () => {
656+
term.scrollPages(1);
657+
logEvent('Action', 'Called scrollPages(1) - page down');
695658
});
696659

697-
document.getElementById('btn-selectLines').addEventListener('click', () => {
698-
// Select last 3 visible lines
699-
const cursor = term.wasmTerm.getCursor();
700-
const endRow = cursor.y;
701-
const startRow = Math.max(0, endRow - 2);
702-
term.selectLines(startRow, endRow);
703-
logEvent('Action', `Called selectLines(${startRow}, ${endRow}) - selects 3 lines`);
660+
document.getElementById('btn-scrollToTop').addEventListener('click', () => {
661+
const scrollbackLen = term.getScrollbackLength();
662+
term.scrollToTop();
663+
logEvent('Action', `Called scrollToTop() (${scrollbackLen} lines)`);
704664
});
705665

706-
document.getElementById('btn-getSelectionPos').addEventListener('click', () => {
707-
const pos = term.getSelectionPosition();
708-
if (pos) {
709-
logEvent(
710-
'getSelectionPosition',
711-
`start=(${pos.start.x},${pos.start.y}) end=(${pos.end.x},${pos.end.y})`
712-
);
713-
} else {
714-
logEvent('getSelectionPosition', 'No selection');
715-
}
666+
document.getElementById('btn-scrollToBottom').addEventListener('click', () => {
667+
term.scrollToBottom();
668+
logEvent('Action', 'Called scrollToBottom()');
716669
});
717670

718-
document.getElementById('btn-setTitle').addEventListener('click', () => {
719-
// Send OSC 2 sequence to change title
720-
const newTitle = `Ghostty Terminal ${new Date().toLocaleTimeString()}`;
721-
term.write(`\x1b]2;${newTitle}\x07`);
722-
logEvent('Action', `Sent OSC 2 sequence with title: ${newTitle}`);
671+
document.getElementById('btn-scrollToLine').addEventListener('click', () => {
672+
term.scrollToLine(10);
673+
logEvent('Action', 'Called scrollToLine(10)');
723674
});
724675

725-
document.getElementById('btn-customHandler').addEventListener('click', () => {
726-
customHandlerEnabled = !customHandlerEnabled;
727-
728-
if (customHandlerEnabled) {
729-
// Custom handler that blocks Ctrl+K
730-
term.attachCustomKeyEventHandler((event) => {
731-
if (event.ctrlKey && event.key === 'k') {
732-
logEvent('CustomHandler', 'Blocked Ctrl+K');
733-
return true; // Block default handling
734-
}
735-
return false; // Allow default handling
736-
});
737-
logEvent('Action', 'Custom handler enabled (blocks Ctrl+K)');
738-
document.getElementById('btn-customHandler').style.background =
739-
'rgba(33, 150, 243, 0.8)';
740-
} else {
741-
term.attachCustomKeyEventHandler(undefined);
742-
logEvent('Action', 'Custom handler disabled');
743-
document.getElementById('btn-customHandler').style.background =
744-
'rgba(76, 175, 80, 0.8)';
676+
document.getElementById('btn-generateContent').addEventListener('click', () => {
677+
// First clear the terminal to start fresh
678+
term.clear();
679+
680+
// Generate lots of content to create scrollback
681+
// Terminal is ~24 rows, so 200 lines will create ~176 lines of scrollback
682+
for (let i = 1; i <= 200; i++) {
683+
term.write(
684+
`Line ${i.toString().padStart(3, '0')}: Test content for scrollback buffer\r\n`
685+
);
745686
}
746-
});
747687

748-
document.getElementById('btn-clear-log').addEventListener('click', () => {
749-
document.getElementById('event-log').innerHTML =
750-
'<div style="opacity: 0.5; text-align: center;">Log cleared</div>';
688+
// Wait a bit for rendering, then check scrollback
689+
setTimeout(() => {
690+
const scrollbackLen = term.getScrollbackLength();
691+
logEvent('Action', `Generated 200 lines, scrollback: ${scrollbackLen} lines`);
692+
}, 100);
751693
});
752694

753695
// Expose terminal to console for debugging
754696
window.term = term;
755-
console.log('Terminal exposed to window.term for debugging');
756697
}
757698

758699
// =========================================================================
@@ -805,7 +746,6 @@ <h3>🎯 xterm.js API Features</h3>
805746

806747
// Fit terminal to container
807748
fitAddon.fit();
808-
console.log(`After fit(): Terminal size is ${term.cols}x${term.rows}`);
809749

810750
// Handle window resize
811751
window.addEventListener('resize', () => {
@@ -816,7 +756,6 @@ <h3>🎯 xterm.js API Features</h3>
816756
term.onResize(({ cols, rows }) => {
817757
if (ws && ws.readyState === WebSocket.OPEN) {
818758
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
819-
console.log(`Sent resize to server: ${cols}x${rows}`);
820759
}
821760
});
822761

lib/ghostty.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,10 @@ export class GhosttyTerminal {
459459
*/
460460
getScrollbackLine(offset: number): GhosttyCell[] | null {
461461
const scrollbackLen = this.getScrollbackLength();
462-
if (offset < 0 || offset >= scrollbackLen) return null;
462+
463+
if (offset < 0 || offset >= scrollbackLen) {
464+
return null;
465+
}
463466

464467
const bufferSize = this._cols * GhosttyTerminal.CELL_SIZE;
465468
const ptr = this.exports.ghostty_wasm_alloc_u8_array(bufferSize);
@@ -472,7 +475,9 @@ export class GhosttyTerminal {
472475
this._cols
473476
);
474477

475-
if (count < 0) return null;
478+
if (count < 0) {
479+
return null;
480+
}
476481

477482
// Parse cells (same logic as getLine)
478483
const cells: GhosttyCell[] = [];

0 commit comments

Comments
 (0)