mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 11:02:17 +00:00
Fix image aspect ratio by querying terminal cell size
- Add getCellDimensions/setCellDimensions to terminal-image.ts - TUI queries cell size on startup via CSI 16 t - Parse response and filter it from user input - Invalidate and re-render when cell dimensions received - Pass rows parameter to Kitty protocol for correct aspect ratio
This commit is contained in:
parent
fcad447f32
commit
215c10664a
3 changed files with 93 additions and 9 deletions
|
|
@ -27,6 +27,7 @@ export {
|
|||
encodeITerm2,
|
||||
encodeKitty,
|
||||
getCapabilities,
|
||||
getCellDimensions,
|
||||
getGifDimensions,
|
||||
getImageDimensions,
|
||||
getJpegDimensions,
|
||||
|
|
@ -38,6 +39,7 @@ export {
|
|||
imageFallback,
|
||||
renderImage,
|
||||
resetCapabilitiesCache,
|
||||
setCellDimensions,
|
||||
type TerminalCapabilities,
|
||||
} from "./terminal-image.js";
|
||||
export { type Component, Container, TUI } from "./tui.js";
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ export interface ImageRenderOptions {
|
|||
|
||||
let cachedCapabilities: TerminalCapabilities | null = null;
|
||||
|
||||
// Default cell dimensions - updated by TUI when terminal responds to query
|
||||
let cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 };
|
||||
|
||||
export function getCellDimensions(): CellDimensions {
|
||||
return cellDimensions;
|
||||
}
|
||||
|
||||
export function setCellDimensions(dims: CellDimensions): void {
|
||||
cellDimensions = dims;
|
||||
}
|
||||
|
||||
export function detectCapabilities(): TerminalCapabilities {
|
||||
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
|
||||
const term = process.env.TERM?.toLowerCase() || "";
|
||||
|
|
@ -307,11 +318,10 @@ export function renderImage(
|
|||
}
|
||||
|
||||
const maxWidth = options.maxWidthCells ?? 80;
|
||||
const cellDims: CellDimensions = { widthPx: 9, heightPx: 18 };
|
||||
const rows = calculateImageRows(imageDimensions, maxWidth, cellDims);
|
||||
const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
|
||||
|
||||
if (caps.images === "kitty") {
|
||||
const sequence = encodeKitty(base64Data, { columns: maxWidth });
|
||||
const sequence = encodeKitty(base64Data, { columns: maxWidth, rows });
|
||||
return { sequence, rows };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import * as fs from "node:fs";
|
|||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { Terminal } from "./terminal.js";
|
||||
import { getCapabilities } from "./terminal-image.js";
|
||||
import { getCapabilities, setCellDimensions } from "./terminal-image.js";
|
||||
import { visibleWidth } from "./utils.js";
|
||||
|
||||
/**
|
||||
|
|
@ -80,6 +80,8 @@ export class TUI extends Container {
|
|||
private focusedComponent: Component | null = null;
|
||||
private renderRequested = false;
|
||||
private cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)
|
||||
private inputBuffer = ""; // Buffer for parsing terminal responses
|
||||
private cellSizeQueryPending = false;
|
||||
|
||||
constructor(terminal: Terminal) {
|
||||
super();
|
||||
|
|
@ -96,9 +98,17 @@ export class TUI extends Container {
|
|||
() => this.requestRender(),
|
||||
);
|
||||
this.terminal.hideCursor();
|
||||
this.queryCellSize();
|
||||
this.requestRender();
|
||||
}
|
||||
|
||||
private queryCellSize(): void {
|
||||
// Query terminal for cell size in pixels: CSI 16 t
|
||||
// Response format: CSI 6 ; height ; width t
|
||||
this.cellSizeQueryPending = true;
|
||||
this.terminal.write("\x1b[16t");
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.terminal.showCursor();
|
||||
this.terminal.stop();
|
||||
|
|
@ -114,6 +124,14 @@ export class TUI extends Container {
|
|||
}
|
||||
|
||||
private handleInput(data: string): void {
|
||||
// If we're waiting for cell size response, buffer input and parse
|
||||
if (this.cellSizeQueryPending) {
|
||||
this.inputBuffer += data;
|
||||
const filtered = this.parseCellSizeResponse();
|
||||
if (filtered.length === 0) return;
|
||||
data = filtered;
|
||||
}
|
||||
|
||||
// Pass input to focused component (including Ctrl+C)
|
||||
// The focused component can decide how to handle Ctrl+C
|
||||
if (this.focusedComponent?.handleInput) {
|
||||
|
|
@ -122,6 +140,52 @@ export class TUI extends Container {
|
|||
}
|
||||
}
|
||||
|
||||
private parseCellSizeResponse(): string {
|
||||
// Response format: ESC [ 6 ; height ; width t
|
||||
// Match the response pattern
|
||||
const responsePattern = /\x1b\[6;(\d+);(\d+)t/;
|
||||
const match = this.inputBuffer.match(responsePattern);
|
||||
|
||||
if (match) {
|
||||
const heightPx = parseInt(match[1], 10);
|
||||
const widthPx = parseInt(match[2], 10);
|
||||
|
||||
if (heightPx > 0 && widthPx > 0) {
|
||||
setCellDimensions({ widthPx, heightPx });
|
||||
// Invalidate all components so images re-render with correct dimensions
|
||||
this.invalidate();
|
||||
this.requestRender();
|
||||
}
|
||||
|
||||
// Remove the response from buffer
|
||||
this.inputBuffer = this.inputBuffer.replace(responsePattern, "");
|
||||
this.cellSizeQueryPending = false;
|
||||
}
|
||||
|
||||
// Check if we have a partial response starting (wait for more data)
|
||||
// ESC [ 6 ; ... could be incomplete
|
||||
const partialPattern = /\x1b\[6;[\d;]*$/;
|
||||
if (partialPattern.test(this.inputBuffer)) {
|
||||
return ""; // Wait for more data
|
||||
}
|
||||
|
||||
// Check for any ESC that might be start of response
|
||||
const escIndex = this.inputBuffer.lastIndexOf("\x1b");
|
||||
if (escIndex !== -1 && escIndex > this.inputBuffer.length - 10) {
|
||||
// Might be incomplete escape sequence, wait a bit
|
||||
// But return any data before it
|
||||
const before = this.inputBuffer.substring(0, escIndex);
|
||||
this.inputBuffer = this.inputBuffer.substring(escIndex);
|
||||
return before;
|
||||
}
|
||||
|
||||
// No response found, return buffered data as user input
|
||||
const result = this.inputBuffer;
|
||||
this.inputBuffer = "";
|
||||
this.cellSizeQueryPending = false; // Give up waiting
|
||||
return result;
|
||||
}
|
||||
|
||||
private containsImage(line: string): boolean {
|
||||
return line.includes("\x1b_G") || line.includes("\x1b]1337;File=");
|
||||
}
|
||||
|
|
@ -187,12 +251,20 @@ export class TUI extends Container {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if we have images - they require special handling to avoid duplication
|
||||
const hasImagesInPrevious = this.previousLines.some((line) => this.containsImage(line));
|
||||
const hasImagesInNew = newLines.some((line) => this.containsImage(line));
|
||||
// Check if image lines changed - they require special handling to avoid duplication
|
||||
// Only force full re-render if image content actually changed, not just because images exist
|
||||
const imageLineChanged = (() => {
|
||||
for (let i = firstChanged; i < Math.max(newLines.length, this.previousLines.length); i++) {
|
||||
const prevLine = this.previousLines[i] || "";
|
||||
const newLine = newLines[i] || "";
|
||||
if (this.containsImage(prevLine) || this.containsImage(newLine)) {
|
||||
if (prevLine !== newLine) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
// If images are present and content changed, force full re-render
|
||||
if (hasImagesInPrevious || hasImagesInNew) {
|
||||
if (imageLineChanged) {
|
||||
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
||||
// For Kitty protocol, delete all images before re-render
|
||||
if (getCapabilities().images === "kitty") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue