feat(terminal): implement resize handling with ResizeObserver for improved terminal responsiveness

This commit is contained in:
Andras Bacsai
2025-06-06 22:05:16 +02:00
parent 6aa82817df
commit 6e85419adb
2 changed files with 99 additions and 19 deletions

View File

@@ -30,6 +30,9 @@ export function initializeTerminalComponent() {
pingTimeoutId: null, pingTimeoutId: null,
heartbeatMissed: 0, heartbeatMissed: 0,
maxHeartbeatMisses: 3, maxHeartbeatMisses: 3,
// Resize handling
resizeObserver: null,
resizeTimeout: null,
init() { init() {
this.setupTerminal(); this.setupTerminal();
@@ -55,8 +58,18 @@ export function initializeTerminalComponent() {
if (active) { if (active) {
this.$refs.terminalWrapper.style.display = 'block'; this.$refs.terminalWrapper.style.display = 'block';
this.resizeTerminal(); this.resizeTerminal();
// Start observing terminal wrapper for resize changes
if (this.resizeObserver && this.$refs.terminalWrapper) {
this.resizeObserver.observe(this.$refs.terminalWrapper);
}
} else { } else {
this.$refs.terminalWrapper.style.display = 'none'; this.$refs.terminalWrapper.style.display = 'none';
// Stop observing when terminal is inactive
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
} }
}); });
}); });
@@ -70,6 +83,17 @@ export function initializeTerminalComponent() {
window.onresize = () => { window.onresize = () => {
this.resizeTerminal() this.resizeTerminal()
}; };
// Set up ResizeObserver for more reliable terminal resizing
if (window.ResizeObserver) {
this.resizeObserver = new ResizeObserver(() => {
// Debounce resize calls to avoid performance issues
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
this.resizeTerminal();
}, 50);
});
}
}, },
cleanup() { cleanup() {
@@ -79,15 +103,27 @@ export function initializeTerminalComponent() {
if (this.socket) { if (this.socket) {
this.socket.close(1000, 'Client cleanup'); this.socket.close(1000, 'Client cleanup');
} }
// Clean up resize observer
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
// Clear resize timeout
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
}, },
clearAllTimers() { clearAllTimers() {
[this.keepAliveInterval, this.reconnectInterval, this.connectionTimeoutId, this.pingTimeoutId] [this.keepAliveInterval, this.reconnectInterval, this.connectionTimeoutId, this.pingTimeoutId, this.resizeTimeout]
.forEach(timer => timer && clearInterval(timer)); .forEach(timer => timer && clearInterval(timer));
this.keepAliveInterval = null; this.keepAliveInterval = null;
this.reconnectInterval = null; this.reconnectInterval = null;
this.connectionTimeoutId = null; this.connectionTimeoutId = null;
this.pingTimeoutId = null; this.pingTimeoutId = null;
this.resizeTimeout = null;
}, },
resetTerminal() { resetTerminal() {
@@ -308,8 +344,15 @@ export function initializeTerminalComponent() {
this.terminalActive = true; this.terminalActive = true;
this.term.focus(); this.term.focus();
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded-sm'); document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded-sm');
// Initial resize after terminal is ready
this.resizeTerminal(); this.resizeTerminal();
// Additional resize after a short delay to ensure proper sizing
setTimeout(() => {
this.resizeTerminal();
}, 200);
// Notify parent component that terminal is connected // Notify parent component that terminal is connected
this.$wire.dispatch('terminalConnected'); this.$wire.dispatch('terminalConnected');
} else if (event.data === 'unprocessable') { } else if (event.data === 'unprocessable') {
@@ -418,7 +461,13 @@ export function initializeTerminalComponent() {
makeFullscreen() { makeFullscreen() {
this.fullscreen = !this.fullscreen; this.fullscreen = !this.fullscreen;
this.$nextTick(() => { this.$nextTick(() => {
this.resizeTerminal(); // Force a layout reflow to ensure DOM changes are applied
this.$refs.terminalWrapper.offsetHeight;
// Add a small delay to ensure CSS transitions complete
setTimeout(() => {
this.resizeTerminal();
}, 100);
}); });
}, },
@@ -426,25 +475,53 @@ export function initializeTerminalComponent() {
if (!this.terminalActive || !this.term || !this.fitAddon) return; if (!this.terminalActive || !this.term || !this.fitAddon) return;
try { try {
// Force a refresh of the fit addon dimensions
this.fitAddon.fit(); this.fitAddon.fit();
const height = this.$refs.terminalWrapper.clientHeight;
const width = this.$refs.terminalWrapper.clientWidth;
const charSize = this.term._core._renderService._charSizeService;
if (!charSize.height || !charSize.width) { // Get fresh dimensions after fit
// Fallback values if char size not available yet const wrapperHeight = this.$refs.terminalWrapper.clientHeight;
const wrapperWidth = this.$refs.terminalWrapper.clientWidth;
// Account for terminal container padding (px-2 py-1 = 8px left/right, 4px top/bottom)
const horizontalPadding = 16; // 8px * 2 (left + right)
const verticalPadding = 8; // 4px * 2 (top + bottom)
const height = wrapperHeight - verticalPadding;
const width = wrapperWidth - horizontalPadding;
// Check if dimensions are valid
if (height <= 0 || width <= 0) {
console.warn('[Terminal] Invalid wrapper dimensions, retrying...', { height, width });
setTimeout(() => this.resizeTerminal(), 100); setTimeout(() => this.resizeTerminal(), 100);
return; return;
} }
const charSize = this.term._core._renderService._charSizeService;
if (!charSize.height || !charSize.width) {
// Fallback values if char size not available yet
console.warn('[Terminal] Character size not available, retrying...');
setTimeout(() => this.resizeTerminal(), 100);
return;
}
// Calculate new dimensions with padding considerations
const rows = Math.floor(height / charSize.height) - 1; const rows = Math.floor(height / charSize.height) - 1;
const cols = Math.floor(width / charSize.width) - 1; const cols = Math.floor(width / charSize.width) - 1;
if (rows > 0 && cols > 0) { if (rows > 0 && cols > 0) {
this.term.resize(cols, rows); // Check if dimensions actually changed to avoid unnecessary resizes
this.sendMessage({ const currentCols = this.term.cols;
resize: { cols: cols, rows: rows } const currentRows = this.term.rows;
});
if (cols !== currentCols || rows !== currentRows) {
console.log(`[Terminal] Resizing terminal: ${currentCols}x${currentRows} -> ${cols}x${rows}`);
this.term.resize(cols, rows);
this.sendMessage({
resize: { cols: cols, rows: rows }
});
}
} else {
console.warn('[Terminal] Invalid calculated dimensions:', { rows, cols, height, width, charSize });
} }
} catch (error) { } catch (error) {
console.error('[Terminal] Resize error:', error); console.error('[Terminal] Resize error:', error);

View File

@@ -17,17 +17,20 @@
</div> </div>
@else @else
<div x-ref="terminalWrapper" <div x-ref="terminalWrapper"
:class="fullscreen ? 'fullscreen' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'"> :class="fullscreen ? 'fullscreen !bg-black' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
<div id="terminal" wire:ignore></div> <!-- Terminal container -->
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-6 text-white" <div id="terminal" wire:ignore
x-on:click="makeFullscreen"><svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24" :class="fullscreen ? 'px-2 py-1 h-full bg-black' : 'px-2 py-1 rounded-sm bg-black'"
xmlns="http://www.w3.org/2000/svg"> x-show="terminalActive"></div>
<button title="Minimize" x-show="fullscreen" class="fixed bg-black/40 top-4 right-6 text-white"
x-on:click="makeFullscreen"><svg class="w-5 h-5 text-gray-500 hover:text-white bg-black/80"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" /> stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
</svg></button> </svg></button>
<button title="Fullscreen" x-show="!fullscreen && terminalActive" class="absolute right-5 top-6 text-white" <button title="Fullscreen" x-show="!fullscreen && terminalActive" class="absolute right-5 top-6 text-white "
x-on:click="makeFullscreen"> <svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24" x-on:click="makeFullscreen"> <svg class="w-5 h-5 text-gray-500 hover:text-white bg-black/80"
xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none"> <g fill="none">
<path <path
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" /> d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />