@profullstack/hqtui-demo 0.1.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/bin/hqtui-demo.mjs +3 -0
  3. package/dist/format.js +53 -0
  4. package/dist/format.js.map +1 -0
  5. package/dist/main.js +361 -0
  6. package/dist/main.js.map +1 -0
  7. package/dist/screens/components.js +144 -0
  8. package/dist/screens/components.js.map +1 -0
  9. package/dist/screens/dashboard.js +307 -0
  10. package/dist/screens/dashboard.js.map +1 -0
  11. package/dist/screens/graphics.js +56 -0
  12. package/dist/screens/graphics.js.map +1 -0
  13. package/dist/screens/index.js +7 -0
  14. package/dist/screens/index.js.map +1 -0
  15. package/dist/screens/input.js +37 -0
  16. package/dist/screens/input.js.map +1 -0
  17. package/dist/screens/stress.js +34 -0
  18. package/dist/screens/stress.js.map +1 -0
  19. package/dist/screens/themes.js +40 -0
  20. package/dist/screens/themes.js.map +1 -0
  21. package/dist/simulation.js +254 -0
  22. package/dist/simulation.js.map +1 -0
  23. package/dist/state.js +35 -0
  24. package/dist/state.js.map +1 -0
  25. package/dist/system/common.js +97 -0
  26. package/dist/system/common.js.map +1 -0
  27. package/dist/system/darwin.js +153 -0
  28. package/dist/system/darwin.js.map +1 -0
  29. package/dist/system/index.js +45 -0
  30. package/dist/system/index.js.map +1 -0
  31. package/dist/system/linux.js +260 -0
  32. package/dist/system/linux.js.map +1 -0
  33. package/dist/system/types.js +2 -0
  34. package/dist/system/types.js.map +1 -0
  35. package/dist/system/win32.js +118 -0
  36. package/dist/system/win32.js.map +1 -0
  37. package/package.json +39 -0
  38. package/src/format.ts +52 -0
  39. package/src/main.ts +320 -0
  40. package/src/screens/components.ts +153 -0
  41. package/src/screens/dashboard.ts +328 -0
  42. package/src/screens/graphics.ts +64 -0
  43. package/src/screens/index.ts +6 -0
  44. package/src/screens/input.ts +39 -0
  45. package/src/screens/stress.ts +36 -0
  46. package/src/screens/themes.ts +41 -0
  47. package/src/simulation.ts +356 -0
  48. package/src/state.ts +74 -0
  49. package/src/system/common.ts +101 -0
  50. package/src/system/darwin.ts +160 -0
  51. package/src/system/index.ts +56 -0
  52. package/src/system/linux.ts +258 -0
  53. package/src/system/types.ts +12 -0
  54. package/src/system/win32.ts +132 -0
