github-search-cli 9.4.1 → 9.4.3

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +268 -237
  3. package/bin/ghfind.js +98 -89
  4. package/completions/ghfind.bash +1 -1
  5. package/completions/ghfind.fish +3 -1
  6. package/completions/ghfind.zsh +6 -0
  7. package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
  8. package/dist/cli.js +1179 -403
  9. package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
  10. package/package.json +16 -5
  11. package/release-notes/9.3.0.md +32 -29
  12. package/scripts/build.ts +34 -19
  13. package/scripts/generate-svg-demo.js +218 -168
  14. package/scripts/postinstall.js +176 -176
  15. package/src/bookmarks.ts +69 -69
  16. package/src/cli.ts +650 -566
  17. package/src/compare.ts +69 -47
  18. package/src/config.ts +70 -65
  19. package/src/deepdive.ts +149 -118
  20. package/src/doctor.ts +287 -287
  21. package/src/error-report.ts +8 -12
  22. package/src/errors.ts +62 -59
  23. package/src/explore.ts +34 -29
  24. package/src/export.ts +73 -63
  25. package/src/help.ts +157 -157
  26. package/src/history.ts +69 -55
  27. package/src/init.ts +60 -55
  28. package/src/landing.ts +106 -104
  29. package/src/login.ts +186 -0
  30. package/src/markdown-render.ts +1 -6
  31. package/src/notifications.ts +75 -59
  32. package/src/open-url.ts +16 -16
  33. package/src/org.ts +299 -0
  34. package/src/output.ts +153 -137
  35. package/src/package-browser.ts +317 -305
  36. package/src/package.ts +149 -137
  37. package/src/pipe.ts +60 -54
  38. package/src/releases.ts +182 -144
  39. package/src/saved-searches.ts +72 -50
  40. package/src/search-state.ts +283 -229
  41. package/src/search.ts +678 -653
  42. package/src/session.ts +26 -24
  43. package/src/share.ts +61 -54
  44. package/src/status.ts +68 -61
  45. package/src/storage.ts +136 -106
  46. package/src/themes.ts +259 -116
  47. package/src/tips.ts +42 -42
  48. package/src/toast.ts +70 -69
  49. package/src/trending-parser.ts +104 -0
  50. package/src/trending.ts +481 -462
  51. package/src/tui.ts +3015 -2867
  52. package/src/types.ts +160 -158
  53. package/src/update-check.ts +231 -215
  54. package/src/version.ts +33 -0
  55. package/src/watch.ts +95 -74
