tmux-ide 2.6.0 → 2.6.1
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/bin/cli.js +13 -5
- package/bin/cli.ts +1 -1
- package/bunfig.toml +4 -0
- package/package.json +11 -3
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/src/agent-explain.ts +270 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +336 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +195 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +428 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +113 -0
- package/packages/daemon/src/tui/detect/classify.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +192 -0
- package/packages/daemon/src/tui/detect/manifest.ts +184 -0
- package/packages/daemon/src/tui/detect/manifests.ts +226 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +197 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +70 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +143 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +81 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +166 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +11 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +73 -0
- package/packages/daemon/src/tui/team/sessions.ts +344 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/postinstall.js +26 -2
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createSignal, createMemo } from "solid-js";
|
|
2
|
+
import { RGBA, TextAttributes } from "@opentui/core";
|
|
3
|
+
import { useKeyboard } from "@opentui/solid";
|
|
4
|
+
import { flattenConfigTree, validateSetupConfig, addPane, type TreeNode } from "./setup-model.ts";
|
|
5
|
+
import type { IdeConfig } from "../../schemas/ide-config.ts";
|
|
6
|
+
import type { WidgetTheme, RGBA as RGBAType } from "../lib/theme.ts";
|
|
7
|
+
import { matchGrammar } from "../lib/grammar.ts";
|
|
8
|
+
|
|
9
|
+
function toRGBA(c: RGBAType): RGBA {
|
|
10
|
+
return RGBA.fromInts(c.r, c.g, c.b, c.a);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ConfigTreeProps {
|
|
14
|
+
config: IdeConfig;
|
|
15
|
+
onEditField: (path: string[]) => void;
|
|
16
|
+
onAddPane: (rowIdx: number) => void;
|
|
17
|
+
onDelete: (path: string[]) => void;
|
|
18
|
+
onSave: (config: IdeConfig) => void;
|
|
19
|
+
onConfigChange: (config: IdeConfig) => void;
|
|
20
|
+
theme: WidgetTheme;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Check if a path is a row-level container (e.g. ["rows", "0"]). */
|
|
24
|
+
function isRowPath(path: string[]): boolean {
|
|
25
|
+
return path.length === 2 && path[0] === "rows" && /^\d+$/.test(path[1]!);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Extract row index from a pane-level path (e.g. ["rows","0","panes","1","title"] → 0). */
|
|
29
|
+
function rowIndexFromPath(path: string[]): number | null {
|
|
30
|
+
if (path.length >= 2 && path[0] === "rows" && /^\d+$/.test(path[1]!)) {
|
|
31
|
+
return parseInt(path[1]!, 10);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ConfigTree(props: ConfigTreeProps) {
|
|
37
|
+
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
|
38
|
+
const [collapsed, setCollapsed] = createSignal<Set<string>>(new Set());
|
|
39
|
+
const [confirmDelete, setConfirmDelete] = createSignal<string | null>(null);
|
|
40
|
+
const [inputMode, setInputMode] = createSignal<"keyboard" | "mouse">("keyboard");
|
|
41
|
+
|
|
42
|
+
const theme = props.theme;
|
|
43
|
+
|
|
44
|
+
const allNodes = createMemo(() => flattenConfigTree(props.config));
|
|
45
|
+
|
|
46
|
+
// Filter out children of collapsed nodes
|
|
47
|
+
const visibleNodes = createMemo(() => {
|
|
48
|
+
const all = allNodes();
|
|
49
|
+
const collapsedSet = collapsed();
|
|
50
|
+
const result: TreeNode[] = [];
|
|
51
|
+
for (const node of all) {
|
|
52
|
+
// Check if any ancestor is collapsed
|
|
53
|
+
let hidden = false;
|
|
54
|
+
for (let len = 1; len < node.path.length; len++) {
|
|
55
|
+
const ancestorKey = node.path.slice(0, len).join(".");
|
|
56
|
+
if (collapsedSet.has(ancestorKey)) {
|
|
57
|
+
hidden = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!hidden) result.push(node);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const validation = createMemo(() => validateSetupConfig(props.config));
|
|
67
|
+
|
|
68
|
+
function toggleCollapse(node: TreeNode) {
|
|
69
|
+
const key = node.path.join(".");
|
|
70
|
+
setCollapsed((prev) => {
|
|
71
|
+
const next = new Set(prev);
|
|
72
|
+
if (next.has(key)) {
|
|
73
|
+
next.delete(key);
|
|
74
|
+
} else {
|
|
75
|
+
next.add(key);
|
|
76
|
+
}
|
|
77
|
+
return next;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isCollapsed(node: TreeNode): boolean {
|
|
82
|
+
return collapsed().has(node.path.join("."));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
useKeyboard((evt) => {
|
|
86
|
+
setInputMode("keyboard");
|
|
87
|
+
|
|
88
|
+
// Delete confirmation mode
|
|
89
|
+
if (confirmDelete() !== null) {
|
|
90
|
+
if (evt.name === "y") {
|
|
91
|
+
const pathStr = confirmDelete()!;
|
|
92
|
+
setConfirmDelete(null);
|
|
93
|
+
props.onDelete(pathStr.split("."));
|
|
94
|
+
evt.preventDefault();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setConfirmDelete(null);
|
|
98
|
+
evt.preventDefault();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const nodes = visibleNodes();
|
|
103
|
+
|
|
104
|
+
// The shared grammar runs FIRST; add/delete/save keys fall through below.
|
|
105
|
+
const grammar = matchGrammar(evt);
|
|
106
|
+
if (grammar === "navUp") {
|
|
107
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
108
|
+
evt.preventDefault();
|
|
109
|
+
return;
|
|
110
|
+
} else if (grammar === "navDown") {
|
|
111
|
+
setSelectedIndex((i) => Math.min(nodes.length - 1, i + 1));
|
|
112
|
+
evt.preventDefault();
|
|
113
|
+
return;
|
|
114
|
+
} else if (grammar === "activate") {
|
|
115
|
+
const node = nodes[selectedIndex()];
|
|
116
|
+
if (!node) return;
|
|
117
|
+
if (node.expandable) {
|
|
118
|
+
toggleCollapse(node);
|
|
119
|
+
} else {
|
|
120
|
+
props.onEditField(node.path);
|
|
121
|
+
}
|
|
122
|
+
evt.preventDefault();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (evt.name === "a") {
|
|
127
|
+
const node = nodes[selectedIndex()];
|
|
128
|
+
if (!node) return;
|
|
129
|
+
const rowIdx = rowIndexFromPath(node.path);
|
|
130
|
+
if (rowIdx !== null) {
|
|
131
|
+
props.onAddPane(rowIdx);
|
|
132
|
+
}
|
|
133
|
+
evt.preventDefault();
|
|
134
|
+
} else if (evt.name === "d") {
|
|
135
|
+
const node = nodes[selectedIndex()];
|
|
136
|
+
if (!node) return;
|
|
137
|
+
setConfirmDelete(node.path.join("."));
|
|
138
|
+
evt.preventDefault();
|
|
139
|
+
} else if (evt.ctrl && evt.name === "s") {
|
|
140
|
+
props.onSave(props.config);
|
|
141
|
+
evt.preventDefault();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
function nodeLabel(node: TreeNode): string {
|
|
146
|
+
const indent = " ".repeat(node.depth);
|
|
147
|
+
const prefix = node.expandable ? (isCollapsed(node) ? "▸ " : "▾ ") : " ";
|
|
148
|
+
if (node.value !== null) {
|
|
149
|
+
return `${indent}${prefix}${node.label}: ${node.value}`;
|
|
150
|
+
}
|
|
151
|
+
return `${indent}${prefix}${node.label}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
156
|
+
{/* Header */}
|
|
157
|
+
<box flexShrink={0} flexDirection="row" justifyContent="space-between" paddingBottom={1}>
|
|
158
|
+
<text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
|
|
159
|
+
Config Editor
|
|
160
|
+
</text>
|
|
161
|
+
<text fg={toRGBA(theme.fgMuted)} onMouseUp={() => props.onSave(props.config)}>
|
|
162
|
+
Ctrl+S:save
|
|
163
|
+
</text>
|
|
164
|
+
</box>
|
|
165
|
+
|
|
166
|
+
{/* Tree */}
|
|
167
|
+
<box flexGrow={1}>
|
|
168
|
+
{visibleNodes().map((node, index) => {
|
|
169
|
+
const isSelected = () => selectedIndex() === index;
|
|
170
|
+
const isRow = isRowPath(node.path);
|
|
171
|
+
return (
|
|
172
|
+
<box
|
|
173
|
+
flexShrink={0}
|
|
174
|
+
backgroundColor={isSelected() ? toRGBA(theme.selected) : undefined}
|
|
175
|
+
onMouseMove={() => {
|
|
176
|
+
setInputMode("mouse");
|
|
177
|
+
setSelectedIndex(index);
|
|
178
|
+
}}
|
|
179
|
+
onMouseDown={() => setSelectedIndex(index)}
|
|
180
|
+
onMouseUp={() => {
|
|
181
|
+
if (node.expandable) toggleCollapse(node);
|
|
182
|
+
else props.onEditField(node.path);
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<box flexDirection="row">
|
|
186
|
+
<text
|
|
187
|
+
fg={toRGBA(
|
|
188
|
+
isSelected() ? theme.accent : node.expandable ? theme.fg : theme.fgMuted,
|
|
189
|
+
)}
|
|
190
|
+
attributes={isSelected() ? TextAttributes.BOLD : 0}
|
|
191
|
+
wrapMode="none"
|
|
192
|
+
>
|
|
193
|
+
{nodeLabel(node)}
|
|
194
|
+
</text>
|
|
195
|
+
{isSelected() && isRow ? (
|
|
196
|
+
<text fg={toRGBA(theme.fgMuted)}> [a:add pane]</text>
|
|
197
|
+
) : null}
|
|
198
|
+
</box>
|
|
199
|
+
</box>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</box>
|
|
203
|
+
|
|
204
|
+
{/* Delete confirmation */}
|
|
205
|
+
{confirmDelete() !== null ? (
|
|
206
|
+
<box flexShrink={0} paddingTop={1}>
|
|
207
|
+
<text fg={toRGBA(theme.gitDeleted)} attributes={TextAttributes.BOLD}>
|
|
208
|
+
Delete {confirmDelete()}? y/n
|
|
209
|
+
</text>
|
|
210
|
+
</box>
|
|
211
|
+
) : null}
|
|
212
|
+
|
|
213
|
+
{/* Validation status */}
|
|
214
|
+
<box flexShrink={0} paddingTop={1}>
|
|
215
|
+
{validation().valid ? (
|
|
216
|
+
<text fg={toRGBA(theme.gitAdded)}>Config valid</text>
|
|
217
|
+
) : (
|
|
218
|
+
<box>
|
|
219
|
+
<text fg={toRGBA(theme.gitDeleted)} attributes={TextAttributes.BOLD}>
|
|
220
|
+
Validation errors:
|
|
221
|
+
</text>
|
|
222
|
+
{(validation() as { valid: false; errors: string[] }).errors.slice(0, 3).map((err) => (
|
|
223
|
+
<text fg={toRGBA(theme.gitDeleted)}> {err}</text>
|
|
224
|
+
))}
|
|
225
|
+
</box>
|
|
226
|
+
)}
|
|
227
|
+
</box>
|
|
228
|
+
|
|
229
|
+
{/* Footer */}
|
|
230
|
+
<box flexShrink={0}>
|
|
231
|
+
<box flexShrink={0} height={1}>
|
|
232
|
+
<text fg={toRGBA(theme.border)} wrapMode="none">
|
|
233
|
+
{"─".repeat(40)}
|
|
234
|
+
</text>
|
|
235
|
+
</box>
|
|
236
|
+
<box flexDirection="row" gap={2}>
|
|
237
|
+
<text fg={toRGBA(theme.fgMuted)}>j/k:nav</text>
|
|
238
|
+
<text fg={toRGBA(theme.fgMuted)}>Enter:edit/toggle</text>
|
|
239
|
+
<text fg={toRGBA(theme.fgMuted)}>a:add pane</text>
|
|
240
|
+
<text fg={toRGBA(theme.fgMuted)}>d:delete</text>
|
|
241
|
+
<text fg={toRGBA(theme.fgMuted)}>Ctrl+S:save</text>
|
|
242
|
+
</box>
|
|
243
|
+
</box>
|
|
244
|
+
</box>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { RGBA, TextAttributes } from "@opentui/core";
|
|
2
|
+
import { useKeyboard } from "@opentui/solid";
|
|
3
|
+
import type { WidgetTheme, RGBA as RGBAType } from "../lib/theme.ts";
|
|
4
|
+
|
|
5
|
+
function toRGBA(c: RGBAType): RGBA {
|
|
6
|
+
return RGBA.fromInts(c.r, c.g, c.b, c.a);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DetectedStackInfo {
|
|
10
|
+
packageManager: string | null;
|
|
11
|
+
language: string | null;
|
|
12
|
+
frameworks: string[];
|
|
13
|
+
devCommand: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DetectPanelProps {
|
|
17
|
+
detected: DetectedStackInfo;
|
|
18
|
+
onContinue: () => void;
|
|
19
|
+
theme: WidgetTheme;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function DetectPanel(props: DetectPanelProps) {
|
|
23
|
+
const theme = props.theme;
|
|
24
|
+
|
|
25
|
+
useKeyboard((evt) => {
|
|
26
|
+
if (evt.name === "return") {
|
|
27
|
+
props.onContinue();
|
|
28
|
+
evt.preventDefault();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function field(label: string, value: string | null): string {
|
|
33
|
+
return `${label}: ${value ?? "not detected"}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
38
|
+
{/* Header */}
|
|
39
|
+
<box flexShrink={0} paddingBottom={1}>
|
|
40
|
+
<text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
|
|
41
|
+
Project Detected
|
|
42
|
+
</text>
|
|
43
|
+
</box>
|
|
44
|
+
|
|
45
|
+
{/* Detection results */}
|
|
46
|
+
<box flexShrink={0} paddingBottom={1}>
|
|
47
|
+
<text fg={toRGBA(theme.fg)}>{field("Package Manager", props.detected.packageManager)}</text>
|
|
48
|
+
<text fg={toRGBA(theme.fg)}>{field("Language", props.detected.language)}</text>
|
|
49
|
+
<text fg={toRGBA(theme.fg)}>
|
|
50
|
+
{field(
|
|
51
|
+
"Frameworks",
|
|
52
|
+
props.detected.frameworks.length > 0 ? props.detected.frameworks.join(", ") : null,
|
|
53
|
+
)}
|
|
54
|
+
</text>
|
|
55
|
+
<text fg={toRGBA(theme.fg)}>{field("Dev Command", props.detected.devCommand)}</text>
|
|
56
|
+
</box>
|
|
57
|
+
|
|
58
|
+
{/* Spacer */}
|
|
59
|
+
<box flexGrow={1} />
|
|
60
|
+
|
|
61
|
+
{/* Footer */}
|
|
62
|
+
<box flexShrink={0} onMouseDown={() => props.onContinue()}>
|
|
63
|
+
<box flexShrink={0} height={1}>
|
|
64
|
+
<text fg={toRGBA(theme.border)} wrapMode="none">
|
|
65
|
+
{"─".repeat(40)}
|
|
66
|
+
</text>
|
|
67
|
+
</box>
|
|
68
|
+
<text fg={toRGBA(theme.accent)}>Enter:continue</text>
|
|
69
|
+
</box>
|
|
70
|
+
</box>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createSignal, onMount } from "solid-js";
|
|
2
|
+
import { RGBA, TextAttributes, type InputRenderable } from "@opentui/core";
|
|
3
|
+
import { useKeyboard } from "@opentui/solid";
|
|
4
|
+
import type { WidgetTheme, RGBA as RGBAType } from "../lib/theme.ts";
|
|
5
|
+
|
|
6
|
+
function toRGBA(c: RGBAType): RGBA {
|
|
7
|
+
return RGBA.fromInts(c.r, c.g, c.b, c.a);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type FieldType = "string" | "enum" | "boolean" | "size";
|
|
11
|
+
|
|
12
|
+
export interface FieldEditorProps {
|
|
13
|
+
path: string[];
|
|
14
|
+
value: string;
|
|
15
|
+
fieldType: FieldType;
|
|
16
|
+
enumValues?: string[];
|
|
17
|
+
onSave: (path: string[], newValue: unknown) => void;
|
|
18
|
+
onCancel: () => void;
|
|
19
|
+
theme: WidgetTheme;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Validate a size string (e.g. "70%", "50%"). */
|
|
23
|
+
function isValidSize(v: string): boolean {
|
|
24
|
+
return /^\d{1,3}%$/.test(v);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// String / Size editor (input-based)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function StringEditor(props: {
|
|
32
|
+
value: string;
|
|
33
|
+
isSize: boolean;
|
|
34
|
+
onSave: (v: string) => void;
|
|
35
|
+
onCancel: () => void;
|
|
36
|
+
theme: WidgetTheme;
|
|
37
|
+
}) {
|
|
38
|
+
const [text, setText] = createSignal(props.value);
|
|
39
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
40
|
+
let inputRef: InputRenderable | undefined;
|
|
41
|
+
|
|
42
|
+
onMount(() => {
|
|
43
|
+
setTimeout(() => inputRef?.focus(), 50);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
useKeyboard((evt) => {
|
|
47
|
+
if (evt.name === "escape") {
|
|
48
|
+
props.onCancel();
|
|
49
|
+
evt.preventDefault();
|
|
50
|
+
} else if (evt.name === "return") {
|
|
51
|
+
const val = text();
|
|
52
|
+
if (props.isSize && !isValidSize(val)) {
|
|
53
|
+
setError("Must be a percentage (e.g. 70%)");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
props.onSave(val);
|
|
57
|
+
evt.preventDefault();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<box>
|
|
63
|
+
<input
|
|
64
|
+
value={text()}
|
|
65
|
+
placeholder={props.isSize ? "e.g. 70%" : "Enter value..."}
|
|
66
|
+
onInput={(v: string) => {
|
|
67
|
+
setText(v);
|
|
68
|
+
setError(null);
|
|
69
|
+
}}
|
|
70
|
+
focusedBackgroundColor={toRGBA(props.theme.selected)}
|
|
71
|
+
cursorColor={toRGBA(props.theme.accent)}
|
|
72
|
+
focusedTextColor={toRGBA(props.theme.fg)}
|
|
73
|
+
onMouseDown={() => inputRef?.focus()}
|
|
74
|
+
ref={(r: InputRenderable) => {
|
|
75
|
+
inputRef = r;
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
{error() ? <text fg={toRGBA(props.theme.gitDeleted)}>{error()}</text> : null}
|
|
79
|
+
</box>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Enum editor (list selection)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
function EnumEditor(props: {
|
|
88
|
+
value: string;
|
|
89
|
+
values: string[];
|
|
90
|
+
onSave: (v: string) => void;
|
|
91
|
+
onCancel: () => void;
|
|
92
|
+
theme: WidgetTheme;
|
|
93
|
+
}) {
|
|
94
|
+
const initial = Math.max(0, props.values.indexOf(props.value));
|
|
95
|
+
const [selectedIndex, setSelectedIndex] = createSignal(initial);
|
|
96
|
+
const [inputMode, setInputMode] = createSignal<"keyboard" | "mouse">("keyboard");
|
|
97
|
+
|
|
98
|
+
useKeyboard((evt) => {
|
|
99
|
+
setInputMode("keyboard");
|
|
100
|
+
if (evt.name === "escape") {
|
|
101
|
+
props.onCancel();
|
|
102
|
+
evt.preventDefault();
|
|
103
|
+
} else if (evt.name === "k" || evt.name === "up") {
|
|
104
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
105
|
+
evt.preventDefault();
|
|
106
|
+
} else if (evt.name === "j" || evt.name === "down") {
|
|
107
|
+
setSelectedIndex((i) => Math.min(props.values.length - 1, i + 1));
|
|
108
|
+
evt.preventDefault();
|
|
109
|
+
} else if (evt.name === "return") {
|
|
110
|
+
const val = props.values[selectedIndex()];
|
|
111
|
+
if (val !== undefined) props.onSave(val);
|
|
112
|
+
evt.preventDefault();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<box>
|
|
118
|
+
{props.values.map((val, index) => {
|
|
119
|
+
const isSelected = () => selectedIndex() === index;
|
|
120
|
+
return (
|
|
121
|
+
<box
|
|
122
|
+
flexShrink={0}
|
|
123
|
+
backgroundColor={isSelected() ? toRGBA(props.theme.selected) : undefined}
|
|
124
|
+
onMouseMove={() => {
|
|
125
|
+
setInputMode("mouse");
|
|
126
|
+
setSelectedIndex(index);
|
|
127
|
+
}}
|
|
128
|
+
onMouseDown={() => setSelectedIndex(index)}
|
|
129
|
+
onMouseUp={() => props.onSave(val)}
|
|
130
|
+
>
|
|
131
|
+
<text
|
|
132
|
+
fg={toRGBA(isSelected() ? props.theme.accent : props.theme.fg)}
|
|
133
|
+
attributes={isSelected() ? TextAttributes.BOLD : 0}
|
|
134
|
+
>
|
|
135
|
+
{isSelected() ? "▸ " : " "}
|
|
136
|
+
{val}
|
|
137
|
+
</text>
|
|
138
|
+
</box>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</box>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Boolean editor (toggle)
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function BooleanEditor(props: {
|
|
150
|
+
value: string;
|
|
151
|
+
onSave: (v: boolean) => void;
|
|
152
|
+
onCancel: () => void;
|
|
153
|
+
theme: WidgetTheme;
|
|
154
|
+
}) {
|
|
155
|
+
const [val, setVal] = createSignal(props.value === "true");
|
|
156
|
+
|
|
157
|
+
useKeyboard((evt) => {
|
|
158
|
+
if (evt.name === "escape") {
|
|
159
|
+
props.onCancel();
|
|
160
|
+
evt.preventDefault();
|
|
161
|
+
} else if (evt.name === "space") {
|
|
162
|
+
setVal((v) => !v);
|
|
163
|
+
evt.preventDefault();
|
|
164
|
+
} else if (evt.name === "return") {
|
|
165
|
+
props.onSave(val());
|
|
166
|
+
evt.preventDefault();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<box flexDirection="row" gap={1}>
|
|
172
|
+
<text
|
|
173
|
+
fg={toRGBA(props.theme.accent)}
|
|
174
|
+
attributes={TextAttributes.BOLD}
|
|
175
|
+
onMouseUp={() => setVal((v) => !v)}
|
|
176
|
+
>
|
|
177
|
+
{val() ? "[x]" : "[ ]"}
|
|
178
|
+
</text>
|
|
179
|
+
<text fg={toRGBA(props.theme.fg)} onMouseUp={() => setVal((v) => !v)}>
|
|
180
|
+
{val() ? "true" : "false"}
|
|
181
|
+
</text>
|
|
182
|
+
<text fg={toRGBA(props.theme.fgMuted)}>(Space to toggle, click to flip)</text>
|
|
183
|
+
</box>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Main FieldEditor
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
export function FieldEditor(props: FieldEditorProps) {
|
|
192
|
+
const theme = props.theme;
|
|
193
|
+
const pathLabel = props.path.join(".");
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
197
|
+
{/* Header */}
|
|
198
|
+
<box flexShrink={0} flexDirection="row" justifyContent="space-between" paddingBottom={1}>
|
|
199
|
+
<text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
|
|
200
|
+
Edit: {pathLabel}
|
|
201
|
+
</text>
|
|
202
|
+
<text fg={toRGBA(theme.fgMuted)} onMouseUp={() => props.onCancel()}>
|
|
203
|
+
Esc:cancel
|
|
204
|
+
</text>
|
|
205
|
+
</box>
|
|
206
|
+
|
|
207
|
+
{/* Current value */}
|
|
208
|
+
<box flexShrink={0} paddingBottom={1}>
|
|
209
|
+
<text fg={toRGBA(theme.fgMuted)}>Current: {props.value || "(empty)"}</text>
|
|
210
|
+
</box>
|
|
211
|
+
|
|
212
|
+
{/* Editor */}
|
|
213
|
+
<box flexShrink={0} paddingBottom={1}>
|
|
214
|
+
{props.fieldType === "enum" ? (
|
|
215
|
+
<EnumEditor
|
|
216
|
+
value={props.value}
|
|
217
|
+
values={props.enumValues ?? []}
|
|
218
|
+
onSave={(v) => props.onSave(props.path, v)}
|
|
219
|
+
onCancel={props.onCancel}
|
|
220
|
+
theme={theme}
|
|
221
|
+
/>
|
|
222
|
+
) : props.fieldType === "boolean" ? (
|
|
223
|
+
<BooleanEditor
|
|
224
|
+
value={props.value}
|
|
225
|
+
onSave={(v) => props.onSave(props.path, v)}
|
|
226
|
+
onCancel={props.onCancel}
|
|
227
|
+
theme={theme}
|
|
228
|
+
/>
|
|
229
|
+
) : (
|
|
230
|
+
<StringEditor
|
|
231
|
+
value={props.value}
|
|
232
|
+
isSize={props.fieldType === "size"}
|
|
233
|
+
onSave={(v) => props.onSave(props.path, v)}
|
|
234
|
+
onCancel={props.onCancel}
|
|
235
|
+
theme={theme}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
</box>
|
|
239
|
+
|
|
240
|
+
{/* Spacer */}
|
|
241
|
+
<box flexGrow={1} />
|
|
242
|
+
|
|
243
|
+
{/* Footer */}
|
|
244
|
+
<box flexShrink={0}>
|
|
245
|
+
<box flexShrink={0} height={1}>
|
|
246
|
+
<text fg={toRGBA(theme.border)} wrapMode="none">
|
|
247
|
+
{"─".repeat(40)}
|
|
248
|
+
</text>
|
|
249
|
+
</box>
|
|
250
|
+
<box flexDirection="row" gap={2}>
|
|
251
|
+
{props.fieldType === "enum" ? (
|
|
252
|
+
<text fg={toRGBA(theme.fgMuted)}>j/k:navigate Enter:select</text>
|
|
253
|
+
) : props.fieldType === "boolean" ? (
|
|
254
|
+
<text fg={toRGBA(theme.fgMuted)}>Space:toggle Enter:confirm</text>
|
|
255
|
+
) : (
|
|
256
|
+
<text fg={toRGBA(theme.fgMuted)}>Enter:save</text>
|
|
257
|
+
)}
|
|
258
|
+
<text fg={toRGBA(theme.fgMuted)} onMouseUp={() => props.onCancel()}>
|
|
259
|
+
Esc:cancel
|
|
260
|
+
</text>
|
|
261
|
+
</box>
|
|
262
|
+
</box>
|
|
263
|
+
</box>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { RGBA, TextAttributes } from "@opentui/core";
|
|
2
|
+
import type { WidgetTheme, RGBA as RGBAType } from "../lib/theme.ts";
|
|
3
|
+
|
|
4
|
+
function toRGBA(c: RGBAType): RGBA {
|
|
5
|
+
return RGBA.fromInts(c.r, c.g, c.b, c.a);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ViewKind =
|
|
9
|
+
| "detect"
|
|
10
|
+
| "layout-picker"
|
|
11
|
+
| "agent-naming"
|
|
12
|
+
| "orchestrator"
|
|
13
|
+
| "review"
|
|
14
|
+
| "editor-tree"
|
|
15
|
+
| "editor-field";
|
|
16
|
+
|
|
17
|
+
export interface FooterActions {
|
|
18
|
+
onConfirm?: () => void;
|
|
19
|
+
onBack?: () => void;
|
|
20
|
+
onQuit?: () => void;
|
|
21
|
+
onAdd?: () => void;
|
|
22
|
+
onDelete?: () => void;
|
|
23
|
+
onSave?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FooterProps {
|
|
27
|
+
viewKind: ViewKind;
|
|
28
|
+
theme: WidgetTheme;
|
|
29
|
+
actions?: FooterActions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface HintDef {
|
|
33
|
+
label: string;
|
|
34
|
+
actionKey?: keyof FooterActions;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const VIEW_HINTS: Record<ViewKind, HintDef[]> = {
|
|
38
|
+
detect: [
|
|
39
|
+
{ label: "Enter:continue", actionKey: "onConfirm" },
|
|
40
|
+
{ label: "q:quit", actionKey: "onQuit" },
|
|
41
|
+
],
|
|
42
|
+
"layout-picker": [
|
|
43
|
+
{ label: "j/k:navigate" },
|
|
44
|
+
{ label: "Enter:select", actionKey: "onConfirm" },
|
|
45
|
+
{ label: "q:quit", actionKey: "onQuit" },
|
|
46
|
+
],
|
|
47
|
+
"agent-naming": [
|
|
48
|
+
{ label: "Tab:next field" },
|
|
49
|
+
{ label: "Enter:continue", actionKey: "onConfirm" },
|
|
50
|
+
{ label: "Esc:back", actionKey: "onBack" },
|
|
51
|
+
{ label: "q:quit", actionKey: "onQuit" },
|
|
52
|
+
],
|
|
53
|
+
orchestrator: [
|
|
54
|
+
{ label: "Space:toggle" },
|
|
55
|
+
{ label: "Enter:continue", actionKey: "onConfirm" },
|
|
56
|
+
{ label: "Esc:back", actionKey: "onBack" },
|
|
57
|
+
{ label: "q:quit", actionKey: "onQuit" },
|
|
58
|
+
],
|
|
59
|
+
review: [
|
|
60
|
+
{ label: "Enter:confirm", actionKey: "onConfirm" },
|
|
61
|
+
{ label: "Esc:back", actionKey: "onBack" },
|
|
62
|
+
{ label: "q:quit", actionKey: "onQuit" },
|
|
63
|
+
],
|
|
64
|
+
"editor-tree": [
|
|
65
|
+
{ label: "j/k:nav" },
|
|
66
|
+
{ label: "Enter:edit", actionKey: "onConfirm" },
|
|
67
|
+
{ label: "a:add pane", actionKey: "onAdd" },
|
|
68
|
+
{ label: "d:delete", actionKey: "onDelete" },
|
|
69
|
+
{ label: "Ctrl+S:save", actionKey: "onSave" },
|
|
70
|
+
{ label: "q:quit", actionKey: "onQuit" },
|
|
71
|
+
],
|
|
72
|
+
"editor-field": [
|
|
73
|
+
{ label: "Enter:save", actionKey: "onConfirm" },
|
|
74
|
+
{ label: "Esc:cancel", actionKey: "onBack" },
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function Footer(props: FooterProps) {
|
|
79
|
+
const theme = props.theme;
|
|
80
|
+
const hints = () => VIEW_HINTS[props.viewKind] ?? [];
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<box flexShrink={0}>
|
|
84
|
+
<box flexShrink={0} height={1}>
|
|
85
|
+
<text fg={toRGBA(theme.border)} wrapMode="none">
|
|
86
|
+
{"─".repeat(60)}
|
|
87
|
+
</text>
|
|
88
|
+
</box>
|
|
89
|
+
<box flexDirection="row" gap={2}>
|
|
90
|
+
{hints().map((hint) => {
|
|
91
|
+
const handler = hint.actionKey ? props.actions?.[hint.actionKey] : undefined;
|
|
92
|
+
const isClickable = !!handler;
|
|
93
|
+
return (
|
|
94
|
+
<text
|
|
95
|
+
fg={toRGBA(isClickable ? theme.fg : theme.fgMuted)}
|
|
96
|
+
attributes={isClickable ? TextAttributes.UNDERLINE : 0}
|
|
97
|
+
wrapMode="none"
|
|
98
|
+
onMouseDown={handler ? () => handler() : undefined}
|
|
99
|
+
>
|
|
100
|
+
{hint.label}
|
|
101
|
+
</text>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
</box>
|
|
105
|
+
</box>
|
|
106
|
+
);
|
|
107
|
+
}
|