{"version":3,"file":"doomOverlay.mjs","names":[],"sources":["../../../../src/tui/workflow/doomOverlay.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\n\nimport { fitLine, frameLine, padLine } from './rendering';\n\nconst DEFAULT_TERMINAL_ROWS = 24;\nconst FULL_CHROME_ROWS = 6;\n/** Two frame columns and two gutter columns, leaving content worth framing. */\nconst MIN_FRAMED_WIDTH = 8;\nconst GUTTER = ' ';\nconst GUTTER_COLUMNS = 2;\n/** Blank columns kept between a row's left cluster and its right-aligned status. */\nconst ROW_GAP = 2;\n/** A secondary segment thinner than this is dropped rather than shredded into a stub. */\nconst MIN_SEGMENT_WIDTH = 8;\nconst ELLIPSIS = '…';\nconst BORDER_COLOR = 'borderMuted';\nconst TEXT_COLOR = 'text';\n/** Filled key caps read as pressable, matching the doom mockups' footers. */\nconst HINT_CAP_BACKGROUND = 'selectedBg';\nconst HINT_SEPARATOR = '   ';\n/** A cap's two pad columns plus the space before its label. */\nconst HINT_CHROME_COLUMNS = 6;\nconst TITLE_COLOR = 'accent';\nconst SECONDARY_COLOR = 'dim';\nconst HORIZONTAL_BORDER = '─';\nconst VERTICAL_BORDER = '│';\nconst TOP_CORNERS = ['╭', '╮'] as const;\nconst DIVIDER_JOINTS = ['├', '┤'] as const;\nconst BOTTOM_CORNERS = ['╰', '╯'] as const;\n\nexport interface DoomOverlayTui {\n  terminal?: {\n    rows: number;\n    columns?: number;\n  };\n  requestRender(force?: boolean): void;\n}\n\nexport interface DoomOverlayChrome {\n  title: string;\n  breadcrumb?: string;\n  headerRight?: string;\n  footer: string;\n  footerRight?: string;\n  /**\n   * Key/label pairs rendered as filled caps, e.g. `[ enter ] edit subject`.\n   * Takes precedence over `footer`, which stays for surfaces that want a plain\n   * sentence rather than a key legend.\n   */\n  footerHints?: readonly (readonly [string, string])[];\n  /**\n   * Signature colour for the surface. When set, the frame takes it and the\n   * title renders as a filled badge rather than plain accent text, matching the\n   * doom mockups. Left unset, the frame stays muted and the title plain.\n   */\n  accent?: Parameters<Theme['fg']>[0];\n}\n\n/**\n * One signature colour for every doom overlay: the frame and the title badge.\n * Per-surface colours were tried and read as arbitrary, and they also borrowed\n * tokens that mean something else (`warning`, `syntaxNumber`), so a theme edit\n * elsewhere would have repainted a frame. Change it here to change them all.\n */\nexport const DOOM_OVERLAY_ACCENT = 'mdHeading' satisfies DoomOverlayChrome['accent'];\n\nexport const DOOM_FULLSCREEN_UI_OPTIONS = {\n  overlay: true,\n  overlayOptions: {\n    anchor: 'top-left',\n    width: '100%',\n    maxHeight: '100%',\n    margin: 0,\n  },\n} as const;\n\n/**\n * Fits a secondary segment into `budget`, or drops it entirely when too few\n * columns remain for it to stay readable. Truncating everything to fit is what\n * makes a tight header read as one run-on string.\n */\nfunction fitSegment(text: string | undefined, budget: number): string {\n  if (!text || budget <= 0) return '';\n  return budget < Math.min(visibleWidth(text), MIN_SEGMENT_WIDTH) ? '' : truncateToWidth(text, budget, ELLIPSIS);\n}\n\nexport abstract class DoomOverlay implements Component {\n  protected constructor(\n    protected readonly tui: DoomOverlayTui,\n    protected readonly theme: Theme,\n  ) {}\n\n  invalidate(): void {\n    this.tui.requestRender();\n  }\n\n  render(width: number): string[] {\n    const safeWidth = Math.max(0, Math.floor(width));\n    const height = Math.max(0, Math.floor(this.tui.terminal?.rows ?? DEFAULT_TERMINAL_ROWS));\n    if (safeWidth === 0 || height === 0) return [];\n\n    const chrome = this.getChrome();\n    if (height < FULL_CHROME_ROWS || safeWidth < MIN_FRAMED_WIDTH) {\n      return this.renderCompact(chrome, safeWidth, height);\n    }\n\n    const innerWidth = safeWidth - GUTTER_COLUMNS;\n    const contentWidth = innerWidth - GUTTER_COLUMNS;\n    const bodyHeight = height - FULL_CHROME_ROWS;\n    const body = this.renderBody(contentWidth, bodyHeight).slice(0, bodyHeight);\n    while (body.length < bodyHeight) body.push('');\n\n    const frame = chrome.accent ?? BORDER_COLOR;\n    return [\n      this.borderRow(TOP_CORNERS, innerWidth, frame),\n      this.contentRow(this.headerRow(chrome, contentWidth), contentWidth, safeWidth, frame),\n      this.borderRow(DIVIDER_JOINTS, innerWidth, frame, BORDER_COLOR),\n      ...body.map((line) => this.contentRow(line, contentWidth, safeWidth, frame)),\n      this.borderRow(DIVIDER_JOINTS, innerWidth, frame, BORDER_COLOR),\n      this.contentRow(this.footerRow(chrome, contentWidth), contentWidth, safeWidth, frame),\n      this.borderRow(BOTTOM_CORNERS, innerWidth, frame),\n    ].map((line) => padLine(line, safeWidth));\n  }\n\n  protected abstract getChrome(): DoomOverlayChrome;\n\n  protected abstract renderBody(width: number, height: number): string[];\n\n  /**\n   * Edge glyphs and the rule between them are coloured separately: the outer\n   * rectangle carries the signature colour while an internal separator stays\n   * muted, meeting the frame at accent-coloured junctions.\n   */\n  private borderRow(\n    [left, right]: readonly [string, string],\n    innerWidth: number,\n    edge: DoomOverlayChrome['accent'] = BORDER_COLOR,\n    fill: DoomOverlayChrome['accent'] = edge,\n  ): string {\n    const edgeColour = edge ?? BORDER_COLOR;\n    return [\n      this.theme.fg(edgeColour, left),\n      this.theme.fg(fill ?? edgeColour, HORIZONTAL_BORDER.repeat(innerWidth)),\n      this.theme.fg(edgeColour, right),\n    ].join('');\n  }\n\n  private contentRow(\n    content: string,\n    contentWidth: number,\n    width: number,\n    colour: DoomOverlayChrome['accent'] = BORDER_COLOR,\n  ): string {\n    const edge = this.theme.fg(colour ?? BORDER_COLOR, VERTICAL_BORDER);\n    return frameLine(`${GUTTER}${padLine(content, contentWidth)}${GUTTER}`, width, edge, edge);\n  }\n\n  private headerRow(chrome: DoomOverlayChrome, width: number): string {\n    // A signature colour turns the title into a filled badge, as the mockups\n    // show; without one it stays plain accent text.\n    const accent = chrome.accent;\n    const titleBudget = accent ? width - GUTTER_COLUMNS : width;\n    const title = truncateToWidth(chrome.title, Math.max(0, titleBudget), ELLIPSIS);\n    let left = accent\n      ? this.theme.inverse(this.theme.bold(this.theme.fg(accent, ` ${title} `)))\n      : this.theme.bold(this.theme.fg(TITLE_COLOR, title));\n    let leftWidth = visibleWidth(title) + (accent ? GUTTER_COLUMNS : 0);\n\n    // The breadcrumb claims room before the right status: it says where you are,\n    // which outranks ambient context when the terminal is narrow.\n    const breadcrumb = fitSegment(chrome.breadcrumb, width - leftWidth - ROW_GAP);\n    if (breadcrumb) {\n      left += `${' '.repeat(ROW_GAP)}${this.theme.fg(SECONDARY_COLOR, breadcrumb)}`;\n      leftWidth += ROW_GAP + visibleWidth(breadcrumb);\n    }\n    return this.withRightStatus(left, leftWidth, chrome.headerRight, width);\n  }\n\n  private footerRow(chrome: DoomOverlayChrome, width: number): string {\n    const footer = chrome.footerHints?.length\n      ? this.hintLegend(chrome.footerHints, width)\n      : this.theme.fg(SECONDARY_COLOR, truncateToWidth(chrome.footer, width, ELLIPSIS));\n    return this.withRightStatus(footer, visibleWidth(footer), chrome.footerRight, width);\n  }\n\n  /**\n   * Key legend with each key in a filled cap. Every segment sets its own colour\n   * so the caps survive: a cap's reset would otherwise strip the surrounding\n   * dim from the labels that follow it.\n   */\n  private hintLegend(hints: readonly (readonly [string, string])[], width: number): string {\n    let legend = '';\n    let used = 0;\n    for (const [key, label] of hints) {\n      const segmentWidth = visibleWidth(key) + visibleWidth(label) + HINT_CHROME_COLUMNS;\n      if (used + segmentWidth > width) break;\n      const cap = this.theme.bg(HINT_CAP_BACKGROUND, this.theme.fg(TEXT_COLOR, ` ${key} `));\n      legend += `${legend ? HINT_SEPARATOR : ''}${cap}${this.theme.fg(SECONDARY_COLOR, ` ${label}`)}`;\n      used += segmentWidth;\n    }\n    return legend;\n  }\n\n  /**\n   * Right-aligns a status cluster, dropping it whole when the row cannot spare\n   * the columns. A clipped status reads as a stub (`doom-log…`) that carries\n   * less than the blank space it costs.\n   */\n  private withRightStatus(left: string, leftWidth: number, right: string | undefined, width: number): string {\n    const statusWidth = right ? visibleWidth(right) : 0;\n    if (!right || statusWidth === 0 || leftWidth + ROW_GAP + statusWidth > width) return left;\n    return `${left}${' '.repeat(width - leftWidth - statusWidth)}${this.theme.fg(SECONDARY_COLOR, right)}`;\n  }\n\n  private renderCompact(chrome: DoomOverlayChrome, width: number, height: number): string[] {\n    const lines = Array.from({ length: height }, () => '');\n    lines[0] = this.theme.bold(this.theme.fg(TITLE_COLOR, chrome.title));\n    if (height > 1) lines[height - 1] = this.theme.fg(SECONDARY_COLOR, chrome.footer);\n    return lines.map((line) => padLine(fitLine(line, width), width));\n  }\n}\n"],"mappings":"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"}