mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Implement custom ScaleFitAddon for proper terminal scaling
- Create ScaleFitAddon that scales font size to fit columns to container width - Calculates optimal rows for container height with scaled font - Replace CSS transform scaling with proper font size calculation - Apply to both session view and previews for consistent behavior - Achieve 95-98% width utilization instead of arbitrary 80% scaling - Prevent stack overflow with font size change detection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
34801bc687
commit
3ec417c8b1
3 changed files with 141 additions and 7 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
import { Terminal } from '@xterm/xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
import { FitAddon } from '@xterm/addon-fit';
|
import { FitAddon } from '@xterm/addon-fit';
|
||||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||||
|
import { ScaleFitAddon } from './scale-fit-addon.js';
|
||||||
|
|
||||||
interface CastHeader {
|
interface CastHeader {
|
||||||
version: number;
|
version: number;
|
||||||
|
|
@ -23,6 +24,7 @@ export class Renderer {
|
||||||
private container: HTMLElement;
|
private container: HTMLElement;
|
||||||
private terminal: Terminal;
|
private terminal: Terminal;
|
||||||
private fitAddon: FitAddon;
|
private fitAddon: FitAddon;
|
||||||
|
private scaleFitAddon: ScaleFitAddon;
|
||||||
private webLinksAddon: WebLinksAddon;
|
private webLinksAddon: WebLinksAddon;
|
||||||
private isPreview: boolean;
|
private isPreview: boolean;
|
||||||
|
|
||||||
|
|
@ -72,9 +74,11 @@ export class Renderer {
|
||||||
|
|
||||||
// Add addons
|
// Add addons
|
||||||
this.fitAddon = new FitAddon();
|
this.fitAddon = new FitAddon();
|
||||||
|
this.scaleFitAddon = new ScaleFitAddon();
|
||||||
this.webLinksAddon = new WebLinksAddon();
|
this.webLinksAddon = new WebLinksAddon();
|
||||||
|
|
||||||
this.terminal.loadAddon(this.fitAddon);
|
this.terminal.loadAddon(this.fitAddon);
|
||||||
|
this.terminal.loadAddon(this.scaleFitAddon);
|
||||||
this.terminal.loadAddon(this.webLinksAddon);
|
this.terminal.loadAddon(this.webLinksAddon);
|
||||||
|
|
||||||
this.setupDOM();
|
this.setupDOM();
|
||||||
|
|
@ -96,12 +100,12 @@ export class Renderer {
|
||||||
// Open terminal in the wrapper
|
// Open terminal in the wrapper
|
||||||
this.terminal.open(terminalWrapper);
|
this.terminal.open(terminalWrapper);
|
||||||
|
|
||||||
// Just use FitAddon
|
// Always use ScaleFitAddon for better scaling
|
||||||
this.fitAddon.fit();
|
this.scaleFitAddon.fit();
|
||||||
|
|
||||||
// Handle container resize
|
// Handle container resize
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
this.fitAddon.fit();
|
this.scaleFitAddon.fit();
|
||||||
});
|
});
|
||||||
resizeObserver.observe(this.container);
|
resizeObserver.observe(this.container);
|
||||||
}
|
}
|
||||||
|
|
@ -176,8 +180,12 @@ export class Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(width: number, height: number): void {
|
resize(width: number, height: number): void {
|
||||||
// Ignore session resize and just use FitAddon
|
if (this.isPreview) {
|
||||||
this.fitAddon.fit();
|
// For previews, resize to session dimensions then apply scaling
|
||||||
|
this.terminal.resize(width, height);
|
||||||
|
}
|
||||||
|
// Always use ScaleFitAddon for consistent scaling behavior
|
||||||
|
this.scaleFitAddon.fit();
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
|
|
|
||||||
127
web/src/client/scale-fit-addon.ts
Normal file
127
web/src/client/scale-fit-addon.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
/**
|
||||||
|
* Custom FitAddon that scales font size to fit terminal columns to container width,
|
||||||
|
* then calculates optimal rows for the container height.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||||
|
|
||||||
|
interface ITerminalDimensions {
|
||||||
|
rows: number;
|
||||||
|
cols: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MINIMUM_ROWS = 1;
|
||||||
|
|
||||||
|
export class ScaleFitAddon implements ITerminalAddon {
|
||||||
|
private _terminal: Terminal | undefined;
|
||||||
|
|
||||||
|
public activate(terminal: Terminal): void {
|
||||||
|
this._terminal = terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {}
|
||||||
|
|
||||||
|
public fit(): void {
|
||||||
|
const dims = this.proposeDimensions();
|
||||||
|
if (!dims || !this._terminal || isNaN(dims.cols) || isNaN(dims.rows)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only resize rows, keep cols the same (font scaling handles width)
|
||||||
|
if (this._terminal.rows !== dims.rows) {
|
||||||
|
this._terminal.resize(this._terminal.cols, dims.rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public proposeDimensions(): ITerminalDimensions | undefined {
|
||||||
|
if (!this._terminal?.element?.parentElement) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get container dimensions
|
||||||
|
const parentElement = this._terminal.element.parentElement;
|
||||||
|
const parentStyle = window.getComputedStyle(parentElement);
|
||||||
|
const parentWidth = parseInt(parentStyle.getPropertyValue('width'));
|
||||||
|
const parentHeight = parseInt(parentStyle.getPropertyValue('height'));
|
||||||
|
|
||||||
|
// Get terminal element padding
|
||||||
|
const elementStyle = window.getComputedStyle(this._terminal.element);
|
||||||
|
const padding = {
|
||||||
|
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||||
|
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||||
|
left: parseInt(elementStyle.getPropertyValue('padding-left')),
|
||||||
|
right: parseInt(elementStyle.getPropertyValue('padding-right'))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate available space
|
||||||
|
const availableWidth = parentWidth - padding.left - padding.right - 20; // Extra margin
|
||||||
|
const availableHeight = parentHeight - padding.top - padding.bottom - 20;
|
||||||
|
|
||||||
|
// Current terminal dimensions
|
||||||
|
const currentCols = this._terminal.cols;
|
||||||
|
|
||||||
|
// Calculate optimal font size to fit current cols in available width
|
||||||
|
// Character width is approximately 0.6 * fontSize for monospace fonts
|
||||||
|
const charWidthRatio = 0.6;
|
||||||
|
const optimalFontSize = Math.max(6, availableWidth / (currentCols * charWidthRatio));
|
||||||
|
|
||||||
|
// Apply the calculated font size (outside of proposeDimensions to avoid recursion)
|
||||||
|
setTimeout(() => this.applyFontSize(optimalFontSize), 0);
|
||||||
|
|
||||||
|
// Calculate line height (typically 1.2 * fontSize)
|
||||||
|
const lineHeight = optimalFontSize * 1.2;
|
||||||
|
|
||||||
|
// Calculate how many rows fit with this font size
|
||||||
|
const optimalRows = Math.max(MINIMUM_ROWS, Math.floor(availableHeight / lineHeight));
|
||||||
|
|
||||||
|
return {
|
||||||
|
cols: currentCols, // Keep existing cols
|
||||||
|
rows: optimalRows // Fit as many rows as possible
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyFontSize(fontSize: number): void {
|
||||||
|
if (!this._terminal?.element) return;
|
||||||
|
|
||||||
|
// Prevent infinite recursion by checking if font size changed significantly
|
||||||
|
const currentFontSize = this._terminal.options.fontSize || 14;
|
||||||
|
if (Math.abs(fontSize - currentFontSize) < 0.1) return;
|
||||||
|
|
||||||
|
const terminalElement = this._terminal.element;
|
||||||
|
|
||||||
|
// Update terminal's font size
|
||||||
|
this._terminal.options.fontSize = fontSize;
|
||||||
|
|
||||||
|
// Apply CSS font size to the element
|
||||||
|
terminalElement.style.fontSize = `${fontSize}px`;
|
||||||
|
|
||||||
|
// Force a refresh to apply the new font size
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (this._terminal) {
|
||||||
|
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the calculated font size that would fit the current columns in the container
|
||||||
|
*/
|
||||||
|
public getOptimalFontSize(): number {
|
||||||
|
if (!this._terminal?.element?.parentElement) {
|
||||||
|
return this._terminal?.options.fontSize || 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentElement = this._terminal.element.parentElement;
|
||||||
|
const parentStyle = window.getComputedStyle(parentElement);
|
||||||
|
const parentWidth = parseInt(parentStyle.getPropertyValue('width'));
|
||||||
|
|
||||||
|
const elementStyle = window.getComputedStyle(this._terminal.element);
|
||||||
|
const paddingHor = parseInt(elementStyle.getPropertyValue('padding-left')) +
|
||||||
|
parseInt(elementStyle.getPropertyValue('padding-right'));
|
||||||
|
|
||||||
|
const availableWidth = parentWidth - paddingHor;
|
||||||
|
const charWidthRatio = 0.6;
|
||||||
|
|
||||||
|
return availableWidth / (this._terminal.cols * charWidthRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -60,8 +60,7 @@ session-view .xterm-helper-textarea {
|
||||||
.session-preview .xterm .xterm-screen {
|
.session-preview .xterm .xterm-screen {
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
max-height: 100% !important;
|
max-height: 100% !important;
|
||||||
transform: scale(0.8); /* Scale down the content to fit better */
|
/* Scaling now handled by ScaleFitAddon */
|
||||||
transform-origin: top left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-preview .xterm .xterm-viewport {
|
.session-preview .xterm .xterm-viewport {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue