UNPKG

@owloops/claude-powerline

Version:

Beautiful vim-style powerline statusline for Claude Code with real-time usage tracking, git integration, and custom themes

1,048 lines (936 loc) 30.1 kB
import type { ClaudeHookData } from "./utils/claude"; import type { PowerlineColors, ColorTheme } from "./themes"; import type { PowerlineConfig, LineConfig } from "./config/loader"; import type { UsageInfo, ContextInfo, MetricsInfo, PowerlineSymbols, AnySegmentConfig, DirectorySegmentConfig, GitSegmentConfig, UsageSegmentConfig, ContextSegmentConfig, MetricsSegmentConfig, BlockSegmentConfig, TodaySegmentConfig, VersionSegmentConfig, SessionIdSegmentConfig, EnvSegmentConfig, WeeklySegmentConfig, AgentSegmentConfig, ThinkingSegmentConfig, CacheTimerSegmentConfig, } from "./segments"; import type { BlockInfo } from "./segments/block"; import type { TodayInfo } from "./segments/today"; import type { CacheTimerInfo } from "./segments/cacheTimer"; import type { TuiData } from "./tui"; import { hexToAnsi, extractBgToFg, hexToBasicAnsi, hexTo256Ansi, hexColorDistance, } from "./utils/colors"; import { getColorSupport } from "./utils/color-support"; import { getTheme } from "./themes"; import { UsageProvider, ContextProvider, GitService, TmuxService, MetricsProvider, SegmentRenderer, } from "./segments"; import { BlockProvider } from "./segments/block"; import { TodayProvider } from "./segments/today"; import { CacheTimerProvider } from "./segments/cacheTimer"; import { SYMBOLS, TEXT_SYMBOLS, RESET_CODE, BOX_CHARS, BOX_CHARS_TEXT, } from "./utils/constants"; import { visibleLength } from "./utils/terminal"; import { getTerminalWidth, getRawTerminalWidth } from "./utils/terminal-width"; import { renderTuiPanel } from "./tui"; interface RenderedSegment { type: string; text: string; bgColor: string; fgColor: string; bold?: boolean; } export class PowerlineRenderer { private readonly symbols: PowerlineSymbols; private _usageProvider?: UsageProvider; private _blockProvider?: BlockProvider; private _todayProvider?: TodayProvider; private _contextProvider?: ContextProvider; private _gitService?: GitService; private _tmuxService?: TmuxService; private _metricsProvider?: MetricsProvider; private _cacheTimerProvider?: CacheTimerProvider; private _segmentRenderer?: SegmentRenderer; constructor(private readonly config: PowerlineConfig) { this.symbols = this.initializeSymbols(); } private get usageProvider(): UsageProvider { if (!this._usageProvider) { this._usageProvider = new UsageProvider(); } return this._usageProvider; } private get blockProvider(): BlockProvider { if (!this._blockProvider) { this._blockProvider = new BlockProvider(); } return this._blockProvider; } private get todayProvider(): TodayProvider { if (!this._todayProvider) { this._todayProvider = new TodayProvider(); } return this._todayProvider; } private get contextProvider(): ContextProvider { if (!this._contextProvider) { this._contextProvider = new ContextProvider(this.config); } return this._contextProvider; } private get gitService(): GitService { if (!this._gitService) { this._gitService = new GitService(); } return this._gitService; } private get tmuxService(): TmuxService { if (!this._tmuxService) { this._tmuxService = new TmuxService(); } return this._tmuxService; } private get metricsProvider(): MetricsProvider { if (!this._metricsProvider) { this._metricsProvider = new MetricsProvider(); } return this._metricsProvider; } private get cacheTimerProvider(): CacheTimerProvider { if (!this._cacheTimerProvider) { this._cacheTimerProvider = new CacheTimerProvider(); } return this._cacheTimerProvider; } private get segmentRenderer(): SegmentRenderer { if (!this._segmentRenderer) { this._segmentRenderer = new SegmentRenderer(this.config, this.symbols); } return this._segmentRenderer; } private needsSegmentInfo(segmentType: keyof LineConfig["segments"]): boolean { return this.config.display.lines.some( (line) => line.segments[segmentType]?.enabled, ); } async generateStatusline(hookData: ClaudeHookData): Promise<string> { if (this.config.display.style === "tui") { return this.generateTuiStatusline(hookData); } const usageInfo = this.needsSegmentInfo("session") ? await this.usageProvider.getUsageInfo(hookData.session_id, hookData) : null; const blockInfo = this.needsSegmentInfo("block") ? await this.blockProvider.getActiveBlockInfo(hookData) : null; const todayInfo = this.needsSegmentInfo("today") ? await this.todayProvider.getTodayInfo() : null; const contextSegmentConfig = this.config.display.lines .map((line) => line.segments.context) .find((c) => c?.enabled) as ContextSegmentConfig | undefined; const autocompactBuffer = contextSegmentConfig?.autocompactBuffer ?? 33000; const contextInfo = this.needsSegmentInfo("context") ? await this.contextProvider.getContextInfo(hookData, autocompactBuffer) : null; const metricsInfo = this.needsSegmentInfo("metrics") ? await this.metricsProvider.getMetricsInfo(hookData.session_id, hookData) : null; const cacheTimerInfo = this.needsSegmentInfo("cacheTimer") ? await this.cacheTimerProvider.getCacheTimerInfo(hookData) : null; if (this.config.display.autoWrap) { return this.generateAutoWrapStatusline( hookData, usageInfo, blockInfo, todayInfo, contextInfo, metricsInfo, cacheTimerInfo, ); } const lines = await Promise.all( this.config.display.lines.map((lineConfig) => this.renderLine( lineConfig, hookData, usageInfo, blockInfo, todayInfo, contextInfo, metricsInfo, cacheTimerInfo, ), ), ); return lines.filter((line) => line.length > 0).join("\n"); } private async generateAutoWrapStatusline( hookData: ClaudeHookData, usageInfo: UsageInfo | null, blockInfo: BlockInfo | null, todayInfo: TodayInfo | null, contextInfo: ContextInfo | null, metricsInfo: MetricsInfo | null, cacheTimerInfo: CacheTimerInfo | null, ): Promise<string> { const colors = this.getThemeColors(); const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/"; const terminalWidth = getTerminalWidth(); const outputLines: string[] = []; for (const lineConfig of this.config.display.lines) { const segments = Object.entries(lineConfig.segments) .filter( ([_, config]: [string, AnySegmentConfig | undefined]) => config?.enabled, ) .map(([type, config]: [string, AnySegmentConfig]) => ({ type, config, })); const renderedSegments: RenderedSegment[] = []; for (const segment of segments) { const segmentData = await this.renderSegment( segment, hookData, usageInfo, blockInfo, todayInfo, contextInfo, metricsInfo, cacheTimerInfo, colors, currentDir, ); if (segmentData) { renderedSegments.push({ type: segment.type, text: segmentData.text, bgColor: segmentData.bgColor, fgColor: segmentData.fgColor, bold: segmentData.bold, }); } } if (renderedSegments.length === 0) continue; if (!terminalWidth || terminalWidth <= 0) { outputLines.push(this.buildLineFromSegments(renderedSegments, colors)); continue; } let currentLineSegments: RenderedSegment[] = []; let currentLineWidth = 0; for (const segment of renderedSegments) { const segmentWidth = this.calculateSegmentWidth( segment, currentLineSegments.length === 0, ); if ( currentLineSegments.length > 0 && currentLineWidth + segmentWidth > terminalWidth ) { outputLines.push( this.buildLineFromSegments(currentLineSegments, colors), ); currentLineSegments = []; currentLineWidth = 0; } currentLineSegments.push(segment); currentLineWidth += segmentWidth; } if (currentLineSegments.length > 0) { outputLines.push( this.buildLineFromSegments(currentLineSegments, colors), ); } } return outputLines.join("\n"); } private async generateTuiStatusline( hookData: ClaudeHookData, ): Promise<string> { const colors = this.getThemeColors(); const terminalWidth = getTerminalWidth(); const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/"; const charset = this.config.display.charset || "unicode"; const boxChars = charset === "text" ? BOX_CHARS_TEXT : BOX_CHARS; const contextSegmentConfig = this.config.display.lines .map((line) => line.segments.context) .find((c) => c?.enabled) as ContextSegmentConfig | undefined; const autocompactBuffer = contextSegmentConfig?.autocompactBuffer ?? 33000; const results = await Promise.allSettled([ this.usageProvider.getUsageInfo(hookData.session_id, hookData), this.blockProvider.getActiveBlockInfo(hookData), this.todayProvider.getTodayInfo(), this.contextProvider.getContextInfo(hookData, autocompactBuffer), this.metricsProvider.getMetricsInfo(hookData.session_id, hookData), this.gitService.getGitInfo( currentDir, { showSha: false, showWorkingTree: true, showOperation: false, showTag: false, showTimeSinceCommit: false, showStashCount: false, showUpstream: false, showRepoName: false, }, hookData.workspace?.project_dir, ), this.tmuxService.getSessionId(), this.cacheTimerProvider.getCacheTimerInfo(hookData), ]); const val = <T>(r: PromiseSettledResult<T>) => r.status === "fulfilled" ? r.value : null; const [ usageInfo, blockInfo, todayInfo, contextInfo, metricsInfo, gitInfo, tmuxSessionId, cacheTimerInfo, ] = [ val(results[0]!), val(results[1]!), val(results[2]!), val(results[3]!), val(results[4]!), val(results[5]!), val(results[6]!), val(results[7]!), ] as const; const tuiData: TuiData = { hookData, usageInfo, blockInfo, todayInfo, contextInfo, metricsInfo, gitInfo, cacheTimerInfo, tmuxSessionId, colors, }; return renderTuiPanel( tuiData, boxChars, colors.reset, terminalWidth, this.config, { rawTerminalWidth: getRawTerminalWidth() }, ); } private calculateSegmentWidth( segment: RenderedSegment, isFirst: boolean, ): number { const isCapsuleStyle = this.config.display.style === "capsule"; const textWidth = visibleLength(segment.text); const padding = this.config.display.padding ?? 1; const paddingWidth = padding * 2; if (isCapsuleStyle) { const capsuleOverhead = 2 + paddingWidth + (isFirst ? 0 : 1); return textWidth + capsuleOverhead; } const powerlineOverhead = 1 + paddingWidth; return textWidth + powerlineOverhead; } private buildLineFromSegments( segments: RenderedSegment[], colors: PowerlineColors, ): string { const isCapsuleStyle = this.config.display.style === "capsule"; let line = colors.reset; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; if (!segment) continue; const isFirst = i === 0; const isLast = i === segments.length - 1; const nextSegment = !isLast ? segments[i + 1] : null; if (isCapsuleStyle && !isFirst) { line += " "; } const bold = segment.bold ?? this.getSegmentBoldFlag(segment.type, colors); line += this.formatSegment( segment.bgColor, segment.fgColor, segment.text, nextSegment?.bgColor, colors, bold, ); } return line; } private async renderLine( lineConfig: LineConfig, hookData: ClaudeHookData, usageInfo: UsageInfo | null, blockInfo: BlockInfo | null, todayInfo: TodayInfo | null, contextInfo: ContextInfo | null, metricsInfo: MetricsInfo | null, cacheTimerInfo: CacheTimerInfo | null, ): Promise<string> { const colors = this.getThemeColors(); const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/"; const segments = Object.entries(lineConfig.segments) .filter( ([_, config]: [string, AnySegmentConfig | undefined]) => config?.enabled, ) .map(([type, config]: [string, AnySegmentConfig]) => ({ type, config })); const renderedSegments: RenderedSegment[] = []; for (const segment of segments) { const segmentData = await this.renderSegment( segment, hookData, usageInfo, blockInfo, todayInfo, contextInfo, metricsInfo, cacheTimerInfo, colors, currentDir, ); if (segmentData) { renderedSegments.push({ type: segment.type, text: segmentData.text, bgColor: segmentData.bgColor, fgColor: segmentData.fgColor, bold: segmentData.bold, }); } } return this.buildLineFromSegments(renderedSegments, colors); } private async renderSegment( segment: { type: string; config: AnySegmentConfig }, hookData: ClaudeHookData, usageInfo: UsageInfo | null, blockInfo: BlockInfo | null, todayInfo: TodayInfo | null, contextInfo: ContextInfo | null, metricsInfo: MetricsInfo | null, cacheTimerInfo: CacheTimerInfo | null, colors: PowerlineColors, currentDir: string, ) { if (segment.type === "directory") { return this.segmentRenderer.renderDirectory( hookData, colors, segment.config as DirectorySegmentConfig, ); } if (segment.type === "model") { return this.segmentRenderer.renderModel(hookData, colors, segment.config); } if (segment.type === "git") { return await this.renderGitSegment( segment.config as GitSegmentConfig, hookData, colors, currentDir, ); } if (segment.type === "session") { return this.renderSessionSegment( segment.config as UsageSegmentConfig, usageInfo, colors, ); } if (segment.type === "sessionId") { return hookData.session_id ? this.segmentRenderer.renderSessionId( hookData.session_id, colors, segment.config as SessionIdSegmentConfig, ) : null; } if (segment.type === "tmux") { return await this.renderTmuxSegment(colors); } if (segment.type === "context") { return this.renderContextSegment( segment.config as ContextSegmentConfig, contextInfo, colors, ); } if (segment.type === "metrics") { return this.renderMetricsSegment( segment.config as MetricsSegmentConfig, metricsInfo, blockInfo, colors, ); } if (segment.type === "block") { return this.renderBlockSegment( segment.config as BlockSegmentConfig, blockInfo, colors, ); } if (segment.type === "today") { return this.renderTodaySegment( segment.config as TodaySegmentConfig, todayInfo, colors, ); } if (segment.type === "version") { return this.renderVersionSegment( segment.config as VersionSegmentConfig, hookData, colors, ); } if (segment.type === "env") { return this.segmentRenderer.renderEnv( colors, segment.config as EnvSegmentConfig, ); } if (segment.type === "weekly") { return this.segmentRenderer.renderWeekly( hookData, colors, segment.config as WeeklySegmentConfig, ); } if (segment.type === "agent") { return this.segmentRenderer.renderAgent( hookData, colors, segment.config as AgentSegmentConfig, ); } if (segment.type === "thinking") { return this.segmentRenderer.renderThinking( hookData, colors, segment.config as ThinkingSegmentConfig, ); } if (segment.type === "cacheTimer") { if (!cacheTimerInfo) return null; return this.segmentRenderer.renderCacheTimer( cacheTimerInfo, colors, segment.config as CacheTimerSegmentConfig, ); } return null; } private async renderGitSegment( config: GitSegmentConfig, hookData: ClaudeHookData, colors: PowerlineColors, currentDir: string, ) { if (!this.needsSegmentInfo("git")) return null; const gitInfo = await this.gitService.getGitInfo( currentDir, { showSha: config?.showSha, showWorkingTree: config?.showWorkingTree, showOperation: config?.showOperation, showTag: config?.showTag, showTimeSinceCommit: config?.showTimeSinceCommit, showStashCount: config?.showStashCount, showUpstream: config?.showUpstream, showRepoName: config?.showRepoName, }, hookData.workspace?.project_dir, ); return gitInfo ? this.segmentRenderer.renderGit(gitInfo, colors, config) : null; } private renderSessionSegment( config: UsageSegmentConfig, usageInfo: UsageInfo | null, colors: PowerlineColors, ) { if (!usageInfo) return null; return this.segmentRenderer.renderSession(usageInfo, colors, config); } private async renderTmuxSegment(colors: PowerlineColors) { if (!this.needsSegmentInfo("tmux")) return null; const tmuxSessionId = await this.tmuxService.getSessionId(); return this.segmentRenderer.renderTmux(tmuxSessionId, colors); } private renderContextSegment( config: ContextSegmentConfig, contextInfo: ContextInfo | null, colors: PowerlineColors, ) { if (!this.needsSegmentInfo("context")) return null; return this.segmentRenderer.renderContext(contextInfo, colors, config); } private renderMetricsSegment( config: MetricsSegmentConfig, metricsInfo: MetricsInfo | null, _blockInfo: BlockInfo | null, colors: PowerlineColors, ) { return this.segmentRenderer.renderMetrics(metricsInfo, colors, config); } private renderBlockSegment( config: BlockSegmentConfig, blockInfo: BlockInfo | null, colors: PowerlineColors, ) { if (!blockInfo) return null; return this.segmentRenderer.renderBlock(blockInfo, colors, config); } private renderTodaySegment( config: TodaySegmentConfig, todayInfo: TodayInfo | null, colors: PowerlineColors, ) { if (!todayInfo) return null; return this.segmentRenderer.renderToday(todayInfo, colors, config); } private renderVersionSegment( config: VersionSegmentConfig, hookData: ClaudeHookData, colors: PowerlineColors, ) { return this.segmentRenderer.renderVersion(hookData, colors, config); } private initializeSymbols(): PowerlineSymbols { const style = this.config.display.style; const charset = this.config.display.charset || "unicode"; const isMinimalStyle = style === "minimal"; const isCapsuleStyle = style === "capsule"; const symbolSet = charset === "text" ? TEXT_SYMBOLS : SYMBOLS; return { right: isMinimalStyle ? "" : isCapsuleStyle ? symbolSet.right_rounded : symbolSet.right, left: isCapsuleStyle ? symbolSet.left_rounded : "", branch: symbolSet.branch, model: symbolSet.model, git_clean: symbolSet.git_clean, git_dirty: symbolSet.git_dirty, git_conflicts: symbolSet.git_conflicts, git_ahead: symbolSet.git_ahead, git_behind: symbolSet.git_behind, git_worktree: symbolSet.git_worktree, git_tag: symbolSet.git_tag, git_sha: symbolSet.git_sha, git_upstream: symbolSet.git_upstream, git_stash: symbolSet.git_stash, git_time: symbolSet.git_time, session_cost: symbolSet.session_cost, block_cost: symbolSet.block_cost, today_cost: symbolSet.today_cost, context_time: symbolSet.context_time, metrics_response: symbolSet.metrics_response, metrics_last_response: symbolSet.metrics_last_response, metrics_duration: symbolSet.metrics_duration, metrics_messages: symbolSet.metrics_messages, metrics_lines_added: symbolSet.metrics_lines_added, metrics_lines_removed: symbolSet.metrics_lines_removed, metrics_burn: symbolSet.metrics_burn, version: symbolSet.version, bar_filled: symbolSet.bar_filled, bar_empty: symbolSet.bar_empty, env: symbolSet.env, session_id: symbolSet.session_id, weekly_cost: symbolSet.weekly_cost, agent: symbolSet.agent, thinking: symbolSet.thinking, cache_timer: symbolSet.cache_timer, }; } private getThemeColors(): PowerlineColors { const theme = this.config.theme; let colorTheme; const colorMode = this.config.display.colorCompatibility || "auto"; const colorSupport = colorMode === "auto" ? getColorSupport() : colorMode; if (theme === "custom") { colorTheme = this.config.colors?.custom; if (!colorTheme) { throw new Error( "Custom theme selected but no colors provided in configuration", ); } } else { colorTheme = getTheme(theme, colorSupport); if (!colorTheme) { console.warn( `Built-in theme '${theme}' not found, falling back to 'dark' theme`, ); colorTheme = getTheme("dark", colorSupport)!; } } const convertHex = (hex: string, isBg: boolean): string => { if (colorSupport === "none") return ""; if (colorSupport === "ansi") return hexToBasicAnsi(hex, isBg); if (colorSupport === "ansi256") return hexTo256Ansi(hex, isBg); return hexToAnsi(hex, isBg); }; const fallbackTheme = getTheme("dark", colorSupport)!; const isTui = this.config.display.style === "tui"; const isLightTheme = theme === "light"; const terminalRef = isLightTheme ? "#f0f0f0" : "#1e1e1e"; const getSegmentColors = (segment: Exclude<keyof ColorTheme, "tui">) => { const fallback = fallbackTheme[segment]; const custom = colorTheme[segment]; const colors = { fg: custom?.fg || fallback.fg, bg: custom?.bg || fallback.bg, }; let fgHex = colors.fg; if (isTui && hexColorDistance(fgHex, terminalRef) < 60) { fgHex = colors.bg; } const bold = colorSupport !== "none" && Boolean(custom?.bold ?? fallback.bold); return { bg: convertHex(colors.bg, true), fg: convertHex(fgHex, false), bold, }; }; const directory = getSegmentColors("directory"); const git = getSegmentColors("git"); const model = getSegmentColors("model"); const session = getSegmentColors("session"); const block = getSegmentColors("block"); const today = getSegmentColors("today"); const tmux = getSegmentColors("tmux"); const context = getSegmentColors("context"); const contextWarning = getSegmentColors("contextWarning"); const contextCritical = getSegmentColors("contextCritical"); const metrics = getSegmentColors("metrics"); const version = getSegmentColors("version"); const env = getSegmentColors("env"); const weekly = getSegmentColors("weekly"); const agent = getSegmentColors("agent"); const thinking = getSegmentColors("thinking"); const cacheTimer = getSegmentColors("cacheTimer"); return { reset: colorSupport === "none" ? "" : RESET_CODE, modeBg: directory.bg, modeFg: directory.fg, modeBold: directory.bold, gitBg: git.bg, gitFg: git.fg, gitBold: git.bold, modelBg: model.bg, modelFg: model.fg, modelBold: model.bold, sessionBg: session.bg, sessionFg: session.fg, sessionBold: session.bold, blockBg: block.bg, blockFg: block.fg, blockBold: block.bold, todayBg: today.bg, todayFg: today.fg, todayBold: today.bold, tmuxBg: tmux.bg, tmuxFg: tmux.fg, tmuxBold: tmux.bold, contextBg: context.bg, contextFg: context.fg, contextBold: context.bold, contextWarningBg: contextWarning.bg, contextWarningFg: contextWarning.fg, contextWarningBold: contextWarning.bold, contextCriticalBg: contextCritical.bg, contextCriticalFg: contextCritical.fg, contextCriticalBold: contextCritical.bold, metricsBg: metrics.bg, metricsFg: metrics.fg, metricsBold: metrics.bold, versionBg: version.bg, versionFg: version.fg, versionBold: version.bold, envBg: env.bg, envFg: env.fg, envBold: env.bold, weeklyBg: weekly.bg, weeklyFg: weekly.fg, weeklyBold: weekly.bold, agentBg: agent.bg, agentFg: agent.fg, agentBold: agent.bold, thinkingBg: thinking.bg, thinkingFg: thinking.fg, thinkingBold: thinking.bold, cacheTimerBg: cacheTimer.bg, cacheTimerFg: cacheTimer.fg, cacheTimerBold: cacheTimer.bold, partFg: theme === "custom" ? this.resolvePartColors(convertHex) : {}, }; } private resolvePartColors( convertHex: (hex: string, isBg: boolean) => string, ): Record<string, string> { const custom = this.config.colors?.custom as | Record<string, { fg?: string }> | undefined; if (!custom) return {}; const result: Record<string, string> = {}; for (const key of Object.keys(custom)) { const entry = custom[key]; if (!entry?.fg) continue; result[key] = convertHex(entry.fg, false); } return result; } private getSegmentBgColor( segmentType: string, colors: PowerlineColors, ): string { switch (segmentType) { case "directory": return colors.modeBg; case "git": return colors.gitBg; case "model": return colors.modelBg; case "session": case "sessionId": return colors.sessionBg; case "block": return colors.blockBg; case "today": return colors.todayBg; case "tmux": return colors.tmuxBg; case "context": return colors.contextBg; case "metrics": return colors.metricsBg; case "version": return colors.versionBg; case "env": return colors.envBg; case "weekly": return colors.weeklyBg; case "agent": return colors.agentBg; case "thinking": return colors.thinkingBg; case "cacheTimer": return colors.cacheTimerBg; default: return colors.modeBg; } } private getSegmentBoldFlag( segmentType: string, colors: PowerlineColors, ): boolean { switch (segmentType) { case "directory": return colors.modeBold; case "git": return colors.gitBold; case "model": return colors.modelBold; case "session": case "sessionId": return colors.sessionBold; case "block": return colors.blockBold; case "today": return colors.todayBold; case "tmux": return colors.tmuxBold; case "context": return colors.contextBold; case "metrics": return colors.metricsBold; case "version": return colors.versionBold; case "env": return colors.envBold; case "weekly": return colors.weeklyBold; case "agent": return colors.agentBold; case "thinking": return colors.thinkingBold; case "cacheTimer": return colors.cacheTimerBold; default: return colors.modeBold; } } private formatSegment( bgColor: string, fgColor: string, text: string, nextBgColor: string | undefined, colors: PowerlineColors, bold: boolean, ): string { const isCapsuleStyle = this.config.display.style === "capsule"; const padding = " ".repeat(this.config.display.padding ?? 1); const useBold = bold && colors.reset !== ""; const boldOn = useBold ? "\x1b[1m" : ""; const boldOff = useBold ? "\x1b[22m" : ""; if (isCapsuleStyle) { const colorMode = this.config.display.colorCompatibility || "auto"; const colorSupport = colorMode === "auto" ? getColorSupport() : colorMode; const isBasicMode = colorSupport === "ansi"; const capFgColor = extractBgToFg(bgColor, isBasicMode); const leftCap = `${capFgColor}${this.symbols.left}${colors.reset}`; const content = `${bgColor}${fgColor}${boldOn}${padding}${text}${padding}${boldOff}${colors.reset}`; const rightCap = `${capFgColor}${this.symbols.right}${colors.reset}`; return `${leftCap}${content}${rightCap}`; } let output = `${bgColor}${fgColor}${boldOn}${padding}${text}${padding}${boldOff}`; const colorMode = this.config.display.colorCompatibility || "auto"; const colorSupport = colorMode === "auto" ? getColorSupport() : colorMode; const isBasicMode = colorSupport === "ansi"; if (nextBgColor) { const arrowFgColor = extractBgToFg(bgColor, isBasicMode); output += `${colors.reset}${nextBgColor}${arrowFgColor}${this.symbols.right}`; } else { output += `${colors.reset}${extractBgToFg(bgColor, isBasicMode)}${this.symbols.right}${colors.reset}`; } return output; } }