feat(terminal): implement resize handling with ResizeObserver for improved terminal responsiveness
This commit is contained in:
@@ -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(() => {
|
||||||
|
// 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();
|
this.resizeTerminal();
|
||||||
|
}, 100);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -426,26 +475,54 @@ 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) {
|
||||||
|
// Check if dimensions actually changed to avoid unnecessary resizes
|
||||||
|
const currentCols = this.term.cols;
|
||||||
|
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.term.resize(cols, rows);
|
||||||
this.sendMessage({
|
this.sendMessage({
|
||||||
resize: { cols: cols, rows: rows }
|
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);
|
||||||
}
|
}
|
||||||
|
@@ -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" />
|
||||||
|
Reference in New Issue
Block a user