tmux-ide 2.6.0 → 2.7.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.
- package/README.md +14 -9
- package/bin/cli.js +1024 -519
- package/bin/cli.ts +63 -6
- package/bunfig.toml +4 -0
- package/package.json +18 -7
- 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/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -0
- package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
- package/packages/daemon/src/agent-explain.ts +298 -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 +427 -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/tui-binary.ts +165 -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 +382 -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 +510 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +121 -0
- package/packages/daemon/src/tui/detect/classify.ts +208 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest.ts +199 -0
- package/packages/daemon/src/tui/detect/manifests.ts +354 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +217 -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 +82 -0
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
- package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
- package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
- package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
- package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
- package/packages/daemon/src/tui/mirror/palette.ts +274 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
- package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
- package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
- package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
- package/packages/daemon/src/tui/mirror/selection.ts +262 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
- package/packages/daemon/src/tui/mirror/spans.ts +46 -0
- package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +38 -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 +83 -0
- package/packages/daemon/src/tui/team/sessions.ts +433 -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/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +26 -2
- package/skill/SKILL.md +22 -0
- package/templates/AGENTS.md +14 -7
- package/templates/agent-team-monorepo.yml +8 -0
- package/templates/agent-team-nextjs.yml +8 -0
- package/templates/agent-team.yml +10 -0
- package/templates/convex.yml +2 -0
- package/templates/default.yml +11 -5
- package/templates/go.yml +4 -0
- package/templates/missions.yml +6 -0
- package/templates/nextjs.yml +4 -0
- package/templates/python.yml +4 -0
- package/templates/skills/backend.md +5 -12
- package/templates/skills/frontend.md +5 -12
- package/templates/skills/general-worker.md +5 -12
- package/templates/skills/researcher.md +7 -12
- package/templates/skills/reviewer.md +7 -16
- package/templates/vite.yml +4 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createMemo, createEffect, For, Show } from "solid-js";
|
|
2
|
+
import { RGBA, type ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import type { TreeNode } from "./tree-model.ts";
|
|
4
|
+
import type { WidgetTheme } from "../lib/theme.ts";
|
|
5
|
+
|
|
6
|
+
function toRGBA(c: { r: number; g: number; b: number; a: number }): RGBA {
|
|
7
|
+
return RGBA.fromInts(c.r, c.g, c.b, c.a);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TRANSPARENT = RGBA.fromInts(0, 0, 0, 0);
|
|
11
|
+
|
|
12
|
+
function getStatusLabel(status: string): string {
|
|
13
|
+
switch (status) {
|
|
14
|
+
case "M":
|
|
15
|
+
return " M";
|
|
16
|
+
case "A":
|
|
17
|
+
return " A";
|
|
18
|
+
case "D":
|
|
19
|
+
return " D";
|
|
20
|
+
case "?":
|
|
21
|
+
return " ?";
|
|
22
|
+
default:
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getStatusColor(
|
|
28
|
+
status: string,
|
|
29
|
+
theme: WidgetTheme,
|
|
30
|
+
): { r: number; g: number; b: number; a: number } {
|
|
31
|
+
switch (status) {
|
|
32
|
+
case "M":
|
|
33
|
+
return theme.gitModified;
|
|
34
|
+
case "A":
|
|
35
|
+
return theme.gitAdded;
|
|
36
|
+
case "D":
|
|
37
|
+
return theme.gitDeleted;
|
|
38
|
+
case "?":
|
|
39
|
+
return theme.gitUntracked;
|
|
40
|
+
default:
|
|
41
|
+
return theme.fgMuted;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getNameColor(
|
|
46
|
+
name: string,
|
|
47
|
+
isDir: boolean,
|
|
48
|
+
isSelected: boolean,
|
|
49
|
+
isIgnored: boolean,
|
|
50
|
+
theme: WidgetTheme,
|
|
51
|
+
): { r: number; g: number; b: number; a: number } {
|
|
52
|
+
if (isSelected) return theme.selectedText;
|
|
53
|
+
if (isIgnored) return theme.ignored;
|
|
54
|
+
if (isDir) return theme.dirName;
|
|
55
|
+
if (name.endsWith(".lock") || name.startsWith(".") || name === "LICENSE") {
|
|
56
|
+
return theme.fgMuted;
|
|
57
|
+
}
|
|
58
|
+
return theme.fg;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface FileTreeProps {
|
|
62
|
+
nodes: TreeNode[];
|
|
63
|
+
selected: number;
|
|
64
|
+
theme: WidgetTheme;
|
|
65
|
+
inputMode: "keyboard" | "mouse";
|
|
66
|
+
onSelect: (index: number) => void;
|
|
67
|
+
onActivate: (node: TreeNode) => void;
|
|
68
|
+
onInputModeChange: (mode: "keyboard" | "mouse") => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function FileTree(props: FileTreeProps) {
|
|
72
|
+
let scroll: ScrollBoxRenderable | undefined;
|
|
73
|
+
|
|
74
|
+
createEffect(() => {
|
|
75
|
+
const idx = props.selected;
|
|
76
|
+
if (!scroll) return;
|
|
77
|
+
const children = scroll.getChildren();
|
|
78
|
+
const target = children[idx];
|
|
79
|
+
if (!target) return;
|
|
80
|
+
const y = target.y - scroll.y;
|
|
81
|
+
if (y >= scroll.height) scroll.scrollBy(y - scroll.height + 1);
|
|
82
|
+
if (y < 0) scroll.scrollBy(y);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<scrollbox
|
|
87
|
+
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
|
88
|
+
flexGrow={1}
|
|
89
|
+
verticalScrollbarOptions={{
|
|
90
|
+
trackOptions: {
|
|
91
|
+
backgroundColor: toRGBA(props.theme.bg),
|
|
92
|
+
foregroundColor: toRGBA(props.theme.accent),
|
|
93
|
+
},
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<Show
|
|
97
|
+
when={props.nodes.length > 0}
|
|
98
|
+
fallback={
|
|
99
|
+
<box paddingLeft={2} paddingTop={1}>
|
|
100
|
+
<text fg={toRGBA(props.theme.fgMuted)}>empty directory</text>
|
|
101
|
+
</box>
|
|
102
|
+
}
|
|
103
|
+
>
|
|
104
|
+
<For each={props.nodes}>
|
|
105
|
+
{(node, index) => {
|
|
106
|
+
const isSelected = createMemo(() => index() === props.selected);
|
|
107
|
+
const icon = node.entry.isDir ? "> " : " ";
|
|
108
|
+
const nameColor = () =>
|
|
109
|
+
getNameColor(
|
|
110
|
+
node.entry.name,
|
|
111
|
+
node.entry.isDir,
|
|
112
|
+
isSelected(),
|
|
113
|
+
node.entry.ignored,
|
|
114
|
+
props.theme,
|
|
115
|
+
);
|
|
116
|
+
const rowBg = () =>
|
|
117
|
+
isSelected()
|
|
118
|
+
? toRGBA(props.theme.selected)
|
|
119
|
+
: index() % 2 === 1
|
|
120
|
+
? toRGBA(props.theme.rowAlt)
|
|
121
|
+
: TRANSPARENT;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<box
|
|
125
|
+
id={String(index())}
|
|
126
|
+
backgroundColor={rowBg()}
|
|
127
|
+
flexDirection="row"
|
|
128
|
+
paddingLeft={1}
|
|
129
|
+
paddingRight={1}
|
|
130
|
+
onMouseMove={() => {
|
|
131
|
+
props.onInputModeChange("mouse");
|
|
132
|
+
props.onSelect(index());
|
|
133
|
+
}}
|
|
134
|
+
onMouseDown={() => props.onSelect(index())}
|
|
135
|
+
onMouseUp={() => {
|
|
136
|
+
props.onActivate(node);
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<text fg={toRGBA(nameColor())} wrapMode="none" flexGrow={1}>
|
|
140
|
+
{icon}
|
|
141
|
+
{node.entry.name}
|
|
142
|
+
{node.entry.isDir ? "/" : ""}
|
|
143
|
+
</text>
|
|
144
|
+
<Show when={node.gitStatus}>
|
|
145
|
+
<text
|
|
146
|
+
fg={toRGBA(getStatusColor(node.gitStatus!, props.theme))}
|
|
147
|
+
flexShrink={0}
|
|
148
|
+
wrapMode="none"
|
|
149
|
+
>
|
|
150
|
+
{getStatusLabel(node.gitStatus!)}
|
|
151
|
+
</text>
|
|
152
|
+
</Show>
|
|
153
|
+
<Show when={isSelected() && !node.entry.isDir}>
|
|
154
|
+
<text fg={toRGBA(props.theme.fgMuted)} flexShrink={0} wrapMode="none">
|
|
155
|
+
{" c: send to claude code"}
|
|
156
|
+
</text>
|
|
157
|
+
</Show>
|
|
158
|
+
</box>
|
|
159
|
+
);
|
|
160
|
+
}}
|
|
161
|
+
</For>
|
|
162
|
+
</Show>
|
|
163
|
+
</scrollbox>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { IdeConfigSchema, type IdeConfig, type Row } from "../../schemas/ide-config.ts";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Config Tree
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export interface TreeNode {
|
|
8
|
+
path: string[];
|
|
9
|
+
label: string;
|
|
10
|
+
value: string | null;
|
|
11
|
+
depth: number;
|
|
12
|
+
expandable: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function flattenValue(obj: unknown, path: string[], depth: number, nodes: TreeNode[]): void {
|
|
16
|
+
if (Array.isArray(obj)) {
|
|
17
|
+
const label = path[path.length - 1] ?? "";
|
|
18
|
+
nodes.push({ path: [...path], label, value: null, depth, expandable: true });
|
|
19
|
+
for (let i = 0; i < obj.length; i++) {
|
|
20
|
+
flattenValue(obj[i], [...path, String(i)], depth + 1, nodes);
|
|
21
|
+
}
|
|
22
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
23
|
+
const label = path[path.length - 1] ?? "";
|
|
24
|
+
nodes.push({ path: [...path], label, value: null, depth, expandable: true });
|
|
25
|
+
for (const [key, val] of Object.entries(obj as Record<string, unknown>)) {
|
|
26
|
+
flattenValue(val, [...path, key], depth + 1, nodes);
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
const label = path[path.length - 1] ?? "";
|
|
30
|
+
nodes.push({
|
|
31
|
+
path: [...path],
|
|
32
|
+
label,
|
|
33
|
+
value: obj === undefined || obj === null ? null : String(obj),
|
|
34
|
+
depth,
|
|
35
|
+
expandable: false,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function flattenConfigTree(config: IdeConfig): TreeNode[] {
|
|
41
|
+
const nodes: TreeNode[] = [];
|
|
42
|
+
for (const [key, val] of Object.entries(config)) {
|
|
43
|
+
if (val === undefined) continue;
|
|
44
|
+
flattenValue(val, [key], 0, nodes);
|
|
45
|
+
}
|
|
46
|
+
return nodes;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Config Mutations
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export function deepClone<T>(obj: T): T {
|
|
54
|
+
return JSON.parse(JSON.stringify(obj)) as T;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function updateConfigAtPath(config: IdeConfig, path: string[], value: unknown): IdeConfig {
|
|
58
|
+
const cloned = deepClone(config);
|
|
59
|
+
let current: Record<string, unknown> = cloned as unknown as Record<string, unknown>;
|
|
60
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
61
|
+
const key = path[i]!;
|
|
62
|
+
current = current[key] as Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
const lastKey = path[path.length - 1]!;
|
|
65
|
+
current[lastKey] = value;
|
|
66
|
+
return cloned;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function addPane(config: IdeConfig, rowIdx: number): IdeConfig {
|
|
70
|
+
const cloned = deepClone(config);
|
|
71
|
+
const row = cloned.rows[rowIdx];
|
|
72
|
+
if (!row) return cloned;
|
|
73
|
+
row.panes.push({ title: "New Pane" });
|
|
74
|
+
return cloned;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function removePane(config: IdeConfig, rowIdx: number, paneIdx: number): IdeConfig {
|
|
78
|
+
const cloned = deepClone(config);
|
|
79
|
+
const row = cloned.rows[rowIdx];
|
|
80
|
+
if (!row) return cloned;
|
|
81
|
+
if (row.panes.length <= 1) return cloned;
|
|
82
|
+
row.panes.splice(paneIdx, 1);
|
|
83
|
+
return cloned;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function addRow(config: IdeConfig, size?: string): IdeConfig {
|
|
87
|
+
const cloned = deepClone(config);
|
|
88
|
+
const row: Row = { panes: [{ title: "Shell" }] };
|
|
89
|
+
if (size) row.size = size;
|
|
90
|
+
cloned.rows.push(row);
|
|
91
|
+
return cloned;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function removeRow(config: IdeConfig, rowIdx: number): IdeConfig {
|
|
95
|
+
const cloned = deepClone(config);
|
|
96
|
+
if (cloned.rows.length <= 1) return cloned;
|
|
97
|
+
cloned.rows.splice(rowIdx, 1);
|
|
98
|
+
return cloned;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Validation
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
export function validateSetupConfig(
|
|
106
|
+
config: unknown,
|
|
107
|
+
): { valid: true; config: IdeConfig } | { valid: false; errors: string[] } {
|
|
108
|
+
const result = IdeConfigSchema.safeParse(config);
|
|
109
|
+
if (result.success) {
|
|
110
|
+
return { valid: true, config: result.data };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
valid: false,
|
|
114
|
+
errors: result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import ignore, { type Ignore } from "ignore";
|
|
4
|
+
|
|
5
|
+
const ALWAYS_IGNORE = new Set([
|
|
6
|
+
"node_modules",
|
|
7
|
+
".git",
|
|
8
|
+
".svn",
|
|
9
|
+
".hg",
|
|
10
|
+
"dist",
|
|
11
|
+
"build",
|
|
12
|
+
"out",
|
|
13
|
+
".next",
|
|
14
|
+
".turbo",
|
|
15
|
+
".cache",
|
|
16
|
+
"__pycache__",
|
|
17
|
+
"coverage",
|
|
18
|
+
".nyc_output",
|
|
19
|
+
"target",
|
|
20
|
+
"vendor",
|
|
21
|
+
"bower_components",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export { type Ignore };
|
|
25
|
+
|
|
26
|
+
export interface FileEntry {
|
|
27
|
+
name: string;
|
|
28
|
+
path: string; // relative to root
|
|
29
|
+
absolutePath: string;
|
|
30
|
+
isDir: boolean;
|
|
31
|
+
ignored: boolean; // gitignored
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createIgnoreFilter(rootDir: string): Ignore {
|
|
35
|
+
const ig = ignore();
|
|
36
|
+
const gitignorePath = join(rootDir, ".gitignore");
|
|
37
|
+
if (existsSync(gitignorePath)) {
|
|
38
|
+
ig.add(readFileSync(gitignorePath, "utf-8"));
|
|
39
|
+
}
|
|
40
|
+
return ig;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readDirectory(
|
|
44
|
+
dir: string,
|
|
45
|
+
rootDir: string,
|
|
46
|
+
ig: Ignore,
|
|
47
|
+
showHidden: boolean,
|
|
48
|
+
showIgnored: boolean = false,
|
|
49
|
+
): FileEntry[] {
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return entries
|
|
57
|
+
.filter((e) => {
|
|
58
|
+
if (ALWAYS_IGNORE.has(e.name)) return false;
|
|
59
|
+
if (!showHidden && e.name.startsWith(".")) return false;
|
|
60
|
+
if (showIgnored) return true;
|
|
61
|
+
const rel = relative(rootDir, join(dir, e.name));
|
|
62
|
+
try {
|
|
63
|
+
return !ig.ignores(e.isDirectory() ? rel + "/" : rel);
|
|
64
|
+
} catch {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.map((e) => {
|
|
69
|
+
const rel = relative(rootDir, join(dir, e.name));
|
|
70
|
+
let isIgnored = false;
|
|
71
|
+
try {
|
|
72
|
+
isIgnored = ig.ignores(e.isDirectory() ? rel + "/" : rel);
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore malformed paths for ignore rules
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
name: e.name,
|
|
78
|
+
path: rel,
|
|
79
|
+
absolutePath: join(dir, e.name),
|
|
80
|
+
isDir: e.isDirectory(),
|
|
81
|
+
ignored: isIgnored,
|
|
82
|
+
};
|
|
83
|
+
})
|
|
84
|
+
.sort((a, b) => {
|
|
85
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
86
|
+
return a.name.localeCompare(b.name);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface GitFileStatus {
|
|
4
|
+
path: string;
|
|
5
|
+
status: "M" | "A" | "D" | "R" | "?";
|
|
6
|
+
additions: number;
|
|
7
|
+
deletions: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function execGit(dir: string, args: string[]): string {
|
|
11
|
+
try {
|
|
12
|
+
return execFileSync(
|
|
13
|
+
"git",
|
|
14
|
+
["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", ...args],
|
|
15
|
+
{ cwd: dir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
|
|
16
|
+
);
|
|
17
|
+
} catch {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getGitBranch(dir: string): string | null {
|
|
23
|
+
const result = execGit(dir, ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
|
|
24
|
+
return result || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getGitFileStatuses(dir: string): GitFileStatus[] {
|
|
28
|
+
const results: GitFileStatus[] = [];
|
|
29
|
+
|
|
30
|
+
// 1. Identify deleted files first so we can tag them correctly in numstat
|
|
31
|
+
const deleted = execGit(dir, ["diff", "--name-only", "--diff-filter=D", "HEAD"]);
|
|
32
|
+
const deletedPaths = new Set(deleted.split("\n").filter(Boolean));
|
|
33
|
+
|
|
34
|
+
// 2. Modified/deleted files with line counts
|
|
35
|
+
const numstat = execGit(dir, ["diff", "--numstat", "HEAD"]);
|
|
36
|
+
for (const line of numstat.split("\n").filter(Boolean)) {
|
|
37
|
+
const parts = line.split("\t");
|
|
38
|
+
if (parts.length < 3) continue;
|
|
39
|
+
const [added, removed, ...pathParts] = parts;
|
|
40
|
+
const filepath = pathParts.join("\t");
|
|
41
|
+
if (added === "-") continue; // binary file
|
|
42
|
+
results.push({
|
|
43
|
+
path: filepath,
|
|
44
|
+
status: deletedPaths.has(filepath) ? "D" : "M",
|
|
45
|
+
additions: parseInt(added!, 10) || 0,
|
|
46
|
+
deletions: parseInt(removed!, 10) || 0,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Deleted files not in numstat (shouldn't happen, but be safe)
|
|
51
|
+
const numstatPaths = new Set(results.map((r) => r.path));
|
|
52
|
+
for (const filepath of deletedPaths) {
|
|
53
|
+
if (!numstatPaths.has(filepath)) {
|
|
54
|
+
results.push({ path: filepath, status: "D", additions: 0, deletions: 0 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 4. Untracked files
|
|
59
|
+
const untracked = execGit(dir, ["ls-files", "--others", "--exclude-standard"]);
|
|
60
|
+
for (const filepath of untracked.split("\n").filter(Boolean)) {
|
|
61
|
+
results.push({ path: filepath, status: "?", additions: 0, deletions: 0 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getGitStatusMap(dir: string): Map<string, string> {
|
|
68
|
+
const map = new Map<string, string>();
|
|
69
|
+
for (const file of getGitFileStatuses(dir)) {
|
|
70
|
+
map.set(file.path, file.status);
|
|
71
|
+
// Propagate to parent directories
|
|
72
|
+
let parent = file.path;
|
|
73
|
+
while (parent.includes("/")) {
|
|
74
|
+
parent = parent.substring(0, parent.lastIndexOf("/"));
|
|
75
|
+
if (!map.has(parent)) map.set(parent, file.status);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return map;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getFileDiff(dir: string, path: string, staged: boolean): string {
|
|
82
|
+
const args = staged ? ["diff", "--cached", "--", path] : ["diff", "--", path];
|
|
83
|
+
return execGit(dir, args);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isGitRepo(dir: string): boolean {
|
|
87
|
+
return execGit(dir, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
|
|
88
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE interaction grammar shared by every TUI surface — the sidebar,
|
|
3
|
+
* explorer, changes, config, and the team cockpit/picker.
|
|
4
|
+
*
|
|
5
|
+
* Cohesion is muscle memory: `j`/`k` (+ arrows) move, `enter` activates, `/`
|
|
6
|
+
* filters, `esc` closes the topmost thing (filter → detail → widget), `q`
|
|
7
|
+
* quits, `?` toggles help. These keys mean the SAME thing everywhere, so a
|
|
8
|
+
* widget never invents its own drift for a universal verb.
|
|
9
|
+
*
|
|
10
|
+
* Widgets call {@link matchGrammar} FIRST in their key handler and act on the
|
|
11
|
+
* returned {@link GrammarAction}, falling through to their own widget-specific
|
|
12
|
+
* keys only when it returns `null`. The escape precedence is factored into the
|
|
13
|
+
* pure {@link dismiss} state-machine so "esc closes the filter before it quits"
|
|
14
|
+
* is defined (and tested) in one place.
|
|
15
|
+
*
|
|
16
|
+
* This module is the SINGLE SOURCE for the grammar: the per-widget help
|
|
17
|
+
* overlays and the cheat sheet both render their "in panels & sidebar" section
|
|
18
|
+
* from {@link GRAMMAR_HELP}, so documentation can never drift from behaviour.
|
|
19
|
+
*
|
|
20
|
+
* Pure — no io, no solid, no opentui — so it unit-tests as a plain table.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The universal verbs every surface understands. */
|
|
24
|
+
export type GrammarAction =
|
|
25
|
+
| "navDown"
|
|
26
|
+
| "navUp"
|
|
27
|
+
| "activate"
|
|
28
|
+
| "filter"
|
|
29
|
+
| "help"
|
|
30
|
+
| "dismiss"
|
|
31
|
+
| "quit";
|
|
32
|
+
|
|
33
|
+
/** The subset of an @opentui key event the grammar needs. */
|
|
34
|
+
export interface GrammarKeyEvent {
|
|
35
|
+
name: string;
|
|
36
|
+
ctrl?: boolean;
|
|
37
|
+
alt?: boolean;
|
|
38
|
+
meta?: boolean;
|
|
39
|
+
shift?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Deterministic iteration order for {@link matchGrammar}. The grammar keys are
|
|
44
|
+
* mutually exclusive (no key appears in two actions), so order only fixes the
|
|
45
|
+
* lookup for readers/tests — it never changes a result.
|
|
46
|
+
*/
|
|
47
|
+
export const GRAMMAR_ACTION_ORDER: GrammarAction[] = [
|
|
48
|
+
"navDown",
|
|
49
|
+
"navUp",
|
|
50
|
+
"activate",
|
|
51
|
+
"filter",
|
|
52
|
+
"help",
|
|
53
|
+
"dismiss",
|
|
54
|
+
"quit",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The fixed key → action bindings. Unlike the team app's configurable
|
|
59
|
+
* `DEFAULT_KEYMAP`, the GRAMMAR itself is not user-configurable — its whole
|
|
60
|
+
* value is that it is the same everywhere. The team keymap's universal actions
|
|
61
|
+
* (up/down/enter/filter/help/quit) agree with these by construction.
|
|
62
|
+
*/
|
|
63
|
+
export const GRAMMAR_KEYS: Record<GrammarAction, string[]> = {
|
|
64
|
+
navDown: ["j", "down"],
|
|
65
|
+
navUp: ["k", "up"],
|
|
66
|
+
activate: ["return"],
|
|
67
|
+
filter: ["/"],
|
|
68
|
+
help: ["?"],
|
|
69
|
+
dismiss: ["escape"],
|
|
70
|
+
quit: ["q"],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Human-facing rows for the help overlays and the cheat sheet — rendered
|
|
75
|
+
* straight from this constant so the docs are sourced from the grammar itself.
|
|
76
|
+
*/
|
|
77
|
+
export const GRAMMAR_HELP: ReadonlyArray<{ keys: string; label: string }> = [
|
|
78
|
+
{ keys: "j / ↓", label: "move down" },
|
|
79
|
+
{ keys: "k / ↑", label: "move up" },
|
|
80
|
+
{ keys: "enter", label: "activate / open" },
|
|
81
|
+
{ keys: "/", label: "filter list" },
|
|
82
|
+
{ keys: "esc", label: "close filter → detail → widget" },
|
|
83
|
+
{ keys: "q", label: "quit" },
|
|
84
|
+
{ keys: "?", label: "toggle this help" },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Map a key event to its {@link GrammarAction}, or `null` when the key is not
|
|
89
|
+
* part of the grammar (so the caller falls through to its widget-specific
|
|
90
|
+
* keys).
|
|
91
|
+
*
|
|
92
|
+
* `ctrl`/`alt`/`meta` combos are NEVER grammar — those namespaces belong to the
|
|
93
|
+
* widgets (`ctrl+s` save, `ctrl+c` quit, the `M-…` dock popups). `shift` is
|
|
94
|
+
* allowed through because `?` arrives as a shifted `/` on most layouts; no
|
|
95
|
+
* widget binds a `shift+<grammar key>` combo, so letting it pass is safe.
|
|
96
|
+
*/
|
|
97
|
+
export function matchGrammar(evt: GrammarKeyEvent): GrammarAction | null {
|
|
98
|
+
if (evt.ctrl || evt.alt || evt.meta) return null;
|
|
99
|
+
for (const action of GRAMMAR_ACTION_ORDER) {
|
|
100
|
+
if (GRAMMAR_KEYS[action].includes(evt.name)) return action;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Which layer an `esc` (or `q`) should close, given what is currently open. */
|
|
106
|
+
export type DismissTarget = "filter" | "detail" | "widget";
|
|
107
|
+
|
|
108
|
+
/** The open-overlay layers a surface can stack, topmost-wins. */
|
|
109
|
+
export interface OverlayState {
|
|
110
|
+
/** A `/` filter prompt is open (typing narrows a list). */
|
|
111
|
+
filterOpen?: boolean;
|
|
112
|
+
/** A transient detail view is open (help overlay, field editor, preview). */
|
|
113
|
+
detailOpen?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The escape precedence, as a pure function: esc closes the FILTER first, then
|
|
118
|
+
* an open DETAIL, and only when nothing is layered does it fall through to the
|
|
119
|
+
* WIDGET itself (quit / close the popup). Widgets call this to decide what a
|
|
120
|
+
* bare `esc` (grammar `dismiss`) should do without re-deriving the order.
|
|
121
|
+
*/
|
|
122
|
+
export function dismiss(state: OverlayState): DismissTarget {
|
|
123
|
+
if (state.filterOpen) return "filter";
|
|
124
|
+
if (state.detailOpen) return "detail";
|
|
125
|
+
return "widget";
|
|
126
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared `?` help overlay — one box, every surface.
|
|
3
|
+
*
|
|
4
|
+
* Renders the UNIVERSAL interaction grammar (from {@link GRAMMAR_HELP}, the same
|
|
5
|
+
* constant the cheat sheet reads) above the caller's OWN widget-specific keys.
|
|
6
|
+
* Because the grammar rows come from `grammar.ts`, no surface can document
|
|
7
|
+
* `j`/`k`/`enter`/`/`/`esc`/`q`/`?` differently from how it behaves.
|
|
8
|
+
*
|
|
9
|
+
* Each surface keeps a `helpOpen` signal, routes `?` (grammar `help`) to toggle
|
|
10
|
+
* it and `esc`/`q` to close it, and renders `<HelpOverlay …>` in place of its
|
|
11
|
+
* body while open. Kept theme-driven and io-free so it drops into any widget.
|
|
12
|
+
*/
|
|
13
|
+
import { For, Show } from "solid-js";
|
|
14
|
+
import { RGBA, TextAttributes } from "@opentui/core";
|
|
15
|
+
import type { WidgetTheme } from "./theme.ts";
|
|
16
|
+
import { GRAMMAR_HELP } from "./grammar.ts";
|
|
17
|
+
|
|
18
|
+
function toRGBA(c: { r: number; g: number; b: number; a: number }): RGBA {
|
|
19
|
+
return RGBA.fromInts(c.r, c.g, c.b, c.a);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A widget-specific key row: the rendered key(s) and what they do. */
|
|
23
|
+
export interface WidgetKey {
|
|
24
|
+
key: string;
|
|
25
|
+
label: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface HelpOverlayProps {
|
|
29
|
+
theme: WidgetTheme;
|
|
30
|
+
/** Surface name shown in the header (e.g. "explorer", "sidebar"). */
|
|
31
|
+
title: string;
|
|
32
|
+
/** The surface's own keys, listed under the shared grammar. */
|
|
33
|
+
widgetKeys: WidgetKey[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A centered help card: a "grammar" section (shared verbs) then a section of
|
|
38
|
+
* the surface's own keys. Replaces the surface body while `?` is held open;
|
|
39
|
+
* `esc`/`q`/`?` close it.
|
|
40
|
+
*/
|
|
41
|
+
export function HelpOverlay(props: HelpOverlayProps) {
|
|
42
|
+
const keyCol = () => {
|
|
43
|
+
const widths = [
|
|
44
|
+
...GRAMMAR_HELP.map((r) => r.keys.length),
|
|
45
|
+
...props.widgetKeys.map((r) => r.key.length),
|
|
46
|
+
];
|
|
47
|
+
return Math.max(6, ...widths);
|
|
48
|
+
};
|
|
49
|
+
return (
|
|
50
|
+
<box flexDirection="column" flexGrow={1} alignItems="center" paddingTop={2}>
|
|
51
|
+
<box
|
|
52
|
+
flexDirection="column"
|
|
53
|
+
border
|
|
54
|
+
borderColor={toRGBA(props.theme.accent)}
|
|
55
|
+
backgroundColor={toRGBA(props.theme.selected)}
|
|
56
|
+
paddingLeft={2}
|
|
57
|
+
paddingRight={2}
|
|
58
|
+
paddingTop={1}
|
|
59
|
+
paddingBottom={1}
|
|
60
|
+
>
|
|
61
|
+
<text fg={toRGBA(props.theme.accent)} attributes={TextAttributes.BOLD}>
|
|
62
|
+
{props.title} — help
|
|
63
|
+
</text>
|
|
64
|
+
|
|
65
|
+
<box paddingTop={1}>
|
|
66
|
+
<text fg={toRGBA(props.theme.fgMuted)} attributes={TextAttributes.BOLD}>
|
|
67
|
+
grammar
|
|
68
|
+
</text>
|
|
69
|
+
</box>
|
|
70
|
+
<For each={GRAMMAR_HELP}>
|
|
71
|
+
{(row) => (
|
|
72
|
+
<box flexDirection="row" gap={1}>
|
|
73
|
+
<text fg={toRGBA(props.theme.accent)}>{row.keys.padEnd(keyCol())}</text>
|
|
74
|
+
<text fg={toRGBA(props.theme.fg)}>{row.label}</text>
|
|
75
|
+
</box>
|
|
76
|
+
)}
|
|
77
|
+
</For>
|
|
78
|
+
|
|
79
|
+
<Show when={props.widgetKeys.length > 0}>
|
|
80
|
+
<box paddingTop={1}>
|
|
81
|
+
<text fg={toRGBA(props.theme.fgMuted)} attributes={TextAttributes.BOLD}>
|
|
82
|
+
{props.title}
|
|
83
|
+
</text>
|
|
84
|
+
</box>
|
|
85
|
+
<For each={props.widgetKeys}>
|
|
86
|
+
{(row) => (
|
|
87
|
+
<box flexDirection="row" gap={1}>
|
|
88
|
+
<text fg={toRGBA(props.theme.accent)}>{row.key.padEnd(keyCol())}</text>
|
|
89
|
+
<text fg={toRGBA(props.theme.fg)}>{row.label}</text>
|
|
90
|
+
</box>
|
|
91
|
+
)}
|
|
92
|
+
</For>
|
|
93
|
+
</Show>
|
|
94
|
+
|
|
95
|
+
<box paddingTop={1}>
|
|
96
|
+
<text fg={toRGBA(props.theme.fgMuted)}>esc / q / ? to close</text>
|
|
97
|
+
</box>
|
|
98
|
+
</box>
|
|
99
|
+
</box>
|
|
100
|
+
);
|
|
101
|
+
}
|