UNPKG

@teaui/core

Version:

A high-level terminal UI library for Node

195 lines 7.08 kB
import * as unicode from '@teaui/term'; import { View } from '../View.js'; import { Point, Size } from '../geometry.js'; import { Style } from '../Style.js'; import { mapKey } from '../events/key.js'; function formatKey(key) { // Handle 'C-a', 'A-S-x' style modifier prefixes const modRe = /^([CAGS]-)+/; const modMatch = key.match(modRe); if (modMatch) { const modStr = modMatch[0]; const base = key.slice(modStr.length); let sigils = ''; if (modStr.includes('C-')) sigils += MODIFIER_SIGILS.ctrl; if (modStr.includes('A-')) sigils += MODIFIER_SIGILS.alt; if (modStr.includes('G-')) sigils += MODIFIER_SIGILS.gui; if (modStr.includes('S-')) sigils += MODIFIER_SIGILS.shift; return sigils + mapKey(base); } return mapKey(key); } function formatKeys(key) { if (Array.isArray(key)) { return key.map(formatKey).join(KEY_SEPARATOR); } return formatKey(key); } export class AbstractLegend extends View { #separator = ' '; constructor(props) { super(props); this.#update(props); } update(props) { this.#update(props); super.update(props); } #update({ separator }) { this.#separator = separator ?? ' '; } computeItems(items) { return items.map(item => { const keyText = formatKeys(item.key); const keyWidth = unicode.lineWidth(keyText); const labelWidth = unicode.lineWidth(item.label); return { keyText, keyWidth, label: item.label, labelWidth, totalWidth: keyWidth + 1 + labelWidth, // key + space + label }; }); } /** * Lay out items into rows. Returns an array of rows, each row being * an array of indices into the computed items array, plus per-row column * widths for alignment. */ #layout(computed, availableWidth) { if (computed.length === 0) { return { rows: [], maxKeyWidth: 0, maxLabelWidth: 0 }; } const sepWidth = unicode.lineWidth(this.#separator); // Try to fit as many items per row as possible const rows = []; let currentRow = []; let currentWidth = 0; for (let i = 0; i < computed.length; i++) { const item = computed[i]; const neededWidth = currentRow.length === 0 ? item.totalWidth : sepWidth + item.totalWidth; if (currentRow.length > 0 && currentWidth + neededWidth > availableWidth) { rows.push(currentRow); currentRow = [i]; currentWidth = item.totalWidth; } else { currentRow.push(i); currentWidth += neededWidth; } } if (currentRow.length > 0) { rows.push(currentRow); } // If only one row, no column alignment needed if (rows.length <= 1) { const maxKeyWidth = Math.max(...computed.map(c => c.keyWidth)); const maxLabelWidth = Math.max(...computed.map(c => c.labelWidth)); return { rows, maxKeyWidth, maxLabelWidth }; } // Calculate max key and label widths per column let maxKeyWidth = 0; let maxLabelWidth = 0; for (const row of rows) { for (const idx of row) { maxKeyWidth = Math.max(maxKeyWidth, computed[idx].keyWidth); maxLabelWidth = Math.max(maxLabelWidth, computed[idx].labelWidth); } } return { rows, maxKeyWidth, maxLabelWidth }; } naturalSize(available) { const computed = this.collectItems(); if (computed.length === 0) { return Size.zero; } const sepWidth = unicode.lineWidth(this.#separator); const { rows, maxKeyWidth, maxLabelWidth } = this.#layout(computed, available.width); if (rows.length <= 1) { // Single row: tight width let width = 0; for (let i = 0; i < computed.length; i++) { if (i > 0) width += sepWidth; width += computed[i].totalWidth; } return new Size(Math.min(width, available.width), 1); } // Multi-row: use column-aligned width const colWidth = maxKeyWidth + 1 + maxLabelWidth; const numCols = Math.max(...rows.map(r => r.length)); const width = numCols * colWidth + (numCols - 1) * sepWidth; return new Size(Math.min(width, available.width), rows.length); } render(viewport) { if (viewport.isEmpty) { return; } const computed = this.collectItems(); if (computed.length === 0) { return; } const { rows, maxKeyWidth, maxLabelWidth } = this.#layout(computed, viewport.contentSize.width); const keyStyle = new Style({ foreground: this.purpose.contrastTextColor, bold: true, }); const labelStyle = new Style({ foreground: this.purpose.dimTextColor, }); const sepStyle = new Style({ foreground: this.purpose.dimTextColor, }); const sepWidth = unicode.lineWidth(this.#separator); const multiRow = rows.length > 1; const _colWidth = multiRow ? maxKeyWidth + 1 + maxLabelWidth : 0; for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) { if (rowIdx >= viewport.contentSize.height) break; const row = rows[rowIdx]; let x = 0; for (let colIdx = 0; colIdx < row.length; colIdx++) { const itemIdx = row[colIdx]; const item = computed[itemIdx]; if (colIdx > 0) { viewport.write(this.#separator, new Point(x, rowIdx), sepStyle); x += sepWidth; } // Write key (right-aligned within maxKeyWidth for multi-row) if (multiRow) { const pad = maxKeyWidth - item.keyWidth; viewport.write(item.keyText, new Point(x + pad, rowIdx), keyStyle); x += maxKeyWidth; } else { viewport.write(item.keyText, new Point(x, rowIdx), keyStyle); x += item.keyWidth; } // Space between key and label x += 1; // Write label viewport.write(item.label, new Point(x, rowIdx), labelStyle); if (multiRow) { x += maxLabelWidth; } else { x += item.labelWidth; } } } } } const KEY_SEPARATOR = '/'; const MODIFIER_SIGILS = { ctrl: '⌃', alt: '⌥', gui: '⌘', shift: '⇧', }; //# sourceMappingURL=AbstractLegend.js.map