osdy-pi 0.1.3 → 0.1.5
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/README.md +41 -6
- package/extensions/osdy-pi/animation.ts +99 -0
- package/extensions/osdy-pi/border.ts +49 -0
- package/extensions/osdy-pi/constants.ts +338 -0
- package/extensions/osdy-pi/format.ts +42 -0
- package/extensions/osdy-pi/metrics.ts +210 -0
- package/extensions/osdy-pi/runtime-helpers.ts +101 -0
- package/extensions/osdy-pi/runtime.ts +194 -0
- package/extensions/osdy-pi/types.ts +53 -0
- package/extensions/osdy-pi/ui.ts +308 -0
- package/extensions/osdy-pi/utils.ts +54 -0
- package/extensions/osdy-pi/working-controller.ts +87 -0
- package/extensions/osdy-pi.ts +4 -650
- package/package.json +46 -32
- package/themes/osdy-pi-dark.json +6 -0
- package/themes/osdy-pi-light.json +6 -0
- package/themes/osdy-pi-simple.json +83 -0
package/README.md
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
# Osdy Pi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img width="1857" height="847" alt="image" src="https://github.com/user-attachments/assets/028eeb14-3f43-4f1c-9603-0c55a8d2856d" />
|
|
4
4
|
|
|
5
5
|
Theme package for [Pi](https://github.com/earendil-works/pi) with the Osdy terminal style: neon pink/purple colors, a custom ASCII header, and a framed editor experience.
|
|
6
6
|
|
|
7
|
+
Visit the Osdy landing page: [landing-osdy.vercel.app](https://landing-osdy.vercel.app/).
|
|
8
|
+
|
|
7
9
|
## What you get
|
|
8
10
|
|
|
9
11
|
- **Dark theme:** `osdy-pi-dark`, enabled by default when the package starts.
|
|
10
12
|
- **Light theme:** `osdy-pi-light`, with the same Osdy palette adapted for light terminals.
|
|
11
|
-
- **
|
|
13
|
+
- **Simple theme:** `osdy-pi-simple`, a blue/red/slate console theme for the full Pi interface.
|
|
14
|
+
- **Custom header:** two selectable header styles with responsive status metadata: `osdy-theme` (default) and `classic`.
|
|
12
15
|
- **Custom editor:** full-width framed input area with model, thinking, token, cost, and context status.
|
|
13
|
-
- **
|
|
16
|
+
- **Custom working indicator:** a dedicated working widget/spinner appears above the text box, outside the editor frame.
|
|
17
|
+
- **Clean layout:** the built-in working row is hidden while Osdy Pi is enabled to avoid duplicated UI.
|
|
14
18
|
|
|
15
19
|
<img width="1280" height="433" alt="image" src="https://github.com/user-attachments/assets/20c7624d-9ad8-4494-97fb-6b6d81aaf328" />
|
|
16
20
|
|
|
17
21
|
## Install in Pi
|
|
18
22
|
|
|
19
|
-
Install the package
|
|
23
|
+
Install the published package from npm:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pi install npm:osdy-pi
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You can also install it directly from GitHub:
|
|
20
30
|
|
|
21
31
|
```bash
|
|
22
32
|
pi install git:github.com/OsdyOrtiz/Osdy-Pi
|
|
@@ -43,6 +53,7 @@ Then select one of these theme names:
|
|
|
43
53
|
```text
|
|
44
54
|
osdy-pi-dark
|
|
45
55
|
osdy-pi-light
|
|
56
|
+
osdy-pi-simple
|
|
46
57
|
```
|
|
47
58
|
|
|
48
59
|
You can also set it in your Pi `settings.json`:
|
|
@@ -53,7 +64,7 @@ You can also set it in your Pi `settings.json`:
|
|
|
53
64
|
}
|
|
54
65
|
```
|
|
55
66
|
|
|
56
|
-
Use `osdy-pi-light`
|
|
67
|
+
Use `osdy-pi-light` if you prefer the light version, or `osdy-pi-simple` if you want the blue/red/slate palette across the whole console.
|
|
57
68
|
|
|
58
69
|
## Commands
|
|
59
70
|
|
|
@@ -63,11 +74,20 @@ Osdy Pi includes a small command group:
|
|
|
63
74
|
/osdy-pi enable
|
|
64
75
|
/osdy-pi disable
|
|
65
76
|
/osdy-pi status
|
|
77
|
+
/osdy-pi osdy-theme
|
|
78
|
+
/osdy-pi classic
|
|
79
|
+
/osdy-pi-osdy-theme
|
|
80
|
+
/osdy-pi-classic
|
|
66
81
|
```
|
|
67
82
|
|
|
68
83
|
- `enable` applies the dark Osdy theme, custom header, custom editor, and clean layout.
|
|
69
84
|
- `disable` restores Pi's built-in header, editor, footer, and working visibility, then switches back to the previous theme or `dark`.
|
|
70
|
-
- `status` shows whether the Osdy Pi UI is currently enabled.
|
|
85
|
+
- `status` shows whether the Osdy Pi UI is currently enabled, including the active style.
|
|
86
|
+
- `osdy-theme` is the default OsdyTheme header with pink, cyan, and purple styling, plus the mascot glow on the right edge.
|
|
87
|
+
- `classic` keeps the previous classic header shape with the shared mascot.
|
|
88
|
+
- `/osdy-pi-osdy-theme` and `/osdy-pi-classic` are direct aliases.
|
|
89
|
+
|
|
90
|
+
After changing a local extension, run `/reload` or restart Pi so the updated commands are registered.
|
|
71
91
|
|
|
72
92
|
## Local install
|
|
73
93
|
|
|
@@ -83,16 +103,31 @@ To install it from a local path:
|
|
|
83
103
|
pi install /absolute/path/to/Osdy-Pi
|
|
84
104
|
```
|
|
85
105
|
|
|
106
|
+

|
|
107
|
+
|
|
86
108
|
## Package contents
|
|
87
109
|
|
|
88
110
|
```text
|
|
89
111
|
themes/osdy-pi-dark.json
|
|
90
112
|
themes/osdy-pi-light.json
|
|
113
|
+
themes/osdy-pi-simple.json
|
|
91
114
|
extensions/osdy-pi.ts
|
|
115
|
+
extensions/osdy-pi/
|
|
92
116
|
```
|
|
93
117
|
|
|
118
|
+
`extensions/osdy-pi.ts` is the package entrypoint. The implementation lives in the modular `extensions/osdy-pi/` folder (runtime, UI, metrics, working controller, animation, border, and formatting helpers).
|
|
119
|
+
|
|
94
120
|
The Pi manifest is declared in `package.json` through `pi.themes` and `pi.extensions`, so Pi can discover the themes and extension after installation.
|
|
95
121
|
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
If you are working on the package locally, you can run:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm run typecheck
|
|
128
|
+
npm run lint
|
|
129
|
+
```
|
|
130
|
+
|
|
96
131
|
## Uninstall or turn off
|
|
97
132
|
|
|
98
133
|
To temporarily turn off the custom UI inside Pi:
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ANIMATION_ENABLED } from "./constants.js";
|
|
2
|
+
import type { AnimationMode, SimpleTheme } from "./types.js";
|
|
3
|
+
import type { MascotTonePalette } from "./constants.js";
|
|
4
|
+
import { positiveModulo } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
export type AsciiAnimationStyle = "static" | "animated";
|
|
7
|
+
|
|
8
|
+
const ANIMATION_OVERRIDE_MAP: Record<string, AnimationMode> = {
|
|
9
|
+
"0": "off",
|
|
10
|
+
"1": "continuous",
|
|
11
|
+
off: "off",
|
|
12
|
+
on: "continuous",
|
|
13
|
+
intro: "intro",
|
|
14
|
+
continuous: "continuous",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function getAnimationOverrideMode(
|
|
18
|
+
override: string | undefined,
|
|
19
|
+
): AnimationMode | undefined {
|
|
20
|
+
return override ? ANIMATION_OVERRIDE_MAP[override] : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function asciiAnimationMode(): AnimationMode {
|
|
24
|
+
if (!ANIMATION_ENABLED) return "off";
|
|
25
|
+
return getAnimationOverrideMode(process.env.OSDY_PI_ANIMATION) ?? "intro";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getAnimatedAsciiColor(
|
|
29
|
+
wave: number,
|
|
30
|
+
baseColor: string,
|
|
31
|
+
highlightColor: string,
|
|
32
|
+
trailColor: string,
|
|
33
|
+
): string {
|
|
34
|
+
if (wave <= 3) return highlightColor;
|
|
35
|
+
if (wave <= 8) return trailColor;
|
|
36
|
+
return baseColor;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function animateAsciiLine(
|
|
40
|
+
line: string,
|
|
41
|
+
lineIndex: number,
|
|
42
|
+
frame: number,
|
|
43
|
+
theme: SimpleTheme,
|
|
44
|
+
baseColor: string,
|
|
45
|
+
highlightColor: string,
|
|
46
|
+
trailColor: string,
|
|
47
|
+
style: AsciiAnimationStyle,
|
|
48
|
+
): string {
|
|
49
|
+
if (style === "static") return theme.fg(baseColor, line);
|
|
50
|
+
return Array.from(line)
|
|
51
|
+
.map((char, charIndex) => {
|
|
52
|
+
if (char === " ") return char;
|
|
53
|
+
const wave = positiveModulo(charIndex + lineIndex * 2 - frame * 5, 44);
|
|
54
|
+
return theme.fg(
|
|
55
|
+
getAnimatedAsciiColor(wave, baseColor, highlightColor, trailColor),
|
|
56
|
+
char,
|
|
57
|
+
);
|
|
58
|
+
})
|
|
59
|
+
.join("");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function animateAsciiLineWithToneMap(
|
|
63
|
+
line: string,
|
|
64
|
+
toneMap: string,
|
|
65
|
+
lineIndex: number,
|
|
66
|
+
frame: number,
|
|
67
|
+
theme: SimpleTheme,
|
|
68
|
+
palette: MascotTonePalette,
|
|
69
|
+
style: AsciiAnimationStyle,
|
|
70
|
+
): string {
|
|
71
|
+
return Array.from(line)
|
|
72
|
+
.map((char, charIndex) => {
|
|
73
|
+
if (char === " ") return char;
|
|
74
|
+
const tone = toneMap[charIndex];
|
|
75
|
+
if (
|
|
76
|
+
tone !== "b" &&
|
|
77
|
+
tone !== "h" &&
|
|
78
|
+
tone !== "l" &&
|
|
79
|
+
tone !== "m" &&
|
|
80
|
+
tone !== "d" &&
|
|
81
|
+
tone !== "p" &&
|
|
82
|
+
tone !== "c" &&
|
|
83
|
+
tone !== "v"
|
|
84
|
+
) {
|
|
85
|
+
return char;
|
|
86
|
+
}
|
|
87
|
+
const baseColor = palette[tone];
|
|
88
|
+
if (style === "static") return theme.fg(baseColor, char);
|
|
89
|
+
const wave = positiveModulo(charIndex + lineIndex * 2 - frame * 5, 44);
|
|
90
|
+
const highlightColor =
|
|
91
|
+
tone === "b" || tone === "h" || tone === "p" ? palette.h : palette.l;
|
|
92
|
+
const trailColor = tone === "d" ? palette.m : palette.b;
|
|
93
|
+
return theme.fg(
|
|
94
|
+
getAnimatedAsciiColor(wave, baseColor, highlightColor, trailColor),
|
|
95
|
+
char,
|
|
96
|
+
);
|
|
97
|
+
})
|
|
98
|
+
.join("");
|
|
99
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
function borderOverflows(left: string, right: string, width: number): boolean {
|
|
4
|
+
const fixedWidth = 2;
|
|
5
|
+
const minGap = 1;
|
|
6
|
+
return fixedWidth + visibleWidth(left) + visibleWidth(right) + minGap > width;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function trimBorderText(value: string): string {
|
|
10
|
+
return truncateToWidth(value, Math.max(0, visibleWidth(value) - 1), "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function shrinkBorderTexts(
|
|
14
|
+
left: string,
|
|
15
|
+
right: string,
|
|
16
|
+
width: number,
|
|
17
|
+
): { leftText: string; rightText: string } {
|
|
18
|
+
let leftText = left;
|
|
19
|
+
let rightText = right;
|
|
20
|
+
while (
|
|
21
|
+
borderOverflows(leftText, rightText, width) &&
|
|
22
|
+
visibleWidth(rightText) > 0
|
|
23
|
+
) {
|
|
24
|
+
rightText = trimBorderText(rightText);
|
|
25
|
+
}
|
|
26
|
+
while (
|
|
27
|
+
borderOverflows(leftText, rightText, width) &&
|
|
28
|
+
visibleWidth(leftText) > 0
|
|
29
|
+
) {
|
|
30
|
+
leftText = trimBorderText(leftText);
|
|
31
|
+
}
|
|
32
|
+
return { leftText, rightText };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function fitBorder(
|
|
36
|
+
left: string,
|
|
37
|
+
right: string,
|
|
38
|
+
width: number,
|
|
39
|
+
color: (text: string) => string,
|
|
40
|
+
): string {
|
|
41
|
+
if (width <= 0) return "";
|
|
42
|
+
if (width === 1) return color("─");
|
|
43
|
+
const { leftText, rightText } = shrinkBorderTexts(left, right, width);
|
|
44
|
+
const fillWidth = Math.max(
|
|
45
|
+
0,
|
|
46
|
+
width - 2 - visibleWidth(leftText) - visibleWidth(rightText),
|
|
47
|
+
);
|
|
48
|
+
return `${color("─")}${leftText}${color("─".repeat(fillWidth))}${rightText}${color("─")}`;
|
|
49
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { HeaderVariant } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const THEME_NAME = "osdy-pi-dark";
|
|
5
|
+
export const ANIMATION_ENABLED = true;
|
|
6
|
+
export const ANIMATION_INTERVAL_MS = 30;
|
|
7
|
+
export const INTRO_ANIMATION_FRAMES = 28;
|
|
8
|
+
export const WORKING_SPINNER_FRAMES = [
|
|
9
|
+
"⠋",
|
|
10
|
+
"⠙",
|
|
11
|
+
"⠹",
|
|
12
|
+
"⠸",
|
|
13
|
+
"⠼",
|
|
14
|
+
"⠴",
|
|
15
|
+
"⠦",
|
|
16
|
+
"⠧",
|
|
17
|
+
"⠇",
|
|
18
|
+
"⠏",
|
|
19
|
+
];
|
|
20
|
+
export const WORKING_WIDGET_KEY = "osdy-pi-working";
|
|
21
|
+
export const MASCOT_MIN_ROWS = 34;
|
|
22
|
+
export const MASCOT_GAP = 0;
|
|
23
|
+
|
|
24
|
+
const HTML_MASCOT = [
|
|
25
|
+
" ▓▓▓▓▒ ",
|
|
26
|
+
" ░ ▓▓▓ ▒▓ ",
|
|
27
|
+
" ░░░▓▓▓ ▓▓ ",
|
|
28
|
+
" ▒ ▒ ▒▒ ▒ ░░ ▓▓▓▓ ░▒▓▓ ",
|
|
29
|
+
"▓▓▓▓▓▓▓▓░░░░░ ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ░░▒▓ ",
|
|
30
|
+
"▓▓ ▓▓▓▓▓░░░ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░ ░░▒▒ ",
|
|
31
|
+
"▓▓░ ▒▓▓▓▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒▒▒▒▒▒▒░░ ░▒▒ ",
|
|
32
|
+
" ▓▒░░ ▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓█▓▓█▓▓▓▓▓▒▒▒░▒▒░ ",
|
|
33
|
+
" ▓▒░░ ░▒▒▒▒▒▓▓▓▓▓▒█▒▒▒▒▒▓▓▓▓ ░░░ ▓▓▓▓▒▒▒░░ ",
|
|
34
|
+
" ▒▒░░ ░▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▓▓ ░ ▓ ▒▒░░ ",
|
|
35
|
+
" ▒▒▒▒░▒▒▓▓▓▓░ ░▓▒▒▒▒▒ ░▒▒█▓ ▒▓▓▓░▒ ",
|
|
36
|
+
" ░ ░▒▒▓▓▒ ▓░░ ░░ ░ ▓▓▓▒ ",
|
|
37
|
+
" ░░▒▓░ ░▒░▓▓ ░░ ░▓▓▒ ",
|
|
38
|
+
" ░▒ ▒ ░▒▒▒ ▒▒▒▒ ░ ▒▒▒ ",
|
|
39
|
+
" ░▒▓▓░ ▓▓░░░ █▓ ▒ ░▒▒▒ ",
|
|
40
|
+
" ▒▓▓▓░ ▓▓ ▓▓▓▓▓▓▓▓▓ ░░▒ ",
|
|
41
|
+
" ▓▓▓░ ░ ▓▓▓▓▓▓▒▓▓▓▓ ▓▓▓▓▓▓▓▒░░░░ ░ ",
|
|
42
|
+
" ▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░ ",
|
|
43
|
+
" ▓▒▒░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒░░ ▓ ",
|
|
44
|
+
" ░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒ ░░░ ",
|
|
45
|
+
" ░ ░░░░░░░░░░░▒▒░░ ░ ░ ░ ▓ ",
|
|
46
|
+
" ░░░░░▒▒▒▒░░▒░ ▒▒▒▒▒░░ ░░▒▒▒░░░░░░░░ ",
|
|
47
|
+
" ▒▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░ ▒░░▒▒▒▒▓▓▓▓░░░░ ",
|
|
48
|
+
" ░▒▒▒▒▒░ ░░▒▒▒▒▒▒▒▒░░░ ░░▒▓▓▓▓▒▒░░ ",
|
|
49
|
+
" ▓░░░░░░ ░ ░░░░░░░░░░░ ░░░░░░▓▒▒▒▒ ░",
|
|
50
|
+
" ░░░▒▒▓▓▓▓▓▒░░░░░░░░░░ ░░▒▒▒▒░░░░░░░▒▒░░ ",
|
|
51
|
+
" ░▒▒▒▓▓▓▓▓▓▓▓▓▓▒▒▒▒░░░ ░▒▒▒▓▓▒░░░ ░░░▒ ",
|
|
52
|
+
" ▒░▒▒▒▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒░░░ ░▒▒▓▓▒░ ░ ",
|
|
53
|
+
" ░░▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒░░░░ ░▒▒▒▒▒ ░ ",
|
|
54
|
+
" ▒░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░ ▒▒░▒ ",
|
|
55
|
+
" ░░░░░░░░░░▒░░▒░▒▒░░░░ ░ ░░░░ ",
|
|
56
|
+
" ░░░░ ░░░░░░░░░░░ ░░░░ ",
|
|
57
|
+
" ░ ░░░░░░ ▓ ",
|
|
58
|
+
" ░░░░░ ░░░░░░ ",
|
|
59
|
+
" ░ ░ ",
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
62
|
+
const HTML_MASCOT_MAP = [
|
|
63
|
+
" hhhhl ",
|
|
64
|
+
" dmmhhhmmlhm ",
|
|
65
|
+
" dmmmhhhdmmmmhh ",
|
|
66
|
+
" ldldllddmdldmmmmhhlhddmmmlhh ",
|
|
67
|
+
"hhhhhhhhmmmlm llllllllllllllllllmlldddmmmll ",
|
|
68
|
+
"hhmmmmlhhhhmmmdllllllllllllllllllllllllmddmmmll ",
|
|
69
|
+
"hhmmmddlhhhlmlllllllllllllllllllhlllllllllmmlld ",
|
|
70
|
+
" llmmmdddlmllllllllllllllllllhbhhbhhhhhllllllm ",
|
|
71
|
+
" dllmmmddllllllhhhhhlblllllhhhhmmmmmmhhhhlllmm ",
|
|
72
|
+
" dllmmmlllllhhhhhhhhhhllllhhmmmmmmmmmddddhdlllm ",
|
|
73
|
+
" lllllllhhhhmmmmmmmhllllldmdlllbhmmmmddddlhhhlldd ",
|
|
74
|
+
" mdlllhhlmmdhmmmddmmdmmddmmmmdldddddddddmmmhhhl ",
|
|
75
|
+
" lllhmddmlmhhmmmmmddmmmddddddddddddddddmmdmmmhhl ",
|
|
76
|
+
" mlldldddmlllddddddddlllldmddddddddddddmmmddmlll ",
|
|
77
|
+
" llhhmddddmmdddddddddhhmmmdbhdldddddddddmmddmlll ",
|
|
78
|
+
" llhhmmmdddddddddddddhhddddmhhhhhhhhhmmdddddlml ",
|
|
79
|
+
" hhhmmmmmddddddddldhhhhhhlhhhhmhhhhhhhlmmmmmm ",
|
|
80
|
+
" llllmddmddddddddhhhhhhhhhhlhhhhhhhlllmmmm ",
|
|
81
|
+
" hllllmddddddmhhhhhhhhhhhhhllllllmmmh ",
|
|
82
|
+
" dmmmmmmmmlllllllllllllmdmmm ",
|
|
83
|
+
" mmmmmmmmmmmmmmmlllmm ",
|
|
84
|
+
" dlmmmmllllmmlmmllllllm dmllllmmmmmmmm ",
|
|
85
|
+
" lllllmmlllllllllllllmmm lmllllllllhlmmmmm ",
|
|
86
|
+
" mlllllmdmlllllllllmmmd mddmmmmlhhhlllmmmmm ",
|
|
87
|
+
" hmmmmmmdmdmmmmmmmmmmm dddmmmmmmmmlllllmmdmm",
|
|
88
|
+
" mmmlllllhllllmmmmmmmm dmmllllmmmmmmmlllmdm ",
|
|
89
|
+
" mlllhhhhhhhhlllllllmmdddmlllhllmmmmmmlmml ",
|
|
90
|
+
" llllllhhhhhhhllllllllmmdddmmlllllmmmmdmmd ",
|
|
91
|
+
" mllllllllhhllllllllllmmmddmmmlllllmdddmm ",
|
|
92
|
+
" lmllmlllllllllllllllmmmmmddmmmllllmdddd ",
|
|
93
|
+
" lmmmmmmllllmllllllmmmdmdddddmmmmmddmd ",
|
|
94
|
+
" dmmmmmmd dmmmmmmmmmmmmddddmmmmmdd ",
|
|
95
|
+
" dmmmmmd dmmmmmmd h ",
|
|
96
|
+
" mmmmmmmm dmmmmmmm ",
|
|
97
|
+
" dmmmmmm ",
|
|
98
|
+
] as const;
|
|
99
|
+
|
|
100
|
+
const HEADER_CLASSIC = [
|
|
101
|
+
"░█████╗░░██████╗██████╗░██╗░░░██╗░░░░░░██████╗░██╗",
|
|
102
|
+
"██╔══██╗██╔════╝██╔══██╗╚██╗░██╔╝░░░░░░██╔══██╗ ║",
|
|
103
|
+
"██║░░██║╚█████╗░██║░░██║░╚████╔╝░█████╗██████╔╝██║",
|
|
104
|
+
"██║░░██║░╚═══██╗██║░░██║░░╚██╔╝░░╚════╝██╔═══╝░██║",
|
|
105
|
+
"╚█████╔╝██████╔╝██████╔╝░░░██║░░░░░░░░░██║░░░░░██║",
|
|
106
|
+
"░╚════╝░╚═════╝░╚═════╝░░░░╚═╝░░░░░░░░░╚═╝░░░░░╚═╝",
|
|
107
|
+
" ╭━╮╱╱╱╱╱╱╭╮╱╱╱╱╱╭╮╱╱╱╱╱╭╮╱╱╭╮╭╮╱╱╱╱╭━╮",
|
|
108
|
+
" ┃╭╋━┳━━┳━╋╋╮╭━╮╭╯┣━┳┳╮╭╯┣━╮┣╋╯┣━┳━╮┃━┫",
|
|
109
|
+
" ┃╰┫╋┃┃┃┃╋┃┃╰┫╋╰┫╋┃╋┃╭╯┃╋┃┻┫┃┃╋┃┻┫╋╰╋━┃",
|
|
110
|
+
" ╰━┻━┻┻┻┫╭┻┻━┻━━┻━┻━┻╯╱╰━┻━╯╰┻━┻━┻━━┻━╯",
|
|
111
|
+
" ╱╱╱╱╱╱╱╰╯ </>",
|
|
112
|
+
] as const;
|
|
113
|
+
|
|
114
|
+
const HEADER_SIMPLE = [
|
|
115
|
+
" ███████ █████ ███████████ ███ ",
|
|
116
|
+
" ███░░░░░███ ░░███ ░░███░░░░░███ ░░░ ",
|
|
117
|
+
" ███ ░░███ █████ ███████ █████ ████ ░███ ░███ ████ ",
|
|
118
|
+
"░███ ░███ ███░░ ███░░███ ░░███ ░███ ██████████ ░██████████ ░░███ ",
|
|
119
|
+
"░███ ░███░░█████ ░███ ░███ ░███ ░███ ░░░░░░░░░░ ░███░░░░░░ ░███ ",
|
|
120
|
+
"░░███ ███ ░░░░███░███ ░███ ░███ ░███ ░███ ░███ ",
|
|
121
|
+
" ░░░███████░ ██████ ░░████████ ░░███████ █████ █████",
|
|
122
|
+
" ░░░░░░░ ░░░░░░ ░░░░░░░░ ░░░░░███ ░░░░░ ░░░░░ ",
|
|
123
|
+
" ███ ░███",
|
|
124
|
+
" ░░██████",
|
|
125
|
+
" ░░░░░░ </>",
|
|
126
|
+
] as const;
|
|
127
|
+
|
|
128
|
+
type HeaderPalette = {
|
|
129
|
+
baseColor: string;
|
|
130
|
+
highlightColor: string;
|
|
131
|
+
trailColor: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type MascotToneKey = "b" | "h" | "l" | "m" | "d" | "p" | "c" | "v";
|
|
135
|
+
|
|
136
|
+
export type MascotTonePalette = Record<MascotToneKey, string>;
|
|
137
|
+
|
|
138
|
+
type HeaderVariantConfig = {
|
|
139
|
+
label: string;
|
|
140
|
+
header: readonly string[];
|
|
141
|
+
headerMap?: readonly string[];
|
|
142
|
+
headerTonePalette?: MascotTonePalette;
|
|
143
|
+
minRowsForFull?: number;
|
|
144
|
+
fallbackHeader?: readonly string[];
|
|
145
|
+
fallbackLinePalette?: (lineIndex: number) => HeaderPalette;
|
|
146
|
+
mascot?: readonly string[];
|
|
147
|
+
mascotMap?: readonly string[];
|
|
148
|
+
linePalette: (lineIndex: number) => HeaderPalette;
|
|
149
|
+
mascotPalette: HeaderPalette;
|
|
150
|
+
mascotTonePalette?: MascotTonePalette;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const CLASSIC_PALETTE: HeaderPalette = {
|
|
154
|
+
baseColor: "accent",
|
|
155
|
+
highlightColor: "mdHeading",
|
|
156
|
+
trailColor: "mdLink",
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const CLASSIC_LINK_PALETTE: HeaderPalette = {
|
|
160
|
+
baseColor: "mdLink",
|
|
161
|
+
highlightColor: "mdHeading",
|
|
162
|
+
trailColor: "mdLink",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const OSDY_THEME_PINK_PALETTE: HeaderPalette = {
|
|
166
|
+
baseColor: "accent",
|
|
167
|
+
highlightColor: "mdHeading",
|
|
168
|
+
trailColor: "mdLink",
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const OSDY_THEME_CYAN_PALETTE: HeaderPalette = {
|
|
172
|
+
baseColor: "mdLink",
|
|
173
|
+
highlightColor: "accent",
|
|
174
|
+
trailColor: "mdHeading",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const OSDY_THEME_PURPLE_PALETTE: HeaderPalette = {
|
|
178
|
+
baseColor: "mdHeading",
|
|
179
|
+
highlightColor: "mdLink",
|
|
180
|
+
trailColor: "accent",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const OSDY_THEME_MASCOT_PALETTE: HeaderPalette = {
|
|
184
|
+
baseColor: "mdLink",
|
|
185
|
+
highlightColor: "accent",
|
|
186
|
+
trailColor: "mdHeading",
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const HTML_MASCOT_TONES: MascotTonePalette = {
|
|
190
|
+
b: "mascotBg",
|
|
191
|
+
h: "mascotBright",
|
|
192
|
+
l: "mascotLight",
|
|
193
|
+
m: "mascotMid",
|
|
194
|
+
d: "mascotDark",
|
|
195
|
+
p: "htmlPink",
|
|
196
|
+
c: "mdLink",
|
|
197
|
+
v: "mdHeading",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
type MascotArt = {
|
|
201
|
+
mascot: readonly string[];
|
|
202
|
+
toneMap: readonly string[];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function sampleIndex(
|
|
206
|
+
outputIndex: number,
|
|
207
|
+
outputCount: number,
|
|
208
|
+
inputCount: number,
|
|
209
|
+
): number {
|
|
210
|
+
if (outputCount <= 1) return 0;
|
|
211
|
+
return Math.min(
|
|
212
|
+
inputCount - 1,
|
|
213
|
+
Math.round((outputIndex * (inputCount - 1)) / (outputCount - 1)),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function sampleLine(
|
|
218
|
+
line: string,
|
|
219
|
+
outputWidth: number,
|
|
220
|
+
inputWidth: number,
|
|
221
|
+
): string {
|
|
222
|
+
const chars = Array.from(line.padEnd(inputWidth, " "));
|
|
223
|
+
return Array.from({ length: outputWidth }, (_value, outputIndex) => {
|
|
224
|
+
const inputIndex = sampleIndex(outputIndex, outputWidth, inputWidth);
|
|
225
|
+
return chars[inputIndex] ?? " ";
|
|
226
|
+
}).join("");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function scaleMascotArt(
|
|
230
|
+
mascot: readonly string[],
|
|
231
|
+
toneMap: readonly string[],
|
|
232
|
+
scale: number,
|
|
233
|
+
): MascotArt {
|
|
234
|
+
const inputRows = Math.min(mascot.length, toneMap.length);
|
|
235
|
+
const inputWidth = Math.max(
|
|
236
|
+
...mascot.slice(0, inputRows).map((line) => Array.from(line).length),
|
|
237
|
+
...toneMap.slice(0, inputRows).map((line) => Array.from(line).length),
|
|
238
|
+
);
|
|
239
|
+
const outputRows = Math.max(1, Math.round(inputRows * scale));
|
|
240
|
+
const outputWidth = Math.max(1, Math.round(inputWidth * scale));
|
|
241
|
+
|
|
242
|
+
return Array.from({ length: outputRows }, (_value, outputIndex) => {
|
|
243
|
+
const inputIndex = sampleIndex(outputIndex, outputRows, inputRows);
|
|
244
|
+
return {
|
|
245
|
+
mascot: sampleLine(mascot[inputIndex] ?? "", outputWidth, inputWidth),
|
|
246
|
+
toneMap: sampleLine(toneMap[inputIndex] ?? "", outputWidth, inputWidth),
|
|
247
|
+
};
|
|
248
|
+
}).reduce<MascotArt>(
|
|
249
|
+
(accumulator, line) => ({
|
|
250
|
+
mascot: [...accumulator.mascot, line.mascot],
|
|
251
|
+
toneMap: [...accumulator.toneMap, line.toneMap],
|
|
252
|
+
}),
|
|
253
|
+
{ mascot: [], toneMap: [] },
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function addRightEdgeGlow(
|
|
258
|
+
mascot: readonly string[],
|
|
259
|
+
toneMap: readonly string[],
|
|
260
|
+
): readonly string[] {
|
|
261
|
+
return toneMap.map((lineMap, lineIndex) => {
|
|
262
|
+
const chars = Array.from(lineMap);
|
|
263
|
+
const mascotLine = mascot[lineIndex] ?? "";
|
|
264
|
+
const rightEdgeIndex = Math.max(
|
|
265
|
+
...Array.from(mascotLine).map((char, index) =>
|
|
266
|
+
char === " " ? -1 : index,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
if (rightEdgeIndex < 0) return lineMap;
|
|
270
|
+
const glowTones = ["p", "c", "v"] as const;
|
|
271
|
+
for (let offset = 0; offset < glowTones.length; offset += 1) {
|
|
272
|
+
const glowTone = glowTones[offset];
|
|
273
|
+
const index = rightEdgeIndex - offset;
|
|
274
|
+
if (
|
|
275
|
+
glowTone &&
|
|
276
|
+
index >= 0 &&
|
|
277
|
+
index < chars.length &&
|
|
278
|
+
chars[index] !== " "
|
|
279
|
+
) {
|
|
280
|
+
chars[index] = glowTone;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return chars.join("");
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const OSDY_THEME_MASCOT_ART = scaleMascotArt(HTML_MASCOT, HTML_MASCOT_MAP, 0.9);
|
|
288
|
+
const OSDY_THEME_MASCOT_MAP = addRightEdgeGlow(
|
|
289
|
+
OSDY_THEME_MASCOT_ART.mascot,
|
|
290
|
+
OSDY_THEME_MASCOT_ART.toneMap,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
function osdyThemePalette(lineIndex: number): HeaderPalette {
|
|
294
|
+
if (lineIndex < 4) return OSDY_THEME_PINK_PALETTE;
|
|
295
|
+
if (lineIndex < 8) return OSDY_THEME_CYAN_PALETTE;
|
|
296
|
+
return OSDY_THEME_PURPLE_PALETTE;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export const HEADER_VARIANTS: Record<HeaderVariant, HeaderVariantConfig> = {
|
|
300
|
+
"osdy-theme": {
|
|
301
|
+
label: "OsdyTheme",
|
|
302
|
+
header: HEADER_SIMPLE,
|
|
303
|
+
mascot: OSDY_THEME_MASCOT_ART.mascot,
|
|
304
|
+
mascotMap: OSDY_THEME_MASCOT_MAP,
|
|
305
|
+
linePalette: osdyThemePalette,
|
|
306
|
+
mascotPalette: OSDY_THEME_MASCOT_PALETTE,
|
|
307
|
+
mascotTonePalette: HTML_MASCOT_TONES,
|
|
308
|
+
},
|
|
309
|
+
classic: {
|
|
310
|
+
label: "Classic",
|
|
311
|
+
header: HEADER_CLASSIC,
|
|
312
|
+
mascot: HTML_MASCOT,
|
|
313
|
+
mascotMap: HTML_MASCOT_MAP,
|
|
314
|
+
linePalette: (lineIndex) =>
|
|
315
|
+
lineIndex >= 6 ? CLASSIC_LINK_PALETTE : CLASSIC_PALETTE,
|
|
316
|
+
mascotPalette: OSDY_THEME_MASCOT_PALETTE,
|
|
317
|
+
mascotTonePalette: HTML_MASCOT_TONES,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export function headerWidth(variant: HeaderVariant): number {
|
|
322
|
+
return HEADER_VARIANTS[variant].header.reduce(
|
|
323
|
+
(maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
|
|
324
|
+
0,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function mascotWidth(variant: HeaderVariant): number {
|
|
329
|
+
return (HEADER_VARIANTS[variant].mascot ?? []).reduce(
|
|
330
|
+
(maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
|
|
331
|
+
0,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export const HEADER_FALLBACK = [
|
|
336
|
+
"OSDY - PI",
|
|
337
|
+
"</> Compilador de ideas",
|
|
338
|
+
] as const;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export function formatPath(cwd: string): string {
|
|
4
|
+
const home = process.env.HOME;
|
|
5
|
+
return home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getMiddleWidths(maxWidth: number): {
|
|
9
|
+
headWidth: number;
|
|
10
|
+
tailWidth: number;
|
|
11
|
+
} {
|
|
12
|
+
const headWidth = Math.max(1, Math.ceil((maxWidth - 1) / 2));
|
|
13
|
+
return {
|
|
14
|
+
headWidth,
|
|
15
|
+
tailWidth: Math.max(1, maxWidth - 1 - headWidth),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getTrailingText(value: string, tailWidth: number): string {
|
|
20
|
+
const reversedValue = Array.from(value).reverse().join("");
|
|
21
|
+
return truncateToWidth(reversedValue, tailWidth, "")
|
|
22
|
+
.split("")
|
|
23
|
+
.reverse()
|
|
24
|
+
.join("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function truncateMiddle(value: string, maxWidth: number): string {
|
|
28
|
+
if (maxWidth <= 0) return "";
|
|
29
|
+
if (visibleWidth(value) <= maxWidth) return value;
|
|
30
|
+
if (maxWidth <= 3) return truncateToWidth(value, maxWidth);
|
|
31
|
+
const { headWidth, tailWidth } = getMiddleWidths(maxWidth);
|
|
32
|
+
const head = truncateToWidth(value, headWidth, "");
|
|
33
|
+
const tail = getTrailingText(value, tailWidth);
|
|
34
|
+
return `${head}…${tail}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function shortNumber(value: number): string {
|
|
38
|
+
if (!Number.isFinite(value)) return "0";
|
|
39
|
+
if (Math.abs(value) < 1_000) return Math.round(value).toString();
|
|
40
|
+
if (Math.abs(value) < 1_000_000) return `${(value / 1_000).toFixed(1)}k`;
|
|
41
|
+
return `${(value / 1_000_000).toFixed(1)}m`;
|
|
42
|
+
}
|