Skip to content

Commit a247b6d

Browse files
committed
add command palette
1 parent d32a2af commit a247b6d

5 files changed

Lines changed: 384 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist
33
*.log
44
.DS_Store
55

6+
*.png

src/commandPalette.tsx

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Command Palette Component
3+
* Modal overlay with command search and execution
4+
*/
5+
6+
import { For, Show, createMemo } from "solid-js";
7+
import { useStore } from "./store";
8+
import { getCommands, filterCommands } from "./commands";
9+
import { useTerminalDimensions } from "@opentui/solid";
10+
import { RGBA } from "@opentui/core";
11+
12+
export function CommandPalette() {
13+
const { store, actions } = useStore();
14+
const dimensions = useTerminalDimensions();
15+
16+
// Get filtered commands
17+
const filteredCommands = createMemo(() => {
18+
const allCommands = getCommands(actions);
19+
return filterCommands(allCommands, store.ui.commandPaletteQuery);
20+
});
21+
22+
return (
23+
<Show when={store.ui.showCommandPalette}>
24+
{/* Full-screen overlay - positioned absolutely */}
25+
<box
26+
position="absolute"
27+
left={0}
28+
top={0}
29+
width={dimensions().width}
30+
height={dimensions().height}
31+
alignItems="center"
32+
paddingTop={Math.floor(dimensions().height / 4)}
33+
backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
34+
>
35+
{/* Modal container */}
36+
<box
37+
width={60}
38+
maxWidth={dimensions().width - 2}
39+
flexDirection="column"
40+
backgroundColor="#1a1b26"
41+
border={["all"]}
42+
borderColor="#414868"
43+
paddingTop={1}
44+
>
45+
{/* Header */}
46+
<text style={{ fg: "#7aa2f7" }} paddingLeft={2} marginBottom={1}>
47+
Command Palette
48+
</text>
49+
50+
{/* Input field */}
51+
<box height={1} marginBottom={1} paddingLeft={2} paddingRight={2}>
52+
<text style={{ fg: "#c0caf5" }}>
53+
{`> ${store.ui.commandPaletteQuery}_`}
54+
</text>
55+
</box>
56+
57+
{/* Command list */}
58+
<scrollbox
59+
flexDirection="column"
60+
height={12}
61+
flexShrink={0}
62+
paddingLeft={2}
63+
paddingRight={2}
64+
>
65+
<Show
66+
when={filteredCommands().length > 0}
67+
fallback={
68+
<text style={{ fg: "#565f89" }}>No commands found</text>
69+
}
70+
>
71+
<For each={filteredCommands()}>
72+
{(command, index) => {
73+
const isSelected = () =>
74+
index() === store.ui.selectedCommandIndex;
75+
76+
return (
77+
<box
78+
flexDirection="row"
79+
justifyContent="space-between"
80+
backgroundColor={isSelected() ? "#414868" : undefined}
81+
>
82+
<text
83+
style={{
84+
fg: isSelected() ? "#7aa2f7" : "#c0caf5",
85+
}}
86+
>
87+
{`${isSelected() ? "> " : " "}${command.label}`}
88+
</text>
89+
<Show when={command.shortcut}>
90+
<text style={{ fg: "#565f89" }}>
91+
{`[${command.shortcut}]`}
92+
</text>
93+
</Show>
94+
</box>
95+
);
96+
}}
97+
</For>
98+
</Show>
99+
</scrollbox>
100+
101+
{/* Footer hint */}
102+
<box
103+
paddingTop={1}
104+
paddingLeft={2}
105+
borderColor="#30363D"
106+
border={["top"]}
107+
>
108+
<text style={{ fg: "#565f89" }}>
109+
{`↑/↓ Navigate • Enter Execute • Esc Close`}
110+
</text>
111+
</box>
112+
</box>
113+
</box>
114+
</Show>
115+
);
116+
}

