@oakoliver/lipgloss 1.0.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/LICENSE +24 -0
- package/README.md +259 -0
- package/dist/ansi.d.ts +82 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/border.d.ts +54 -0
- package/dist/border.d.ts.map +1 -0
- package/dist/color.d.ts +86 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/index.cjs +2408 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2309 -0
- package/dist/layout.d.ts +28 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/style.d.ts +247 -0
- package/dist/style.d.ts.map +1 -0
- package/package.json +70 -0
- package/src/ansi.ts +292 -0
- package/src/border.ts +168 -0
- package/src/color.ts +294 -0
- package/src/index.ts +44 -0
- package/src/layout.ts +206 -0
- package/src/style.ts +1131 -0
package/src/border.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Border definitions and built-in border styles.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { stringWidth } from './ansi.js';
|
|
6
|
+
|
|
7
|
+
/** Border character set */
|
|
8
|
+
export interface Border {
|
|
9
|
+
top: string;
|
|
10
|
+
bottom: string;
|
|
11
|
+
left: string;
|
|
12
|
+
right: string;
|
|
13
|
+
topLeft: string;
|
|
14
|
+
topRight: string;
|
|
15
|
+
bottomLeft: string;
|
|
16
|
+
bottomRight: string;
|
|
17
|
+
middleLeft: string;
|
|
18
|
+
middleRight: string;
|
|
19
|
+
middle: string;
|
|
20
|
+
middleTop: string;
|
|
21
|
+
middleBottom: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function border(b: Partial<Border>): Border {
|
|
25
|
+
return {
|
|
26
|
+
top: '', bottom: '', left: '', right: '',
|
|
27
|
+
topLeft: '', topRight: '', bottomLeft: '', bottomRight: '',
|
|
28
|
+
middleLeft: '', middleRight: '', middle: '', middleTop: '', middleBottom: '',
|
|
29
|
+
...b,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** No border */
|
|
34
|
+
export const noBorder: Border = border({});
|
|
35
|
+
|
|
36
|
+
/** Standard border with 90-degree corners */
|
|
37
|
+
export function normalBorder(): Border {
|
|
38
|
+
return border({
|
|
39
|
+
top: '─', bottom: '─', left: '│', right: '│',
|
|
40
|
+
topLeft: '┌', topRight: '┐', bottomLeft: '└', bottomRight: '┘',
|
|
41
|
+
middleLeft: '├', middleRight: '┤', middle: '┼', middleTop: '┬', middleBottom: '┴',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Rounded corners */
|
|
46
|
+
export function roundedBorder(): Border {
|
|
47
|
+
return border({
|
|
48
|
+
top: '─', bottom: '─', left: '│', right: '│',
|
|
49
|
+
topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯',
|
|
50
|
+
middleLeft: '├', middleRight: '┤', middle: '┼', middleTop: '┬', middleBottom: '┴',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Full block border */
|
|
55
|
+
export function blockBorder(): Border {
|
|
56
|
+
return border({
|
|
57
|
+
top: '█', bottom: '█', left: '█', right: '█',
|
|
58
|
+
topLeft: '█', topRight: '█', bottomLeft: '█', bottomRight: '█',
|
|
59
|
+
middleLeft: '█', middleRight: '█', middle: '█', middleTop: '█', middleBottom: '█',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Outer half-block border */
|
|
64
|
+
export function outerHalfBlockBorder(): Border {
|
|
65
|
+
return border({
|
|
66
|
+
top: '▀', bottom: '▄', left: '▌', right: '▐',
|
|
67
|
+
topLeft: '▛', topRight: '▜', bottomLeft: '▙', bottomRight: '▟',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Inner half-block border */
|
|
72
|
+
export function innerHalfBlockBorder(): Border {
|
|
73
|
+
return border({
|
|
74
|
+
top: '▄', bottom: '▀', left: '▐', right: '▌',
|
|
75
|
+
topLeft: '▗', topRight: '▖', bottomLeft: '▝', bottomRight: '▘',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Thick border */
|
|
80
|
+
export function thickBorder(): Border {
|
|
81
|
+
return border({
|
|
82
|
+
top: '━', bottom: '━', left: '┃', right: '┃',
|
|
83
|
+
topLeft: '┏', topRight: '┓', bottomLeft: '┗', bottomRight: '┛',
|
|
84
|
+
middleLeft: '┣', middleRight: '┫', middle: '╋', middleTop: '┳', middleBottom: '┻',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Double-line border */
|
|
89
|
+
export function doubleBorder(): Border {
|
|
90
|
+
return border({
|
|
91
|
+
top: '═', bottom: '═', left: '║', right: '║',
|
|
92
|
+
topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
|
|
93
|
+
middleLeft: '╠', middleRight: '╣', middle: '╬', middleTop: '╦', middleBottom: '╩',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Hidden border (spaces) — maintains layout without visible borders */
|
|
98
|
+
export function hiddenBorder(): Border {
|
|
99
|
+
return border({
|
|
100
|
+
top: ' ', bottom: ' ', left: ' ', right: ' ',
|
|
101
|
+
topLeft: ' ', topRight: ' ', bottomLeft: ' ', bottomRight: ' ',
|
|
102
|
+
middleLeft: ' ', middleRight: ' ', middle: ' ', middleTop: ' ', middleBottom: ' ',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Markdown-style table border */
|
|
107
|
+
export function markdownBorder(): Border {
|
|
108
|
+
return border({
|
|
109
|
+
top: '-', bottom: '-', left: '|', right: '|',
|
|
110
|
+
topLeft: '|', topRight: '|', bottomLeft: '|', bottomRight: '|',
|
|
111
|
+
middleLeft: '|', middleRight: '|', middle: '|', middleTop: '|', middleBottom: '|',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** ASCII border using +, -, | characters */
|
|
116
|
+
export function asciiBorder(): Border {
|
|
117
|
+
return border({
|
|
118
|
+
top: '-', bottom: '-', left: '|', right: '|',
|
|
119
|
+
topLeft: '+', topRight: '+', bottomLeft: '+', bottomRight: '+',
|
|
120
|
+
middleLeft: '+', middleRight: '+', middle: '+', middleTop: '+', middleBottom: '+',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Get the max rune width of a border edge string */
|
|
125
|
+
export function maxRuneWidth(str: string): number {
|
|
126
|
+
if (!str) return 0;
|
|
127
|
+
if (str.length === 1) return stringWidth(str);
|
|
128
|
+
let width = 0;
|
|
129
|
+
for (const ch of str) {
|
|
130
|
+
width = Math.max(width, stringWidth(ch));
|
|
131
|
+
}
|
|
132
|
+
return width;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Get the width of a border edge (top, bottom, etc.) */
|
|
136
|
+
function getBorderEdgeWidth(...parts: string[]): number {
|
|
137
|
+
let max = 0;
|
|
138
|
+
for (const part of parts) {
|
|
139
|
+
max = Math.max(max, maxRuneWidth(part));
|
|
140
|
+
}
|
|
141
|
+
return max;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Get the size of the top edge of the border */
|
|
145
|
+
export function getTopSize(b: Border): number {
|
|
146
|
+
return getBorderEdgeWidth(b.topLeft, b.top, b.topRight);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Get the size of the right edge of the border */
|
|
150
|
+
export function getRightSize(b: Border): number {
|
|
151
|
+
return getBorderEdgeWidth(b.topRight, b.right, b.bottomRight);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Get the size of the bottom edge of the border */
|
|
155
|
+
export function getBottomSize(b: Border): number {
|
|
156
|
+
return getBorderEdgeWidth(b.bottomLeft, b.bottom, b.bottomRight);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Get the size of the left edge of the border */
|
|
160
|
+
export function getLeftSize(b: Border): number {
|
|
161
|
+
return getBorderEdgeWidth(b.topLeft, b.left, b.bottomLeft);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Check if a border is the "no border" (all empty strings) */
|
|
165
|
+
export function isNoBorder(b: Border): boolean {
|
|
166
|
+
return !b.top && !b.bottom && !b.left && !b.right &&
|
|
167
|
+
!b.topLeft && !b.topRight && !b.bottomLeft && !b.bottomRight;
|
|
168
|
+
}
|
package/src/color.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color types and utilities for terminal styling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ColorValue } from './ansi.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Represents no color — terminal default.
|
|
9
|
+
*/
|
|
10
|
+
export const NO_COLOR: unique symbol = Symbol('NoColor');
|
|
11
|
+
export type NoColor = typeof NO_COLOR;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* RGB color representation.
|
|
15
|
+
*/
|
|
16
|
+
export interface RGBColor {
|
|
17
|
+
r: number;
|
|
18
|
+
g: number;
|
|
19
|
+
b: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A color that can be used in styling.
|
|
24
|
+
* - null/undefined/NO_COLOR = no color (terminal default)
|
|
25
|
+
* - number 0-15 = ANSI basic color
|
|
26
|
+
* - number 16-255 = ANSI 256 color
|
|
27
|
+
* - string "#RRGGBB" or "#RGB" = hex color
|
|
28
|
+
* - RGBColor = explicit RGB
|
|
29
|
+
*/
|
|
30
|
+
export type Color = string | number | RGBColor | NoColor | null | undefined;
|
|
31
|
+
|
|
32
|
+
/** ANSI basic color constants (0-15) */
|
|
33
|
+
export const Black = 0;
|
|
34
|
+
export const Red = 1;
|
|
35
|
+
export const Green = 2;
|
|
36
|
+
export const Yellow = 3;
|
|
37
|
+
export const Blue = 4;
|
|
38
|
+
export const Magenta = 5;
|
|
39
|
+
export const Cyan = 6;
|
|
40
|
+
export const White = 7;
|
|
41
|
+
export const BrightBlack = 8;
|
|
42
|
+
export const BrightRed = 9;
|
|
43
|
+
export const BrightGreen = 10;
|
|
44
|
+
export const BrightYellow = 11;
|
|
45
|
+
export const BrightBlue = 12;
|
|
46
|
+
export const BrightMagenta = 13;
|
|
47
|
+
export const BrightCyan = 14;
|
|
48
|
+
export const BrightWhite = 15;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse a color specification into an internal ColorValue.
|
|
52
|
+
* Returns null for "no color".
|
|
53
|
+
*/
|
|
54
|
+
export function parseColor(c: Color): ColorValue | null {
|
|
55
|
+
if (c === null || c === undefined || c === NO_COLOR) return null;
|
|
56
|
+
|
|
57
|
+
if (typeof c === 'number') {
|
|
58
|
+
if (c < 0) c = -c;
|
|
59
|
+
if (c < 16) return { type: 'basic', value: c };
|
|
60
|
+
if (c < 256) return { type: 'ansi256', value: c };
|
|
61
|
+
// Treat as packed RGB: 0xRRGGBB
|
|
62
|
+
return {
|
|
63
|
+
type: 'rgb',
|
|
64
|
+
value: 0,
|
|
65
|
+
r: (c >> 16) & 0xff,
|
|
66
|
+
g: (c >> 8) & 0xff,
|
|
67
|
+
b: c & 0xff,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof c === 'string') {
|
|
72
|
+
if (c.startsWith('#')) {
|
|
73
|
+
const rgb = parseHex(c);
|
|
74
|
+
if (!rgb) return null;
|
|
75
|
+
return { type: 'rgb', value: 0, ...rgb };
|
|
76
|
+
}
|
|
77
|
+
// Try parsing as number
|
|
78
|
+
const n = parseInt(c, 10);
|
|
79
|
+
if (!isNaN(n)) {
|
|
80
|
+
return parseColor(n);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// RGBColor object
|
|
86
|
+
if (typeof c === 'object' && 'r' in c && 'g' in c && 'b' in c) {
|
|
87
|
+
return { type: 'rgb', value: 0, r: c.r, g: c.g, b: c.b };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse a hex color string (#RGB or #RRGGBB) into RGB values.
|
|
95
|
+
*/
|
|
96
|
+
export function parseHex(hex: string): RGBColor | null {
|
|
97
|
+
if (!hex.startsWith('#')) return null;
|
|
98
|
+
|
|
99
|
+
if (hex.length === 7) {
|
|
100
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
101
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
102
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
103
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
104
|
+
return { r, g, b };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (hex.length === 4) {
|
|
108
|
+
const r = parseInt(hex[1], 16) * 17;
|
|
109
|
+
const g = parseInt(hex[2], 16) * 17;
|
|
110
|
+
const b = parseInt(hex[3], 16) * 17;
|
|
111
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
112
|
+
return { r, g, b };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convert any Color to RGB values (approximation for ANSI colors).
|
|
120
|
+
*/
|
|
121
|
+
export function colorToRGB(c: Color): RGBColor | null {
|
|
122
|
+
const cv = parseColor(c);
|
|
123
|
+
if (!cv) return null;
|
|
124
|
+
if (cv.type === 'rgb') return { r: cv.r!, g: cv.g!, b: cv.b! };
|
|
125
|
+
if (cv.type === 'ansi256') return ansi256ToRGB(cv.value);
|
|
126
|
+
if (cv.type === 'basic') return ansi256ToRGB(cv.value);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Standard ANSI 16 color palette (approximate RGB values) */
|
|
131
|
+
const ANSI_16_COLORS: RGBColor[] = [
|
|
132
|
+
{ r: 0, g: 0, b: 0 }, // Black
|
|
133
|
+
{ r: 170, g: 0, b: 0 }, // Red
|
|
134
|
+
{ r: 0, g: 170, b: 0 }, // Green
|
|
135
|
+
{ r: 170, g: 85, b: 0 }, // Yellow
|
|
136
|
+
{ r: 0, g: 0, b: 170 }, // Blue
|
|
137
|
+
{ r: 170, g: 0, b: 170 }, // Magenta
|
|
138
|
+
{ r: 0, g: 170, b: 170 }, // Cyan
|
|
139
|
+
{ r: 170, g: 170, b: 170 }, // White
|
|
140
|
+
{ r: 85, g: 85, b: 85 }, // Bright Black
|
|
141
|
+
{ r: 255, g: 85, b: 85 }, // Bright Red
|
|
142
|
+
{ r: 85, g: 255, b: 85 }, // Bright Green
|
|
143
|
+
{ r: 255, g: 255, b: 85 }, // Bright Yellow
|
|
144
|
+
{ r: 85, g: 85, b: 255 }, // Bright Blue
|
|
145
|
+
{ r: 255, g: 85, b: 255 }, // Bright Magenta
|
|
146
|
+
{ r: 85, g: 255, b: 255 }, // Bright Cyan
|
|
147
|
+
{ r: 255, g: 255, b: 255 }, // Bright White
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Convert ANSI 256 color index to approximate RGB.
|
|
152
|
+
*/
|
|
153
|
+
export function ansi256ToRGB(index: number): RGBColor {
|
|
154
|
+
if (index < 16) return ANSI_16_COLORS[index];
|
|
155
|
+
|
|
156
|
+
if (index < 232) {
|
|
157
|
+
// 6x6x6 color cube
|
|
158
|
+
const i = index - 16;
|
|
159
|
+
const r = Math.floor(i / 36);
|
|
160
|
+
const g = Math.floor((i % 36) / 6);
|
|
161
|
+
const b = i % 6;
|
|
162
|
+
return {
|
|
163
|
+
r: r ? r * 40 + 55 : 0,
|
|
164
|
+
g: g ? g * 40 + 55 : 0,
|
|
165
|
+
b: b ? b * 40 + 55 : 0,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Grayscale (232-255)
|
|
170
|
+
const v = (index - 232) * 10 + 8;
|
|
171
|
+
return { r: v, g: v, b: v };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Determine if a color is "dark" based on luminance.
|
|
176
|
+
*/
|
|
177
|
+
export function isDarkColor(c: Color): boolean {
|
|
178
|
+
const rgb = colorToRGB(c);
|
|
179
|
+
if (!rgb) return true;
|
|
180
|
+
// HSL lightness approximation
|
|
181
|
+
const max = Math.max(rgb.r, rgb.g, rgb.b) / 255;
|
|
182
|
+
const min = Math.min(rgb.r, rgb.g, rgb.b) / 255;
|
|
183
|
+
const l = (max + min) / 2;
|
|
184
|
+
return l < 0.5;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* LightDark returns a function that picks a color based on background darkness.
|
|
189
|
+
*/
|
|
190
|
+
export function lightDark(isDark: boolean): (light: Color, dark: Color) => Color {
|
|
191
|
+
return (light: Color, dark: Color) => isDark ? dark : light;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Returns the complementary color (180 degrees on the color wheel).
|
|
196
|
+
*/
|
|
197
|
+
export function complementary(c: Color): Color {
|
|
198
|
+
const rgb = colorToRGB(c);
|
|
199
|
+
if (!rgb) return null;
|
|
200
|
+
|
|
201
|
+
// Convert to HSV, rotate hue 180 degrees, convert back
|
|
202
|
+
const { h, s, v } = rgbToHsv(rgb.r, rgb.g, rgb.b);
|
|
203
|
+
const newH = (h + 180) % 360;
|
|
204
|
+
const result = hsvToRgb(newH, s, v);
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Darken a color by a percentage (0-1).
|
|
210
|
+
*/
|
|
211
|
+
export function darken(c: Color, percent: number): Color {
|
|
212
|
+
const rgb = colorToRGB(c);
|
|
213
|
+
if (!rgb) return null;
|
|
214
|
+
const mult = 1 - clamp(percent, 0, 1);
|
|
215
|
+
return {
|
|
216
|
+
r: Math.round(rgb.r * mult),
|
|
217
|
+
g: Math.round(rgb.g * mult),
|
|
218
|
+
b: Math.round(rgb.b * mult),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Lighten a color by a percentage (0-1).
|
|
224
|
+
*/
|
|
225
|
+
export function lighten(c: Color, percent: number): Color {
|
|
226
|
+
const rgb = colorToRGB(c);
|
|
227
|
+
if (!rgb) return null;
|
|
228
|
+
const add = 255 * clamp(percent, 0, 1);
|
|
229
|
+
return {
|
|
230
|
+
r: Math.min(255, Math.round(rgb.r + add)),
|
|
231
|
+
g: Math.min(255, Math.round(rgb.g + add)),
|
|
232
|
+
b: Math.min(255, Math.round(rgb.b + add)),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Adjust alpha (0 = transparent, 1 = opaque). Returns an RGBColor
|
|
238
|
+
* (terminal colors don't truly support alpha, but useful for blending).
|
|
239
|
+
*/
|
|
240
|
+
export function alpha(c: Color, a: number): RGBColor | null {
|
|
241
|
+
const rgb = colorToRGB(c);
|
|
242
|
+
if (!rgb) return null;
|
|
243
|
+
const al = clamp(a, 0, 1);
|
|
244
|
+
return {
|
|
245
|
+
r: Math.round(rgb.r * al),
|
|
246
|
+
g: Math.round(rgb.g * al),
|
|
247
|
+
b: Math.round(rgb.b * al),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---- Helpers ----
|
|
252
|
+
|
|
253
|
+
function clamp(v: number, lo: number, hi: number): number {
|
|
254
|
+
return Math.min(hi, Math.max(lo, v));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function rgbToHsv(r: number, g: number, b: number): { h: number; s: number; v: number } {
|
|
258
|
+
r /= 255; g /= 255; b /= 255;
|
|
259
|
+
const max = Math.max(r, g, b);
|
|
260
|
+
const min = Math.min(r, g, b);
|
|
261
|
+
const d = max - min;
|
|
262
|
+
let h = 0;
|
|
263
|
+
const s = max === 0 ? 0 : d / max;
|
|
264
|
+
const v = max;
|
|
265
|
+
|
|
266
|
+
if (d !== 0) {
|
|
267
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
268
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
269
|
+
else h = ((r - g) / d + 4) / 6;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { h: h * 360, s, v };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function hsvToRgb(h: number, s: number, v: number): RGBColor {
|
|
276
|
+
h = ((h % 360) + 360) % 360;
|
|
277
|
+
const c = v * s;
|
|
278
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
279
|
+
const m = v - c;
|
|
280
|
+
let r = 0, g = 0, b = 0;
|
|
281
|
+
|
|
282
|
+
if (h < 60) { r = c; g = x; }
|
|
283
|
+
else if (h < 120) { r = x; g = c; }
|
|
284
|
+
else if (h < 180) { g = c; b = x; }
|
|
285
|
+
else if (h < 240) { g = x; b = c; }
|
|
286
|
+
else if (h < 300) { r = x; b = c; }
|
|
287
|
+
else { r = c; b = x; }
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
r: Math.round((r + m) * 255),
|
|
291
|
+
g: Math.round((g + m) * 255),
|
|
292
|
+
b: Math.round((b + m) * 255),
|
|
293
|
+
};
|
|
294
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lipgloss — CSS-like terminal styling for JavaScript.
|
|
3
|
+
* Zero dependencies, multi-runtime (Node.js, Bun, Deno).
|
|
4
|
+
*
|
|
5
|
+
* Ported from charmbracelet/lipgloss (Go) by Antonio Oliveira.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Style
|
|
9
|
+
export { Style, newStyle, getLines, getFirstRune } from './style.js';
|
|
10
|
+
export type { Position } from './style.js';
|
|
11
|
+
export { Top, Bottom, Center, Left, Right } from './style.js';
|
|
12
|
+
|
|
13
|
+
// Colors
|
|
14
|
+
export {
|
|
15
|
+
NO_COLOR, Black, Red, Green, Yellow, Blue, Magenta, Cyan, White,
|
|
16
|
+
BrightBlack, BrightRed, BrightGreen, BrightYellow, BrightBlue,
|
|
17
|
+
BrightMagenta, BrightCyan, BrightWhite,
|
|
18
|
+
parseColor, parseHex, colorToRGB, ansi256ToRGB,
|
|
19
|
+
isDarkColor, lightDark, complementary, darken, lighten, alpha,
|
|
20
|
+
} from './color.js';
|
|
21
|
+
export type { Color, RGBColor, NoColor } from './color.js';
|
|
22
|
+
|
|
23
|
+
// Borders
|
|
24
|
+
export {
|
|
25
|
+
noBorder, normalBorder, roundedBorder, blockBorder,
|
|
26
|
+
outerHalfBlockBorder, innerHalfBlockBorder, thickBorder,
|
|
27
|
+
doubleBorder, hiddenBorder, markdownBorder, asciiBorder,
|
|
28
|
+
maxRuneWidth, getTopSize, getRightSize, getBottomSize, getLeftSize, isNoBorder,
|
|
29
|
+
} from './border.js';
|
|
30
|
+
export type { Border } from './border.js';
|
|
31
|
+
|
|
32
|
+
// Layout
|
|
33
|
+
export {
|
|
34
|
+
joinHorizontal, joinVertical,
|
|
35
|
+
place, placeHorizontal, placeVertical,
|
|
36
|
+
} from './layout.js';
|
|
37
|
+
|
|
38
|
+
// ANSI utilities
|
|
39
|
+
export {
|
|
40
|
+
SGR, stringWidth, stripAnsi, truncate, styled,
|
|
41
|
+
fgColor, fgAnsi256, fgBasic, bgColor, bgAnsi256, bgBasic,
|
|
42
|
+
ulColor, ulAnsi256, setHyperlink, resetHyperlink,
|
|
43
|
+
} from './ansi.js';
|
|
44
|
+
export type { ColorValue, UnderlineStyle, AnsiStyleOptions } from './ansi.js';
|
package/src/layout.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout utilities: JoinHorizontal, JoinVertical, Place, PlaceHorizontal, PlaceVertical.
|
|
3
|
+
* Ported from charmbracelet/lipgloss join.go and position.go.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { stringWidth } from './ansi.js';
|
|
7
|
+
import { getLines } from './style.js';
|
|
8
|
+
import type { Position } from './style.js';
|
|
9
|
+
import { Top, Bottom, Left, Right } from './style.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// JoinHorizontal
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Horizontally join multi-line strings along a vertical axis.
|
|
17
|
+
* `pos` is the vertical alignment: 0 = top, 0.5 = center, 1 = bottom.
|
|
18
|
+
*/
|
|
19
|
+
export function joinHorizontal(pos: Position, ...strs: string[]): string {
|
|
20
|
+
if (strs.length === 0) return '';
|
|
21
|
+
if (strs.length === 1) return strs[0];
|
|
22
|
+
|
|
23
|
+
const blocks: string[][] = [];
|
|
24
|
+
const maxWidths: number[] = [];
|
|
25
|
+
let maxHeight = 0;
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < strs.length; i++) {
|
|
28
|
+
const { lines, widest } = getLines(strs[i]);
|
|
29
|
+
blocks.push(lines);
|
|
30
|
+
maxWidths.push(widest);
|
|
31
|
+
if (lines.length > maxHeight) maxHeight = lines.length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Equalize heights
|
|
35
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
36
|
+
if (blocks[i].length >= maxHeight) continue;
|
|
37
|
+
const extra = maxHeight - blocks[i].length;
|
|
38
|
+
const empties = new Array<string>(extra).fill('');
|
|
39
|
+
|
|
40
|
+
const p = Math.min(1, Math.max(0, pos));
|
|
41
|
+
if (p <= 0) {
|
|
42
|
+
// Top aligned
|
|
43
|
+
blocks[i] = [...blocks[i], ...empties];
|
|
44
|
+
} else if (p >= 1) {
|
|
45
|
+
// Bottom aligned
|
|
46
|
+
blocks[i] = [...empties, ...blocks[i]];
|
|
47
|
+
} else {
|
|
48
|
+
const split = Math.round(extra * p);
|
|
49
|
+
const top = extra - split;
|
|
50
|
+
const bottom = extra - top;
|
|
51
|
+
blocks[i] = [
|
|
52
|
+
...new Array<string>(top).fill(''),
|
|
53
|
+
...blocks[i],
|
|
54
|
+
...new Array<string>(bottom).fill(''),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Merge
|
|
60
|
+
const result: string[] = [];
|
|
61
|
+
for (let row = 0; row < maxHeight; row++) {
|
|
62
|
+
let line = '';
|
|
63
|
+
for (let col = 0; col < blocks.length; col++) {
|
|
64
|
+
const cell = blocks[col][row] || '';
|
|
65
|
+
line += cell;
|
|
66
|
+
// Pad to max width
|
|
67
|
+
const pad = maxWidths[col] - stringWidth(cell);
|
|
68
|
+
if (pad > 0) line += ' '.repeat(pad);
|
|
69
|
+
}
|
|
70
|
+
result.push(line);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// JoinVertical
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Vertically join multi-line strings along a horizontal axis.
|
|
82
|
+
* `pos` is the horizontal alignment: 0 = left, 0.5 = center, 1 = right.
|
|
83
|
+
*/
|
|
84
|
+
export function joinVertical(pos: Position, ...strs: string[]): string {
|
|
85
|
+
if (strs.length === 0) return '';
|
|
86
|
+
if (strs.length === 1) return strs[0];
|
|
87
|
+
|
|
88
|
+
const blocks: string[][] = [];
|
|
89
|
+
let maxWidth = 0;
|
|
90
|
+
|
|
91
|
+
for (const str of strs) {
|
|
92
|
+
const { lines, widest } = getLines(str);
|
|
93
|
+
blocks.push(lines);
|
|
94
|
+
if (widest > maxWidth) maxWidth = widest;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result: string[] = [];
|
|
98
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
99
|
+
for (let j = 0; j < blocks[i].length; j++) {
|
|
100
|
+
const line = blocks[i][j];
|
|
101
|
+
const gap = maxWidth - stringWidth(line);
|
|
102
|
+
|
|
103
|
+
if (gap <= 0) {
|
|
104
|
+
result.push(line);
|
|
105
|
+
} else {
|
|
106
|
+
const p = Math.min(1, Math.max(0, pos));
|
|
107
|
+
if (p <= 0) {
|
|
108
|
+
// Left
|
|
109
|
+
result.push(line + ' '.repeat(gap));
|
|
110
|
+
} else if (p >= 1) {
|
|
111
|
+
// Right
|
|
112
|
+
result.push(' '.repeat(gap) + line);
|
|
113
|
+
} else {
|
|
114
|
+
const split = Math.round(gap * p);
|
|
115
|
+
const left = gap - split;
|
|
116
|
+
const right = gap - left;
|
|
117
|
+
result.push(' '.repeat(left) + line + ' '.repeat(right));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Place
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Place a string in an unstyled box of given width and height.
|
|
132
|
+
*/
|
|
133
|
+
export function place(width: number, height: number, hPos: Position, vPos: Position, str: string): string {
|
|
134
|
+
return placeVertical(height, vPos, placeHorizontal(width, hPos, str));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Place a string horizontally in an unstyled block of given width.
|
|
139
|
+
*/
|
|
140
|
+
export function placeHorizontal(width: number, pos: Position, str: string): string {
|
|
141
|
+
const { lines, widest: contentWidth } = getLines(str);
|
|
142
|
+
const gap = width - contentWidth;
|
|
143
|
+
if (gap <= 0) return str;
|
|
144
|
+
|
|
145
|
+
const result: string[] = [];
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const short = Math.max(0, contentWidth - stringWidth(line));
|
|
148
|
+
const totalGap = gap + short;
|
|
149
|
+
const p = Math.min(1, Math.max(0, pos));
|
|
150
|
+
|
|
151
|
+
if (p <= 0) {
|
|
152
|
+
// Left
|
|
153
|
+
result.push(line + ' '.repeat(totalGap));
|
|
154
|
+
} else if (p >= 1) {
|
|
155
|
+
// Right
|
|
156
|
+
result.push(' '.repeat(totalGap) + line);
|
|
157
|
+
} else {
|
|
158
|
+
const split = Math.round(totalGap * p);
|
|
159
|
+
const left = totalGap - split;
|
|
160
|
+
const right = totalGap - left;
|
|
161
|
+
result.push(' '.repeat(left) + line + ' '.repeat(right));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result.join('\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Place a string vertically in an unstyled block of given height.
|
|
170
|
+
*/
|
|
171
|
+
export function placeVertical(height: number, pos: Position, str: string): string {
|
|
172
|
+
const contentHeight = str.split('\n').length;
|
|
173
|
+
const gap = height - contentHeight;
|
|
174
|
+
if (gap <= 0) return str;
|
|
175
|
+
|
|
176
|
+
const { widest: width } = getLines(str);
|
|
177
|
+
const emptyLine = ' '.repeat(width);
|
|
178
|
+
const p = Math.min(1, Math.max(0, pos));
|
|
179
|
+
|
|
180
|
+
if (p <= 0) {
|
|
181
|
+
// Top
|
|
182
|
+
const bottom: string[] = [];
|
|
183
|
+
for (let i = 0; i < gap; i++) bottom.push(emptyLine);
|
|
184
|
+
return str + '\n' + bottom.join('\n');
|
|
185
|
+
} else if (p >= 1) {
|
|
186
|
+
// Bottom
|
|
187
|
+
const top: string[] = [];
|
|
188
|
+
for (let i = 0; i < gap; i++) top.push(emptyLine);
|
|
189
|
+
return top.join('\n') + '\n' + str;
|
|
190
|
+
} else {
|
|
191
|
+
const split = Math.round(gap * p);
|
|
192
|
+
const topCount = gap - split;
|
|
193
|
+
const bottomCount = gap - topCount;
|
|
194
|
+
|
|
195
|
+
const topLines: string[] = [];
|
|
196
|
+
for (let i = 0; i < topCount; i++) topLines.push(emptyLine);
|
|
197
|
+
const bottomLines: string[] = [];
|
|
198
|
+
for (let i = 0; i < bottomCount; i++) bottomLines.push(emptyLine);
|
|
199
|
+
|
|
200
|
+
let result = '';
|
|
201
|
+
if (topLines.length > 0) result += topLines.join('\n') + '\n';
|
|
202
|
+
result += str;
|
|
203
|
+
if (bottomLines.length > 0) result += '\n' + bottomLines.join('\n');
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
}
|