@stemtrooper/learningcode 0.2.1 → 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 +69 -32
- package/package.json +1 -1
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
|
@@ -5,22 +5,25 @@ import { foregroundAnsi, rgbColor } from "@earendil-works/pi-tui";
|
|
|
5
5
|
* LEARNINGCODE startup banner.
|
|
6
6
|
*
|
|
7
7
|
* Pi lets an extension replace the whole header, which is the supported way to
|
|
8
|
-
* brand a fork without touching Pi's internals.
|
|
9
|
-
* logo plus key hints; we trade that for the TLC banner and a line of slash
|
|
10
|
-
* commands, so nothing is claimed about keybindings we cannot read.
|
|
8
|
+
* brand a fork without touching Pi's internals.
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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:
|
|
15
13
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
* Only the parts of Pi's Theme this file touches, to keep the runtime import
|
|
23
|
-
*
|
|
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.
|
|
24
27
|
*/
|
|
25
28
|
type ThemeLike = {
|
|
26
29
|
fg(token: string, text: string): string;
|
|
@@ -45,14 +48,20 @@ type HeaderComponent = { render(width: number): string[] };
|
|
|
45
48
|
*
|
|
46
49
|
* Note this must not go through `theme.fg()`. That resolves theme *tokens*, and
|
|
47
50
|
* `Theme.tokenAnsi` throws `Unknown theme color: #29c8f2` for anything that is
|
|
48
|
-
* not one, so a raw hex there crashes the header at render time.
|
|
49
|
-
* takes a colour value directly, and unlike a hand-rolled escape
|
|
50
|
-
* degrades to 256 colour on terminals without truecolour support.
|
|
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.
|
|
51
54
|
*/
|
|
52
55
|
const CYAN_DARK_BG = rgbColor(0x29, 0xc8, 0xf2);
|
|
53
56
|
const CYAN_LIGHT_BG = rgbColor(0x0d, 0xac, 0xd6);
|
|
54
57
|
const RESET = "\x1b[0m";
|
|
55
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";
|
|
63
|
+
|
|
64
|
+
/** figlet "ANSI Shadow", verbatim. 97 columns, 6 rows. */
|
|
56
65
|
const ART: readonly string[] = [
|
|
57
66
|
"██╗ ███████╗ █████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
58
67
|
"██║ ██╔════╝██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
@@ -62,26 +71,47 @@ const ART: readonly string[] = [
|
|
|
62
71
|
"╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
63
72
|
];
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
};
|
|
67
92
|
|
|
68
|
-
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
|
+
};
|
|
69
99
|
|
|
70
100
|
/** A copy, so a caller mutating the result cannot corrupt later renders. */
|
|
71
101
|
export const bannerArt = (): string[] => [...ART];
|
|
72
102
|
|
|
103
|
+
export const condensedArt_ = condensedArt;
|
|
104
|
+
|
|
73
105
|
export const bannerWidth = (): number => Math.max(...ART.map((line) => line.length));
|
|
106
|
+
export const condensedWidth = (): number => Math.max(...condensedArt().map((line) => line.length));
|
|
74
107
|
|
|
75
|
-
/**
|
|
76
|
-
* The art is 97 columns, which does not fit the classic 80 column terminal.
|
|
77
|
-
* Below the art plus its indent we show a wordmark instead, because clipped box
|
|
78
|
-
* characters look like a rendering bug rather than a design.
|
|
79
|
-
*/
|
|
108
|
+
/** Art plus its indent. Derived, never guessed. */
|
|
80
109
|
const MIN_FULL_WIDTH = bannerWidth() + PAD.length;
|
|
110
|
+
const MIN_CONDENSED_WIDTH = condensedWidth() + PAD.length;
|
|
81
111
|
|
|
82
112
|
export function createBanner(theme: ThemeLike): HeaderComponent {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
113
|
+
const full = bannerArt();
|
|
114
|
+
const small = condensedArt();
|
|
85
115
|
|
|
86
116
|
// Two different things, easy to swap by accident:
|
|
87
117
|
// appearance light or dark, decides which brand cyan is readable
|
|
@@ -95,16 +125,23 @@ export function createBanner(theme: ThemeLike): HeaderComponent {
|
|
|
95
125
|
|
|
96
126
|
return {
|
|
97
127
|
render(width: number): string[] {
|
|
98
|
-
if (width
|
|
99
|
-
|
|
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;
|
|
100
142
|
}
|
|
101
143
|
|
|
102
|
-
|
|
103
|
-
lines.push(theme.fg("border", PAD + "═".repeat(artWidth)));
|
|
104
|
-
lines.push(theme.fg("muted", PAD + TAGLINE));
|
|
105
|
-
lines.push(theme.fg("dim", PAD + HINTS));
|
|
106
|
-
lines.push("");
|
|
107
|
-
return lines;
|
|
144
|
+
return ["", ink(PAD + "learningcode"), theme.fg("muted", PAD + TAGLINE)];
|
|
108
145
|
},
|
|
109
146
|
};
|
|
110
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",
|