Skip to main content
Glama
terminal.ts9.2 kB
/** * Terminal Detection Utilities * * Provides terminal capability detection and size measurement * for adaptive TUI rendering. * * @package WP_Navigator_Pro * @since 2.5.0 * * @example * import { getTerminalSize, getCapabilities, isSmallTerminal } from './terminal.js'; * * const size = getTerminalSize(); * console.log(`Terminal: ${size.width}x${size.height}`); * * const caps = getCapabilities(); * if (caps.supportsAnsi) { * // Use ANSI escape codes * } */ import { supportsColor } from './components.js'; import { supportsHyperlinks } from './links.js'; // ============================================================================= // Types // ============================================================================= /** * Terminal dimensions */ export interface TerminalSize { /** Number of columns (characters per line) */ width: number; /** Number of rows (lines visible) */ height: number; } /** * Terminal capability flags */ export interface TerminalCapabilities { /** Whether output is going to a TTY (interactive terminal) */ isTTY: boolean; /** Whether terminal likely supports ANSI escape codes */ supportsAnsi: boolean; /** Whether terminal supports ANSI colors */ supportsColor: boolean; /** Whether terminal supports OSC 8 hyperlinks */ supportsHyperlinks: boolean; /** Whether terminal is a "dumb" terminal with no capabilities */ isDumb: boolean; } /** * Content area dimensions (terminal minus reserved header/footer) */ export interface ContentArea { /** Available width for content */ width: number; /** Available height for content */ height: number; /** Starting row for content (1-based) */ startRow: number; } // ============================================================================= // Constants // ============================================================================= /** Default terminal width for non-TTY environments */ export const DEFAULT_WIDTH = 80; /** Default terminal height for non-TTY environments */ export const DEFAULT_HEIGHT = 24; /** Minimum terminal width for page-based display */ export const MIN_PAGE_WIDTH = 40; /** Minimum terminal height for page-based display */ export const MIN_PAGE_HEIGHT = 10; // ============================================================================= // Terminal Size Detection // ============================================================================= /** * Get current terminal dimensions * * Uses process.stdout dimensions when available, * falls back to standard 80x24 for non-TTY environments. * * @returns Terminal width and height */ export function getTerminalSize(): TerminalSize { const isTTY = process.stdout.isTTY ?? false; if (!isTTY) { return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }; } return { width: process.stdout.columns ?? DEFAULT_WIDTH, height: process.stdout.rows ?? DEFAULT_HEIGHT, }; } /** * Get terminal size with resize listener support * * @param onResize - Callback when terminal is resized * @returns Current size and cleanup function */ export function watchTerminalSize(onResize: (size: TerminalSize) => void): { size: TerminalSize; cleanup: () => void; } { const size = getTerminalSize(); const handler = (): void => { onResize(getTerminalSize()); }; process.stdout.on('resize', handler); return { size, cleanup: (): void => { process.stdout.off('resize', handler); }, }; } // ============================================================================= // Capability Detection // ============================================================================= /** * Check if terminal is a "dumb" terminal * * Dumb terminals don't support ANSI escape codes. * Common in CI environments, pipes, or legacy systems. * * @returns True if terminal is dumb */ export function isDumbTerminal(): boolean { const term = process.env.TERM?.toLowerCase() ?? ''; return term === 'dumb' || term === ''; } /** * Check if terminal supports ANSI escape codes * * Checks for: * - TTY output * - Non-dumb terminal * - Not explicitly disabled via CI environment * * @returns True if ANSI is likely supported */ export function supportsAnsi(): boolean { // Not a TTY - no ANSI support if (!process.stdout.isTTY) return false; // Dumb terminal - no ANSI support if (isDumbTerminal()) return false; // CI environments often don't support full ANSI // But many modern CI systems do, so we check TERM const term = process.env.TERM ?? ''; if (term.includes('xterm') || term.includes('screen') || term.includes('tmux')) { return true; } // Windows Terminal, VS Code, iTerm2 all support ANSI const termProgram = process.env.TERM_PROGRAM ?? ''; if (termProgram === 'iTerm.app' || termProgram === 'Apple_Terminal' || termProgram === 'vscode') { return true; } // Windows Terminal if (process.env.WT_SESSION) return true; // Default to checking TTY + color support as proxy return supportsColor(); } /** * Get all terminal capabilities * * @returns Object with all capability flags */ export function getCapabilities(): TerminalCapabilities { const isTTY = process.stdout.isTTY ?? false; const isDumb = isDumbTerminal(); return { isTTY, isDumb, supportsAnsi: supportsAnsi(), supportsColor: supportsColor(), supportsHyperlinks: supportsHyperlinks(), }; } // ============================================================================= // Size Checks // ============================================================================= /** * Check if terminal is too small for page-based display * * Page-based rendering needs minimum space for: * - Header (1-3 lines) * - Content area (at least 5 lines) * - Footer (1-2 lines) * * @param minWidth - Minimum acceptable width (default: 40) * @param minHeight - Minimum acceptable height (default: 10) * @returns True if terminal is smaller than minimums */ export function isSmallTerminal(minWidth = MIN_PAGE_WIDTH, minHeight = MIN_PAGE_HEIGHT): boolean { const { width, height } = getTerminalSize(); return width < minWidth || height < minHeight; } /** * Check if page-based TUI is supported * * Requires: * - TTY output * - ANSI support * - Minimum terminal size * * @returns True if page-based TUI can be used */ export function supportsPageTUI(): boolean { const caps = getCapabilities(); if (!caps.isTTY) return false; if (!caps.supportsAnsi) return false; if (caps.isDumb) return false; if (isSmallTerminal()) return false; return true; } // ============================================================================= // Content Area Calculation // ============================================================================= /** * Calculate available content area * * Subtracts reserved space for header and footer from total terminal size. * * @param headerLines - Lines reserved for header (default: 3) * @param footerLines - Lines reserved for footer (default: 2) * @returns Available content area dimensions */ export function getContentArea(headerLines = 3, footerLines = 2): ContentArea { const { width, height } = getTerminalSize(); const contentHeight = Math.max(1, height - headerLines - footerLines); const startRow = headerLines + 1; // 1-based row after header return { width, height: contentHeight, startRow, }; } // ============================================================================= // Utility Functions // ============================================================================= /** * Truncate text to fit terminal width * * @param text - Text to truncate * @param maxWidth - Maximum width (default: terminal width - 2 for padding) * @param ellipsis - Ellipsis character (default: '...') * @returns Truncated text */ export function truncateToWidth(text: string, maxWidth?: number, ellipsis = '...'): string { const width = maxWidth ?? getTerminalSize().width - 2; if (text.length <= width) return text; return text.slice(0, width - ellipsis.length) + ellipsis; } /** * Wrap text to fit terminal width * * Simple word-wrap implementation for multi-line content. * * @param text - Text to wrap * @param maxWidth - Maximum width per line (default: terminal width - 4) * @returns Array of wrapped lines */ export function wrapText(text: string, maxWidth?: number): string[] { const width = maxWidth ?? getTerminalSize().width - 4; const words = text.split(/\s+/); const lines: string[] = []; let currentLine = ''; for (const word of words) { if (currentLine.length + word.length + 1 <= width) { currentLine += (currentLine ? ' ' : '') + word; } else { if (currentLine) lines.push(currentLine); // Handle words longer than max width if (word.length > width) { let remaining = word; while (remaining.length > width) { lines.push(remaining.slice(0, width)); remaining = remaining.slice(width); } currentLine = remaining; } else { currentLine = word; } } } if (currentLine) lines.push(currentLine); return lines; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server