@underactive/pi-topping-statusline 0.2.7 → 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.
@@ -1,138 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import type { Theme } from "@earendil-works/pi-coding-agent";
4
- import { stripAnsi } from "../footer.ts";
5
- import { MenuComponent } from "../menu.ts";
6
- import type { MenuSection } from "../menu.ts";
7
-
8
- const theme = {
9
- fg: (_c: string, s: string) => s,
10
- bg: (color: string, s: string) => color === "selectedBg" ? `\x1b[48;5;1m${s}\x1b[49m` : s,
11
- bold: (s: string) => s,
12
- getBgAnsi: (color: string) => color === "selectedBg" ? "\x1b[48;5;1m" : "",
13
- } as unknown as Theme;
14
- const plain = (lines: string[]) => lines.map(stripAnsi);
15
-
16
- function makeSections(feeds: { customType: string; prefix: string }[]): MenuSection[] {
17
- const items: any[] = [];
18
- feeds.forEach((f, i) => {
19
- items.push({ id: `feed.${i}.customType`, label: `${i + 1}. type`, value: f.customType, text: true, placeholder: "ext/type" });
20
- items.push({ id: `feed.${i}.prefix`, label: `${i + 1}. prefix`, value: f.prefix, text: true, placeholder: "(none)" });
21
- items.push({ id: `feed.${i}.remove`, label: `${i + 1}. remove`, value: false, action: true });
22
- });
23
- items.push({ id: "feed.add", label: "+ add feed", value: false, action: true });
24
- return [{ title: "Feeds", items }];
25
- }
26
-
27
- function drive() {
28
- let result: any;
29
- const feeds = [{ customType: "pi-prompt-cache/savings", prefix: "CS" }];
30
- const menu = new MenuComponent(
31
- {
32
- title: "T",
33
- sections: makeSections(feeds),
34
- onAction: (id, values) => {
35
- const list: { customType: string; prefix: string }[] = [];
36
- for (let i = 0; values[`feed.${i}.customType`] !== undefined; i++) {
37
- list.push({ customType: String(values[`feed.${i}.customType`]), prefix: String(values[`feed.${i}.prefix`]) });
38
- }
39
- if (id === "feed.add") list.push({ customType: "", prefix: "" });
40
- else {
41
- const m = /^feed\.(\d+)\.remove$/.exec(id);
42
- if (!m) return undefined;
43
- list.splice(Number(m[1]), 1);
44
- }
45
- return makeSections(list);
46
- },
47
- },
48
- theme,
49
- r => { result = r; },
50
- );
51
- return { menu, get result() { return result; } };
52
- }
53
-
54
- const type = (menu: any, s: string) => { for (const ch of s) menu.handleInput(ch); };
55
-
56
- test("selected rows use the pi-topping marker and background", () => {
57
- const { menu } = drive();
58
- const lines = menu.render(60);
59
- const plainText = plain(lines).join("\n");
60
- assert.match(plainText, /> 1\. type/);
61
- assert.ok(!plainText.includes("▸"));
62
- assert.ok(lines.some(line => line.includes("\x1b[48;5;1m")), "selected row background is missing");
63
- });
64
-
65
- test("selected row background survives an embedded reset from truncated content", () => {
66
- const { menu } = drive();
67
- // Width narrow enough that the "1. type" label truncates, which embeds a raw \x1b[0m reset.
68
- const lines = menu.render(15);
69
- const selectedLine = lines.find(line => line.includes("\x1b[48;5;1m"))!;
70
- assert.ok(selectedLine, "selected row not found");
71
- assert.ok(selectedLine.includes("\x1b[0m"), "expected a reset from truncateToWidth in the selected row");
72
- assert.doesNotMatch(
73
- selectedLine,
74
- /\x1b\[0m(?!\x1b\[48;5;1m)/,
75
- "selected background must be reopened after every reset",
76
- );
77
- });
78
-
79
- test("typing into a text row edits only on commit", () => {
80
- const { menu } = drive();
81
- menu.handleInput("\r"); // Enter on row 0 -> begin editing
82
- type(menu, "-x");
83
- assert.match(plain(menu.render(60)).join("\n"), /pi-prompt-cache\/savings-x/, "buffer shown live");
84
- menu.handleInput("\x7f"); // backspace
85
- menu.handleInput("\r"); // commit
86
- menu.handleInput("\x1b"); // escape closes the menu (cancel)
87
- assert.equal((menu as any).values["feed.0.customType"], "pi-prompt-cache/savings-");
88
- });
89
-
90
- test("escape during editing discards the buffer, not the menu", () => {
91
- const { menu, } = drive();
92
- menu.handleInput("\r");
93
- type(menu, "ZZZ");
94
- menu.handleInput("\x1b"); // cancel the edit
95
- assert.equal((menu as any).values["feed.0.customType"], "pi-prompt-cache/savings");
96
- assert.equal((menu as any).editing, undefined);
97
- assert.equal(drive().result, undefined);
98
- });
99
-
100
- test("escape sequences never enter the buffer", () => {
101
- const { menu } = drive();
102
- menu.handleInput("\r");
103
- menu.handleInput("\x1b[A"); // an arrow key, mid-edit
104
- menu.handleInput("\r");
105
- assert.equal((menu as any).values["feed.0.customType"], "pi-prompt-cache/savings");
106
- });
107
-
108
- test("add feed appends a blank row; remove drops the right one", () => {
109
- const { menu } = drive();
110
- const rowsBefore = (menu as any).flat.length;
111
- // cursor to "+ add feed" (last row)
112
- for (let i = 0; i < rowsBefore - 1; i++) menu.handleInput("\x1b[B");
113
- menu.handleInput("\r");
114
- assert.equal((menu as any).flat.length, rowsBefore + 3, "one feed = 3 rows");
115
- assert.equal((menu as any).values["feed.1.customType"], "");
116
-
117
- // remove feed 1 -> back to the original row count
118
- (menu as any).cursor = 5; // "2. remove"
119
- menu.handleInput("\r");
120
- assert.equal((menu as any).flat.length, rowsBefore);
121
- assert.equal((menu as any).values["feed.1.customType"], undefined, "removed values do not linger");
122
- assert.equal((menu as any).values["feed.0.customType"], "pi-prompt-cache/savings", "survivor kept");
123
- });
124
-
125
- test("cursor stays in range after a removal at the end", () => {
126
- const { menu } = drive();
127
- (menu as any).cursor = 2; // "1. remove"
128
- menu.handleInput("\r");
129
- assert.ok((menu as any).cursor < (menu as any).flat.length);
130
- assert.doesNotThrow(() => menu.render(60));
131
- });
132
-
133
- test("placeholder shows for empty text rows", () => {
134
- const { menu } = drive();
135
- for (let i = 0; i < (menu as any).flat.length - 1; i++) menu.handleInput("\x1b[B");
136
- menu.handleInput("\r"); // add feed
137
- assert.match(plain(menu.render(60)).join("\n"), /ext\/type/, "placeholder visible");
138
- });