@zakmandhro/bunti 0.1.5 → 0.2.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/AGENTS.md +65 -0
- package/README.md +81 -11
- package/dist/cli.d.ts +44 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +147 -0
- package/dist/cli.js.map +1 -0
- package/dist/colors.d.ts +77 -7
- package/dist/colors.d.ts.map +1 -1
- package/dist/colors.js +238 -39
- package/dist/colors.js.map +1 -1
- package/dist/components/Box.d.ts +6 -0
- package/dist/components/Box.d.ts.map +1 -1
- package/dist/components/Box.js +5 -0
- package/dist/components/Box.js.map +1 -1
- package/dist/components/Button.d.ts +12 -2
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +35 -24
- package/dist/components/Button.js.map +1 -1
- package/dist/components/Card.d.ts +24 -2
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/Card.js +56 -14
- package/dist/components/Card.js.map +1 -1
- package/dist/components/Header.d.ts +11 -1
- package/dist/components/Header.d.ts.map +1 -1
- package/dist/components/Header.js +12 -8
- package/dist/components/Header.js.map +1 -1
- package/dist/components/Input.d.ts +10 -1
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/Input.js +172 -41
- package/dist/components/Input.js.map +1 -1
- package/dist/components/Link.d.ts +21 -0
- package/dist/components/Link.d.ts.map +1 -0
- package/dist/components/Link.js +29 -0
- package/dist/components/Link.js.map +1 -0
- package/dist/components/Modal.d.ts +25 -0
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/Modal.js +83 -24
- package/dist/components/Modal.js.map +1 -1
- package/dist/components/Progress.d.ts +27 -0
- package/dist/components/Progress.d.ts.map +1 -0
- package/dist/components/Progress.js +39 -0
- package/dist/components/Progress.js.map +1 -0
- package/dist/components/Spinner.d.ts +19 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +27 -0
- package/dist/components/Spinner.js.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -0
- package/dist/components/index.js.map +1 -1
- package/dist/data/nf-glyphs.d.ts +11 -0
- package/dist/data/nf-glyphs.d.ts.map +1 -0
- package/dist/data/nf-glyphs.js +535 -0
- package/dist/data/nf-glyphs.js.map +1 -0
- package/dist/data/nf-names.d.ts +8 -0
- package/dist/data/nf-names.d.ts.map +1 -0
- package/dist/data/nf-names.js +2 -0
- package/dist/data/nf-names.js.map +1 -0
- package/dist/demo-registry.d.ts +27 -0
- package/dist/demo-registry.d.ts.map +1 -0
- package/dist/demo-registry.js +57 -0
- package/dist/demo-registry.js.map +1 -0
- package/dist/demos/2048.ts +743 -0
- package/dist/demos/animation.ts +246 -0
- package/dist/demos/demo-layout.ts +124 -0
- package/dist/demos/engine.ts +374 -0
- package/dist/demos/interaction.ts +91 -0
- package/dist/demos/login.ts +203 -0
- package/dist/demos/mission-control.ts +558 -0
- package/dist/demos/showcase.ts +224 -0
- package/dist/detect.d.ts +92 -3
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +218 -40
- package/dist/detect.js.map +1 -1
- package/dist/diagnostics.d.ts +73 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +225 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/dsl/context.d.ts +11 -0
- package/dist/dsl/context.d.ts.map +1 -0
- package/dist/dsl/context.js +563 -0
- package/dist/dsl/context.js.map +1 -0
- package/dist/dsl/hooks.d.ts +12 -0
- package/dist/dsl/hooks.d.ts.map +1 -0
- package/dist/dsl/hooks.js +148 -0
- package/dist/dsl/hooks.js.map +1 -0
- package/dist/dsl/interaction.d.ts +16 -0
- package/dist/dsl/interaction.d.ts.map +1 -0
- package/dist/dsl/interaction.js +101 -0
- package/dist/dsl/interaction.js.map +1 -0
- package/dist/dsl/layers.d.ts +14 -0
- package/dist/dsl/layers.d.ts.map +1 -0
- package/dist/dsl/layers.js +87 -0
- package/dist/dsl/layers.js.map +1 -0
- package/dist/dsl/motion.d.ts +10 -0
- package/dist/dsl/motion.d.ts.map +1 -0
- package/dist/dsl/motion.js +150 -0
- package/dist/dsl/motion.js.map +1 -0
- package/dist/dsl/render.d.ts +22 -0
- package/dist/dsl/render.d.ts.map +1 -0
- package/dist/dsl/render.js +69 -0
- package/dist/dsl/render.js.map +1 -0
- package/dist/dsl/types.d.ts +510 -0
- package/dist/dsl/types.d.ts.map +1 -0
- package/dist/dsl/types.js +33 -0
- package/dist/dsl/types.js.map +1 -0
- package/dist/dsl.d.ts +13 -154
- package/dist/dsl.d.ts.map +1 -1
- package/dist/dsl.js +12 -658
- package/dist/dsl.js.map +1 -1
- package/dist/easing.d.ts +41 -1
- package/dist/easing.d.ts.map +1 -1
- package/dist/easing.js +105 -2
- package/dist/easing.js.map +1 -1
- package/dist/geometry.d.ts +39 -0
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +20 -0
- package/dist/geometry.js.map +1 -1
- package/dist/icons-full.d.ts +5 -0
- package/dist/icons-full.d.ts.map +1 -0
- package/dist/icons-full.js +31 -0
- package/dist/icons-full.js.map +1 -0
- package/dist/icons.d.ts +426 -5
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +144 -15
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +93 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -7
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +177 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +533 -0
- package/dist/input.js.map +1 -0
- package/dist/layout.d.ts +111 -6
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +163 -23
- package/dist/layout.js.map +1 -1
- package/dist/render.d.ts +33 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +431 -116
- package/dist/render.js.map +1 -1
- package/dist/state.d.ts +137 -3
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +25 -0
- package/dist/state.js.map +1 -1
- package/dist/theme.d.ts +129 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +167 -0
- package/dist/theme.js.map +1 -0
- package/dist/themes/catppuccin-mocha.d.ts +19 -0
- package/dist/themes/catppuccin-mocha.d.ts.map +1 -0
- package/dist/themes/catppuccin-mocha.js +36 -0
- package/dist/themes/catppuccin-mocha.js.map +1 -0
- package/dist/themes/dracula.d.ts +21 -0
- package/dist/themes/dracula.d.ts.map +1 -0
- package/dist/themes/dracula.js +37 -0
- package/dist/themes/dracula.js.map +1 -0
- package/dist/themes/github-light.d.ts +21 -0
- package/dist/themes/github-light.d.ts.map +1 -0
- package/dist/themes/github-light.js +36 -0
- package/dist/themes/github-light.js.map +1 -0
- package/dist/themes/index.d.ts +19 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +17 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/themes/nord.d.ts +23 -0
- package/dist/themes/nord.d.ts.map +1 -0
- package/dist/themes/nord.js +38 -0
- package/dist/themes/nord.js.map +1 -0
- package/dist/themes/one-dark-pro.d.ts +21 -0
- package/dist/themes/one-dark-pro.d.ts.map +1 -0
- package/dist/themes/one-dark-pro.js +36 -0
- package/dist/themes/one-dark-pro.js.map +1 -0
- package/dist/themes/tokyo-night.d.ts +20 -0
- package/dist/themes/tokyo-night.d.ts.map +1 -0
- package/dist/themes/tokyo-night.js +37 -0
- package/dist/themes/tokyo-night.js.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -0
- package/dist/utils.js.map +1 -1
- package/dist/vendor/colors.d.ts +45 -0
- package/dist/vendor/colors.d.ts.map +1 -0
- package/dist/vendor/colors.js +72 -0
- package/dist/vendor/colors.js.map +1 -0
- package/llms.txt +79 -0
- package/package.json +23 -7
- package/dist/data/glyphs.d.ts +0 -5
- package/dist/data/glyphs.d.ts.map +0 -1
- package/dist/data/glyphs.js +0 -29
- package/dist/data/glyphs.js.map +0 -1
package/dist/layout.d.ts
CHANGED
|
@@ -2,39 +2,104 @@
|
|
|
2
2
|
* Bunti Functional Layout & Drawing
|
|
3
3
|
* Strictly functional primitives for buffer manipulation and layout generation.
|
|
4
4
|
*/
|
|
5
|
+
import type { Rect } from './geometry';
|
|
5
6
|
import type { Cell, RGB, ScreenState } from './state';
|
|
7
|
+
import type { ThemeColor } from './theme';
|
|
8
|
+
/**
|
|
9
|
+
* Writes one cell into the back buffer (bounds-checked). Wide glyphs mark
|
|
10
|
+
* their tail cells as skip so the differ keeps columns aligned.
|
|
11
|
+
*/
|
|
6
12
|
export declare function setCell(state: ScreenState, x: number, y: number, cell: Partial<Cell>): void;
|
|
13
|
+
/**
|
|
14
|
+
* Repaints existing back-buffer cells inside `area` at reduced luminance.
|
|
15
|
+
* Each cell's current fg/bg is resolved to exact RGB (numeric ANSI-256 codes
|
|
16
|
+
* via the xterm table) and scaled toward black by `amount` (0 = unchanged,
|
|
17
|
+
* 1 = fully black). Cells with an undefined bg keep their transparent
|
|
18
|
+
* background (fg-only dim); undefined fg is left alone (there is no color to
|
|
19
|
+
* scale). Wide-glyph tail cells (skip) are dimmed too so they stay in sync
|
|
20
|
+
* with their head cell. No-op on the mono tier — there is no color to dim.
|
|
21
|
+
*/
|
|
22
|
+
export declare function dimRect(state: ScreenState, area: Rect, amount: number): void;
|
|
7
23
|
import type { Gradient } from './colors';
|
|
24
|
+
/** Fill style for rect(). */
|
|
8
25
|
export interface RectOptions {
|
|
26
|
+
/** Fill glyph; omitted = pure background fill that keeps existing chars. */
|
|
9
27
|
char?: string;
|
|
10
|
-
|
|
11
|
-
|
|
28
|
+
/** Foreground color for the fill glyphs. */
|
|
29
|
+
fg?: string | number | RGB | ThemeColor;
|
|
30
|
+
/** Background color (Gradients resolve per-cell across the rect). */
|
|
31
|
+
bg?: string | number | RGB | Gradient | ThemeColor;
|
|
12
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Fills a w×h cell rectangle at absolute coordinates — bars, panels,
|
|
35
|
+
* charts, backgrounds. Pure-background fills (no `char`) preserve the
|
|
36
|
+
* characters already painted underneath.
|
|
37
|
+
* @example rect(state, 0, 0, state.width, 3, { bg: 'midnight' });
|
|
38
|
+
*/
|
|
13
39
|
export declare function rect(state: ScreenState, x: number, y: number, w: number, h: number, style: RectOptions): void;
|
|
40
|
+
/**
|
|
41
|
+
* Paints a multi-line, ANSI-styled string into the back buffer at absolute
|
|
42
|
+
* cell coordinates, parsing SGR codes into per-cell attributes. Grapheme-
|
|
43
|
+
* aware: emoji and wide glyphs occupy their real column width.
|
|
44
|
+
* @example blit(state, 2, 1, fg('gold', 'FPS 60'));
|
|
45
|
+
*/
|
|
14
46
|
export declare function blit(state: ScreenState, startX: number, startY: number, content: string, style?: Partial<Cell>): void;
|
|
47
|
+
/**
|
|
48
|
+
* Border glyph sets. Wireframe: 'default' | 'rounded' | 'double' |
|
|
49
|
+
* 'dashed' | 'dotted'. Block: 'frame' | 'thick-frame'. ASCII: 'classic'.
|
|
50
|
+
* 'none' keeps the box purely structural.
|
|
51
|
+
*/
|
|
15
52
|
export type BorderStyle = 'default' | 'rounded' | 'double' | 'dashed' | 'dotted' | 'frame' | 'thick-frame' | 'classic' | 'none';
|
|
53
|
+
/** Per-side border colors; missing sides inherit top (or left/right). */
|
|
16
54
|
export interface SideColors {
|
|
17
55
|
top?: any;
|
|
18
56
|
bottom?: any;
|
|
19
57
|
left?: any;
|
|
20
58
|
right?: any;
|
|
21
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A dimension: absolute cells (20), percent of parent ('50%'), or fill
|
|
62
|
+
* fraction ('1fr'). undefined/'auto' sizes to content.
|
|
63
|
+
*/
|
|
22
64
|
export type SizeUnit = number | string;
|
|
65
|
+
/**
|
|
66
|
+
* Box styling shared by box()/table()/components.
|
|
67
|
+
*/
|
|
23
68
|
export interface StyleOptions {
|
|
69
|
+
/** Outer width: cells, '50%', '1fr', or 'auto' (content-sized). */
|
|
24
70
|
width?: SizeUnit;
|
|
71
|
+
/** Lower bound on the outer width in cells. */
|
|
25
72
|
minWidth?: number;
|
|
73
|
+
/** Upper bound on the outer width in cells. */
|
|
26
74
|
maxWidth?: number;
|
|
75
|
+
/** Outer height: rows, '50%', '1fr', or 'auto' (content-sized). */
|
|
27
76
|
height?: SizeUnit;
|
|
77
|
+
/** Lower bound on the outer height in rows. */
|
|
28
78
|
minHeight?: number;
|
|
79
|
+
/** Upper bound on the outer height in rows. */
|
|
29
80
|
maxHeight?: number;
|
|
81
|
+
/**
|
|
82
|
+
* [vertical, horizontal] padding INSIDE the border, in cells —
|
|
83
|
+
* [1, 2] = 1 blank row above/below, 2 blank columns left/right.
|
|
84
|
+
* Note the order: rows first (a 2:1 h:v ratio usually looks balanced
|
|
85
|
+
* because terminal cells are taller than wide).
|
|
86
|
+
*/
|
|
30
87
|
padding?: [number, number];
|
|
88
|
+
/** Border glyph set (default 'none'). */
|
|
31
89
|
border?: BorderStyle;
|
|
32
|
-
|
|
33
|
-
|
|
90
|
+
/** Border color: color value, styler function, or per-side SideColors. */
|
|
91
|
+
borderColor?: string | number | RGB | ThemeColor | ((s: string) => string) | SideColors;
|
|
92
|
+
/** Background fill (Gradient backgrounds apply in direct/rect mode). */
|
|
93
|
+
bgColor?: string | number | RGB | Gradient | ThemeColor;
|
|
94
|
+
/** Horizontal content alignment (default 'left'). */
|
|
34
95
|
align?: 'left' | 'center' | 'right';
|
|
96
|
+
/** Vertical content alignment (default 'top'). */
|
|
35
97
|
valign?: 'top' | 'middle' | 'bottom';
|
|
98
|
+
/** Word-wraps content to the inner width instead of truncating. */
|
|
36
99
|
wrap?: boolean;
|
|
100
|
+
/** Title rendered into the top border. */
|
|
37
101
|
title?: string;
|
|
102
|
+
/** Styler applied to the rendered title. */
|
|
38
103
|
titleStyle?: (s: string) => string;
|
|
39
104
|
}
|
|
40
105
|
/**
|
|
@@ -42,47 +107,87 @@ export interface StyleOptions {
|
|
|
42
107
|
*/
|
|
43
108
|
export declare function resolveSize(unit: SizeUnit | undefined, parentDim: number, contentDim: number): number;
|
|
44
109
|
/**
|
|
45
|
-
* Generates a styled box string with perfect alignment and optional
|
|
110
|
+
* Generates a styled box string with perfect alignment and optional
|
|
111
|
+
* wrapping. This is the pure string-building form; ctx.box() adds
|
|
112
|
+
* placement and painting on top of it.
|
|
113
|
+
* @example box('hello', { width: 20, border: 'rounded', padding: [0, 1] });
|
|
46
114
|
*/
|
|
47
115
|
export declare function box(content: string, options?: StyleOptions, parentW?: number, parentH?: number): string;
|
|
116
|
+
/** Joins rendered blocks side-by-side, padding each to its own width. */
|
|
48
117
|
export declare function joinHorizontal(...blocks: string[]): string;
|
|
118
|
+
/** Stacks rendered blocks vertically (skips empty strings). */
|
|
49
119
|
export declare function joinVertical(...blocks: string[]): string;
|
|
120
|
+
/** Returns a box() partially applied with default StyleOptions. */
|
|
50
121
|
export declare function createStyle(defaults: StyleOptions): (content: string, overrides?: StyleOptions) => string;
|
|
122
|
+
/** Uppercased ` TEXT ` chip, wrapped by the given styler. */
|
|
51
123
|
export declare function badge(text: string, colorFn?: (s: string) => string): string;
|
|
124
|
+
/** Options for table() / ctx.table(). */
|
|
52
125
|
export interface TableOptions {
|
|
126
|
+
/** Overall table width: cells, '50%', or '100%' (default 80 cells). */
|
|
53
127
|
width?: SizeUnit;
|
|
128
|
+
/** Per-column width/alignment overrides, by column index. */
|
|
54
129
|
columns?: {
|
|
55
130
|
width?: SizeUnit;
|
|
56
131
|
align?: 'left' | 'center' | 'right';
|
|
57
132
|
}[];
|
|
133
|
+
/** Outer border glyph set (default 'default'). */
|
|
58
134
|
border?: BorderStyle;
|
|
135
|
+
/** [vertical, horizontal] cell padding (default [0, 1]). */
|
|
59
136
|
padding?: [number, number];
|
|
60
137
|
}
|
|
61
138
|
/**
|
|
62
139
|
* Renders a data table with perfectly aligned columns and shared borders.
|
|
63
140
|
*/
|
|
64
141
|
export declare function table(rows: string[][], options?: TableOptions, parentW?: number): string;
|
|
142
|
+
/**
|
|
143
|
+
* Computes the scroll window that keeps `selectedIndex` visible in
|
|
144
|
+
* `maxVisible` rows (used by list windowing).
|
|
145
|
+
*/
|
|
65
146
|
export declare function getWindow<T>(items: T[], selectedIndex: number, maxVisible: number): {
|
|
66
147
|
visible: T[];
|
|
67
148
|
start: number;
|
|
68
149
|
hasMoreAbove: boolean;
|
|
69
150
|
hasMoreBelow: boolean;
|
|
70
151
|
};
|
|
152
|
+
/** Options for list() / ctx.list(). */
|
|
71
153
|
export interface ListOptions {
|
|
154
|
+
/** Prefix rendered before every item (e.g. '• '). */
|
|
72
155
|
bullet?: string;
|
|
156
|
+
/** Leading spaces before each row. */
|
|
73
157
|
indent?: number;
|
|
158
|
+
/** Row rendered as selected (ctx.list manages this from state). */
|
|
74
159
|
focusedIndex?: number;
|
|
160
|
+
/** Styler for the selected row's text (default theme.focus). */
|
|
75
161
|
focusStyle?: (s: string) => string;
|
|
76
|
-
|
|
162
|
+
/** Background wash behind the selected row (default theme.selection). */
|
|
163
|
+
selectedBg?: string | number | RGB | ThemeColor;
|
|
164
|
+
/** Mouse-hovered row (styled with hoverBg; selection wins over hover). */
|
|
165
|
+
hoveredIndex?: number;
|
|
166
|
+
/** Background wash for the hovered row. */
|
|
167
|
+
hoverBg?: string | number | RGB | ThemeColor;
|
|
168
|
+
/** Row width for background washes: cells, '50%', or '100%'. */
|
|
77
169
|
width?: SizeUnit;
|
|
170
|
+
/** Windows the list to N rows, scrolled to keep the selection visible. */
|
|
78
171
|
maxVisible?: number;
|
|
172
|
+
/** Set false to disable ctx.list's built-in keyboard/mouse handling. */
|
|
79
173
|
interactive?: boolean;
|
|
80
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Renders items as list rows with selection/hover washes and optional
|
|
177
|
+
* maxVisible windowing. Pure string form; ctx.list() adds the keyboard
|
|
178
|
+
* and mouse behavior on top.
|
|
179
|
+
*/
|
|
81
180
|
export declare function list(items: string[], options?: ListOptions, parentW?: number): string;
|
|
181
|
+
/**
|
|
182
|
+
* Crops a multi-line string to a width×height window starting at
|
|
183
|
+
* `scrollY` — the primitive for scrollable content.
|
|
184
|
+
*/
|
|
82
185
|
export declare function viewport(content: string, width: number, height: number, scrollY?: number): string;
|
|
186
|
+
/** Fills the whole screen background with a solid color. */
|
|
83
187
|
export declare function wallpaper(state: any, options: {
|
|
84
188
|
bg: any;
|
|
85
189
|
}): void;
|
|
190
|
+
/** Fills the whole screen background with a multi-stop gradient. */
|
|
86
191
|
export declare function gradient(state: any, colors: any[], options?: {
|
|
87
192
|
direction?: 'vertical' | 'horizontal';
|
|
88
193
|
}): void;
|
package/dist/layout.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"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"}
|
package/dist/layout.js
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
* Bunti Functional Layout & Drawing
|
|
3
3
|
* Strictly functional primitives for buffer manipulation and layout generation.
|
|
4
4
|
*/
|
|
5
|
-
import { resolveColor } from './colors';
|
|
5
|
+
import { fg as fgColor, isThemeColor, resolveColor, resolveColorToRGB, } from './colors';
|
|
6
|
+
import { colorTier } from './detect';
|
|
6
7
|
import { replaceEmojis } from './icons';
|
|
7
8
|
import { charWidth, truncate, visibleWidth, wrapText } from './utils';
|
|
8
9
|
// --- Functional Primitives (Buffer Manipulation) ---
|
|
10
|
+
/**
|
|
11
|
+
* Writes one cell into the back buffer (bounds-checked). Wide glyphs mark
|
|
12
|
+
* their tail cells as skip so the differ keeps columns aligned.
|
|
13
|
+
*/
|
|
9
14
|
export function setCell(state, x, y, cell) {
|
|
10
15
|
if (x >= 0 && x < state.width && y >= 0 && y < state.height) {
|
|
11
16
|
const target = state.backBuffer[y * state.width + x];
|
|
@@ -20,6 +25,10 @@ export function setCell(state, x, y, cell) {
|
|
|
20
25
|
target.fg = undefined;
|
|
21
26
|
target.fgCode = undefined;
|
|
22
27
|
target.bold = false;
|
|
28
|
+
target.italic = false;
|
|
29
|
+
target.underline = false;
|
|
30
|
+
target.dim = false;
|
|
31
|
+
target.strike = false;
|
|
23
32
|
}
|
|
24
33
|
if (cell.fg !== undefined) {
|
|
25
34
|
target.fg = cell.fg;
|
|
@@ -31,6 +40,14 @@ export function setCell(state, x, y, cell) {
|
|
|
31
40
|
}
|
|
32
41
|
if (cell.bold !== undefined)
|
|
33
42
|
target.bold = cell.bold;
|
|
43
|
+
if (cell.italic !== undefined)
|
|
44
|
+
target.italic = cell.italic;
|
|
45
|
+
if (cell.underline !== undefined)
|
|
46
|
+
target.underline = cell.underline;
|
|
47
|
+
if (cell.dim !== undefined)
|
|
48
|
+
target.dim = cell.dim;
|
|
49
|
+
if (cell.strike !== undefined)
|
|
50
|
+
target.strike = cell.strike;
|
|
34
51
|
target.skip = cell.skip ?? false;
|
|
35
52
|
for (let dx = 1; dx < displayWidth; dx++) {
|
|
36
53
|
const skipX = x + dx;
|
|
@@ -43,10 +60,67 @@ export function setCell(state, x, y, cell) {
|
|
|
43
60
|
skip.fgCode = target.fgCode;
|
|
44
61
|
skip.bgCode = target.bgCode;
|
|
45
62
|
skip.bold = target.bold;
|
|
63
|
+
skip.italic = target.italic;
|
|
64
|
+
skip.underline = target.underline;
|
|
65
|
+
skip.dim = target.dim;
|
|
66
|
+
skip.strike = target.strike;
|
|
46
67
|
skip.skip = true;
|
|
47
68
|
}
|
|
48
69
|
}
|
|
49
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Repaints existing back-buffer cells inside `area` at reduced luminance.
|
|
73
|
+
* Each cell's current fg/bg is resolved to exact RGB (numeric ANSI-256 codes
|
|
74
|
+
* via the xterm table) and scaled toward black by `amount` (0 = unchanged,
|
|
75
|
+
* 1 = fully black). Cells with an undefined bg keep their transparent
|
|
76
|
+
* background (fg-only dim); undefined fg is left alone (there is no color to
|
|
77
|
+
* scale). Wide-glyph tail cells (skip) are dimmed too so they stay in sync
|
|
78
|
+
* with their head cell. No-op on the mono tier — there is no color to dim.
|
|
79
|
+
*/
|
|
80
|
+
export function dimRect(state, area, amount) {
|
|
81
|
+
if (colorTier() === 'mono')
|
|
82
|
+
return;
|
|
83
|
+
const t = Math.max(0, Math.min(1, amount));
|
|
84
|
+
if (t === 0)
|
|
85
|
+
return;
|
|
86
|
+
const keep = 1 - t;
|
|
87
|
+
const x0 = Math.max(0, area.x);
|
|
88
|
+
const y0 = Math.max(0, area.y);
|
|
89
|
+
const x1 = Math.min(state.width, area.x + area.width);
|
|
90
|
+
const y1 = Math.min(state.height, area.y + area.height);
|
|
91
|
+
for (let y = y0; y < y1; y++) {
|
|
92
|
+
const rowOffset = y * state.width;
|
|
93
|
+
for (let x = x0; x < x1; x++) {
|
|
94
|
+
const cell = state.backBuffer[rowOffset + x];
|
|
95
|
+
if (cell.fg !== undefined) {
|
|
96
|
+
const rgb = resolveColorToRGB(cell.fg);
|
|
97
|
+
const dimmed = {
|
|
98
|
+
r: Math.round(rgb.r * keep),
|
|
99
|
+
g: Math.round(rgb.g * keep),
|
|
100
|
+
b: Math.round(rgb.b * keep),
|
|
101
|
+
};
|
|
102
|
+
cell.fg = dimmed;
|
|
103
|
+
cell.fgCode = resolveColor(dimmed);
|
|
104
|
+
}
|
|
105
|
+
if (cell.bg !== undefined) {
|
|
106
|
+
const rgb = resolveColorToRGB(cell.bg);
|
|
107
|
+
const dimmed = {
|
|
108
|
+
r: Math.round(rgb.r * keep),
|
|
109
|
+
g: Math.round(rgb.g * keep),
|
|
110
|
+
b: Math.round(rgb.b * keep),
|
|
111
|
+
};
|
|
112
|
+
cell.bg = dimmed;
|
|
113
|
+
cell.bgCode = resolveColor(dimmed);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fills a w×h cell rectangle at absolute coordinates — bars, panels,
|
|
120
|
+
* charts, backgrounds. Pure-background fills (no `char`) preserve the
|
|
121
|
+
* characters already painted underneath.
|
|
122
|
+
* @example rect(state, 0, 0, state.width, 3, { bg: 'midnight' });
|
|
123
|
+
*/
|
|
50
124
|
export function rect(state, x, y, w, h, style) {
|
|
51
125
|
const isGradient = style.bg && typeof style.bg === 'object' && 'colors' in style.bg;
|
|
52
126
|
for (let dy = 0; dy < h; dy++) {
|
|
@@ -88,6 +162,12 @@ export function rect(state, x, y, w, h, style) {
|
|
|
88
162
|
}
|
|
89
163
|
}
|
|
90
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Paints a multi-line, ANSI-styled string into the back buffer at absolute
|
|
167
|
+
* cell coordinates, parsing SGR codes into per-cell attributes. Grapheme-
|
|
168
|
+
* aware: emoji and wide glyphs occupy their real column width.
|
|
169
|
+
* @example blit(state, 2, 1, fg('gold', 'FPS 60'));
|
|
170
|
+
*/
|
|
91
171
|
export function blit(state, startX, startY, content, style = {}) {
|
|
92
172
|
const lines = content.split('\n');
|
|
93
173
|
for (let row = 0; row < lines.length; row++) {
|
|
@@ -95,6 +175,10 @@ export function blit(state, startX, startY, content, style = {}) {
|
|
|
95
175
|
let x = startX;
|
|
96
176
|
let currentFg, currentBg;
|
|
97
177
|
let currentBold = false;
|
|
178
|
+
let currentItalic = false;
|
|
179
|
+
let currentUnderline = false;
|
|
180
|
+
let currentDim = false;
|
|
181
|
+
let currentStrike = false;
|
|
98
182
|
const regex = /\x1B\[([0-9;]*)m|([^\x1B]+)/g;
|
|
99
183
|
let match = null;
|
|
100
184
|
while ((match = regex.exec(line)) !== null) {
|
|
@@ -106,11 +190,32 @@ export function blit(state, startX, startY, content, style = {}) {
|
|
|
106
190
|
currentFg = undefined;
|
|
107
191
|
currentBg = undefined;
|
|
108
192
|
currentBold = false;
|
|
193
|
+
currentItalic = false;
|
|
194
|
+
currentUnderline = false;
|
|
195
|
+
currentDim = false;
|
|
196
|
+
currentStrike = false;
|
|
109
197
|
}
|
|
110
198
|
else if (code === 1)
|
|
111
199
|
currentBold = true;
|
|
112
|
-
else if (code ===
|
|
200
|
+
else if (code === 2)
|
|
201
|
+
currentDim = true;
|
|
202
|
+
else if (code === 3)
|
|
203
|
+
currentItalic = true;
|
|
204
|
+
else if (code === 4)
|
|
205
|
+
currentUnderline = true;
|
|
206
|
+
else if (code === 9)
|
|
207
|
+
currentStrike = true;
|
|
208
|
+
else if (code === 22) {
|
|
209
|
+
// SGR 22 resets intensity: clears BOTH bold and dim.
|
|
113
210
|
currentBold = false;
|
|
211
|
+
currentDim = false;
|
|
212
|
+
}
|
|
213
|
+
else if (code === 23)
|
|
214
|
+
currentItalic = false;
|
|
215
|
+
else if (code === 24)
|
|
216
|
+
currentUnderline = false;
|
|
217
|
+
else if (code === 29)
|
|
218
|
+
currentStrike = false;
|
|
114
219
|
else if (code >= 30 && code <= 37)
|
|
115
220
|
currentFg = (code - 30).toString();
|
|
116
221
|
else if (code >= 40 && code <= 47)
|
|
@@ -172,6 +277,14 @@ export function blit(state, startX, startY, content, style = {}) {
|
|
|
172
277
|
cell.bg = style.bg;
|
|
173
278
|
if (currentBold)
|
|
174
279
|
cell.bold = true;
|
|
280
|
+
if (currentItalic)
|
|
281
|
+
cell.italic = true;
|
|
282
|
+
if (currentUnderline)
|
|
283
|
+
cell.underline = true;
|
|
284
|
+
if (currentDim)
|
|
285
|
+
cell.dim = true;
|
|
286
|
+
if (currentStrike)
|
|
287
|
+
cell.strike = true;
|
|
175
288
|
setCell(state, x, startY + row, cell);
|
|
176
289
|
x += w;
|
|
177
290
|
}
|
|
@@ -218,7 +331,10 @@ export function resolveSize(unit, parentDim, contentDim) {
|
|
|
218
331
|
return contentDim;
|
|
219
332
|
}
|
|
220
333
|
/**
|
|
221
|
-
* Generates a styled box string with perfect alignment and optional
|
|
334
|
+
* Generates a styled box string with perfect alignment and optional
|
|
335
|
+
* wrapping. This is the pure string-building form; ctx.box() adds
|
|
336
|
+
* placement and painting on top of it.
|
|
337
|
+
* @example box('hello', { width: 20, border: 'rounded', padding: [0, 1] });
|
|
222
338
|
*/
|
|
223
339
|
export function box(content, options = {}, parentW, parentH) {
|
|
224
340
|
// Ensure we operate on the swapped glyphs for all layout math
|
|
@@ -257,8 +373,13 @@ export function box(content, options = {}, parentW, parentH) {
|
|
|
257
373
|
? null
|
|
258
374
|
: BORDERS[borderStyle] || BORDERS.default;
|
|
259
375
|
// Color Resolution
|
|
260
|
-
|
|
261
|
-
|
|
376
|
+
// ThemeColors are callable, so they must be detected BEFORE the
|
|
377
|
+
// function-style border-wrapper path (brand check via isThemeColor).
|
|
378
|
+
const resolveSide = (color) => isThemeColor(color)
|
|
379
|
+
? (s) => fgColor(color.rgb, s)
|
|
380
|
+
: typeof color === 'function'
|
|
381
|
+
? color
|
|
382
|
+
: (s) => fgColor(color, s);
|
|
262
383
|
const wrapBg = (s) => {
|
|
263
384
|
if (!options.bgColor)
|
|
264
385
|
return s;
|
|
@@ -267,12 +388,12 @@ export function box(content, options = {}, parentW, parentH) {
|
|
|
267
388
|
// In string-building mode, we must ignore gradients. They are handled in absolute rect mode.
|
|
268
389
|
return s;
|
|
269
390
|
}
|
|
270
|
-
const { resolveColor } = require('./colors');
|
|
271
391
|
const code = resolveColor(options.bgColor);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
392
|
+
if (code === undefined)
|
|
393
|
+
return s; // mono tier: no color output
|
|
394
|
+
const codeStr = String(code);
|
|
395
|
+
const prefix = codeStr.startsWith('2;') ? '48' : '48;5';
|
|
396
|
+
const bgOn = `\x1b[${prefix};${codeStr}m`;
|
|
276
397
|
// Apply bg, and if the string contains resets, re-apply the bg right after the reset.
|
|
277
398
|
// We add a final reset at the very end to clean up.
|
|
278
399
|
return `${bgOn + s.replace(/\x1b\[0m/g, `\x1b[0m${bgOn}`)}\x1b[0m`;
|
|
@@ -374,6 +495,7 @@ export function box(content, options = {}, parentW, parentH) {
|
|
|
374
495
|
}
|
|
375
496
|
return out.join('\n');
|
|
376
497
|
}
|
|
498
|
+
/** Joins rendered blocks side-by-side, padding each to its own width. */
|
|
377
499
|
export function joinHorizontal(...blocks) {
|
|
378
500
|
const parsed = blocks.map((b) => b.split('\n'));
|
|
379
501
|
const maxH = Math.max(...parsed.map((p) => p.length));
|
|
@@ -397,14 +519,17 @@ export function joinHorizontal(...blocks) {
|
|
|
397
519
|
}
|
|
398
520
|
return out.join('\n');
|
|
399
521
|
}
|
|
522
|
+
/** Stacks rendered blocks vertically (skips empty strings). */
|
|
400
523
|
export function joinVertical(...blocks) {
|
|
401
524
|
return blocks.filter(Boolean).join('\n');
|
|
402
525
|
}
|
|
526
|
+
/** Returns a box() partially applied with default StyleOptions. */
|
|
403
527
|
export function createStyle(defaults) {
|
|
404
528
|
return (content, overrides = {}) => {
|
|
405
529
|
return box(content, { ...defaults, ...overrides });
|
|
406
530
|
};
|
|
407
531
|
}
|
|
532
|
+
/** Uppercased ` TEXT ` chip, wrapped by the given styler. */
|
|
408
533
|
export function badge(text, colorFn = (s) => s) {
|
|
409
534
|
return colorFn(` ${text.toUpperCase()} `);
|
|
410
535
|
}
|
|
@@ -448,6 +573,10 @@ export function table(rows, options = {}, parentW) {
|
|
|
448
573
|
align: 'left',
|
|
449
574
|
});
|
|
450
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Computes the scroll window that keeps `selectedIndex` visible in
|
|
578
|
+
* `maxVisible` rows (used by list windowing).
|
|
579
|
+
*/
|
|
451
580
|
export function getWindow(items, selectedIndex, maxVisible) {
|
|
452
581
|
const start = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), items.length - maxVisible));
|
|
453
582
|
const visible = items.slice(start, start + maxVisible);
|
|
@@ -458,22 +587,27 @@ export function getWindow(items, selectedIndex, maxVisible) {
|
|
|
458
587
|
hasMoreBelow: start + maxVisible < items.length,
|
|
459
588
|
};
|
|
460
589
|
}
|
|
590
|
+
/**
|
|
591
|
+
* Renders items as list rows with selection/hover washes and optional
|
|
592
|
+
* maxVisible windowing. Pure string form; ctx.list() adds the keyboard
|
|
593
|
+
* and mouse behavior on top.
|
|
594
|
+
*/
|
|
461
595
|
export function list(items, options = {}, parentW) {
|
|
462
596
|
const bullet = options.bullet || '';
|
|
463
597
|
const indent = ' '.repeat(options.indent || 0);
|
|
464
598
|
const resolvedW = resolveSize(options.width, parentW || 0, 0);
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
return line;
|
|
469
|
-
const code = resolveColor(options.selectedBg);
|
|
599
|
+
const wrapLineBg = (line, bg) => {
|
|
600
|
+
const code = resolveColor(bg);
|
|
601
|
+
if (code === undefined)
|
|
602
|
+
return line; // mono tier: no color output
|
|
470
603
|
const codeStr = String(code);
|
|
471
|
-
const prefix =
|
|
472
|
-
? '48'
|
|
473
|
-
: '48;5';
|
|
604
|
+
const prefix = codeStr.startsWith('2;') ? '48' : '48;5';
|
|
474
605
|
const bgOn = `\x1b[${prefix};${codeStr}m`;
|
|
475
606
|
return `${bgOn + line.replace(/\x1b\[0m/g, `\x1b[0m${bgOn}`)}\x1b[0m`;
|
|
476
607
|
};
|
|
608
|
+
const padToWidth = (line) => resolvedW > 0
|
|
609
|
+
? line + ' '.repeat(Math.max(0, resolvedW - visibleWidth(line)))
|
|
610
|
+
: line;
|
|
477
611
|
let targetItems = items, offset = 0, hasMoreAbove = false, hasMoreBelow = false;
|
|
478
612
|
if (options.maxVisible && items.length > options.maxVisible) {
|
|
479
613
|
const win = getWindow(items, options.focusedIndex || 0, options.maxVisible);
|
|
@@ -490,11 +624,11 @@ export function list(items, options = {}, parentW) {
|
|
|
490
624
|
const styledLine = options.focusStyle ? options.focusStyle(line) : line;
|
|
491
625
|
if (!options.selectedBg)
|
|
492
626
|
return styledLine;
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return
|
|
627
|
+
return wrapLineBg(padToWidth(styledLine), options.selectedBg);
|
|
628
|
+
}
|
|
629
|
+
// Hover wash (selection wins: checked above).
|
|
630
|
+
if (options.hoveredIndex === actualIdx && options.hoverBg) {
|
|
631
|
+
return wrapLineBg(padToWidth(line), options.hoverBg);
|
|
498
632
|
}
|
|
499
633
|
return line;
|
|
500
634
|
})
|
|
@@ -506,6 +640,10 @@ export function list(items, options = {}, parentW) {
|
|
|
506
640
|
out = `${out}\n${indent} ↓ more…`;
|
|
507
641
|
return out;
|
|
508
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Crops a multi-line string to a width×height window starting at
|
|
645
|
+
* `scrollY` — the primitive for scrollable content.
|
|
646
|
+
*/
|
|
509
647
|
export function viewport(content, width, height, scrollY = 0) {
|
|
510
648
|
const lines = content.split('\n');
|
|
511
649
|
const visibleLines = lines.slice(scrollY, scrollY + height);
|
|
@@ -515,10 +653,12 @@ export function viewport(content, width, height, scrollY = 0) {
|
|
|
515
653
|
})
|
|
516
654
|
.join('\n');
|
|
517
655
|
}
|
|
656
|
+
/** Fills the whole screen background with a solid color. */
|
|
518
657
|
export function wallpaper(state, options) {
|
|
519
658
|
const { bg } = options;
|
|
520
659
|
rect(state, 0, 0, state.width, state.height, { char: ' ', bg });
|
|
521
660
|
}
|
|
661
|
+
/** Fills the whole screen background with a multi-stop gradient. */
|
|
522
662
|
export function gradient(state, colors, options = {}) {
|
|
523
663
|
const { direction = 'vertical' } = options;
|
|
524
664
|
rect(state, 0, 0, state.width, state.height, {
|