@prover-coder-ai/docker-git 1.0.13 → 1.0.15
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/.package.json.release.bak +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/src/docker-git/menu-actions.js +5 -1
- package/dist/src/docker-git/menu-input-handler.js +67 -0
- package/dist/src/docker-git/menu-render.js +4 -1
- package/dist/src/docker-git/menu-startup.js +57 -0
- package/dist/src/docker-git/menu.js +39 -70
- package/package.json +1 -1
- package/src/docker-git/menu-actions.ts +5 -0
- package/src/docker-git/menu-input-handler.ts +107 -0
- package/src/docker-git/menu-render.ts +13 -7
- package/src/docker-git/menu-startup.ts +83 -0
- package/src/docker-git/menu.ts +55 -118
- package/tests/docker-git/fixtures/project-item.ts +24 -0
- package/tests/docker-git/menu-select-connect.test.ts +13 -22
- package/tests/docker-git/menu-startup.test.ts +51 -0
|
@@ -2,7 +2,7 @@ import {} from "@effect-template/lib/core/domain";
|
|
|
2
2
|
import { readProjectConfig } from "@effect-template/lib/shell/config";
|
|
3
3
|
import { runDockerComposeDown, runDockerComposeLogs, runDockerComposePs } from "@effect-template/lib/shell/docker";
|
|
4
4
|
import { renderError } from "@effect-template/lib/usecases/errors";
|
|
5
|
-
import { downAllDockerGitProjects, listProjectItems, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
|
|
5
|
+
import { downAllDockerGitProjects, listProjectItems, listProjectStatus, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
|
|
6
6
|
import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up";
|
|
7
7
|
import { Effect, Match, pipe } from "effect";
|
|
8
8
|
import { startCreateView } from "./menu-create.js";
|
|
@@ -80,6 +80,10 @@ const runDeleteAction = (context) => {
|
|
|
80
80
|
context.runner.runEffect(loadSelectView(listProjectItems, "Delete", context));
|
|
81
81
|
};
|
|
82
82
|
const runComposeAction = (action, context) => {
|
|
83
|
+
if (action._tag === "Status" && context.state.activeDir === null) {
|
|
84
|
+
runWithSuspendedTui(listProjectStatus, context, "docker compose ps (all projects)");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
83
87
|
if (!requireActiveProject(context)) {
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { handleCreateInput } from "./menu-create.js";
|
|
2
|
+
import { handleMenuInput } from "./menu-menu.js";
|
|
3
|
+
import { handleSelectInput } from "./menu-select.js";
|
|
4
|
+
const activateInput = (input, key, context) => {
|
|
5
|
+
if (context.inputStage === "active") {
|
|
6
|
+
return { activated: false, allowProcessing: true };
|
|
7
|
+
}
|
|
8
|
+
if (input.trim().length > 0) {
|
|
9
|
+
context.setInputStage("active");
|
|
10
|
+
return { activated: true, allowProcessing: true };
|
|
11
|
+
}
|
|
12
|
+
if (key.upArrow || key.downArrow || key.return) {
|
|
13
|
+
context.setInputStage("active");
|
|
14
|
+
return { activated: true, allowProcessing: false };
|
|
15
|
+
}
|
|
16
|
+
if (input.length > 0) {
|
|
17
|
+
context.setInputStage("active");
|
|
18
|
+
return { activated: true, allowProcessing: true };
|
|
19
|
+
}
|
|
20
|
+
return { activated: false, allowProcessing: false };
|
|
21
|
+
};
|
|
22
|
+
const shouldHandleMenuInput = (input, key, context) => {
|
|
23
|
+
const activation = activateInput(input, key, context);
|
|
24
|
+
if (activation.activated && !activation.allowProcessing) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return activation.allowProcessing;
|
|
28
|
+
};
|
|
29
|
+
export const handleUserInput = (input, key, context) => {
|
|
30
|
+
if (context.busy || context.sshActive) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (context.view._tag === "Menu") {
|
|
34
|
+
if (!shouldHandleMenuInput(input, key, context)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
handleMenuInput(input, key, {
|
|
38
|
+
selected: context.selected,
|
|
39
|
+
setSelected: context.setSelected,
|
|
40
|
+
state: context.state,
|
|
41
|
+
runner: context.runner,
|
|
42
|
+
exit: context.exit,
|
|
43
|
+
setView: context.setView,
|
|
44
|
+
setMessage: context.setMessage
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (context.view._tag === "Create") {
|
|
49
|
+
handleCreateInput(input, key, context.view, {
|
|
50
|
+
state: context.state,
|
|
51
|
+
setView: context.setView,
|
|
52
|
+
setMessage: context.setMessage,
|
|
53
|
+
runner: context.runner,
|
|
54
|
+
setActiveDir: context.setActiveDir
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
handleSelectInput(input, key, context.view, {
|
|
59
|
+
setView: context.setView,
|
|
60
|
+
setMessage: context.setMessage,
|
|
61
|
+
setActiveDir: context.setActiveDir,
|
|
62
|
+
activeDir: context.state.activeDir,
|
|
63
|
+
runner: context.runner,
|
|
64
|
+
setSshActive: context.setSshActive,
|
|
65
|
+
setSkipInputs: context.setSkipInputs
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -36,9 +36,11 @@ const renderMenuMessage = (el, message) => {
|
|
|
36
36
|
.split("\n")
|
|
37
37
|
.map((line, index) => el(Text, { key: `${index}-${line}`, color: "magenta" }, line)));
|
|
38
38
|
};
|
|
39
|
-
export const renderMenu = (
|
|
39
|
+
export const renderMenu = (input) => {
|
|
40
|
+
const { activeDir, busy, cwd, message, runningDockerGitContainers, selected } = input;
|
|
40
41
|
const el = React.createElement;
|
|
41
42
|
const activeLabel = `Active: ${activeDir ?? "(none)"}`;
|
|
43
|
+
const runningLabel = `Running docker-git containers: ${runningDockerGitContainers}`;
|
|
42
44
|
const cwdLabel = `CWD: ${cwd}`;
|
|
43
45
|
const items = menuItems.map((item, index) => {
|
|
44
46
|
const indexLabel = `${index + 1})`;
|
|
@@ -52,6 +54,7 @@ export const renderMenu = (cwd, activeDir, selected, busy, message) => {
|
|
|
52
54
|
const hints = renderMenuHints(el);
|
|
53
55
|
return renderLayout("docker-git", compactElements([
|
|
54
56
|
el(Text, null, activeLabel),
|
|
57
|
+
el(Text, null, runningLabel),
|
|
55
58
|
el(Text, null, cwdLabel),
|
|
56
59
|
el(Box, { flexDirection: "column", marginTop: 1 }, ...items),
|
|
57
60
|
hints,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const dockerGitContainerPrefix = "dg-";
|
|
2
|
+
const emptySnapshot = () => ({
|
|
3
|
+
activeDir: null,
|
|
4
|
+
runningDockerGitContainers: 0,
|
|
5
|
+
message: null
|
|
6
|
+
});
|
|
7
|
+
const uniqueDockerGitContainerNames = (runningContainerNames) => [
|
|
8
|
+
...new Set(runningContainerNames.filter((name) => name.startsWith(dockerGitContainerPrefix)))
|
|
9
|
+
];
|
|
10
|
+
const detectKnownRunningProjects = (items, runningDockerGitNames) => {
|
|
11
|
+
const runningSet = new Set(runningDockerGitNames);
|
|
12
|
+
return items.filter((item) => runningSet.has(item.containerName));
|
|
13
|
+
};
|
|
14
|
+
const renderRunningHint = (runningCount) => runningCount === 1
|
|
15
|
+
? "Detected 1 running docker-git container."
|
|
16
|
+
: `Detected ${runningCount} running docker-git containers.`;
|
|
17
|
+
// CHANGE: infer initial menu state from currently running docker-git containers
|
|
18
|
+
// WHY: avoid "(none)" confusion when containers are already up outside this TUI session
|
|
19
|
+
// QUOTE(ISSUE): "У меня запущены контейнеры от docker-git но он говорит что они не запущены"
|
|
20
|
+
// REF: issue-13
|
|
21
|
+
// SOURCE: n/a
|
|
22
|
+
// FORMAT THEOREM: forall startupState: snapshot(startupState) -> deterministic(menuState)
|
|
23
|
+
// PURITY: CORE
|
|
24
|
+
// EFFECT: n/a
|
|
25
|
+
// INVARIANT: activeDir is set only when exactly one known project is running
|
|
26
|
+
// COMPLEXITY: O(|containers| + |projects|)
|
|
27
|
+
export const resolveMenuStartupSnapshot = (items, runningContainerNames) => {
|
|
28
|
+
const runningDockerGitNames = uniqueDockerGitContainerNames(runningContainerNames);
|
|
29
|
+
if (runningDockerGitNames.length === 0) {
|
|
30
|
+
return emptySnapshot();
|
|
31
|
+
}
|
|
32
|
+
const knownRunningProjects = detectKnownRunningProjects(items, runningDockerGitNames);
|
|
33
|
+
if (knownRunningProjects.length === 1 && runningDockerGitNames.length === 1) {
|
|
34
|
+
const selected = knownRunningProjects[0];
|
|
35
|
+
if (!selected) {
|
|
36
|
+
return emptySnapshot();
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
activeDir: selected.projectDir,
|
|
40
|
+
runningDockerGitContainers: 1,
|
|
41
|
+
message: `Auto-selected active project: ${selected.displayName}.`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (knownRunningProjects.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
activeDir: null,
|
|
47
|
+
runningDockerGitContainers: runningDockerGitNames.length,
|
|
48
|
+
message: `${renderRunningHint(runningDockerGitNames.length)} No matching project config found.`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
activeDir: null,
|
|
53
|
+
runningDockerGitContainers: runningDockerGitNames.length,
|
|
54
|
+
message: `${renderRunningHint(runningDockerGitNames.length)} Use Select project to choose active.`
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export const defaultMenuStartupSnapshot = emptySnapshot;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { runDockerPsNames } from "@effect-template/lib/shell/docker";
|
|
1
2
|
import { InputReadError } from "@effect-template/lib/shell/errors";
|
|
2
3
|
import { renderError } from "@effect-template/lib/usecases/errors";
|
|
4
|
+
import { listProjectItems } from "@effect-template/lib/usecases/projects";
|
|
3
5
|
import { NodeContext } from "@effect/platform-node";
|
|
4
6
|
import { Effect, pipe } from "effect";
|
|
5
7
|
import { render, useApp, useInput } from "ink";
|
|
6
8
|
import React, { useEffect, useMemo, useState } from "react";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
+
import { resolveCreateInputs } from "./menu-create.js";
|
|
10
|
+
import { handleUserInput } from "./menu-input-handler.js";
|
|
9
11
|
import { renderCreate, renderMenu, renderSelect, renderStepLabel } from "./menu-render.js";
|
|
10
|
-
import { handleSelectInput } from "./menu-select.js";
|
|
11
12
|
import { leaveTui, resumeTui } from "./menu-shared.js";
|
|
13
|
+
import { defaultMenuStartupSnapshot, resolveMenuStartupSnapshot } from "./menu-startup.js";
|
|
12
14
|
import { createSteps } from "./menu-types.js";
|
|
13
15
|
// CHANGE: keep menu state in the TUI layer
|
|
14
16
|
// WHY: provide a dynamic interface with live selection and inputs
|
|
@@ -35,76 +37,16 @@ const useRunner = (setBusy, setMessage) => {
|
|
|
35
37
|
};
|
|
36
38
|
return { runEffect };
|
|
37
39
|
};
|
|
38
|
-
const
|
|
39
|
-
if (context.inputStage === "active") {
|
|
40
|
-
return { activated: false, allowProcessing: true };
|
|
41
|
-
}
|
|
42
|
-
if (input.trim().length > 0) {
|
|
43
|
-
context.setInputStage("active");
|
|
44
|
-
return { activated: true, allowProcessing: true };
|
|
45
|
-
}
|
|
46
|
-
if (key.upArrow || key.downArrow || key.return) {
|
|
47
|
-
context.setInputStage("active");
|
|
48
|
-
return { activated: true, allowProcessing: false };
|
|
49
|
-
}
|
|
50
|
-
if (input.length > 0) {
|
|
51
|
-
context.setInputStage("active");
|
|
52
|
-
return { activated: true, allowProcessing: true };
|
|
53
|
-
}
|
|
54
|
-
return { activated: false, allowProcessing: false };
|
|
55
|
-
};
|
|
56
|
-
const shouldHandleMenuInput = (input, key, context) => {
|
|
57
|
-
const activation = activateInput(input, key, context);
|
|
58
|
-
if (activation.activated && !activation.allowProcessing) {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
return activation.allowProcessing;
|
|
62
|
-
};
|
|
63
|
-
const handleUserInput = (input, key, context) => {
|
|
64
|
-
if (context.busy) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (context.sshActive) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
40
|
+
const renderView = (context) => {
|
|
70
41
|
if (context.view._tag === "Menu") {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
42
|
+
return renderMenu({
|
|
43
|
+
cwd: context.state.cwd,
|
|
44
|
+
activeDir: context.activeDir,
|
|
45
|
+
runningDockerGitContainers: context.runningDockerGitContainers,
|
|
75
46
|
selected: context.selected,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
runner: context.runner,
|
|
79
|
-
exit: context.exit,
|
|
80
|
-
setView: context.setView,
|
|
81
|
-
setMessage: context.setMessage
|
|
82
|
-
});
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
if (context.view._tag === "Create") {
|
|
86
|
-
handleCreateInput(input, key, context.view, {
|
|
87
|
-
state: context.state,
|
|
88
|
-
setView: context.setView,
|
|
89
|
-
setMessage: context.setMessage,
|
|
90
|
-
runner: context.runner,
|
|
91
|
-
setActiveDir: context.setActiveDir
|
|
47
|
+
busy: context.busy,
|
|
48
|
+
message: context.message
|
|
92
49
|
});
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
handleSelectInput(input, key, context.view, {
|
|
96
|
-
setView: context.setView,
|
|
97
|
-
setMessage: context.setMessage,
|
|
98
|
-
setActiveDir: context.setActiveDir,
|
|
99
|
-
activeDir: context.state.activeDir,
|
|
100
|
-
runner: context.runner,
|
|
101
|
-
setSshActive: context.setSshActive,
|
|
102
|
-
setSkipInputs: context.setSkipInputs
|
|
103
|
-
});
|
|
104
|
-
};
|
|
105
|
-
const renderView = (context) => {
|
|
106
|
-
if (context.view._tag === "Menu") {
|
|
107
|
-
return renderMenu(context.state.cwd, context.activeDir, context.selected, context.busy, context.message);
|
|
108
50
|
}
|
|
109
51
|
if (context.view._tag === "Create") {
|
|
110
52
|
const currentDefaults = resolveCreateInputs(context.state.cwd, context.view.values);
|
|
@@ -124,6 +66,7 @@ const renderView = (context) => {
|
|
|
124
66
|
};
|
|
125
67
|
const useMenuState = () => {
|
|
126
68
|
const [activeDir, setActiveDir] = useState(null);
|
|
69
|
+
const [runningDockerGitContainers, setRunningDockerGitContainers] = useState(0);
|
|
127
70
|
const [selected, setSelected] = useState(0);
|
|
128
71
|
const [busy, setBusy] = useState(false);
|
|
129
72
|
const [message, setMessage] = useState(null);
|
|
@@ -138,6 +81,8 @@ const useMenuState = () => {
|
|
|
138
81
|
return {
|
|
139
82
|
activeDir,
|
|
140
83
|
setActiveDir,
|
|
84
|
+
runningDockerGitContainers,
|
|
85
|
+
setRunningDockerGitContainers,
|
|
141
86
|
selected,
|
|
142
87
|
setSelected,
|
|
143
88
|
busy,
|
|
@@ -168,6 +113,28 @@ const useReadyGate = (setReady) => {
|
|
|
168
113
|
};
|
|
169
114
|
}, [setReady]);
|
|
170
115
|
};
|
|
116
|
+
const useStartupSnapshot = (setActiveDir, setRunningDockerGitContainers, setMessage) => {
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
let cancelled = false;
|
|
119
|
+
const startup = pipe(Effect.all([listProjectItems, runDockerPsNames(process.cwd())]), Effect.map(([items, runningNames]) => resolveMenuStartupSnapshot(items, runningNames)), Effect.match({
|
|
120
|
+
onFailure: () => defaultMenuStartupSnapshot(),
|
|
121
|
+
onSuccess: (snapshot) => snapshot
|
|
122
|
+
}), Effect.provide(NodeContext.layer));
|
|
123
|
+
void Effect.runPromise(startup).then((snapshot) => {
|
|
124
|
+
if (cancelled) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
setRunningDockerGitContainers(snapshot.runningDockerGitContainers);
|
|
128
|
+
setMessage(snapshot.message);
|
|
129
|
+
if (snapshot.activeDir !== null) {
|
|
130
|
+
setActiveDir(snapshot.activeDir);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return () => {
|
|
134
|
+
cancelled = true;
|
|
135
|
+
};
|
|
136
|
+
}, [setActiveDir, setMessage, setRunningDockerGitContainers]);
|
|
137
|
+
};
|
|
171
138
|
const useSigintGuard = (exit, sshActive) => {
|
|
172
139
|
useEffect(() => {
|
|
173
140
|
const handleSigint = () => {
|
|
@@ -186,6 +153,7 @@ const TuiApp = () => {
|
|
|
186
153
|
const { exit } = useApp();
|
|
187
154
|
const menu = useMenuState();
|
|
188
155
|
useReadyGate(menu.setReady);
|
|
156
|
+
useStartupSnapshot(menu.setActiveDir, menu.setRunningDockerGitContainers, menu.setMessage);
|
|
189
157
|
useSigintGuard(exit, menu.sshActive);
|
|
190
158
|
useInput((input, key) => {
|
|
191
159
|
if (!menu.ready) {
|
|
@@ -220,6 +188,7 @@ const TuiApp = () => {
|
|
|
220
188
|
state: menu.state,
|
|
221
189
|
view: menu.view,
|
|
222
190
|
activeDir: menu.activeDir,
|
|
191
|
+
runningDockerGitContainers: menu.runningDockerGitContainers,
|
|
223
192
|
selected: menu.selected,
|
|
224
193
|
busy: menu.busy,
|
|
225
194
|
message: menu.message
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import { renderError } from "@effect-template/lib/usecases/errors"
|
|
|
6
6
|
import {
|
|
7
7
|
downAllDockerGitProjects,
|
|
8
8
|
listProjectItems,
|
|
9
|
+
listProjectStatus,
|
|
9
10
|
listRunningProjectItems
|
|
10
11
|
} from "@effect-template/lib/usecases/projects"
|
|
11
12
|
import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up"
|
|
@@ -195,6 +196,10 @@ const runDeleteAction = (context: MenuContext) => {
|
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
const runComposeAction = (action: MenuAction, context: MenuContext) => {
|
|
199
|
+
if (action._tag === "Status" && context.state.activeDir === null) {
|
|
200
|
+
runWithSuspendedTui(listProjectStatus, context, "docker compose ps (all projects)")
|
|
201
|
+
return
|
|
202
|
+
}
|
|
198
203
|
if (!requireActiveProject(context)) {
|
|
199
204
|
return
|
|
200
205
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { handleCreateInput } from "./menu-create.js"
|
|
2
|
+
import { handleMenuInput } from "./menu-menu.js"
|
|
3
|
+
import { handleSelectInput } from "./menu-select.js"
|
|
4
|
+
import type { MenuKeyInput, MenuRunner, MenuState, MenuViewContext, ViewState } from "./menu-types.js"
|
|
5
|
+
|
|
6
|
+
export type InputStage = "cold" | "active"
|
|
7
|
+
|
|
8
|
+
export type MenuInputContext = MenuViewContext & {
|
|
9
|
+
readonly busy: boolean
|
|
10
|
+
readonly view: ViewState
|
|
11
|
+
readonly inputStage: InputStage
|
|
12
|
+
readonly setInputStage: (stage: InputStage) => void
|
|
13
|
+
readonly selected: number
|
|
14
|
+
readonly setSelected: (update: (value: number) => number) => void
|
|
15
|
+
readonly setSkipInputs: (update: (value: number) => number) => void
|
|
16
|
+
readonly sshActive: boolean
|
|
17
|
+
readonly setSshActive: (active: boolean) => void
|
|
18
|
+
readonly state: MenuState
|
|
19
|
+
readonly runner: MenuRunner
|
|
20
|
+
readonly exit: () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const activateInput = (
|
|
24
|
+
input: string,
|
|
25
|
+
key: Pick<MenuKeyInput, "upArrow" | "downArrow" | "return">,
|
|
26
|
+
context: Pick<MenuInputContext, "inputStage" | "setInputStage">
|
|
27
|
+
): { readonly activated: boolean; readonly allowProcessing: boolean } => {
|
|
28
|
+
if (context.inputStage === "active") {
|
|
29
|
+
return { activated: false, allowProcessing: true }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (input.trim().length > 0) {
|
|
33
|
+
context.setInputStage("active")
|
|
34
|
+
return { activated: true, allowProcessing: true }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (key.upArrow || key.downArrow || key.return) {
|
|
38
|
+
context.setInputStage("active")
|
|
39
|
+
return { activated: true, allowProcessing: false }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (input.length > 0) {
|
|
43
|
+
context.setInputStage("active")
|
|
44
|
+
return { activated: true, allowProcessing: true }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { activated: false, allowProcessing: false }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const shouldHandleMenuInput = (
|
|
51
|
+
input: string,
|
|
52
|
+
key: Pick<MenuKeyInput, "upArrow" | "downArrow" | "return">,
|
|
53
|
+
context: Pick<MenuInputContext, "inputStage" | "setInputStage">
|
|
54
|
+
): boolean => {
|
|
55
|
+
const activation = activateInput(input, key, context)
|
|
56
|
+
if (activation.activated && !activation.allowProcessing) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
return activation.allowProcessing
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const handleUserInput = (
|
|
63
|
+
input: string,
|
|
64
|
+
key: MenuKeyInput,
|
|
65
|
+
context: MenuInputContext
|
|
66
|
+
) => {
|
|
67
|
+
if (context.busy || context.sshActive) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (context.view._tag === "Menu") {
|
|
72
|
+
if (!shouldHandleMenuInput(input, key, context)) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
handleMenuInput(input, key, {
|
|
76
|
+
selected: context.selected,
|
|
77
|
+
setSelected: context.setSelected,
|
|
78
|
+
state: context.state,
|
|
79
|
+
runner: context.runner,
|
|
80
|
+
exit: context.exit,
|
|
81
|
+
setView: context.setView,
|
|
82
|
+
setMessage: context.setMessage
|
|
83
|
+
})
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (context.view._tag === "Create") {
|
|
88
|
+
handleCreateInput(input, key, context.view, {
|
|
89
|
+
state: context.state,
|
|
90
|
+
setView: context.setView,
|
|
91
|
+
setMessage: context.setMessage,
|
|
92
|
+
runner: context.runner,
|
|
93
|
+
setActiveDir: context.setActiveDir
|
|
94
|
+
})
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
handleSelectInput(input, key, context.view, {
|
|
99
|
+
setView: context.setView,
|
|
100
|
+
setMessage: context.setMessage,
|
|
101
|
+
setActiveDir: context.setActiveDir,
|
|
102
|
+
activeDir: context.state.activeDir,
|
|
103
|
+
runner: context.runner,
|
|
104
|
+
setSshActive: context.setSshActive,
|
|
105
|
+
setSkipInputs: context.setSkipInputs
|
|
106
|
+
})
|
|
107
|
+
}
|
|
@@ -103,15 +103,20 @@ const renderMenuMessage = (
|
|
|
103
103
|
)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
cwd: string
|
|
108
|
-
activeDir: string | null
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
type MenuRenderInput = {
|
|
107
|
+
readonly cwd: string
|
|
108
|
+
readonly activeDir: string | null
|
|
109
|
+
readonly runningDockerGitContainers: number
|
|
110
|
+
readonly selected: number
|
|
111
|
+
readonly busy: boolean
|
|
112
|
+
readonly message: string | null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const renderMenu = (input: MenuRenderInput): React.ReactElement => {
|
|
116
|
+
const { activeDir, busy, cwd, message, runningDockerGitContainers, selected } = input
|
|
113
117
|
const el = React.createElement
|
|
114
118
|
const activeLabel = `Active: ${activeDir ?? "(none)"}`
|
|
119
|
+
const runningLabel = `Running docker-git containers: ${runningDockerGitContainers}`
|
|
115
120
|
const cwdLabel = `CWD: ${cwd}`
|
|
116
121
|
const items = menuItems.map((item, index) => {
|
|
117
122
|
const indexLabel = `${index + 1})`
|
|
@@ -134,6 +139,7 @@ export const renderMenu = (
|
|
|
134
139
|
"docker-git",
|
|
135
140
|
compactElements([
|
|
136
141
|
el(Text, null, activeLabel),
|
|
142
|
+
el(Text, null, runningLabel),
|
|
137
143
|
el(Text, null, cwdLabel),
|
|
138
144
|
el(Box, { flexDirection: "column", marginTop: 1 }, ...items),
|
|
139
145
|
hints,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
2
|
+
|
|
3
|
+
export type MenuStartupSnapshot = {
|
|
4
|
+
readonly activeDir: string | null
|
|
5
|
+
readonly runningDockerGitContainers: number
|
|
6
|
+
readonly message: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const dockerGitContainerPrefix = "dg-"
|
|
10
|
+
|
|
11
|
+
const emptySnapshot = (): MenuStartupSnapshot => ({
|
|
12
|
+
activeDir: null,
|
|
13
|
+
runningDockerGitContainers: 0,
|
|
14
|
+
message: null
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const uniqueDockerGitContainerNames = (
|
|
18
|
+
runningContainerNames: ReadonlyArray<string>
|
|
19
|
+
): ReadonlyArray<string> => [
|
|
20
|
+
...new Set(runningContainerNames.filter((name) => name.startsWith(dockerGitContainerPrefix)))
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const detectKnownRunningProjects = (
|
|
24
|
+
items: ReadonlyArray<ProjectItem>,
|
|
25
|
+
runningDockerGitNames: ReadonlyArray<string>
|
|
26
|
+
): ReadonlyArray<ProjectItem> => {
|
|
27
|
+
const runningSet = new Set(runningDockerGitNames)
|
|
28
|
+
return items.filter((item) => runningSet.has(item.containerName))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const renderRunningHint = (runningCount: number): string =>
|
|
32
|
+
runningCount === 1
|
|
33
|
+
? "Detected 1 running docker-git container."
|
|
34
|
+
: `Detected ${runningCount} running docker-git containers.`
|
|
35
|
+
|
|
36
|
+
// CHANGE: infer initial menu state from currently running docker-git containers
|
|
37
|
+
// WHY: avoid "(none)" confusion when containers are already up outside this TUI session
|
|
38
|
+
// QUOTE(ISSUE): "У меня запущены контейнеры от docker-git но он говорит что они не запущены"
|
|
39
|
+
// REF: issue-13
|
|
40
|
+
// SOURCE: n/a
|
|
41
|
+
// FORMAT THEOREM: forall startupState: snapshot(startupState) -> deterministic(menuState)
|
|
42
|
+
// PURITY: CORE
|
|
43
|
+
// EFFECT: n/a
|
|
44
|
+
// INVARIANT: activeDir is set only when exactly one known project is running
|
|
45
|
+
// COMPLEXITY: O(|containers| + |projects|)
|
|
46
|
+
export const resolveMenuStartupSnapshot = (
|
|
47
|
+
items: ReadonlyArray<ProjectItem>,
|
|
48
|
+
runningContainerNames: ReadonlyArray<string>
|
|
49
|
+
): MenuStartupSnapshot => {
|
|
50
|
+
const runningDockerGitNames = uniqueDockerGitContainerNames(runningContainerNames)
|
|
51
|
+
if (runningDockerGitNames.length === 0) {
|
|
52
|
+
return emptySnapshot()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const knownRunningProjects = detectKnownRunningProjects(items, runningDockerGitNames)
|
|
56
|
+
if (knownRunningProjects.length === 1 && runningDockerGitNames.length === 1) {
|
|
57
|
+
const selected = knownRunningProjects[0]
|
|
58
|
+
if (!selected) {
|
|
59
|
+
return emptySnapshot()
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
activeDir: selected.projectDir,
|
|
63
|
+
runningDockerGitContainers: 1,
|
|
64
|
+
message: `Auto-selected active project: ${selected.displayName}.`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (knownRunningProjects.length === 0) {
|
|
69
|
+
return {
|
|
70
|
+
activeDir: null,
|
|
71
|
+
runningDockerGitContainers: runningDockerGitNames.length,
|
|
72
|
+
message: `${renderRunningHint(runningDockerGitNames.length)} No matching project config found.`
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
activeDir: null,
|
|
78
|
+
runningDockerGitContainers: runningDockerGitNames.length,
|
|
79
|
+
message: `${renderRunningHint(runningDockerGitNames.length)} Use Select project to choose active.`
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const defaultMenuStartupSnapshot = emptySnapshot
|