@stemtrooper/learningcode 0.2.0 → 0.3.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/README.md +16 -5
- package/extensions/banner.ts +91 -36
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -66,12 +66,23 @@ The LEARNINGCODE block banner replaces Pi's header at startup:
|
|
|
66
66
|
The figlet "ANSI Shadow" face, 97 columns wide, kept verbatim because the
|
|
67
67
|
double-line box characters only align if every row keeps its exact offset.
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
It steps down through three tiers, because a phone will never have 97 columns and
|
|
70
|
+
clipping box characters looks like a rendering bug rather than a design:
|
|
71
|
+
|
|
72
|
+
| Terminal width | Shows |
|
|
73
|
+
|---|---|
|
|
74
|
+
| 99 or more | the figlet face above |
|
|
75
|
+
| 51 to 98 | a condensed 4 row face, 47 columns |
|
|
76
|
+
| under 51 | the wordmark |
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
The condensed tier still spells LEARNINGCODE:
|
|
79
|
+
|
|
80
|
+
\\n █ ███ █ ███ █ █ ███ █ █ ███ ███ ███ ███ ███
|
|
81
|
+
█ █ █ █ █ █ ███ █ ███ █ █ █ █ █ █ █
|
|
82
|
+
█ ███ ███ ███ █ █ █ █ █ █ █ █ █ █ █ █ ███
|
|
83
|
+
███ ███ █ █ █ █ █ █ ███ █ █ ███ ███ ███ ███ ███
|
|
84
|
+
\\n
|
|
85
|
+
Colour comes from the active theme, so it stays legible in light and dark.
|
|
75
86
|
|
|
76
87
|
It installs via `ctx.ui.setHeader`, the supported way to brand a fork.
|
|
77
88
|
|
package/extensions/banner.ts
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { foregroundAnsi, rgbColor } from "@earendil-works/pi-tui";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* LEARNINGCODE startup banner.
|
|
5
6
|
*
|
|
6
7
|
* Pi lets an extension replace the whole header, which is the supported way to
|
|
7
|
-
* brand a fork without touching Pi's internals.
|
|
8
|
-
* logo plus key hints; we trade that for the TLC banner and a line of slash
|
|
9
|
-
* commands, so nothing is claimed about keybindings we cannot read.
|
|
8
|
+
* brand a fork without touching Pi's internals.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Three tiers, because the full face does not fit everywhere. A phone will never
|
|
11
|
+
* have 97 columns, and clipping box characters looks like a rendering bug rather
|
|
12
|
+
* than a design, so the banner steps down instead:
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* >= 99 columns figlet "ANSI Shadow", 97 wide, 6 rows
|
|
15
|
+
* >= 51 columns condensed 4 row face, 47 wide
|
|
16
|
+
* below that the wordmark
|
|
17
|
+
*
|
|
18
|
+
* The full art is stored verbatim rather than rebuilt from a glyph map. That face
|
|
19
|
+
* only aligns because every row keeps its exact offset, the top row flush left
|
|
20
|
+
* and the rest flush too; a one-space drift makes the whole banner look broken.
|
|
21
|
+
* A test asserts those offsets.
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Only the parts of Pi's Theme this file touches, to keep the runtime import down
|
|
26
|
+
* to the two colour helpers actually needed.
|
|
27
|
+
*/
|
|
21
28
|
type ThemeLike = {
|
|
22
29
|
fg(token: string, text: string): string;
|
|
23
|
-
|
|
30
|
+
/** "light" | "dark" - the background the active theme is designed for. */
|
|
31
|
+
appearance?: "light" | "dark";
|
|
32
|
+
/** The terminal's colour capability, e.g. "truecolor". Not light or dark. */
|
|
33
|
+
getColorMode?(): string;
|
|
24
34
|
};
|
|
25
35
|
type HeaderComponent = { render(width: number): string[] };
|
|
26
36
|
|
|
@@ -35,10 +45,23 @@ type HeaderComponent = { render(width: number): string[] };
|
|
|
35
45
|
*
|
|
36
46
|
* Two values because the logo ships two: the darker cyan holds contrast on a
|
|
37
47
|
* white field, the brighter one on black.
|
|
48
|
+
*
|
|
49
|
+
* Note this must not go through `theme.fg()`. That resolves theme *tokens*, and
|
|
50
|
+
* `Theme.tokenAnsi` throws `Unknown theme color: #29c8f2` for anything that is
|
|
51
|
+
* not one, so a raw hex there crashes the header at render time.
|
|
52
|
+
* `foregroundAnsi` takes a colour value directly, and unlike a hand-rolled escape
|
|
53
|
+
* it still degrades to 256 colour on terminals without truecolour support.
|
|
38
54
|
*/
|
|
39
|
-
const CYAN_DARK_BG =
|
|
40
|
-
const CYAN_LIGHT_BG =
|
|
55
|
+
const CYAN_DARK_BG = rgbColor(0x29, 0xc8, 0xf2);
|
|
56
|
+
const CYAN_LIGHT_BG = rgbColor(0x0d, 0xac, 0xd6);
|
|
57
|
+
const RESET = "\x1b[0m";
|
|
58
|
+
|
|
59
|
+
const WORD = "LEARNINGCODE";
|
|
60
|
+
const PAD = " ";
|
|
61
|
+
const TAGLINE = "The Learning Curve · Sarawak";
|
|
62
|
+
const HINTS = "/help commands · /quota today's spend · /hotkeys keys";
|
|
41
63
|
|
|
64
|
+
/** figlet "ANSI Shadow", verbatim. 97 columns, 6 rows. */
|
|
42
65
|
const ART: readonly string[] = [
|
|
43
66
|
"██╗ ███████╗ █████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
44
67
|
"██║ ██╔════╝██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
@@ -48,45 +71,77 @@ const ART: readonly string[] = [
|
|
|
48
71
|
"╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
49
72
|
];
|
|
50
73
|
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Condensed fallback for narrow terminals. Three columns per glyph, which is
|
|
76
|
+
* the most a twelve letter word can be compressed before the letterforms stop
|
|
77
|
+
* reading: an N needs four columns to show its diagonal, and dropping it to
|
|
78
|
+
* three turns the letter into a filled block.
|
|
79
|
+
*/
|
|
80
|
+
const CONDENSED_GLYPHS: Record<string, readonly string[]> = {
|
|
81
|
+
L: ["█ ", "█ ", "█ ", "███"],
|
|
82
|
+
E: ["███", "█ ", "███", "███"],
|
|
83
|
+
A: [" █ ", "█ █", "███", "█ █"],
|
|
84
|
+
R: ["███", "█ █", "███", "█ █"],
|
|
85
|
+
N: ["█ █", "███", "█ █", "█ █"],
|
|
86
|
+
I: ["███", " █ ", " █ ", "███"],
|
|
87
|
+
G: ["███", "█ ", "█ █", "███"],
|
|
88
|
+
C: ["███", "█ ", "█ ", "███"],
|
|
89
|
+
O: ["███", "█ █", "█ █", "███"],
|
|
90
|
+
D: ["███", "█ █", "█ █", "███"],
|
|
91
|
+
};
|
|
53
92
|
|
|
54
|
-
const
|
|
93
|
+
const condensedArt = (): string[] => {
|
|
94
|
+
const rows = CONDENSED_GLYPHS[WORD[0]].length;
|
|
95
|
+
return Array.from({ length: rows }, (_, row) =>
|
|
96
|
+
[...WORD].map((letter) => CONDENSED_GLYPHS[letter][row]).join(" ").trimEnd(),
|
|
97
|
+
);
|
|
98
|
+
};
|
|
55
99
|
|
|
56
100
|
/** A copy, so a caller mutating the result cannot corrupt later renders. */
|
|
57
101
|
export const bannerArt = (): string[] => [...ART];
|
|
58
102
|
|
|
103
|
+
export const condensedArt_ = condensedArt;
|
|
104
|
+
|
|
59
105
|
export const bannerWidth = (): number => Math.max(...ART.map((line) => line.length));
|
|
106
|
+
export const condensedWidth = (): number => Math.max(...condensedArt().map((line) => line.length));
|
|
60
107
|
|
|
61
|
-
/**
|
|
62
|
-
* The art is 97 columns, which does not fit the classic 80 column terminal.
|
|
63
|
-
* Below the art plus its indent we show a wordmark instead, because clipped box
|
|
64
|
-
* characters look like a rendering bug rather than a design.
|
|
65
|
-
*/
|
|
108
|
+
/** Art plus its indent. Derived, never guessed. */
|
|
66
109
|
const MIN_FULL_WIDTH = bannerWidth() + PAD.length;
|
|
110
|
+
const MIN_CONDENSED_WIDTH = condensedWidth() + PAD.length;
|
|
67
111
|
|
|
68
112
|
export function createBanner(theme: ThemeLike): HeaderComponent {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
113
|
+
const full = bannerArt();
|
|
114
|
+
const small = condensedArt();
|
|
71
115
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
// Two different things, easy to swap by accident:
|
|
117
|
+
// appearance light or dark, decides which brand cyan is readable
|
|
118
|
+
// getColorMode the terminal's colour capability, decides how to emit it
|
|
119
|
+
// Default to dark so the banner keeps its colour on a host that reports
|
|
120
|
+
// neither, rather than dropping the brand.
|
|
121
|
+
const isLight = theme.appearance === "light";
|
|
122
|
+
const mode = typeof theme.getColorMode === "function" ? theme.getColorMode() : "truecolor";
|
|
123
|
+
const brand = foregroundAnsi(isLight ? CYAN_LIGHT_BG : CYAN_DARK_BG, mode as never);
|
|
124
|
+
const ink = (text: string) => `${brand}${text}${RESET}`;
|
|
77
125
|
|
|
78
126
|
return {
|
|
79
127
|
render(width: number): string[] {
|
|
80
|
-
if (width
|
|
81
|
-
|
|
128
|
+
if (width >= MIN_FULL_WIDTH) {
|
|
129
|
+
const lines = ["", ...full.map((line) => ink(PAD + line))];
|
|
130
|
+
lines.push(theme.fg("border", PAD + "═".repeat(bannerWidth())));
|
|
131
|
+
lines.push(theme.fg("muted", PAD + TAGLINE));
|
|
132
|
+
lines.push(theme.fg("dim", PAD + HINTS));
|
|
133
|
+
lines.push("");
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (width >= MIN_CONDENSED_WIDTH) {
|
|
138
|
+
const lines = ["", ...small.map((line) => ink(PAD + line))];
|
|
139
|
+
lines.push(theme.fg("muted", PAD + TAGLINE));
|
|
140
|
+
lines.push("");
|
|
141
|
+
return lines;
|
|
82
142
|
}
|
|
83
143
|
|
|
84
|
-
|
|
85
|
-
lines.push(theme.fg("border", PAD + "═".repeat(artWidth)));
|
|
86
|
-
lines.push(theme.fg("muted", PAD + TAGLINE));
|
|
87
|
-
lines.push(theme.fg("dim", PAD + HINTS));
|
|
88
|
-
lines.push("");
|
|
89
|
-
return lines;
|
|
144
|
+
return ["", ink(PAD + "learningcode"), theme.fg("muted", PAD + TAGLINE)];
|
|
90
145
|
},
|
|
91
146
|
};
|
|
92
147
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stemtrooper/learningcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "TLC Spark coding agent for students: Pi wired to the Spark OpenAI-compatible endpoint with per-student tokens, quota and seat-queue awareness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "node ./bin/learningcode.mjs",
|
|
20
|
-
"test": "node --test \"test/**/*.test.mjs\""
|
|
20
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
21
|
+
"preview": "node ./scripts/preview.mjs"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@earendil-works/pi-coding-agent": "1.0.4"
|
|
24
|
+
"@earendil-works/pi-coding-agent": "1.0.4",
|
|
25
|
+
"@earendil-works/pi-tui": "1.0.4"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
28
|
"jiti": "2.7.0"
|