package/src/themes.ts CHANGED
@@ -1,116 +1,259 @@
1
- /**
2
- * Theme system — Premium palettes with nuanced color layers.
3
- * Each theme provides a rich color hierarchy for premium visual depth.
4
- */
5
- export interface Theme {
6
- name: string;
7
- /** Primary background — deepest layer */
8
- bg: string;
9
- /** Secondary surface — cards, panels, elevated areas */
10
- surface: string;
11
- /** Tertiary surface — subtle hover/focus states */
12
- surfaceAlt: string;
13
- /** Primary text — highest contrast */
14
- text: string;
15
- /** Secondary text — labels, metadata */
16
- muted: string;
17
- /** Accent colors for semantic meaning */
18
- blue: string;
19
- green: string;
20
- yellow: string;
21
- red: string;
22
- teal: string;
23
- purple: string;
24
- orange: string;
25
- /** Border and divider color */
26
- border: string;
27
- /** Subtle border for inner dividers */
28
- borderAlt: string;
29
- /** Darkest thin colorblock - thicker than border, acts as visual separator */
30
- separator: string;
31
- /** Selection/hover background */
32
- selectionBg: string;
33
- /** Selection/hover text */
34
- selectionText: string;
35
- /** Success state color */
36
- success: string;
37
- /** Warning state color */
38
- warning: string;
39
- /** Error state color */
40
- error: string;
41
- /** Modal/overlay panel background (darker than surface) */
42
- surfaceDim: string;
43
- /** Primary accent for titles and highlights */
44
- accent: string;
45
- /** Dimmed accent for secondary accents */
46
- accentDim: string;
47
- /** Border for floating/modal panels */
48
- borderAccent: string;
49
- }
50
-
51
- const THEMES: Record<string, Theme> = {
52
- "tokyo-night": {
53
- name: "tokyo-night",
54
- // Genuine Tokyo Night palette — cool blue-violet mood
55
- bg: "#1a1b26",
56
- surface: "#24283b",
57
- surfaceAlt: "#292e42",
58
- text: "#c0caf5",
59
- muted: "#565f89",
60
- blue: "#7aa2f7",
61
- green: "#9ece6a",
62
- yellow: "#e0af68",
63
- red: "#f7768e",
64
- teal: "#73daca",
65
- purple: "#bb9af7",
66
- orange: "#ff9e64",
67
- border: "#24283b",
68
- borderAlt: "#1f2335",
69
- separator: "#1f2335",
70
- selectionBg: "#2d3f76",
71
- selectionText: "#c0caf5",
72
- success: "#9ece6a",
73
- warning: "#e0af68",
74
- error: "#f7768e",
75
- surfaceDim: "#16161e",
76
- accent: "#7aa2f7",
77
- accentDim: "#5d7abf",
78
- borderAccent: "#1f2335",
79
- },
80
- "premium-dark": {
81
- name: "premium-dark",
82
- // Warm counter-mood to Tokyo Night — amber/rust accent
83
- bg: "#1e1e2e",
84
- surface: "#262637",
85
- surfaceAlt: "#2e2e42",
86
- text: "#cdd6f4",
87
- muted: "#6c7086",
88
- blue: "#89b4fa",
89
- green: "#a6e3a1",
90
- yellow: "#f9e2af",
91
- red: "#f38ba8",
92
- teal: "#94e2d5",
93
- purple: "#cba6f7",
94
- orange: "#fab387",
95
- border: "#313244",
96
- borderAlt: "#2a2a3d",
97
- separator: "#2a2a3d",
98
- selectionBg: "#45475a",
99
- selectionText: "#cdd6f4",
100
- success: "#a6e3a1",
101
- warning: "#f9e2af",
102
- error: "#f38ba8",
103
- surfaceDim: "#181825",
104
- accent: "#f9e2af",
105
- accentDim: "#c9a87a",
106
- borderAccent: "#313244",
107
- },
108
- };
109
-
110
- export function loadTheme(name: string): Theme {
111
- return THEMES[name] ?? THEMES["tokyo-night"];
112
- }
113
-
114
- export function listThemes(): string[] {
115
- return Object.keys(THEMES);
116
- }
1
+ /**
2
+ * Theme system — Premium palettes with nuanced color layers.
3
+ * Each theme provides a rich color hierarchy for premium visual depth.
4
+ */
5
+ export interface Theme {
6
+ name: string;
7
+ /** Primary background — deepest layer */
8
+ bg: string;
9
+ /** Secondary surface — cards, panels, elevated areas */
10
+ surface: string;
11
+ /** Tertiary surface — subtle hover/focus states */
12
+ surfaceAlt: string;
13
+ /** Primary text — highest contrast */
14
+ text: string;
15
+ /** Secondary text — labels, metadata */
16
+ muted: string;
17
+ /** Accent colors for semantic meaning */
18
+ blue: string;
19
+ green: string;
20
+ yellow: string;
21
+ red: string;
22
+ teal: string;
23
+ purple: string;
24
+ orange: string;
25
+ /** Border and divider color */
26
+ border: string;
27
+ /** Subtle border for inner dividers */
28
+ borderAlt: string;
29
+ /** Darkest thin colorblock - thicker than border, acts as visual separator */
30
+ separator: string;
31
+ /** Selection/hover background */
32
+ selectionBg: string;
33
+ /** Selection/hover text */
34
+ selectionText: string;
35
+ /** Success state color */
36
+ success: string;
37
+ /** Warning state color */
38
+ warning: string;
39
+ /** Error state color */
40
+ error: string;
41
+ /** Modal/overlay panel background (darker than surface) */
42
+ surfaceDim: string;
43
+ /** Primary accent for titles and highlights */
44
+ accent: string;
45
+ /** Dimmed accent for secondary accents */
46
+ accentDim: string;
47
+ /** Border for floating/modal panels */
48
+ borderAccent: string;
49
+ }
50
+
51
+ const THEMES: Record<string, Theme> = {
52
+ "tokyo-night": {
53
+ name: "tokyo-night",
54
+ // Genuine Tokyo Night palette — cool blue-violet mood
55
+ bg: "#1a1b26",
56
+ surface: "#24283b",
57
+ surfaceAlt: "#292e42",
58
+ text: "#c0caf5",
59
+ muted: "#565f89",
60
+ blue: "#7aa2f7",
61
+ green: "#9ece6a",
62
+ yellow: "#e0af68",
63
+ red: "#f7768e",
64
+ teal: "#73daca",
65
+ purple: "#bb9af7",
66
+ orange: "#ff9e64",
67
+ border: "#24283b",
68
+ borderAlt: "#1f2335",
69
+ separator: "#1f2335",
70
+ selectionBg: "#2d3f76",
71
+ selectionText: "#c0caf5",
72
+ success: "#9ece6a",
73
+ warning: "#e0af68",
74
+ error: "#f7768e",
75
+ surfaceDim: "#16161e",
76
+ accent: "#7aa2f7",
77
+ accentDim: "#5d7abf",
78
+ borderAccent: "#1f2335",
79
+ },
80
+ "premium-dark": {
81
+ name: "premium-dark",
82
+ // Warm counter-mood to Tokyo Night — amber/rust accent
83
+ bg: "#1e1e2e",
84
+ surface: "#262637",
85
+ surfaceAlt: "#2e2e42",
86
+ text: "#cdd6f4",
87
+ muted: "#6c7086",
88
+ blue: "#89b4fa",
89
+ green: "#a6e3a1",
90
+ yellow: "#f9e2af",
91
+ red: "#f38ba8",
92
+ teal: "#94e2d5",
93
+ purple: "#cba6f7",
94
+ orange: "#fab387",
95
+ border: "#313244",
96
+ borderAlt: "#2a2a3d",
97
+ separator: "#2a2a3d",
98
+ selectionBg: "#45475a",
99
+ selectionText: "#cdd6f4",
100
+ success: "#a6e3a1",
101
+ warning: "#f9e2af",
102
+ error: "#f38ba8",
103
+ surfaceDim: "#181825",
104
+ accent: "#f9e2af",
105
+ accentDim: "#c9a87a",
106
+ borderAccent: "#313244",
107
+ },
108
+ };
109
+
110
+ export function loadTheme(name: string): Theme {
111
+ return THEMES[name] ?? THEMES["tokyo-night"];
112
+ }
113
+
114
+ export function listThemes(): string[] {
115
+ return Object.keys(THEMES);
116
+ }
117
+
118
+ // ─── Terminal-matched surface blending ────────────────────────────────
119
+ /** Parse a hex color to [r,g,b]; expands 3-digit form (#abc → #aabbcc). */
120
+ function parseHex(hex: string): [number, number, number] | null {
121
+ let h = hex.trim().replace(/^#/, "");
122
+ if (h.length === 3)
123
+ h = h
124
+ .split("")
125
+ .map((c) => c + c)
126
+ .join("");
127
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
128
+ return [
129
+ parseInt(h.slice(0, 2), 16),
130
+ parseInt(h.slice(2, 4), 16),
131
+ parseInt(h.slice(4, 6), 16),
132
+ ];
133
+ }
134
+
135
+ const toHex2 = (n: number) => n.toString(16).padStart(2, "0");
136
+
137
+ /** Linear blend between two hex colors; t=0 → a, t=1 → b. */
138
+ export function mixHex(a: string, b: string, t: number): string {
139
+ const ca = parseHex(a);
140
+ const cb = parseHex(b);
141
+ if (!ca || !cb) return a;
142
+ const mix = (x: number, y: number) => Math.round(x + (y - x) * t);
143
+ return `#${toHex2(mix(ca[0], cb[0]))}${toHex2(mix(ca[1], cb[1]))}${toHex2(mix(ca[2], cb[2]))}`;
144
+ }
145
+
146
+ /** Perceived luminance (0–1); < 0.5 reads as a dark terminal background. */
147
+ export function isDarkHex(hex: string): boolean {
148
+ const c = parseHex(hex);
149
+ if (!c) return false;
150
+ return (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255 < 0.5;
151
+ }
152
+
153
+ /**
154
+ * Derive card/surface/border layer colors from the terminal background so
155
+ * panels blend into the user's terminal instead of fighting it. Layers are
156
+ * the background lightened by small steps (and surfaceDim darkened, for
157
+ * sunken modals). Returns null for light backgrounds, where the dark-theme
158
+ * chrome wouldn't work — callers should keep the theme's own layers then.
159
+ */
160
+ export function deriveSurfaceLayers(
161
+ bgHex: string,
162
+ ): Record<string, string> | null {
163
+ if (!isDarkHex(bgHex)) return null;
164
+ const w = "#ffffff";
165
+ return {
166
+ surface: mixHex(bgHex, w, 0.045),
167
+ surfaceAlt: mixHex(bgHex, w, 0.07),
168
+ // Sunken modal/overlay panels (toast etc.) — darker than the backdrop
169
+ surfaceDim: mixHex(bgHex, "#000000", 0.15),
170
+ border: mixHex(bgHex, w, 0.09),
171
+ borderAlt: mixHex(bgHex, w, 0.055),
172
+ separator: mixHex(bgHex, w, 0.055),
173
+ borderAccent: mixHex(bgHex, w, 0.065),
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Parse an OSC color reply spec into #rrggbb.
179
+ * Handles `rgb:RR/GG/BB`, `rgb:RRRR/GGGG/BBBB` (values scaled to 8-bit),
180
+ * and `rgba:...` (alpha component ignored).
181
+ */
182
+ export function parseOscColor(spec: string): string | null {
183
+ const m = spec
184
+ .trim()
185
+ .match(
186
+ /^(?:rgb|rgba):([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})(?:\/[0-9a-fA-F]{1,4})?$/,
187
+ );
188
+ if (!m) return null;
189
+ // Scale the channel to 8-bit regardless of digit count (1–4 hex digits).
190
+ const chan = (h: string): number => {
191
+ const value = parseInt(h, 16);
192
+ const max = 16 ** h.length - 1;
193
+ return Math.round((value * 255) / max);
194
+ };
195
+ const hex = (n: number) => n.toString(16).padStart(2, "0");
196
+ return `#${hex(chan(m[1]))}${hex(chan(m[2]))}${hex(chan(m[3]))}`;
197
+ }
198
+
199
+ /**
200
+ * Query the terminal's actual default background color via OSC 11.
201
+ *
202
+ * Must be called BEFORE the opentui renderer takes over stdin. Enables raw
203
+ * mode, writes `ESC ] 11 ; ? BEL`, and parses the reply (`rgb:R/G/B`, ST- or
204
+ * BEL-terminated). Returns null when the terminal doesn't answer — e.g.
205
+ * piped output, Windows conhost, or terminals without OSC 11 support — in
206
+ * which case callers should fall back to the theme's bg.
207
+ *
208
+ * (Implemented directly rather than via opentui's TerminalPalette: its stdin
209
+ * subscription depends on renderer session machinery that isn't running yet
210
+ * at boot, so it never receives the reply.)
211
+ */
212
+ export async function detectTerminalBackground(
213
+ timeoutMs = 400,
214
+ ): Promise<string | null> {
215
+ const stdin = process.stdin;
216
+ const stdout = process.stdout;
217
+ const setRawMode = (
218
+ stdin as typeof stdin & { setRawMode?: (m: boolean) => unknown }
219
+ ).setRawMode;
220
+ if (!stdout.isTTY || !stdin.isTTY || typeof setRawMode !== "function")
221
+ return null;
222
+
223
+ return new Promise<string | null>((resolve) => {
224
+ const wasRaw = stdin.isRaw === true;
225
+ let settled = false;
226
+ let buffer = "";
227
+
228
+ const finish = (result: string | null) => {
229
+ if (settled) return;
230
+ settled = true;
231
+ clearTimeout(timer);
232
+ stdin.removeListener("data", onData);
233
+ try {
234
+ setRawMode.call(stdin, wasRaw);
235
+ } catch {
236
+ /* best effort restore */
237
+ }
238
+ stdin.pause();
239
+ resolve(result);
240
+ };
241
+
242
+ // Reply: ESC ] 11 ; <spec> (BEL | ESC \)
243
+ const onData = (d: Buffer) => {
244
+ buffer += d.toString("latin1");
245
+ const m = buffer.match(/\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)/);
246
+ if (m) finish(parseOscColor(m[1]));
247
+ };
248
+
249
+ const timer = setTimeout(() => finish(null), timeoutMs);
250
+ try {
251
+ setRawMode.call(stdin, true);
252
+ stdin.on("data", onData);
253
+ stdin.resume();
254
+ stdout.write("\x1b]11;?\x07");
255
+ } catch {
256
+ finish(null);
257
+ }
258
+ });
259
+ }
package/src/tips.ts CHANGED
@@ -1,42 +1,42 @@
1
- /**
2
- * Startup tips — rotate through helpful hints.
3
- */
4
-
5
- /** Rotates tips without repeating the same one twice in a row. */
6
- export class TipRotator {
7
- private tips: string[];
8
- private lastIndex = -1;
9
-
10
- constructor(tips?: string[]) {
11
- this.tips = tips ?? TIPS;
12
- }
13
-
14
- next(): string {
15
- let idx: number;
16
- do {
17
- idx = Math.floor(Math.random() * this.tips.length);
18
- } while (idx === this.lastIndex && this.tips.length > 1);
19
- this.lastIndex = idx;
20
- return this.tips[idx];
21
- }
22
- }
23
-
24
- const TIPS = [
25
- "💡 Tip: Press ? to see all keyboard shortcuts",
26
- "💡 Tip: Use Ctrl+R to recall past searches",
27
- "💡 Tip: Press b to bookmark a repository",
28
- "💡 Tip: Tab auto-completes qualifiers like language: and stars:",
29
- "💡 Tip: Press Ctrl+E to export results as CSV or Markdown",
30
- "💡 Tip: Set GITHUB_TOKEN for higher API rate limits",
31
- "💡 Tip: Press c to compare up to 5 repos side-by-side",
32
- "💡 Tip: Press d for a deep-dive into any repo",
33
- "💡 Tip: Press E to browse popular GitHub topics",
34
- "💡 Tip: Press Ctrl+P to copy a repo link to clipboard",
35
- ];
36
-
37
- export const defaultRotator = new TipRotator();
38
-
39
- /** Get the next tip (never repeats the same tip twice in a row). */
40
- export function nextTip(): string {
41
- return defaultRotator.next();
42
- }
1
+ /**
2
+ * Startup tips — rotate through helpful hints.
3
+ */
4
+
5
+ /** Rotates tips without repeating the same one twice in a row. */
6
+ export class TipRotator {
7
+ private tips: string[];
8
+ private lastIndex = -1;
9
+
10
+ constructor(tips?: string[]) {
11
+ this.tips = tips ?? TIPS;
12
+ }
13
+
14
+ next(): string {
15
+ let idx: number;
16
+ do {
17
+ idx = Math.floor(Math.random() * this.tips.length);
18
+ } while (idx === this.lastIndex && this.tips.length > 1);
19
+ this.lastIndex = idx;
20
+ return this.tips[idx];
21
+ }
22
+ }
23
+
24
+ const TIPS = [
25
+ "💡 Tip: Press ? to see all keyboard shortcuts",
26
+ "💡 Tip: Use Ctrl+R to recall past searches",
27
+ "💡 Tip: Press b to bookmark a repository",
28
+ "💡 Tip: Tab auto-completes qualifiers like language: and stars:",
29
+ "💡 Tip: Press Ctrl+E to export results as CSV or Markdown",
30
+ "💡 Tip: Set GITHUB_TOKEN for higher API rate limits",
31
+ "💡 Tip: Press c to compare up to 5 repos side-by-side",
32
+ "💡 Tip: Press d for a deep-dive into any repo",
33
+ "💡 Tip: Press E to browse popular GitHub topics",
34
+ "💡 Tip: Press Ctrl+P to copy a repo link to clipboard",
35
+ ];
36
+
37
+ export const defaultRotator = new TipRotator();
38
+
39
+ /** Get the next tip (never repeats the same tip twice in a row). */
40
+ export function nextTip(): string {
41
+ return defaultRotator.next();
42
+ }
package/src/toast.ts CHANGED
@@ -1,69 +1,70 @@
1
- /** Toast popup — absolute-positioned card pinned top-right, auto-hides. */
2
- import { BoxRenderable, TextRenderable } from "@opentui/core";
3
-
4
- /** Only the color keys the toast reads (lazily, so runtime theme swaps apply). */
5
- export interface ToastColors {
6
- green: string;
7
- yellow: string;
8
- text: string;
9
- surfaceDim: string;
10
- }
11
-
12
- export function createToast(
13
- renderer: Parameters<BoxRenderable["constructor"]>[0],
14
- root: { add(child: unknown): unknown },
15
- colors: ToastColors,
16
- ) {
17
- let timer: ReturnType<typeof setTimeout> | undefined;
18
- const box = new BoxRenderable(renderer, {
19
- visible: false,
20
- position: "absolute",
21
- top: 1,
22
- right: 2,
23
- height: 3,
24
- border: true,
25
- borderStyle: "rounded",
26
- borderColor: colors.green,
27
- backgroundColor: colors.surfaceDim,
28
- zIndex: 100,
29
- });
30
- const text = new TextRenderable(renderer, {
31
- content: "",
32
- color: colors.text,
33
- backgroundColor: colors.surfaceDim,
34
- height: 1,
35
- });
36
- box.add(text);
37
- root.add(box);
38
-
39
- return {
40
- /** Show a message; accent picks the border color. Re-showing resets the timer. */
41
- show(msg: string, ms = 2000, accent?: string): void {
42
- text.content = msg;
43
- box.borderColor = accent ?? colors.green;
44
- // Message + paddingX(2) + side borders(2)
45
- box.width = msg.length + 4;
46
- box.visible = true;
47
- renderer.requestRender();
48
- clearTimeout(timer);
49
- timer = setTimeout(() => {
50
- box.visible = false;
51
- renderer.requestRender();
52
- }, ms);
53
- },
54
- hide(): void {
55
- clearTimeout(timer);
56
- if (!box.visible) return;
57
- box.visible = false;
58
- renderer.requestRender();
59
- },
60
- get visible(): boolean {
61
- return box.visible;
62
- },
63
- get borderColor(): string {
64
- return box.borderColor as string;
65
- },
66
- };
67
- }
68
-
69
- export type Toast = ReturnType<typeof createToast>;
1
+ /** Toast popup — absolute-positioned card pinned top-right, auto-hides. */
2
+ import { BoxRenderable, TextRenderable } from "@opentui/core";
3
+ import type { RenderContext } from "@opentui/core";
4
+
5
+ /** Only the color keys the toast reads (lazily, so runtime theme swaps apply). */
6
+ export interface ToastColors {
7
+ green: string;
8
+ yellow: string;
9
+ text: string;
10
+ surfaceDim: string;
11
+ }
12
+
13
+ export function createToast(
14
+ renderer: RenderContext,
15
+ root: { add(child: unknown): unknown },
16
+ colors: ToastColors,
17
+ ) {
18
+ let timer: ReturnType<typeof setTimeout> | undefined;
19
+ const box = new BoxRenderable(renderer, {
20
+ visible: false,
21
+ position: "absolute",
22
+ top: 1,
23
+ right: 2,
24
+ height: 3,
25
+ border: true,
26
+ borderStyle: "rounded",
27
+ borderColor: colors.green,
28
+ backgroundColor: colors.surfaceDim,
29
+ zIndex: 100,
30
+ });
31
+ const text = new TextRenderable(renderer, {
32
+ content: "",
33
+ fg: colors.text,
34
+ bg: colors.surfaceDim,
35
+ height: 1,
36
+ });
37
+ box.add(text);
38
+ root.add(box);
39
+
40
+ return {
41
+ /** Show a message; accent picks the border color. Re-showing resets the timer. */
42
+ show(msg: string, ms = 2000, accent?: string): void {
43
+ text.content = msg;
44
+ box.borderColor = accent ?? colors.green;
45
+ // Message + paddingX(2) + side borders(2)
46
+ box.width = msg.length + 4;
47
+ box.visible = true;
48
+ renderer.requestRender();
49
+ clearTimeout(timer);
50
+ timer = setTimeout(() => {
51
+ box.visible = false;
52
+ renderer.requestRender();
53
+ }, ms);
54
+ },
55
+ hide(): void {
56
+ clearTimeout(timer);
57
+ if (!box.visible) return;
58
+ box.visible = false;
59
+ renderer.requestRender();
60
+ },
61
+ get visible(): boolean {
62
+ return box.visible;
63
+ },
64
+ get borderColor(): string {
65
+ return String(box.borderColor);
66
+ },
67
+ };
68
+ }
69
+
70
+ export type Toast = ReturnType<typeof createToast>;