@xynogen/pix-pretty 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/confirm.ts +6 -1
- package/src/gate-overlay.ts +13 -3
- package/src/icon-catalog.ts +17 -161
- package/src/modal-frame.ts +83 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-pretty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, diff rendering, and FFF search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -61,10 +61,10 @@
|
|
|
61
61
|
"access": "public"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@xynogen/pix-runtime": "^0.8.
|
|
64
|
+
"@xynogen/pix-runtime": "^0.8.4",
|
|
65
65
|
"chalk": "^4.1.2",
|
|
66
66
|
"cli-highlight": "^2.1.11",
|
|
67
|
-
"@ff-labs/fff-node": "^0.
|
|
67
|
+
"@ff-labs/fff-node": "^0.10.6",
|
|
68
68
|
"diff": "^8.0.3"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
package/src/confirm.ts
CHANGED
|
@@ -43,6 +43,9 @@ export interface ConfirmUI {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export interface ConfirmOptions {
|
|
46
|
+
/** Optional leading glyph for the title row, e.g. `icon("update")`. Rendered
|
|
47
|
+
* as "<icon> <title>" so callers pass a resolved icon + plain text. */
|
|
48
|
+
icon?: string;
|
|
46
49
|
/** Title shown bold at the top (e.g. "Update Pi & extensions?"). */
|
|
47
50
|
title: string;
|
|
48
51
|
/** Optional body lines rendered under the title. */
|
|
@@ -133,7 +136,9 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
|
|
|
133
136
|
width: mw,
|
|
134
137
|
maxHeight: terminalModalHeight(tui.terminal?.rows),
|
|
135
138
|
minHeight: MIN_PERMISSION_MODAL_HEIGHT,
|
|
136
|
-
header: [
|
|
139
|
+
header: [
|
|
140
|
+
theme.fg(accent, theme.bold(opts.icon ? `${opts.icon} ${opts.title}` : opts.title)),
|
|
141
|
+
],
|
|
137
142
|
body: (opts.body ?? []).map((line) => theme.fg("text", line)),
|
|
138
143
|
footer,
|
|
139
144
|
bodyOffset: pager.bodyOffset,
|
package/src/gate-overlay.ts
CHANGED
|
@@ -49,6 +49,13 @@ export interface OverlayChoice {
|
|
|
49
49
|
interface BaseConfig {
|
|
50
50
|
/** Accent colour token (e.g. "error", "warning", "accent"). Default "accent". */
|
|
51
51
|
accent?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Optional leading glyph for the title row, e.g. `icon("lock")`. Kept
|
|
54
|
+
* separate from `title` so call sites pass a resolved icon and plain text
|
|
55
|
+
* rather than splicing them together; the overlay renders "<icon> <title>"
|
|
56
|
+
* in the accent color.
|
|
57
|
+
*/
|
|
58
|
+
icon?: string;
|
|
52
59
|
/** Title shown bold at the top. */
|
|
53
60
|
title: string;
|
|
54
61
|
/** Optional body lines under the title. */
|
|
@@ -174,7 +181,8 @@ function buildSections(opts: {
|
|
|
174
181
|
width,
|
|
175
182
|
} = opts;
|
|
176
183
|
const inner = width - 4; // CHROME = 2 border + 2 padding
|
|
177
|
-
const
|
|
184
|
+
const titleText = config.icon ? `${config.icon} ${config.title}` : config.title;
|
|
185
|
+
const header = [theme.fg(accent, theme.bold(titleText))];
|
|
178
186
|
const body = (config.body ?? []).map((line) => {
|
|
179
187
|
if (line.startsWith("Warning:")) return theme.fg("warning", line);
|
|
180
188
|
if (line.startsWith("(") && line.endsWith(")")) return theme.fg("muted", line);
|
|
@@ -228,7 +236,8 @@ function buildSections(opts: {
|
|
|
228
236
|
* ```ts
|
|
229
237
|
* const result = await showOverlay(ui, {
|
|
230
238
|
* mode: "confirm",
|
|
231
|
-
*
|
|
239
|
+
* icon: icon("warn"),
|
|
240
|
+
* title: "Dangerous",
|
|
232
241
|
* body: ["rm -rf /tmp/work"],
|
|
233
242
|
* accent: "warning",
|
|
234
243
|
* timeoutMs: 30_000,
|
|
@@ -243,7 +252,8 @@ function buildSections(opts: {
|
|
|
243
252
|
* ```ts
|
|
244
253
|
* const result = await showOverlay(ui, {
|
|
245
254
|
* mode: "sudo",
|
|
246
|
-
*
|
|
255
|
+
* icon: icon("lock"),
|
|
256
|
+
* title: "Root Command Request",
|
|
247
257
|
* body: ["Intent: install package", "Command: apt install foo"],
|
|
248
258
|
* accent: "error",
|
|
249
259
|
* });
|
package/src/icon-catalog.ts
CHANGED
|
@@ -1,165 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* icon-catalog.ts —
|
|
2
|
+
* icon-catalog.ts — re-export shim.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* catalog: key -> { nerd, unicode, ascii } (the "messages")
|
|
10
|
-
* mode: "nerd" | "unicode" | "ascii" (the "locale")
|
|
11
|
-
* icon(k): catalog[k][mode] (the "t(key)")
|
|
12
|
-
*
|
|
13
|
-
* One global mode governs the whole stack. It is switched via the `/pix`
|
|
14
|
-
* settings command (in pix-data), persisted to `~/.pi/agent/pix.json`
|
|
15
|
-
* (`pretty.icons`), and seeded from the PRETTY_ICONS env var on first load.
|
|
16
|
-
*
|
|
17
|
-
* Why a catalog instead of per-package toggles: reskinning or fixing a
|
|
18
|
-
* missing-glyph ("tofu") problem becomes a one-file edit here, and there is
|
|
19
|
-
* exactly ONE knob (the mode) rather than one env var per package.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/** Presentation modes, in /pix settings cycle order. nerd = Nerd Font PUA glyphs. */
|
|
23
|
-
export type IconMode = "nerd" | "unicode" | "ascii";
|
|
24
|
-
|
|
25
|
-
/** All modes in cycle order. */
|
|
26
|
-
export const ICON_MODES: readonly IconMode[] = ["nerd", "unicode", "ascii"];
|
|
27
|
-
|
|
28
|
-
/** Force text (non-emoji) presentation for symbols that default to emoji. */
|
|
29
|
-
const VS = "\uFE0E";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The catalog. Each semantic key maps to one glyph per mode.
|
|
33
|
-
* - nerd: Nerd Font Private Use Area codepoint (needs a patched font).
|
|
34
|
-
* - unicode: standard BMP glyph that ships with virtually every monospace
|
|
35
|
-
* font (no Nerd Font required); +VS to force text presentation.
|
|
36
|
-
* - ascii: pure ASCII, renders on literally any terminal.
|
|
37
|
-
*
|
|
38
|
-
* Keys are SEMANTIC ROLES, never glyph names — consumers reference meaning.
|
|
39
|
-
*/
|
|
40
|
-
const CATALOG = {
|
|
41
|
-
// ── footer / status segments ──────────────────────────────────────────
|
|
42
|
-
model: { nerd: "\u{F06A9}", unicode: `\u25C8${VS}`, ascii: "M" },
|
|
43
|
-
lsp: { nerd: "\u{F0626}", unicode: `\u25C9${VS}`, ascii: "LSP" },
|
|
44
|
-
mcp: { nerd: "\u{F048D}", unicode: `\u25D0${VS}`, ascii: "MCP" },
|
|
45
|
-
cwd: { nerd: "\u{F024B}", unicode: `\u2302${VS}`, ascii: "~" },
|
|
46
|
-
folder: { nerd: "\u{F024B}", unicode: `\u2302${VS}`, ascii: "/" },
|
|
47
|
-
afk: { nerd: "\u{F0310}", unicode: `\u2328${VS}`, ascii: "kbd" },
|
|
48
|
-
|
|
49
|
-
// ── footer indicators (git status, score) ─────────────────────────────
|
|
50
|
-
"git.unstaged": { nerd: "\u2717", unicode: "\u2717", ascii: "x" },
|
|
51
|
-
"git.ahead": { nerd: "\u21E1", unicode: "\u21E1", ascii: "^" },
|
|
52
|
-
"git.behind": { nerd: "\u21E3", unicode: "\u21E3", ascii: "v" },
|
|
53
|
-
"net.in": { nerd: "\u21E1", unicode: "\u21E1", ascii: "in" },
|
|
54
|
-
"net.out": { nerd: "\u21E3", unicode: "\u21E3", ascii: "out" },
|
|
55
|
-
score: { nerd: "\u26A1", unicode: "\u26A1", ascii: "S" },
|
|
56
|
-
|
|
57
|
-
// ── misc ──────────────────────────────────────────────────────────────
|
|
58
|
-
ok: { nerd: "\u2713", unicode: "\u2713", ascii: "ok" },
|
|
59
|
-
warn: { nerd: "\u26A0", unicode: "\u26A0", ascii: "!" },
|
|
60
|
-
error: { nerd: "\u2717", unicode: "\u2717", ascii: "x" },
|
|
61
|
-
|
|
62
|
-
// ── shared status glyphs (checklists, panels, markers) ────────────────
|
|
63
|
-
// nerd/unicode keep the historical literal so mixed-glyph rows stay
|
|
64
|
-
// aligned; ascii mode swaps in tofu-free tokens. `⚡` (energetic
|
|
65
|
-
// warning/killed/denied) intentionally stays a local literal — it is not
|
|
66
|
-
// part of this set.
|
|
67
|
-
"status.ok": { nerd: "\u2713", unicode: `\u2713${VS}`, ascii: "ok" },
|
|
68
|
-
"status.error": { nerd: "\u2717", unicode: `\u2717${VS}`, ascii: "x" },
|
|
69
|
-
// `⚠` is East-Asian wide (2 cells); consumers that place it in an aligned
|
|
70
|
-
// marker column must normalize width via `padIcon` (pix-pretty/utils).
|
|
71
|
-
"status.warn": { nerd: "\u26A0", unicode: `\u26A0${VS}`, ascii: "!" },
|
|
72
|
-
"status.pending": { nerd: "\u25CB", unicode: `\u25CB${VS}`, ascii: "o" },
|
|
73
|
-
"status.running": { nerd: "\u25D0", unicode: `\u25D0${VS}`, ascii: "*" },
|
|
74
|
-
"status.active": { nerd: "\u25CF", unicode: `\u25CF${VS}`, ascii: "*" },
|
|
75
|
-
"status.done": { nerd: "\u25CF", unicode: `\u25CF${VS}`, ascii: "x" },
|
|
76
|
-
"status.blocked": { nerd: "\u2298", unicode: `\u2298${VS}`, ascii: "!" },
|
|
77
|
-
|
|
78
|
-
// ── welcome banner ────────────────────────────────────────────────────
|
|
79
|
-
ready: { nerd: "\u{F0633}", unicode: `\u2713${VS}`, ascii: "ok" },
|
|
80
|
-
|
|
81
|
-
// ── paste chips (pix-display) ─────────────────────────────────────────
|
|
82
|
-
"paste.image": { nerd: "\u{F02E9}", unicode: `\u25A3${VS}`, ascii: "img" },
|
|
83
|
-
"paste.text": { nerd: "\u{F027F}", unicode: `\u25A4${VS}`, ascii: "txt" },
|
|
84
|
-
|
|
85
|
-
// ── model picker (pix-models) ─────────────────────────────────────────
|
|
86
|
-
"picker.model": { nerd: "\u{F0229}", unicode: `\u25C8${VS}`, ascii: "M" },
|
|
87
|
-
|
|
88
|
-
// ── optimizer suite (pix-optimizer) ───────────────────────────────────
|
|
89
|
-
"opt.caveman": { nerd: "\u{F0710}", unicode: `\u2664${VS}`, ascii: "Cv" },
|
|
90
|
-
"opt.rtk": { nerd: "\u{F04E5}", unicode: `\u2661${VS}`, ascii: "Rk" },
|
|
91
|
-
"opt.toon": { nerd: "\u{F05C0}", unicode: `\u2662${VS}`, ascii: "Tn" },
|
|
92
|
-
"opt.ponytail": { nerd: "\u{F0190}", unicode: `\u2667${VS}`, ascii: "Pt" },
|
|
93
|
-
"opt.title": { nerd: "\u{F0DAB}", unicode: `\u25C8${VS}`, ascii: "*" },
|
|
94
|
-
|
|
95
|
-
// ── subagent widget (pix-subagent) ────────────────────────────────────
|
|
96
|
-
agent: { nerd: "\u{F0BA0}", unicode: `\u2699${VS}`, ascii: "@" },
|
|
97
|
-
turns: { nerd: "\u{F006A}", unicode: `\u21BB${VS}`, ascii: "~" },
|
|
98
|
-
tools: { nerd: "\u{F1064}", unicode: `\u2692${VS}`, ascii: "T" },
|
|
99
|
-
tokens: { nerd: "\u{F027F}", unicode: `\u25A4${VS}`, ascii: "tk" },
|
|
100
|
-
} as const;
|
|
101
|
-
|
|
102
|
-
/** Every valid semantic icon key. */
|
|
103
|
-
export type IconKey = keyof typeof CATALOG;
|
|
104
|
-
|
|
105
|
-
/** All catalog keys (useful for /pix previews and tests). */
|
|
106
|
-
export const ICON_KEYS = Object.keys(CATALOG) as IconKey[];
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Active mode. Seeded from PRETTY_ICONS env (back-compat: none/off => ascii),
|
|
110
|
-
* then overridden by a persisted choice when the host loads pix.json.
|
|
111
|
-
*/
|
|
112
|
-
function envMode(): IconMode {
|
|
113
|
-
const raw = (process.env.PRETTY_ICONS ?? "").toLowerCase();
|
|
114
|
-
if (raw === "nerd" || raw === "unicode" || raw === "ascii") return raw;
|
|
115
|
-
if (raw === "none" || raw === "off") return "ascii";
|
|
116
|
-
return "nerd";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let activeMode: IconMode = envMode();
|
|
120
|
-
|
|
121
|
-
/** Current global icon mode. */
|
|
122
|
-
export function getIconMode(): IconMode {
|
|
123
|
-
return activeMode;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Mode-change subscribers. Most consumers resolve icon() at render time and
|
|
128
|
-
* need no notification, but PUSHED-status consumers (e.g. the optimizer cell,
|
|
129
|
-
* drawn once via setStatus) must repaint when the mode flips. They subscribe
|
|
130
|
-
* here; setIconMode fires every callback on an actual change.
|
|
131
|
-
*/
|
|
132
|
-
type ModeListener = (mode: IconMode) => void;
|
|
133
|
-
const listeners = new Set<ModeListener>();
|
|
134
|
-
|
|
135
|
-
/** Subscribe to global icon-mode changes. Returns an unsubscribe fn. */
|
|
136
|
-
export function onIconModeChange(cb: ModeListener): () => void {
|
|
137
|
-
listeners.add(cb);
|
|
138
|
-
return () => listeners.delete(cb);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Set the global icon mode (does NOT persist — callers that want persistence
|
|
143
|
-
* use the /pix command, which writes pix.json then calls this). Fires
|
|
144
|
-
* subscribers only on an actual change (no-op re-sets are ignored).
|
|
145
|
-
*/
|
|
146
|
-
export function setIconMode(mode: IconMode): void {
|
|
147
|
-
if (!ICON_MODES.includes(mode) || mode === activeMode) return;
|
|
148
|
-
activeMode = mode;
|
|
149
|
-
for (const cb of listeners) cb(mode);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Resolve a semantic icon key to its glyph for the active mode — the `t(key)`
|
|
154
|
-
* of this module. Unknown keys return "" (fail soft: never throw mid-render).
|
|
4
|
+
* The semantic icon catalog now lives in `@xynogen/pix-runtime` (a lower layer)
|
|
5
|
+
* so that pix-runtime's own UI — the `/pix` settings overlay — can resolve
|
|
6
|
+
* icons without a circular dependency on pix-pretty. This file re-exports the
|
|
7
|
+
* whole surface unchanged, so every existing `@xynogen/pix-pretty/icon-catalog`
|
|
8
|
+
* import keeps working.
|
|
155
9
|
*/
|
|
156
|
-
export function icon(key: IconKey): string {
|
|
157
|
-
const entry = CATALOG[key];
|
|
158
|
-
return entry ? entry[activeMode] : "";
|
|
159
|
-
}
|
|
160
10
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
11
|
+
export {
|
|
12
|
+
getIconMode,
|
|
13
|
+
ICON_KEYS,
|
|
14
|
+
ICON_MODES,
|
|
15
|
+
type IconKey,
|
|
16
|
+
type IconMode,
|
|
17
|
+
icon,
|
|
18
|
+
iconFor,
|
|
19
|
+
onIconModeChange,
|
|
20
|
+
setIconMode,
|
|
21
|
+
} from "@xynogen/pix-runtime/icon-catalog";
|
package/src/modal-frame.ts
CHANGED
|
@@ -77,6 +77,15 @@ export interface FrameOptions {
|
|
|
77
77
|
fg?: (s: string) => string;
|
|
78
78
|
/** Optional pre-styled string rendered as the first content row (tab bar etc.) */
|
|
79
79
|
top?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Plain title text embedded into the top border, e.g. `╭─ pix settings ─────╮`.
|
|
82
|
+
* Styled with `titleColor` (falls back to `color`). Kept as plain text so the
|
|
83
|
+
* frame owns the spacing/dashes; pass an already-short string (it's truncated
|
|
84
|
+
* to fit). Distinct from `top`, which is a full pinned content row.
|
|
85
|
+
*/
|
|
86
|
+
title?: string;
|
|
87
|
+
/** Color function for the embedded `title` text. Defaults to `color`. */
|
|
88
|
+
titleColor?: (s: string) => string;
|
|
80
89
|
/**
|
|
81
90
|
* Wrap over-wide content instead of cutting its tail. Default true.
|
|
82
91
|
*
|
|
@@ -140,6 +149,44 @@ export function fitModalLines(lines: string[], inner: number, wrap = true): FitR
|
|
|
140
149
|
return { rows, truncated };
|
|
141
150
|
}
|
|
142
151
|
|
|
152
|
+
// ── Column layout ───────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export interface JoinColumnsOptions {
|
|
155
|
+
/** Left column width in cells. Rows are truncated/padded to exactly this. */
|
|
156
|
+
leftWidth: number;
|
|
157
|
+
/** Right column width in cells. Rows are truncated to this (no right-pad). */
|
|
158
|
+
rightWidth: number;
|
|
159
|
+
/** Spaces between the columns when no `sep` is given. Default 2. */
|
|
160
|
+
gap?: number;
|
|
161
|
+
/**
|
|
162
|
+
* Pre-styled separator string placed between the columns (e.g. a themed
|
|
163
|
+
* `" │ "`). Its own visible width is used as-is — pass `gap` OR `sep`, not
|
|
164
|
+
* both. When omitted the columns are separated by `gap` spaces.
|
|
165
|
+
*/
|
|
166
|
+
sep?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Stitch two column bodies into side-by-side rows (a list + preview pane, say).
|
|
171
|
+
* Left cells are truncated then space-padded to `leftWidth` by *visible* width
|
|
172
|
+
* (ANSI-aware), so styled text aligns; right cells are only truncated. The
|
|
173
|
+
* result has `max(left.length, right.length)` rows — the shorter column is
|
|
174
|
+
* padded with blanks. Pure: no wrapping, no color; callers style the cells and
|
|
175
|
+
* the optional `sep`.
|
|
176
|
+
*/
|
|
177
|
+
export function joinColumns(left: string[], right: string[], opts: JoinColumnsOptions): string[] {
|
|
178
|
+
const divider = opts.sep ?? " ".repeat(Math.max(0, opts.gap ?? 2));
|
|
179
|
+
const height = Math.max(left.length, right.length);
|
|
180
|
+
const rows: string[] = [];
|
|
181
|
+
for (let i = 0; i < height; i++) {
|
|
182
|
+
const l = truncateToWidth(left[i] ?? "", opts.leftWidth);
|
|
183
|
+
const lPad = l + " ".repeat(Math.max(0, opts.leftWidth - visibleWidth(l)));
|
|
184
|
+
const r = truncateToWidth(right[i] ?? "", opts.rightWidth);
|
|
185
|
+
rows.push(`${lPad}${divider}${r}`);
|
|
186
|
+
}
|
|
187
|
+
return rows;
|
|
188
|
+
}
|
|
189
|
+
|
|
143
190
|
/**
|
|
144
191
|
* Render a rounded modal box.
|
|
145
192
|
*
|
|
@@ -165,6 +212,20 @@ export function frameLines(opts: FrameOptions): string[] {
|
|
|
165
212
|
const inner = width - CHROME;
|
|
166
213
|
const dashes = "─".repeat(width - 2);
|
|
167
214
|
const wrap = opts.wrap ?? true;
|
|
215
|
+
|
|
216
|
+
// Top border: bare `╭───╮`, or a titled `╭─ Title ──╮` when `title` is set.
|
|
217
|
+
// The title sits after one lead dash with a padding space on each side; the
|
|
218
|
+
// remaining run fills with dashes so the border stays exactly `width` wide.
|
|
219
|
+
const topBorder = ((): string => {
|
|
220
|
+
// Full width = ╭ + lead dash + " " + label + " " + tail dashes + ╮.
|
|
221
|
+
// Fixed chrome around the label is 5 cols (2 corners, 1 lead dash, 2 pads).
|
|
222
|
+
const span = width - 5;
|
|
223
|
+
if (!opts.title || span < 3) return color(`╭${dashes}╮`);
|
|
224
|
+
const paint = opts.titleColor ?? color;
|
|
225
|
+
const label = truncateToWidth(opts.title, span - 1, ELLIPSIS); // keep ≥1 tail dash
|
|
226
|
+
const tail = "─".repeat(Math.max(1, span - visibleWidth(label)));
|
|
227
|
+
return `${color("╭─ ")}${paint(label)}${color(` ${tail}╮`)}`;
|
|
228
|
+
})();
|
|
168
229
|
// Expand before framing so a long command wraps instead of losing its tail.
|
|
169
230
|
const { rows: lines } = fitModalLines(opts.lines, inner, wrap);
|
|
170
231
|
|
|
@@ -196,7 +257,7 @@ export function frameLines(opts: FrameOptions): string[] {
|
|
|
196
257
|
return bg(`${color("│")} ${body} ${color("│")}`);
|
|
197
258
|
};
|
|
198
259
|
|
|
199
|
-
const out: string[] = [bg(
|
|
260
|
+
const out: string[] = [bg(topBorder)];
|
|
200
261
|
if (top !== undefined) out.push(row(top));
|
|
201
262
|
for (const line of lines) out.push(row(line));
|
|
202
263
|
out.push(bg(color(`╰${dashes}╯`)));
|
|
@@ -475,7 +536,16 @@ export function frameModal(opts: ModalFrameOptions): ModalFrameResult {
|
|
|
475
536
|
wrap,
|
|
476
537
|
).rows.slice(0, Math.max(1, cap - 2));
|
|
477
538
|
return {
|
|
478
|
-
lines: frameLines({
|
|
539
|
+
lines: frameLines({
|
|
540
|
+
width,
|
|
541
|
+
lines: diag,
|
|
542
|
+
color,
|
|
543
|
+
bg,
|
|
544
|
+
fg,
|
|
545
|
+
title: opts.title,
|
|
546
|
+
titleColor: opts.titleColor,
|
|
547
|
+
wrap: false,
|
|
548
|
+
}),
|
|
479
549
|
bodyOffset: 0,
|
|
480
550
|
maxBodyOffset: 0,
|
|
481
551
|
visibleBodyLines: 0,
|
|
@@ -520,7 +590,17 @@ export function frameModal(opts: ModalFrameOptions): ModalFrameResult {
|
|
|
520
590
|
|
|
521
591
|
return {
|
|
522
592
|
// Already fitted above — pass wrap:false so rows are not re-expanded.
|
|
523
|
-
lines: frameLines({
|
|
593
|
+
lines: frameLines({
|
|
594
|
+
width,
|
|
595
|
+
lines,
|
|
596
|
+
color,
|
|
597
|
+
bg,
|
|
598
|
+
fg,
|
|
599
|
+
top,
|
|
600
|
+
title: opts.title,
|
|
601
|
+
titleColor: opts.titleColor,
|
|
602
|
+
wrap: false,
|
|
603
|
+
}),
|
|
524
604
|
bodyOffset: offset,
|
|
525
605
|
maxBodyOffset,
|
|
526
606
|
visibleBodyLines,
|