package/src/main.ts ADDED
@@ -0,0 +1,320 @@
1
+ // Run directly with `bun src/main.ts`, or via the compiled bin on Node.
2
+ /**
3
+ * The HQTUI reference dashboard.
4
+ *
5
+ * bunx @profullstack/hqtui-demo # real system metrics
6
+ * bunx @profullstack/hqtui-demo --sim # deterministic simulation
7
+ *
8
+ * Also runs under Node 22.6+ (`npx @profullstack/hqtui-demo`).
9
+ */
10
+ import { createApp, themeList, themes, type KeyEvent } from "@profullstack/hqtui";
11
+ import { createCollector } from "./system/index.ts";
12
+ import { createState, SCREENS, type ScreenName } from "./state.ts";
13
+ import {
14
+ componentsScreen, dashboardScreen, graphicsScreen, inputScreen, stressScreen, themesScreen, visibleProcesses,
15
+ } from "./screens/index.ts";
16
+ import { clock, num } from "./format.ts";
17
+
18
+ interface Options {
19
+ real: boolean;
20
+ seed: number;
21
+ fps: number;
22
+ theme: string;
23
+ screen: ScreenName;
24
+ interval: number;
25
+ }
26
+
27
+ function parseArgs(argv: string[]): Options {
28
+ const options: Options = {
29
+ real: true,
30
+ seed: 1337,
31
+ fps: 30,
32
+ theme: "dark",
33
+ screen: "dashboard",
34
+ interval: 1000,
35
+ };
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const arg = argv[i];
38
+ const value = argv[i + 1];
39
+ switch (arg) {
40
+ case "--sim":
41
+ case "--fake":
42
+ case "--simulate": options.real = false; break;
43
+ case "--real": options.real = true; break;
44
+ case "--seed": options.seed = Number(value) || 1337; i++; break;
45
+ case "--fps": options.fps = Number(value) || 30; i++; break;
46
+ case "--theme": options.theme = value ?? "dark"; i++; break;
47
+ case "--screen": options.screen = (value as ScreenName) ?? "dashboard"; i++; break;
48
+ case "--interval": options.interval = Number(value) || 1000; i++; break;
49
+ case "-h":
50
+ case "--help": printHelp(); process.exit(0);
51
+ case "-v":
52
+ case "--version": console.log("hqtui-demo 0.1.0"); process.exit(0);
53
+ }
54
+ }
55
+ return options;
56
+ }
57
+
58
+ function printHelp(): void {
59
+ console.log(`hqtui-demo — the HQTUI reference dashboard
60
+
61
+ Usage:
62
+ hqtui-demo [options]
63
+
64
+ Options:
65
+ --sim Use the deterministic simulation instead of real metrics
66
+ --real Read real system metrics (default)
67
+ --seed <n> Simulation seed (default 1337)
68
+ --fps <n> Frame cap (default 30, 15 over SSH)
69
+ --theme <name> dark, dracula, nord, tokyoNight, gruvbox, matrix, monochrome, highContrast, light
70
+ --screen <name> dashboard, components, graphics, themes, input, stress
71
+ --interval <ms> Metric refresh interval (default 1000)
72
+ -h, --help Show this help
73
+ -v, --version Show the version
74
+
75
+ Keys:
76
+ 1-6 / Tab screens F2 theme F3 filter F6 sort Ctrl+K palette
77
+ ↑/↓ select Space pause F1 help q quit
78
+ `);
79
+ }
80
+
81
+ const PALETTE_COMMANDS: { label: string; hint: string; run: (state: ReturnType<typeof createState>) => void }[] = [
82
+ { label: "Go to Dashboard", hint: "1", run: (s) => { s.screen = "dashboard"; } },
83
+ { label: "Go to Components", hint: "2", run: (s) => { s.screen = "components"; } },
84
+ { label: "Go to Graphics", hint: "3", run: (s) => { s.screen = "graphics"; } },
85
+ { label: "Go to Themes", hint: "4", run: (s) => { s.screen = "themes"; } },
86
+ { label: "Go to Input", hint: "5", run: (s) => { s.screen = "input"; } },
87
+ { label: "Go to Stress Test", hint: "6", run: (s) => { s.screen = "stress"; } },
88
+ { label: "Sort by CPU", hint: "F6", run: (s) => { s.sort = "cpu"; } },
89
+ { label: "Sort by Memory", hint: "F6", run: (s) => { s.sort = "mem"; } },
90
+ { label: "Pause updates", hint: "Space", run: (s) => { s.paused = !s.paused; } },
91
+ ];
92
+
93
+ async function main(): Promise<void> {
94
+ const options = parseArgs(process.argv.slice(2));
95
+ const collector = await createCollector({ real: options.real, seed: options.seed });
96
+ await collector.refresh(options.interval / 1000);
97
+
98
+ const state = createState(collector.current(), collector.source, collector.unavailable);
99
+ state.screen = SCREENS.includes(options.screen) ? options.screen : "dashboard";
100
+ state.themeIndex = Math.max(0, themeList.findIndex((t) => t.name === options.theme || t === (themes as never)[options.theme]));
101
+
102
+ const app = await createApp({
103
+ theme: themeList[state.themeIndex] ?? themes.dark,
104
+ fps: options.fps,
105
+ title: "hqtui demo",
106
+ quitKeys: ["ctrl+c"],
107
+ });
108
+
109
+ // Metrics refresh on their own clock; rendering runs at the frame rate.
110
+ const dt = options.interval / 1000;
111
+ const poll = setInterval(() => {
112
+ if (state.paused) return;
113
+ void collector.refresh(dt).then(() => {
114
+ state.sample = collector.current();
115
+ app.invalidate();
116
+ });
117
+ }, options.interval);
118
+ poll.unref?.();
119
+
120
+ // The simulation is cheap enough to advance every frame, which is what makes
121
+ // the demo look alive rather than stepping once a second.
122
+ const animate = setInterval(() => {
123
+ if (state.paused || options.real) return;
124
+ void collector.refresh(0.1);
125
+ state.sample = collector.current();
126
+ app.invalidate();
127
+ }, 100);
128
+ animate.unref?.();
129
+
130
+ app.on("frame", (stats) => {
131
+ state.fps = stats.fps;
132
+ state.renderMs = stats.renderMs;
133
+ state.changedCells = stats.changedCells;
134
+ state.bytes = stats.bytes;
135
+ });
136
+
137
+ app.on("mouse", (event) => {
138
+ state.lastMouse = `${event.action} ${event.button} @ ${event.x},${event.y}${event.scroll ? ` scroll ${event.scroll}` : ""}`;
139
+ if (event.action === "scroll") {
140
+ state.offset = Math.max(0, state.offset + event.scroll * 3);
141
+ }
142
+ });
143
+
144
+ app.on("key", (event: KeyEvent) => {
145
+ state.lastKey = event.key;
146
+ state.keyLog.push(`${clock()} ${event.key}${event.char ? ` "${event.char}"` : ""}`);
147
+ if (state.keyLog.length > 100) state.keyLog.shift();
148
+
149
+ // Overlays capture input first.
150
+ if (state.showPalette) {
151
+ const matches = PALETTE_COMMANDS.filter((c) => c.label.toLowerCase().includes(state.paletteQuery.toLowerCase()));
152
+ if (event.name === "escape") state.showPalette = false;
153
+ else if (event.name === "enter") {
154
+ matches[state.paletteIndex]?.run(state);
155
+ state.showPalette = false;
156
+ } else if (event.name === "up") state.paletteIndex = Math.max(0, state.paletteIndex - 1);
157
+ else if (event.name === "down") state.paletteIndex = Math.min(matches.length - 1, state.paletteIndex + 1);
158
+ else if (event.name === "backspace") state.paletteQuery = state.paletteQuery.slice(0, -1);
159
+ else if (event.char) state.paletteQuery += event.char;
160
+ return;
161
+ }
162
+ if (state.showModal) {
163
+ if (event.name === "escape" || event.name === "enter" || event.name === "n" || event.name === "y") {
164
+ state.showModal = false;
165
+ }
166
+ return;
167
+ }
168
+ if (state.showHelp) {
169
+ state.showHelp = false;
170
+ return;
171
+ }
172
+ if (state.filtering) {
173
+ if (event.name === "escape") { state.filtering = false; state.filter = ""; }
174
+ else if (event.name === "enter") state.filtering = false;
175
+ else if (event.name === "backspace") state.filter = state.filter.slice(0, -1);
176
+ else if (event.char) state.filter += event.char;
177
+ return;
178
+ }
179
+
180
+ switch (event.key) {
181
+ case "q": app.quit(); return;
182
+ case "f1": state.showHelp = true; return;
183
+ case "f2":
184
+ state.themeIndex = (state.themeIndex + 1) % themeList.length;
185
+ app.setTheme(themeList[state.themeIndex]);
186
+ return;
187
+ case "f3": state.filtering = true; return;
188
+ case "f6": {
189
+ const order = ["cpu", "mem", "pid", "name"] as const;
190
+ state.sort = order[(order.indexOf(state.sort) + 1) % order.length];
191
+ return;
192
+ }
193
+ case "f10": app.quit(); return;
194
+ case "ctrl+k":
195
+ state.showPalette = true;
196
+ state.paletteQuery = "";
197
+ state.paletteIndex = 0;
198
+ return;
199
+ case "space": state.paused = !state.paused; return;
200
+ case "up": state.selected = Math.max(0, state.selected - 1); break;
201
+ case "down": state.selected = Math.min(visibleProcesses(state).length - 1, state.selected + 1); break;
202
+ case "pageup": state.selected = Math.max(0, state.selected - 10); break;
203
+ case "pagedown": state.selected = Math.min(visibleProcesses(state).length - 1, state.selected + 10); break;
204
+ case "home": state.selected = 0; break;
205
+ case "end": state.selected = Math.max(0, visibleProcesses(state).length - 1); break;
206
+ case "enter": state.showModal = true; return;
207
+ case "left":
208
+ if (state.screen === "themes") {
209
+ state.themeIndex = (state.themeIndex - 1 + themeList.length) % themeList.length;
210
+ app.setTheme(themeList[state.themeIndex]);
211
+ }
212
+ return;
213
+ case "right":
214
+ if (state.screen === "themes") {
215
+ state.themeIndex = (state.themeIndex + 1) % themeList.length;
216
+ app.setTheme(themeList[state.themeIndex]);
217
+ }
218
+ return;
219
+ case "tab": state.screen = SCREENS[(SCREENS.indexOf(state.screen) + 1) % SCREENS.length]; return;
220
+ }
221
+
222
+ const digit = Number(event.name);
223
+ if (Number.isInteger(digit) && digit >= 1 && digit <= SCREENS.length) {
224
+ state.screen = SCREENS[digit - 1];
225
+ }
226
+
227
+ // Keep the selected row inside the visible window.
228
+ if (state.selected < state.offset) state.offset = state.selected;
229
+ });
230
+
231
+ app.render(({ ui, theme, height }) => {
232
+ ui.row({ size: 1 }, (header) => {
233
+ header.text(" hqtui.com", { fg: theme.title, bold: true, size: 12 });
234
+ header.tabs({
235
+ tabs: SCREENS.map((s, i) => `${i + 1} ${s}`),
236
+ active: SCREENS.indexOf(state.screen),
237
+ onSelect: (index) => { state.screen = SCREENS[index]; },
238
+ });
239
+ header.text(
240
+ `${state.paused ? "paused" : "live"} ${state.source} ${num(state.fps, 0)}fps ${clock()} `,
241
+ { fg: state.paused ? theme.warning : theme.success, align: "right" },
242
+ );
243
+ });
244
+ ui.spacer(1);
245
+
246
+ ui.column({ size: height - 4 }, (body) => {
247
+ switch (state.screen) {
248
+ case "components": componentsScreen(body, state, theme); break;
249
+ case "graphics": graphicsScreen(body, state, theme); break;
250
+ case "themes": themesScreen(body, state, theme); break;
251
+ case "input": inputScreen(body, state, theme); break;
252
+ case "stress": stressScreen(body, state, theme); break;
253
+ default: dashboardScreen(body, state, theme); break;
254
+ }
255
+ });
256
+
257
+ ui.spacer(1);
258
+ ui.statusBar({
259
+ items: [
260
+ { key: "F1", label: "Help" },
261
+ { key: "F2", label: `Theme (${theme.name})` },
262
+ { key: "F3", label: state.filtering ? `Filter: ${state.filter}_` : "Filter", active: state.filtering },
263
+ { key: "F6", label: `Sort: ${state.sort}` },
264
+ { key: "^K", label: "Palette" },
265
+ { key: "Tab", label: "Screen" },
266
+ { key: "q", label: "Quit" },
267
+ ],
268
+ right: [{ label: `${num(state.renderMs, 2)}ms ${state.changedCells} cells ${state.bytes}B` }],
269
+ });
270
+
271
+ if (state.showHelp) {
272
+ ui.modal({
273
+ title: "HQTUI Demo — Help",
274
+ width: 62,
275
+ height: 18,
276
+ message:
277
+ "1-6 or Tab switch screens.\n" +
278
+ "F2 cycles themes, F3 filters processes, F6 changes sort.\n" +
279
+ "Ctrl+K opens the command palette, Space pauses updates.\n" +
280
+ "Arrows, PageUp/PageDown, Home/End move the selection.\n" +
281
+ "Mouse: click tabs and buttons, scroll the process list.\n\n" +
282
+ (state.unavailable.length
283
+ ? `Unavailable on this platform: ${state.unavailable.join(", ")}.\nRun with --sim to see every widget populated.`
284
+ : "All metrics available on this platform.") +
285
+ "\n\nPress any key to close.",
286
+ buttons: [{ label: "Close", focused: true }],
287
+ });
288
+ }
289
+ if (state.showModal) {
290
+ ui.modal({
291
+ title: "Confirm Action",
292
+ message: `Are you sure you want to terminate process ${visibleProcesses(state)[state.selected]?.pid ?? "—"} (${visibleProcesses(state)[state.selected]?.name ?? "—"})?`,
293
+ buttons: [
294
+ { label: "Yes", variant: "success", focused: true },
295
+ { label: "No", variant: "ghost" },
296
+ ],
297
+ });
298
+ }
299
+ if (state.showPalette) {
300
+ const matches = PALETTE_COMMANDS.filter((c) => c.label.toLowerCase().includes(state.paletteQuery.toLowerCase()));
301
+ ui.commandPalette({
302
+ query: state.paletteQuery,
303
+ items: matches.map((c) => ({ label: c.label, hint: c.hint })),
304
+ selected: state.paletteIndex,
305
+ });
306
+ }
307
+ });
308
+
309
+ app.on("exit", () => {
310
+ clearInterval(poll);
311
+ clearInterval(animate);
312
+ });
313
+
314
+ await app.start();
315
+ }
316
+
317
+ main().catch((error) => {
318
+ console.error(error);
319
+ process.exit(1);
320
+ });
@@ -0,0 +1,153 @@
1
+ import type { Container, Theme } from "@profullstack/hqtui";
2
+ import type { DemoState } from "../state.ts";
3
+ import { bytes, percent } from "../format.ts";
4
+
5
+ const FILES = [
6
+ { name: "src", size: "4.2 KB", type: "dir", modified: "2m ago" },
7
+ { name: "test", size: "1.1 KB", type: "dir", modified: "5m ago" },
8
+ { name: "package.json", size: "1.2 KB", type: "file", modified: "10m ago" },
9
+ { name: "README.md", size: "3.4 KB", type: "file", modified: "1h ago" },
10
+ { name: "bun.lockb", size: "12 KB", type: "file", modified: "1h ago" },
11
+ ];
12
+
13
+ const TREE = [
14
+ {
15
+ label: "systemd",
16
+ expanded: true,
17
+ values: [{ text: "1.3", width: 6 }, { text: "0.1", width: 6 }],
18
+ children: [
19
+ { label: "bash", values: [{ text: "0.1", width: 6 }, { text: "0.2", width: 6 }] },
20
+ {
21
+ label: "bun",
22
+ expanded: true,
23
+ values: [{ text: "32.8", width: 6 }, { text: "4.2", width: 6 }],
24
+ children: [
25
+ { label: "bun:worker", values: [{ text: "12.4", width: 6 }, { text: "1.8", width: 6 }] },
26
+ { label: "bun:worker", values: [{ text: "8.7", width: 6 }, { text: "1.3", width: 6 }] },
27
+ ],
28
+ },
29
+ {
30
+ label: "node",
31
+ expanded: true,
32
+ values: [{ text: "18.1", width: 6 }, { text: "2.1", width: 6 }],
33
+ children: [{ label: "node:worker", values: [{ text: "6.1", width: 6 }, { text: "0.8", width: 6 }] }],
34
+ },
35
+ { label: "postgres", values: [{ text: "6.7", width: 6 }, { text: "1.8", width: 6 }] },
36
+ ],
37
+ },
38
+ ];
39
+
40
+ /** Every widget in the library, in one screen. Also the interaction sandbox. */
41
+ export function componentsScreen(ui: Container, state: DemoState, theme: Theme): void {
42
+ ui.row({ size: "1fr", gap: 1 }, (row) => {
43
+ row.column({ gap: 1 }, (left) => {
44
+ left.panel({ title: "Buttons & Inputs", size: 13 }, (p) => {
45
+ p.row({ size: 1, gap: 1 }, (r) => {
46
+ r.button({ label: "Primary", width: 11, size: 11, onPress: () => { state.showModal = true; } });
47
+ r.button({ label: "Success", width: 11, size: 11, variant: "success" });
48
+ r.button({ label: "Warning", width: 11, size: 11, variant: "warning" });
49
+ r.button({ label: "Danger", width: 10, size: 10, variant: "danger" });
50
+ r.spacer("fill");
51
+ });
52
+ p.spacer(1);
53
+ p.row({ size: 1, gap: 2 }, (r) => {
54
+ r.select({
55
+ value: ["Dark", "Dracula", "Nord", "Tokyo Night"][state.selectIndex] ?? "Dark",
56
+ width: 20,
57
+ size: 20,
58
+ open: state.selectOpen,
59
+ options: ["Dark", "Dracula", "Nord", "Tokyo Night"],
60
+ selectedIndex: state.selectIndex,
61
+ onOpen: () => { state.selectOpen = !state.selectOpen; },
62
+ });
63
+ r.checkbox({
64
+ label: "Toggle",
65
+ checked: state.toggle,
66
+ variant: "toggle",
67
+ size: 12,
68
+ onToggle: () => { state.toggle = !state.toggle; },
69
+ });
70
+ r.checkbox({
71
+ label: "Checkbox",
72
+ checked: state.checkbox,
73
+ size: 14,
74
+ onToggle: () => { state.checkbox = !state.checkbox; },
75
+ });
76
+ r.spacer("fill");
77
+ });
78
+ p.spacer(1);
79
+ p.textInput({ label: "Search", value: state.inputValue, placeholder: "type to filter…", size: 1 });
80
+ p.spacer(1);
81
+ p.meter({ label: "Slider", value: state.slider, style: "smooth", heat: false, color: theme.primary });
82
+ p.progress({ label: "Progress", value: 37, max: 120, showCount: true });
83
+ });
84
+
85
+ left.panel({ title: "Table Widget", size: "1fr" }, (p) => {
86
+ p.table({
87
+ rows: FILES,
88
+ selected: state.selected % FILES.length,
89
+ zebra: true,
90
+ columns: [
91
+ { key: "name", title: "Name", min: 10, color: theme.primary },
92
+ { key: "size", title: "Size", width: 9, align: "right" },
93
+ { key: "type", title: "Type", width: 6 },
94
+ { key: "modified", title: "Modified", width: 10, align: "right", color: theme.muted },
95
+ ],
96
+ });
97
+ });
98
+
99
+ left.panel({ title: "Log Viewer", size: 11 }, (p) => {
100
+ p.log({
101
+ entries: state.sample.logs.map((l) => ({ time: l.time, level: l.level, message: l.message, meta: `{${l.meta}}` })),
102
+ });
103
+ });
104
+ });
105
+
106
+ row.column({ gap: 1 }, (right) => {
107
+ right.panel({ title: "Process Tree", size: 13 }, (p) => {
108
+ p.row({ size: 1 }, (r) => {
109
+ r.text("Name", { fg: theme.muted, bold: true });
110
+ r.text("CPU% MEM%", { fg: theme.muted, bold: true, align: "right" });
111
+ });
112
+ p.tree({ nodes: TREE, selected: state.selected % 8 });
113
+ });
114
+
115
+ right.panel({ title: "Sparklines & Gauges", size: 12 }, (p) => {
116
+ p.sparkline({ label: "CPU ", values: state.sample.cpu.history, text: percent(state.sample.cpu.total), color: theme.success });
117
+ p.sparkline({ label: "Mem ", values: state.sample.memory.history, text: percent(state.sample.memory.used / state.sample.memory.total), color: theme.warning });
118
+ p.sparkline({ label: "Net ", values: state.sample.network.downHistory, text: bytes(state.sample.network.downRate) + "/s", color: theme.primary });
119
+ p.spacer(1);
120
+ p.row({ size: "1fr", gap: 2 }, (r) => {
121
+ r.gauge({ value: state.sample.cpu.total, label: percent(state.sample.cpu.total) });
122
+ r.donut({
123
+ segments: [
124
+ { value: state.sample.memory.used, color: theme.primary, label: "Used" },
125
+ { value: state.sample.memory.available, color: theme.warning, label: "Free" },
126
+ ],
127
+ });
128
+ });
129
+ });
130
+
131
+ right.panel({ title: "Lists & Badges", size: "1fr" }, (p) => {
132
+ p.row({ size: 1, gap: 1 }, (r) => {
133
+ r.badge({ text: "active", color: theme.success, size: 10 });
134
+ r.badge({ text: "idle", color: theme.warning, variant: "subtle", size: 8 });
135
+ r.badge({ text: "failed", color: theme.danger, variant: "outline", size: 10 });
136
+ r.spacer("fill");
137
+ });
138
+ p.spacer(1);
139
+ p.list({
140
+ items: [
141
+ { label: "apps/demo", color: theme.primary },
142
+ { label: "packages/hqtui" },
143
+ { label: "apps/web" },
144
+ { label: "docs" },
145
+ ],
146
+ selected: state.selected % 4,
147
+ bullet: "▸",
148
+ scrollbar: true,
149
+ });
150
+ });
151
+ });
152
+ });
153
+ }