src/commands.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Command Registry for Command Palette
3+
* Defines all available commands and their handlers
4+
*/
5+
6+
export interface Command {
7+
id: string;
8+
label: string;
9+
description?: string;
10+
shortcut?: string; // Display string for keyboard shortcut
11+
execute: () => void;
12+
category?: string; // For future grouping
13+
}
14+
15+
/**
16+
* Get all available commands
17+
* Actions are passed in from the store to avoid circular dependencies
18+
*/
19+
export function getCommands(actions: {
20+
expandAllSpans: () => void;
21+
collapseAllSpans: () => void;
22+
clearSpans: () => void;
23+
clearMetrics: () => void;
24+
toggleHelp: () => void;
25+
setFocusedSection: (section: "clients" | "spans" | "metrics") => void;
26+
}): Command[] {
27+
return [
28+
{
29+
id: "expand-all",
30+
label: "Expand All Spans",
31+
description: "Expand all collapsed spans in the tree view",
32+
category: "view",
33+
execute: actions.expandAllSpans,
34+
},
35+
{
36+
id: "collapse-all",
37+
label: "Collapse All Spans",
38+
description: "Collapse all expanded spans in the tree view",
39+
category: "view",
40+
execute: actions.collapseAllSpans,
41+
},
42+
{
43+
id: "clear-spans",
44+
label: "Clear Spans",
45+
description: "Remove all spans from the view",
46+
shortcut: "c",
47+
category: "actions",
48+
execute: actions.clearSpans,
49+
},
50+
{
51+
id: "clear-metrics",
52+
label: "Clear Metrics",
53+
description: "Remove all metrics from the view",
54+
shortcut: "c",
55+
category: "actions",
56+
execute: actions.clearMetrics,
57+
},
58+
{
59+
id: "toggle-help",
60+
label: "Toggle Help",
61+
description: "Show or hide the help overlay",
62+
shortcut: "? or h",
63+
category: "view",
64+
execute: actions.toggleHelp,
65+
},
66+
{
67+
id: "focus-clients",
68+
label: "Focus Clients",
69+
description: "Switch focus to the clients section",
70+
shortcut: "Tab",
71+
category: "navigation",
72+
execute: () => actions.setFocusedSection("clients"),
73+
},
74+
{
75+
id: "focus-spans",
76+
label: "Focus Spans",
77+
description: "Switch focus to the spans section",
78+
shortcut: "Tab",
79+
category: "navigation",
80+
execute: () => actions.setFocusedSection("spans"),
81+
},
82+
{
83+
id: "focus-metrics",
84+
label: "Focus Metrics",
85+
description: "Switch focus to the metrics section",
86+
shortcut: "Tab",
87+
category: "navigation",
88+
execute: () => actions.setFocusedSection("metrics"),
89+
},
90+
];
91+
}
92+
93+
/**
94+
* Filter commands by query (case-insensitive substring match)
95+
*/
96+
export function filterCommands(commands: Command[], query: string): Command[] {
97+
if (!query.trim()) return commands;
98+
99+
const lowerQuery = query.toLowerCase();
100+
return commands.filter(
101+
(cmd) =>
102+
cmd.label.toLowerCase().includes(lowerQuery) ||
103+
cmd.description?.toLowerCase().includes(lowerQuery) ||
104+
cmd.category?.toLowerCase().includes(lowerQuery),
105+
);
106+
}

src/index.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { SpanTreeView, SpanDetailsPanel } from "./spanTree";
88
import { MetricsView, MetricDetailsPanel } from "./metricsView";
99
import { ClientDropdown } from "./clientDropdown";
1010
import { ResizableBox } from "./resizableBox";
11+
import { CommandPalette } from "./commandPalette";
1112
import type { ScrollBoxRenderable } from "@opentui/core";
1213

1314
/**
@@ -67,6 +68,7 @@ Metrics Section:
6768
Navigate to select metric, details shown inline below
6869
6970
General:
71+
[:] - Open command palette (search and execute commands)
7072
[?] or [h] - Toggle this help
7173
[F12] - Toggle debug console (shows internal logs)
7274
[c] - Clear spans or metrics (depending on focused section)
@@ -108,12 +110,58 @@ function AppContent() {
108110
process.exit(0);
109111
}
110112

113+
// Command palette keyboard handling (must be before other handlers)
114+
if (store.ui.showCommandPalette) {
115+
// Close on Esc
116+
if (key.name === "escape") {
117+
actions.toggleCommandPalette();
118+
return;
119+
}
120+
121+
// Navigate commands
122+
if (key.name === "up") {
123+
actions.navigateCommandUp();
124+
return;
125+
}
126+
if (key.name === "down") {
127+
actions.navigateCommandDown();
128+
return;
129+
}
130+
131+
// Execute selected command
132+
if (key.name === "return" || key.name === "enter") {
133+
actions.executeSelectedCommand();
134+
return;
135+
}
136+
137+
// Type to filter (printable characters, backspace, space)
138+
if (key.raw && key.raw.length === 1 && !key.ctrl && !key.meta) {
139+
actions.setCommandPaletteQuery(store.ui.commandPaletteQuery + key.raw);
140+
return;
141+
}
142+
if (key.name === "backspace") {
143+
actions.setCommandPaletteQuery(
144+
store.ui.commandPaletteQuery.slice(0, -1),
145+
);
146+
return;
147+
}
148+
149+
// Prevent other keys from doing anything when palette is open
150+
return;
151+
}
152+
111153
// Help toggle
112154
if (key.name === "?" || (key.name === "h" && !key.ctrl)) {
113155
actions.toggleHelp();
114156
return;
115157
}
116158

159+
// Command palette toggle
160+
if (key.raw === ":" && !key.ctrl && !key.shift) {
161+
actions.toggleCommandPalette();
162+
return;
163+
}
164+
117165
// Toggle console overlay
118166
if (key.name === "f12") {
119167
renderer.console.toggle();
@@ -385,9 +433,12 @@ function AppContent() {
385433
{/* Footer/Status Bar */}
386434
<box height={1} width="100%" backgroundColor="#414868" paddingLeft={1}>
387435
<text style={{ fg: "#c0caf5" }}>
388-
{`${statusText()} | Port: ${PORT} | Clients: ${clientCount()} | Spans: ${spanCount()} | Metrics: ${metricCount()} | [Tab] Focus | [?] Help | [q] Quit`}
436+
{`${statusText()} | Port: ${PORT} | Clients: ${clientCount()} | Spans: ${spanCount()} | Metrics: ${metricCount()} | [Tab] Focus | [?] Help | [:] Command | [q] Quit`}
389437
</text>
390438
</box>
439+
440+
{/* Command Palette Overlay */}
441+
<CommandPalette />
391442
</>
392443
);
393444
}

0 commit comments

Comments
 (0)