lazy-skill 0.4.0 → 0.6.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/dist/commands/connect.js +14 -3
- package/dist/commands/theme.js +34 -40
- package/dist/index.js +3 -3
- package/dist/ui/banner.js +74 -0
- package/dist/ui/onboarding.js +39 -42
- package/dist/ui/select.js +86 -0
- package/dist/ui/sprite-data.js +273 -0
- package/dist/ui/sprite.js +74 -0
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -5,9 +5,11 @@ import { detectAgents } from "../adapters/index.js";
|
|
|
5
5
|
import { THEMES, style } from "../ui/theme.js";
|
|
6
6
|
import { boxWidth, visibleWidth } from "../ui/layout.js";
|
|
7
7
|
import { renderQr } from "../ui/qr.js";
|
|
8
|
-
import { blank, box, centre, fail, hint, line, muted, ok, status
|
|
8
|
+
import { blank, box, centre, fail, hint, line, muted, ok, status } from "../ui/layout.js";
|
|
9
|
+
import { banner, mascot } from "../ui/banner.js";
|
|
9
10
|
import { messages } from "../ui/messages.js";
|
|
10
11
|
import { Spinner } from "../ui/spinner.js";
|
|
12
|
+
const VERSION = "0.6.0";
|
|
11
13
|
function friendlyPlatform() {
|
|
12
14
|
const p = platform();
|
|
13
15
|
return p === "darwin" || p === "win32" || p === "linux" ? p : "unknown";
|
|
@@ -24,7 +26,8 @@ export async function connectCommand(options) {
|
|
|
24
26
|
const config = readConfig();
|
|
25
27
|
const theme = THEMES[config.theme];
|
|
26
28
|
blank();
|
|
27
|
-
|
|
29
|
+
for (const row of banner(theme, VERSION))
|
|
30
|
+
console.log(row);
|
|
28
31
|
blank();
|
|
29
32
|
hint("See it. Search it. Install it.");
|
|
30
33
|
blank();
|
|
@@ -64,10 +67,18 @@ export async function connectCommand(options) {
|
|
|
64
67
|
hint("Open Lazy Skill on your phone and scan this.");
|
|
65
68
|
blank();
|
|
66
69
|
// The QR is the one thing to act on, so it gets the box. Its own quiet zone
|
|
67
|
-
// sits inside the border, untouched.
|
|
70
|
+
// sits inside the border, untouched — nothing is drawn over the modules.
|
|
68
71
|
const plate = await renderQr(session.pairUrl);
|
|
69
72
|
const width = plate.length ? visibleWidth(plate[0]) + 4 : boxWidth();
|
|
70
73
|
box(theme, plate.map((row) => centre(row, width)), width);
|
|
74
|
+
// Bit sits under the code rather than over it: anything overlapping the
|
|
75
|
+
// module area would cost scan reliability for decoration.
|
|
76
|
+
blank();
|
|
77
|
+
const bit = mascot(theme, false);
|
|
78
|
+
for (let i = 0; i < bit.length; i++) {
|
|
79
|
+
const caption = i === 2 ? ` ${style.gray("waiting for your phone")}` : "";
|
|
80
|
+
console.log(` ${centre(bit[i], width - 18)}${caption}`);
|
|
81
|
+
}
|
|
71
82
|
blank();
|
|
72
83
|
muted(session.pairUrl);
|
|
73
84
|
blank();
|
package/dist/commands/theme.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
1
|
import { readConfig, updateConfig } from "../lib/config.js";
|
|
3
2
|
import { THEMES, THEME_IDS, rgb, style } from "../ui/theme.js";
|
|
4
|
-
import {
|
|
3
|
+
import { banner, mascot } from "../ui/banner.js";
|
|
4
|
+
import { pointer, select } from "../ui/select.js";
|
|
5
|
+
import { blank, muted, ok } from "../ui/layout.js";
|
|
5
6
|
/**
|
|
6
7
|
* Theme picker. The choice is stored locally and travels with the next
|
|
7
8
|
* pairing, so the phone ends up on the same colour.
|
|
8
9
|
*/
|
|
9
|
-
export async function themeCommand(requested) {
|
|
10
|
+
export async function themeCommand(requested, version) {
|
|
10
11
|
const current = readConfig().theme;
|
|
11
|
-
const theme = THEMES[current];
|
|
12
12
|
if (requested) {
|
|
13
13
|
const match = THEME_IDS.find((id) => id === requested);
|
|
14
14
|
if (!match) {
|
|
@@ -25,41 +25,35 @@ export async function themeCommand(requested) {
|
|
|
25
25
|
blank();
|
|
26
26
|
return 0;
|
|
27
27
|
}
|
|
28
|
+
const chosen = await select({
|
|
29
|
+
choices: THEME_IDS.map((id) => ({ value: id, label: THEMES[id].label })),
|
|
30
|
+
initial: THEME_IDS.indexOf(current),
|
|
31
|
+
render: (index) => {
|
|
32
|
+
const theme = THEMES[THEME_IDS[index]];
|
|
33
|
+
const bit = mascot(theme, true);
|
|
34
|
+
const out = [""];
|
|
35
|
+
out.push(...banner(theme, version));
|
|
36
|
+
out.push("");
|
|
37
|
+
THEME_IDS.forEach((themeId, i) => {
|
|
38
|
+
const t = THEMES[themeId];
|
|
39
|
+
const active = i === index;
|
|
40
|
+
const mark = themeId === current ? style.green("●") : style.gray("○");
|
|
41
|
+
// Pad before colouring: padEnd counts escape codes as characters, so
|
|
42
|
+
// padding a coloured string leaves the column ragged.
|
|
43
|
+
const padded = t.label.padEnd(14);
|
|
44
|
+
const label = active ? style.bold(padded) : style.gray(padded);
|
|
45
|
+
const beside = i >= 1 && i <= 6 ? " " + bit[i - 1] : "";
|
|
46
|
+
out.push(` ${pointer(active)} ${mark} ${rgb(t.accent, "████")} ${label}${beside}`);
|
|
47
|
+
});
|
|
48
|
+
out.push("");
|
|
49
|
+
out.push(` ${style.gray("↑ ↓ to move · enter to choose")}`);
|
|
50
|
+
out.push("");
|
|
51
|
+
return out;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
updateConfig({ theme: chosen });
|
|
55
|
+
ok(`Theme set to ${rgb(THEMES[chosen].accent, THEMES[chosen].label)}.`);
|
|
56
|
+
muted("Connect again to carry it to your phone.");
|
|
28
57
|
blank();
|
|
29
|
-
|
|
30
|
-
blank();
|
|
31
|
-
box(theme, THEME_IDS.map((id, i) => {
|
|
32
|
-
const t = THEMES[id];
|
|
33
|
-
const marker = id === current ? style.green("●") : style.gray("○");
|
|
34
|
-
return `${marker} ${style.gray(String(i + 1))} ${rgb(t.accent, "████")} ${t.label}`;
|
|
35
|
-
}));
|
|
36
|
-
blank();
|
|
37
|
-
if (!process.stdin.isTTY) {
|
|
38
|
-
muted("Not a terminal — pass an id, e.g. lazy-skill theme matrix-green");
|
|
39
|
-
blank();
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
43
|
-
try {
|
|
44
|
-
const answer = (await rl.question(` Number ${style.gray("(enter to keep)")} `)).trim();
|
|
45
|
-
if (!answer)
|
|
46
|
-
return 0;
|
|
47
|
-
const index = Number(answer) - 1;
|
|
48
|
-
if (!Number.isInteger(index) || index < 0 || index >= THEME_IDS.length) {
|
|
49
|
-
blank();
|
|
50
|
-
muted("Not one of those. Theme unchanged.");
|
|
51
|
-
blank();
|
|
52
|
-
return 1;
|
|
53
|
-
}
|
|
54
|
-
const chosen = THEME_IDS[index];
|
|
55
|
-
updateConfig({ theme: chosen });
|
|
56
|
-
blank();
|
|
57
|
-
ok(`Theme set to ${rgb(THEMES[chosen].accent, THEMES[chosen].label)}.`);
|
|
58
|
-
muted("Connect again to carry it to your phone.");
|
|
59
|
-
blank();
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
finally {
|
|
63
|
-
rl.close();
|
|
64
|
-
}
|
|
58
|
+
return 0;
|
|
65
59
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { installCommand } from "./commands/install.js";
|
|
|
10
10
|
import { themeCommand } from "./commands/theme.js";
|
|
11
11
|
import { listenCommand } from "./commands/listen.js";
|
|
12
12
|
import { warnIfOutdated } from "./lib/version-check.js";
|
|
13
|
-
const VERSION = "0.
|
|
13
|
+
const VERSION = "0.6.0";
|
|
14
14
|
function help() {
|
|
15
15
|
const theme = THEMES[readConfig().theme];
|
|
16
16
|
const cmd = (name) => rgb(theme.accent, name.padEnd(26));
|
|
@@ -66,7 +66,7 @@ async function main() {
|
|
|
66
66
|
// when it pairs a moment later. Only on a genuinely first run, and never
|
|
67
67
|
// for `theme`, which asks on its own.
|
|
68
68
|
if (isFirstRun() && command !== "theme") {
|
|
69
|
-
updateConfig({ theme: await chooseTheme() });
|
|
69
|
+
updateConfig({ theme: await chooseTheme(VERSION) });
|
|
70
70
|
}
|
|
71
71
|
if (longRunning)
|
|
72
72
|
await warnIfOutdated(VERSION, THEMES[readConfig().theme]);
|
|
@@ -91,7 +91,7 @@ async function main() {
|
|
|
91
91
|
case "skills":
|
|
92
92
|
return skillsCommand();
|
|
93
93
|
case "theme":
|
|
94
|
-
return themeCommand(positional[1]);
|
|
94
|
+
return themeCommand(positional[1], VERSION);
|
|
95
95
|
case "install": {
|
|
96
96
|
const agentFlag = argv.find((a) => a.startsWith("--agent="))?.split("=")[1];
|
|
97
97
|
const yes = flags.has("-y") || flags.has("--yes");
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { rgb, style } from "./theme.js";
|
|
2
|
+
/**
|
|
3
|
+
* The wordmark, drawn as blocks.
|
|
4
|
+
*
|
|
5
|
+
* Only the seven letters in LAZY SKILL are defined — a full alphabet would be
|
|
6
|
+
* dead weight for a name that never changes. Five rows tall, which reads as a
|
|
7
|
+
* logo without eating half the terminal.
|
|
8
|
+
*/
|
|
9
|
+
const GLYPHS = {
|
|
10
|
+
L: ["█ ", "█ ", "█ ", "█ ", "████"],
|
|
11
|
+
A: [" ██ ", "█ █", "████", "█ █", "█ █"],
|
|
12
|
+
Z: ["████", " █", " ██ ", "█ ", "████"],
|
|
13
|
+
Y: ["█ █", "█ █", " ██ ", " █ ", " █ "],
|
|
14
|
+
S: [" ███", "█ ", " ██ ", " █", "███ "],
|
|
15
|
+
K: ["█ █", "█ █ ", "██ ", "█ █ ", "█ █"],
|
|
16
|
+
I: ["███", " █ ", " █ ", " █ ", "███"],
|
|
17
|
+
" ": [" ", " ", " ", " ", " "],
|
|
18
|
+
};
|
|
19
|
+
const ROWS = 5;
|
|
20
|
+
function render(word) {
|
|
21
|
+
const lines = Array.from({ length: ROWS }, () => "");
|
|
22
|
+
for (const char of word.toUpperCase()) {
|
|
23
|
+
const glyph = GLYPHS[char];
|
|
24
|
+
if (!glyph)
|
|
25
|
+
continue;
|
|
26
|
+
for (let r = 0; r < ROWS; r++)
|
|
27
|
+
lines[r] += glyph[r] + " ";
|
|
28
|
+
}
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `LAZY` in the terminal's own ink and `SKILL` in the theme accent, matching
|
|
33
|
+
* the two-tone split the web app uses.
|
|
34
|
+
*/
|
|
35
|
+
export function banner(theme, version) {
|
|
36
|
+
const left = render("LAZY");
|
|
37
|
+
const right = render("SKILL");
|
|
38
|
+
const lines = left.map((row, i) => ` ${style.bold(row)} ${rgb(theme.accent, style.bold(right[i]))}`);
|
|
39
|
+
// The version sits on the last row, trailing the mark like a signature.
|
|
40
|
+
lines[ROWS - 1] += ` ${style.gray("v" + version)}`;
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Bit, hand-drawn.
|
|
45
|
+
*
|
|
46
|
+
* The generated 32x32 sprite is right for the phone, where there is
|
|
47
|
+
* resolution to spare. In a terminal it turned to mush: ellipses at that
|
|
48
|
+
* scale produce a cream blob with dark smears, and no amount of nudging the
|
|
49
|
+
* shapes fixed it. This is drawn character by character instead — a hood, a
|
|
50
|
+
* face, two closed eyes, a nose and a smile — which is far fewer pixels but
|
|
51
|
+
* reads instantly, which is the only thing that matters here.
|
|
52
|
+
*
|
|
53
|
+
* Every row is exactly 13 cells wide. Ragged rows shear the silhouette, and
|
|
54
|
+
* the colour codes make that impossible to spot by eye.
|
|
55
|
+
*/
|
|
56
|
+
const FACE = [227, 201, 164];
|
|
57
|
+
const FEATURE = [70, 48, 35];
|
|
58
|
+
export function mascot(theme, awake = false) {
|
|
59
|
+
const hood = (t) => rgb(theme.accent, t);
|
|
60
|
+
const face = (t) => rgb(FACE, t);
|
|
61
|
+
const mark = (t) => rgb(FEATURE, t);
|
|
62
|
+
const zzz = (t) => rgb(theme.support, t);
|
|
63
|
+
// Open eyes are dots; closed eyes are the lower half of a block, which
|
|
64
|
+
// reads as a lid rather than a gap.
|
|
65
|
+
const eyes = awake ? mark("●●") : mark("▄▄");
|
|
66
|
+
return [
|
|
67
|
+
hood(" ▄▄▄▄▄▄▄ "),
|
|
68
|
+
hood(" █") + face("███████") + hood("█ "),
|
|
69
|
+
hood(" █") + face("█") + eyes + face("█") + eyes + face("█") + hood("█ ") + (awake ? "" : zzz("z")),
|
|
70
|
+
hood(" █") + face("███") + mark("▾") + face("███") + hood("█ ") + (awake ? "" : zzz("z")),
|
|
71
|
+
hood(" █") + face("██") + mark("‿‿‿") + face("██") + hood("█ "),
|
|
72
|
+
hood(" ▀▀▀▀▀▀▀ "),
|
|
73
|
+
];
|
|
74
|
+
}
|
package/dist/ui/onboarding.js
CHANGED
|
@@ -1,47 +1,44 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
1
|
import { THEMES, THEME_IDS, rgb, style } from "./theme.js";
|
|
3
|
-
import {
|
|
2
|
+
import { banner, mascot } from "./banner.js";
|
|
3
|
+
import { pointer, select } from "./select.js";
|
|
4
4
|
/**
|
|
5
|
-
* First-run theme choice.
|
|
5
|
+
* First-run theme choice, with live preview.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The whole screen — wordmark, mascot, pointer — repaints in whichever theme
|
|
8
|
+
* is under the cursor, so the choice is made by looking rather than by
|
|
9
|
+
* imagining. It is asked once because it applies to both halves of the
|
|
10
|
+
* product: the terminal colours itself, and the phone adopts the same theme
|
|
11
|
+
* the moment it pairs.
|
|
11
12
|
*/
|
|
12
|
-
export async function chooseTheme() {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
finally {
|
|
45
|
-
rl.close();
|
|
46
|
-
}
|
|
13
|
+
export async function chooseTheme(version) {
|
|
14
|
+
const rows = (index) => {
|
|
15
|
+
const id = THEME_IDS[index];
|
|
16
|
+
const theme = THEMES[id];
|
|
17
|
+
const bit = mascot(theme, true);
|
|
18
|
+
const out = [""];
|
|
19
|
+
out.push(...banner(theme, version));
|
|
20
|
+
out.push("");
|
|
21
|
+
out.push(` ${style.gray("Pick a colour. Your phone will match it when you connect.")}`);
|
|
22
|
+
out.push("");
|
|
23
|
+
THEME_IDS.forEach((themeId, i) => {
|
|
24
|
+
const t = THEMES[themeId];
|
|
25
|
+
const active = i === index;
|
|
26
|
+
const swatch = rgb(t.accent, "████");
|
|
27
|
+
// Pad before colouring: padEnd counts escape codes as characters, so
|
|
28
|
+
// padding a coloured string leaves the column ragged.
|
|
29
|
+
const padded = t.label.padEnd(14);
|
|
30
|
+
const label = active ? style.bold(padded) : style.gray(padded);
|
|
31
|
+
// The mascot sits beside the list, aligned to its middle rows.
|
|
32
|
+
const beside = i >= 1 && i <= 6 ? " " + bit[i - 1] : "";
|
|
33
|
+
out.push(` ${pointer(active)} ${swatch} ${label}${beside}`);
|
|
34
|
+
});
|
|
35
|
+
out.push("");
|
|
36
|
+
out.push(` ${style.gray("↑ ↓ to move · enter to choose")}`);
|
|
37
|
+
out.push("");
|
|
38
|
+
return out;
|
|
39
|
+
};
|
|
40
|
+
return select({
|
|
41
|
+
choices: THEME_IDS.map((id) => ({ value: id, label: THEMES[id].label })),
|
|
42
|
+
render: rows,
|
|
43
|
+
});
|
|
47
44
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { emitKeypressEvents } from "node:readline";
|
|
2
|
+
import { style, hasColor } from "./theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Arrow-key selection with live redraw.
|
|
5
|
+
*
|
|
6
|
+
* Raw mode rather than a prompt library: the whole dependency would exist to
|
|
7
|
+
* read four escape sequences. Cursor state is restored in a `finally` and on
|
|
8
|
+
* SIGINT — leaving a terminal with a hidden cursor and no echo is a genuinely
|
|
9
|
+
* hostile thing to do to someone who pressed Ctrl-C.
|
|
10
|
+
*/
|
|
11
|
+
export async function select({ choices, initial = 0, render, }) {
|
|
12
|
+
const stdin = process.stdin;
|
|
13
|
+
// No TTY means nobody to ask; a pipe or CI takes the default.
|
|
14
|
+
if (!stdin.isTTY || !hasColor) {
|
|
15
|
+
for (const row of render(initial))
|
|
16
|
+
console.log(row);
|
|
17
|
+
return choices[initial].value;
|
|
18
|
+
}
|
|
19
|
+
let index = Math.max(0, Math.min(initial, choices.length - 1));
|
|
20
|
+
let painted = 0;
|
|
21
|
+
const paint = () => {
|
|
22
|
+
// Move back over what was drawn last time and overwrite it, so the list
|
|
23
|
+
// updates in place instead of scrolling.
|
|
24
|
+
if (painted > 0)
|
|
25
|
+
process.stdout.write(`\x1b[${painted}A`);
|
|
26
|
+
const rows = render(index);
|
|
27
|
+
for (const row of rows)
|
|
28
|
+
process.stdout.write(`\r\x1b[K${row}\n`);
|
|
29
|
+
painted = rows.length;
|
|
30
|
+
};
|
|
31
|
+
emitKeypressEvents(stdin);
|
|
32
|
+
const wasRaw = stdin.isRaw;
|
|
33
|
+
stdin.setRawMode(true);
|
|
34
|
+
stdin.resume();
|
|
35
|
+
process.stdout.write("\x1b[?25l"); // hide cursor
|
|
36
|
+
const restore = () => {
|
|
37
|
+
process.stdout.write("\x1b[?25h");
|
|
38
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
39
|
+
stdin.pause();
|
|
40
|
+
};
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const onKey = (_chunk, key) => {
|
|
43
|
+
if (!key)
|
|
44
|
+
return;
|
|
45
|
+
if (key.name === "up" || key.name === "k") {
|
|
46
|
+
index = (index - 1 + choices.length) % choices.length;
|
|
47
|
+
paint();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.name === "down" || key.name === "j") {
|
|
51
|
+
index = (index + 1) % choices.length;
|
|
52
|
+
paint();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (key.name === "return" || key.name === "space") {
|
|
56
|
+
cleanup();
|
|
57
|
+
resolve(choices[index].value);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Ctrl-C and Escape both accept the highlighted value rather than
|
|
61
|
+
// throwing: this choice is cosmetic, and refusing to continue over it
|
|
62
|
+
// would be worse than picking the one under the cursor.
|
|
63
|
+
if ((key.ctrl && key.name === "c") || key.name === "escape") {
|
|
64
|
+
cleanup();
|
|
65
|
+
resolve(choices[index].value);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Number keys jump straight to a row.
|
|
69
|
+
const digit = Number(key.name);
|
|
70
|
+
if (Number.isInteger(digit) && digit >= 1 && digit <= choices.length) {
|
|
71
|
+
index = digit - 1;
|
|
72
|
+
paint();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const cleanup = () => {
|
|
76
|
+
stdin.off("keypress", onKey);
|
|
77
|
+
restore();
|
|
78
|
+
};
|
|
79
|
+
stdin.on("keypress", onKey);
|
|
80
|
+
paint();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** The pointer column for a row. */
|
|
84
|
+
export function pointer(active) {
|
|
85
|
+
return active ? style.bold("❯") : " ";
|
|
86
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// GENERATED by scripts/build-sprite.mjs — do not edit by hand.
|
|
2
|
+
// Run `pnpm sprite` after changing scripts/gen-sloth.mjs.
|
|
3
|
+
/**
|
|
4
|
+
* BIT — the Lazy Skill mascot. A sleepy sloth in a hoodie.
|
|
5
|
+
*
|
|
6
|
+
* Stored as a 32x32 character grid rather than SVG paths so one source
|
|
7
|
+
* scales crisply to any size, retints with the active theme, and swaps
|
|
8
|
+
* expression without a second asset.
|
|
9
|
+
*
|
|
10
|
+
* Legend: . transparent | o outline | H hood | h hood shadow | L hood light
|
|
11
|
+
* F fur | M muzzle | P eye patch | e eye | n nose | w eye white
|
|
12
|
+
*/
|
|
13
|
+
export const SPRITE_SIZE = 32;
|
|
14
|
+
/** Palette keys resolve against CSS custom properties, so themes apply free. */
|
|
15
|
+
/** Terminal colours are literal RGB — no CSS variables to resolve. The hood
|
|
16
|
+
* is substituted per theme at render time; everything else is fixed. */
|
|
17
|
+
export const PX = {
|
|
18
|
+
".": null,
|
|
19
|
+
o: "#120d1c",
|
|
20
|
+
H: "var(--ls-accent)",
|
|
21
|
+
h: "var(--ls-accent-lo)",
|
|
22
|
+
L: "var(--ls-accent-hi)",
|
|
23
|
+
F: "#e3c9a4",
|
|
24
|
+
M: "#f6e6cd",
|
|
25
|
+
P: "#5b4130",
|
|
26
|
+
e: "#2c1e16",
|
|
27
|
+
n: "#3d2b20",
|
|
28
|
+
w: "#fdf6ea",
|
|
29
|
+
};
|
|
30
|
+
export const EXPRESSIONS = ["idle", "curious", "waiting", "happy", "working", "excited", "annoyed"];
|
|
31
|
+
export const SPRITES = {
|
|
32
|
+
idle: [
|
|
33
|
+
"................................",
|
|
34
|
+
"................................",
|
|
35
|
+
"................o...............",
|
|
36
|
+
"............ooooHoooo...........",
|
|
37
|
+
"..........ooLLLLLLhhhoo.........",
|
|
38
|
+
"........ooLLLLLLLhhhhhhoo.......",
|
|
39
|
+
".......oLLLLLLLLhhhhhhhhho......",
|
|
40
|
+
"......oLLLLLLLLhhhhhhhhhhho.....",
|
|
41
|
+
"......oHLLLLLLFFFFFhhhhhhho.....",
|
|
42
|
+
".....oHHHHHFFFFFFFFFFFhhhhho....",
|
|
43
|
+
".....oHHHHFFFFFFFFFFFFFhhhho....",
|
|
44
|
+
".....oHHHFFFFFFFFFFFFFFFhhho....",
|
|
45
|
+
".....oHHFFFFFFFFFFFFFFFFFhho....",
|
|
46
|
+
"....oHHHFFPPPPFFFFFPPPPFFhhho...",
|
|
47
|
+
"....oHHHFPwwwwFFFFFwwwwPFhho....",
|
|
48
|
+
"....oHHFFPeeeeFFFFFeeeePFFho....",
|
|
49
|
+
"....oHHFFPPPPPFFFFFPPPPPFFho....",
|
|
50
|
+
"....oHFFPPPFFFFFFFFFFFPPPFFo....",
|
|
51
|
+
"....oHFFFPPFFFnnnnMFFFPPFFFo....",
|
|
52
|
+
"....oHHHFFFFMMnnnnMMMFFFFhho....",
|
|
53
|
+
"....oHFFFFFFMMMnnMMMMFFFFFFo....",
|
|
54
|
+
"....oHFFFFFFMnMMMMnMMFFFFFFo....",
|
|
55
|
+
"....oHHHHHFFMMnnnnMMMFFhhhho....",
|
|
56
|
+
"....oHHHHHHFFFMMMMMFFFhhhhho....",
|
|
57
|
+
"....oHHHHHHHHHFFFFFhhhhhhhho....",
|
|
58
|
+
"....ooHHHHHHHHHHHhhhhhhhhhoo....",
|
|
59
|
+
"......oHHHHHHHHHHHhhhhhhho......",
|
|
60
|
+
"....ooHHHHHHHHHHHHHHHHhHHHoo....",
|
|
61
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
62
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
63
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
64
|
+
"...oooooooooooooooooooooooooo...",
|
|
65
|
+
],
|
|
66
|
+
curious: [
|
|
67
|
+
"................................",
|
|
68
|
+
"................................",
|
|
69
|
+
"................o...............",
|
|
70
|
+
"............ooooHoooo...........",
|
|
71
|
+
"..........ooLLLLLLhhhoo.........",
|
|
72
|
+
"........ooLLLLLLLhhhhhhoo.......",
|
|
73
|
+
".......oLLLLLLLLhhhhhhhhho......",
|
|
74
|
+
"......oLLLLLLLLhhhhhhhhhhho.....",
|
|
75
|
+
"......oHLLLLLLFFFFFhhhhhhho.....",
|
|
76
|
+
".....oHHHHHFFFFFFFFFFFhhhhho....",
|
|
77
|
+
".....oHHHHFFFFFFFFFFFFFhhhho....",
|
|
78
|
+
".....oHHHFFFFFFFFFFFFFFFhhho....",
|
|
79
|
+
".....oHHFFFFFFFFFFFFFFFFFhho....",
|
|
80
|
+
"....oHHHFFPwwPFFFFFPwwPFFhhho...",
|
|
81
|
+
"....oHHHFPwwewFFFFFwwewPFhho....",
|
|
82
|
+
"....oHHFFPweewFFFFFweewPFFho....",
|
|
83
|
+
"....oHHFFPPwwPFFFFFPwwPPFFho....",
|
|
84
|
+
"....oHFFPPPFFFFFFFFFFFPPPFFo....",
|
|
85
|
+
"....oHFFFPPFFFnnnnMFFFPPFFFo....",
|
|
86
|
+
"....oHHHFFFFMMnnnnMMMFFFFhho....",
|
|
87
|
+
"....oHFFFFFFMMMnnMMMMFFFFFFo....",
|
|
88
|
+
"....oHFFFFFFMnMMMMnMMFFFFFFo....",
|
|
89
|
+
"....oHHHHHFFMMnnnnMMMFFhhhho....",
|
|
90
|
+
"....oHHHHHHFFFMMMMMFFFhhhhho....",
|
|
91
|
+
"....oHHHHHHHHHFFFFFhhhhhhhho....",
|
|
92
|
+
"....ooHHHHHHHHHHHhhhhhhhhhoo....",
|
|
93
|
+
"......oHHHHHHHHHHHhhhhhhho......",
|
|
94
|
+
"....ooHHHHHHHHHHHHHHHHhHHHoo....",
|
|
95
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
96
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
97
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
98
|
+
"...oooooooooooooooooooooooooo...",
|
|
99
|
+
],
|
|
100
|
+
waiting: [
|
|
101
|
+
"................................",
|
|
102
|
+
"................................",
|
|
103
|
+
"................o...............",
|
|
104
|
+
"............ooooHoooo...........",
|
|
105
|
+
"..........ooLLLLLLhhhoo.........",
|
|
106
|
+
"........ooLLLLLLLhhhhhhoo.......",
|
|
107
|
+
".......oLLLLLLLLhhhhhhhhho......",
|
|
108
|
+
"......oLLLLLLLLhhhhhhhhhhho.....",
|
|
109
|
+
"......oHLLLLLLFFFFFhhhhhhho.....",
|
|
110
|
+
".....oHHHHHFFFFFFFFFFFhhhhho....",
|
|
111
|
+
".....oHHHHFFFFFFFFFFFFFhhhho....",
|
|
112
|
+
".....oHHHFFFFFFFFFFFFFFFhhho....",
|
|
113
|
+
".....oHHFFFFFFFFFFFFFFFFFhho....",
|
|
114
|
+
"....oHHHFFPPPPFFFFFPPPPFFhhho...",
|
|
115
|
+
"....oHHHFPeeeeFFFFFwwwwPFhho....",
|
|
116
|
+
"....oHHFFPeeeeFFFFFeeeePFFho....",
|
|
117
|
+
"....oHHFFPPPPPFFFFFPPPPPFFho....",
|
|
118
|
+
"....oHFFPPPFFFFFFFFFFFPPPFFo....",
|
|
119
|
+
"....oHFFFPPFFFnnnnMFFFPPFFFo....",
|
|
120
|
+
"....oHHHFFFFMMnnnnMMMFFFFhho....",
|
|
121
|
+
"....oHFFFFFFMMMnnMMMMFFFFFFo....",
|
|
122
|
+
"....oHFFFFFFMnMMMMnMMFFFFFFo....",
|
|
123
|
+
"....oHHHHHFFMMnnnnMMMFFhhhho....",
|
|
124
|
+
"....oHHHHHHFFFMMMMMFFFhhhhho....",
|
|
125
|
+
"....oHHHHHHHHHFFFFFhhhhhhhho....",
|
|
126
|
+
"....ooHHHHHHHHHHHhhhhhhhhhoo....",
|
|
127
|
+
"......oHHHHHHHHHHHhhhhhhho......",
|
|
128
|
+
"....ooHHHHHHHHHHHHHHHHhHHHoo....",
|
|
129
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
130
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
131
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
132
|
+
"...oooooooooooooooooooooooooo...",
|
|
133
|
+
],
|
|
134
|
+
happy: [
|
|
135
|
+
"................................",
|
|
136
|
+
"................................",
|
|
137
|
+
"................o...............",
|
|
138
|
+
"............ooooHoooo...........",
|
|
139
|
+
"..........ooLLLLLLhhhoo.........",
|
|
140
|
+
"........ooLLLLLLLhhhhhhoo.......",
|
|
141
|
+
".......oLLLLLLLLhhhhhhhhho......",
|
|
142
|
+
"......oLLLLLLLLhhhhhhhhhhho.....",
|
|
143
|
+
"......oHLLLLLLFFFFFhhhhhhho.....",
|
|
144
|
+
".....oHHHHHFFFFFFFFFFFhhhhho....",
|
|
145
|
+
".....oHHHHFFFFFFFFFFFFFhhhho....",
|
|
146
|
+
".....oHHHFFFFFFFFFFFFFFFhhho....",
|
|
147
|
+
".....oHHFFFFFFFFFFFFFFFFFhho....",
|
|
148
|
+
"....oHHHFFPPPPFFFFFPPPPFFhhho...",
|
|
149
|
+
"....oHHHFPPwwPFFFFFPwePPFhho....",
|
|
150
|
+
"....oHHFFPweePFFFFFPeewPFFho....",
|
|
151
|
+
"....oHHFFPPwPPFFFFFPPwPPFFho....",
|
|
152
|
+
"....oHFFPPPFFFFFFFFFFFPPPFFo....",
|
|
153
|
+
"....oHFFFPPFFFnnnnMFFFPPFFFo....",
|
|
154
|
+
"....oHHHFFFFMMnnnnMMMFFFFhho....",
|
|
155
|
+
"....oHFFFFFFMMMnnMMMMFFFFFFo....",
|
|
156
|
+
"....oHFFFFFFMnMMMMnMMFFFFFFo....",
|
|
157
|
+
"....oHHHHHFFMMnnnnMMMFFhhhho....",
|
|
158
|
+
"....oHHHHHHFFFMMMMMFFFhhhhho....",
|
|
159
|
+
"....oHHHHHHHHHFFFFFhhhhhhhho....",
|
|
160
|
+
"....ooHHHHHHHHHHHhhhhhhhhhoo....",
|
|
161
|
+
"......oHHHHHHHHHHHhhhhhhho......",
|
|
162
|
+
"....ooHHHHHHHHHHHHHHHHhHHHoo....",
|
|
163
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
164
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
165
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
166
|
+
"...oooooooooooooooooooooooooo...",
|
|
167
|
+
],
|
|
168
|
+
working: [
|
|
169
|
+
"................................",
|
|
170
|
+
"................................",
|
|
171
|
+
"................o...............",
|
|
172
|
+
"............ooooHoooo...........",
|
|
173
|
+
"..........ooLLLLLLhhhoo.........",
|
|
174
|
+
"........ooLLLLLLLhhhhhhoo.......",
|
|
175
|
+
".......oLLLLLLLLhhhhhhhhho......",
|
|
176
|
+
"......oLLLLLLLLhhhhhhhhhhho.....",
|
|
177
|
+
"......oHLLLLLLFFFFFhhhhhhho.....",
|
|
178
|
+
".....oHHHHHFFFFFFFFFFFhhhhho....",
|
|
179
|
+
".....oHHHHFFFFFFFFFFFFFhhhho....",
|
|
180
|
+
".....oHHHFFFFFFFFFFFFFFFhhho....",
|
|
181
|
+
".....oHHFFFFFFFFFFFFFFFFFhho....",
|
|
182
|
+
"....oHHHFFPPPPFFFFFPPPPFFhhho...",
|
|
183
|
+
"....oHHHFPwwwwFFFFFwwwwPFhho....",
|
|
184
|
+
"....oHHFFPPPPPFFFFFPPPPPFFho....",
|
|
185
|
+
"....oHHFFPPPPPFFFFFPPPPPFFho....",
|
|
186
|
+
"....oHFFPPPFFFFFFFFFFFPPPFFo....",
|
|
187
|
+
"....oHFFFPPFFFnnnnMFFFPPFFFo....",
|
|
188
|
+
"....oHHHFFFFMMnnnnMMMFFFFhho....",
|
|
189
|
+
"....oHFFFFFFMMMnnMMMMFFFFFFo....",
|
|
190
|
+
"....oHFFFFFFMnMMMMnMMFFFFFFo....",
|
|
191
|
+
"....oHHHHHFFMMnnnnMMMFFhhhho....",
|
|
192
|
+
"....oHHHHHHFFFMMMMMFFFhhhhho....",
|
|
193
|
+
"....oHHHHHHHHHFFFFFhhhhhhhho....",
|
|
194
|
+
"....ooHHHHHHHHHHHhhhhhhhhhoo....",
|
|
195
|
+
"......oHHHHHHHHHHHhhhhhhho......",
|
|
196
|
+
"....ooHHHHHHHHHHHHHHHHhHHHoo....",
|
|
197
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
198
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
199
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
200
|
+
"...oooooooooooooooooooooooooo...",
|
|
201
|
+
],
|
|
202
|
+
excited: [
|
|
203
|
+
"................................",
|
|
204
|
+
"................................",
|
|
205
|
+
"................o...............",
|
|
206
|
+
"............ooooHoooo...........",
|
|
207
|
+
"..........ooLLLLLLhhhoo.........",
|
|
208
|
+
"........ooLLLLLLLhhhhhhoo.......",
|
|
209
|
+
".......oLLLLLLLLhhhhhhhhho......",
|
|
210
|
+
"......oLLLLLLLLhhhhhhhhhhho.....",
|
|
211
|
+
"......oHLLLLLLFFFFFhhhhhhho.....",
|
|
212
|
+
".....oHHHHHFFFFFFFFFFFhhhhho....",
|
|
213
|
+
".....oHHHHFFFFFFFFFFFFFhhhho....",
|
|
214
|
+
".....oHHHFFFFFFFFFFFFFFFhhho....",
|
|
215
|
+
".....oHHFFFFFFFFFFFFFFFFFhho....",
|
|
216
|
+
"....oHHHFFPPPPFFFFFPPPPFFhhho...",
|
|
217
|
+
"....oHHHFPPwwPFFFFFPwePPFhho....",
|
|
218
|
+
"....oHHFFPweePFFFFFPeewPFFho....",
|
|
219
|
+
"....oHHFFPPwPPFFFFFPPwPPFFho....",
|
|
220
|
+
"....oHFFPPPFFFFFFFFFFFPPPFFo....",
|
|
221
|
+
"....oHFFFPPFFFnnnnMFFFPPFFFo....",
|
|
222
|
+
"....oHHHFFFFMMnnnnMMMFFFFhho....",
|
|
223
|
+
"....oHFFFFFFMMMnnMMMMFFFFFFo....",
|
|
224
|
+
"....oHFFFFFFMnMMMMnMMFFFFFFo....",
|
|
225
|
+
"....oHHHHHFFMMnnnnMMMFFhhhho....",
|
|
226
|
+
"....oHHHHHHFFFMMMMMFFFhhhhho....",
|
|
227
|
+
"....oHHHHHHHHHFFFFFhhhhhhhho....",
|
|
228
|
+
"....ooHHHHHHHHHHHhhhhhhhhhoo....",
|
|
229
|
+
"......oHHHHHHHHHHHhhhhhhho......",
|
|
230
|
+
"....ooHHHHHHHHHHHHHHHHhHHHoo....",
|
|
231
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
232
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
233
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
234
|
+
"...oooooooooooooooooooooooooo...",
|
|
235
|
+
],
|
|
236
|
+
annoyed: [
|
|
237
|
+
"................................",
|
|
238
|
+
"................................",
|
|
239
|
+
"................o...............",
|
|
240
|
+
"............ooooHoooo...........",
|
|
241
|
+
"..........ooLLLLLLhhhoo.........",
|
|
242
|
+
"........ooLLLLLLLhhhhhhoo.......",
|
|
243
|
+
".......oLLLLLLLLhhhhhhhhho......",
|
|
244
|
+
"......oLLLLLLLLhhhhhhhhhhho.....",
|
|
245
|
+
"......oHLLLLLLFFFFFhhhhhhho.....",
|
|
246
|
+
".....oHHHHHFFFFFFFFFFFhhhhho....",
|
|
247
|
+
".....oHHHHFFFFFFFFFFFFFhhhho....",
|
|
248
|
+
".....oHHHFFFFFFFFFFFFFFFhhho....",
|
|
249
|
+
".....oHHFFFFFFFFFFFFFFFFFhho....",
|
|
250
|
+
"....oHHHFFPPPPFFFFFPPPPFFhhho...",
|
|
251
|
+
"....oHHHFPwwwwFFFFFwwwwPFhho....",
|
|
252
|
+
"....oHHFFPPPPPFFFFFPPPPPFFho....",
|
|
253
|
+
"....oHHFFPPPPPFFFFFPPPPPFFho....",
|
|
254
|
+
"....oHFFPPPFFFFFFFFFFFPPPFFo....",
|
|
255
|
+
"....oHFFFPPFFFnnnnMFFFPPFFFo....",
|
|
256
|
+
"....oHHHFFFFMMnnnnMMMFFFFhho....",
|
|
257
|
+
"....oHFFFFFFMMMnnMMMMFFFFFFo....",
|
|
258
|
+
"....oHFFFFFFMnMMMMnMMFFFFFFo....",
|
|
259
|
+
"....oHHHHHFFMMnnnnMMMFFhhhho....",
|
|
260
|
+
"....oHHHHHHFFFMMMMMFFFhhhhho....",
|
|
261
|
+
"....oHHHHHHHHHFFFFFhhhhhhhho....",
|
|
262
|
+
"....ooHHHHHHHHHHHhhhhhhhhhoo....",
|
|
263
|
+
"......oHHHHHHHHHHHhhhhhhho......",
|
|
264
|
+
"....ooHHHHHHHHHHHHHHHHhHHHoo....",
|
|
265
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
266
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
267
|
+
"...oHHHHHHHHHHHHHHHHHHHHHHHHo...",
|
|
268
|
+
"...oooooooooooooooooooooooooo...",
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
export function spriteRows(expression) {
|
|
272
|
+
return SPRITES[expression] ?? SPRITES.idle;
|
|
273
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { hasColor } from "./theme.js";
|
|
2
|
+
import { PX, SPRITE_SIZE, spriteRows } from "./sprite-data.js";
|
|
3
|
+
function parseHex(hex) {
|
|
4
|
+
const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
|
|
5
|
+
if (!m)
|
|
6
|
+
return null;
|
|
7
|
+
const n = Number.parseInt(m[1], 16);
|
|
8
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
9
|
+
}
|
|
10
|
+
/** Palette key -> literal colour, with the hood taking the active theme. */
|
|
11
|
+
function colourFor(key, theme) {
|
|
12
|
+
if (key === "." || key === undefined)
|
|
13
|
+
return null;
|
|
14
|
+
// The hood is the only themed part. Its highlight and shadow are derived
|
|
15
|
+
// from the accent rather than reusing the support hue — teal highlights on
|
|
16
|
+
// a purple hood read as a different material, not a lit edge.
|
|
17
|
+
if (key === "H")
|
|
18
|
+
return theme.accent;
|
|
19
|
+
if (key === "L") {
|
|
20
|
+
const [r, g, b] = theme.accent;
|
|
21
|
+
const lift = (c) => Math.round(c + (255 - c) * 0.42);
|
|
22
|
+
return [lift(r), lift(g), lift(b)];
|
|
23
|
+
}
|
|
24
|
+
if (key === "h") {
|
|
25
|
+
const [r, g, b] = theme.accent;
|
|
26
|
+
return [Math.round(r * 0.52), Math.round(g * 0.52), Math.round(b * 0.52)];
|
|
27
|
+
}
|
|
28
|
+
const value = PX[key];
|
|
29
|
+
return typeof value === "string" ? parseHex(value) : null;
|
|
30
|
+
}
|
|
31
|
+
const fg = ([r, g, b]) => `\x1b[38;2;${r};${g};${b}m`;
|
|
32
|
+
const bg = ([r, g, b]) => `\x1b[48;2;${r};${g};${b}m`;
|
|
33
|
+
const RESET = "\x1b[0m";
|
|
34
|
+
export function sprite(theme, expression = "idle", { small = false } = {}) {
|
|
35
|
+
const grid = spriteRows(expression);
|
|
36
|
+
const step = small ? 2 : 1;
|
|
37
|
+
const size = SPRITE_SIZE;
|
|
38
|
+
// Without colour there is nothing to draw with — two block characters would
|
|
39
|
+
// be an unreadable smear, so the sprite is simply omitted.
|
|
40
|
+
if (!hasColor)
|
|
41
|
+
return [];
|
|
42
|
+
const lines = [];
|
|
43
|
+
// Two pixel rows per output line.
|
|
44
|
+
for (let y = 0; y < size; y += step * 2) {
|
|
45
|
+
let out = "";
|
|
46
|
+
let openBg = false;
|
|
47
|
+
for (let x = 0; x < size; x += step) {
|
|
48
|
+
const top = colourFor(grid[y]?.[x], theme);
|
|
49
|
+
const bottom = colourFor(grid[y + step]?.[x], theme);
|
|
50
|
+
if (!top && !bottom) {
|
|
51
|
+
if (openBg) {
|
|
52
|
+
out += RESET;
|
|
53
|
+
openBg = false;
|
|
54
|
+
}
|
|
55
|
+
out += " ";
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (top && bottom) {
|
|
59
|
+
out += `${bg(bottom)}${fg(top)}▀${RESET}`;
|
|
60
|
+
openBg = false;
|
|
61
|
+
}
|
|
62
|
+
else if (top) {
|
|
63
|
+
out += `${fg(top)}▀${RESET}`;
|
|
64
|
+
}
|
|
65
|
+
else if (bottom) {
|
|
66
|
+
out += `${fg(bottom)}▄${RESET}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (openBg)
|
|
70
|
+
out += RESET;
|
|
71
|
+
lines.push(out);
|
|
72
|
+
}
|
|
73
|
+
return lines;
|
|
74
|
+
}
|