opendocker 0.1.1 → 0.1.3
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/.github/FUNDING.yml +15 -0
- package/AGENTS.md +22 -0
- package/README.md +6 -20
- package/TODO.md +53 -0
- package/bun.lock +225 -0
- package/bunfig.toml +2 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/main.js +31929 -117
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/docker-compose.yml +24 -0
- package/package.json +6 -6
- package/src/assets/images/banner.jpg +0 -0
- package/src/components/Border.tsx +19 -0
- package/src/components/ContainersPane.tsx +146 -0
- package/src/components/ImagesPane.tsx +83 -0
- package/src/components/LogsPane.tsx +132 -0
- package/src/components/Pane.tsx +34 -0
- package/src/components/VolumesPanes.tsx +84 -0
- package/src/components/ui/Shimmer.tsx +64 -0
- package/src/layouts/BaseLayout.tsx +60 -0
- package/src/lib/docker.ts +68 -0
- package/src/main.tsx +43 -0
- package/src/stores/application.ts +11 -0
- package/src/stores/containers.ts +22 -0
- package/src/stores/images.ts +11 -0
- package/src/stores/volumes.ts +11 -0
- package/src/utils/styling.ts +198 -0
- package/tsconfig.json +15 -0
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
logger1:
|
|
5
|
+
image: alpine:latest
|
|
6
|
+
container_name: logger1
|
|
7
|
+
command: >
|
|
8
|
+
sh -c '
|
|
9
|
+
while true; do
|
|
10
|
+
echo "[$$(date +%Y-%m-%d\ %H:%M:%S)] Container1: $$(openssl rand -hex 8)"
|
|
11
|
+
sleep 1
|
|
12
|
+
done'
|
|
13
|
+
restart: unless-stopped
|
|
14
|
+
|
|
15
|
+
logger2:
|
|
16
|
+
image: alpine:latest
|
|
17
|
+
container_name: logger2
|
|
18
|
+
command: >
|
|
19
|
+
sh -c '
|
|
20
|
+
while true; do
|
|
21
|
+
echo "[$$(date +%Y-%m-%d\ %H:%M:%S)] Container2: $$(openssl rand -hex 8)"
|
|
22
|
+
sleep 1
|
|
23
|
+
done'
|
|
24
|
+
restart: unless-stopped
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"name": "opendocker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"bin": {
|
|
8
8
|
"opendocker": "./dist/main.js"
|
|
9
9
|
},
|
|
10
|
-
"
|
|
10
|
+
"file": ["dist/", "package.json", "README.md"],
|
|
11
11
|
"scripts": {
|
|
12
|
+
"build": "bun build ./src/main.tsx --outdir ./dist --target bun",
|
|
12
13
|
"start": "bun run dist/main.js",
|
|
13
|
-
"dev": "bun --watch run src/main.tsx"
|
|
14
|
-
"build": "bun build ./src/main.tsx --outdir ./dist --target bun --minify"
|
|
14
|
+
"dev": "bun --watch run src/main.tsx"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/bun": "latest"
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"typescript": "^5"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@opentui/core": "^0.1.
|
|
24
|
-
"@opentui/react": "^0.1.
|
|
23
|
+
"@opentui/core": "^0.1.46",
|
|
24
|
+
"@opentui/react": "^0.1.46",
|
|
25
25
|
"zustand": "^5.0.8"
|
|
26
26
|
}
|
|
27
27
|
}
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { colors } from "../utils/styling";
|
|
2
|
+
|
|
3
|
+
export const SplitBorder = {
|
|
4
|
+
border: ["left" as const],
|
|
5
|
+
borderColor: colors.border,
|
|
6
|
+
customBorderChars: {
|
|
7
|
+
topLeft: "",
|
|
8
|
+
bottomLeft: "",
|
|
9
|
+
vertical: "┃",
|
|
10
|
+
topRight: "",
|
|
11
|
+
bottomRight: "",
|
|
12
|
+
horizontal: "",
|
|
13
|
+
bottomT: "",
|
|
14
|
+
topT: "",
|
|
15
|
+
cross: "",
|
|
16
|
+
leftT: "",
|
|
17
|
+
rightT: "",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { useKeyboard } from "@opentui/react";
|
|
3
|
+
import { colors, termColors } from "../utils/styling";
|
|
4
|
+
import Pane from "./Pane";
|
|
5
|
+
import { useContainerStore } from "../stores/containers";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
8
|
+
import { Docker } from "../lib/docker";
|
|
9
|
+
import { TextAttributes } from "@opentui/core";
|
|
10
|
+
|
|
11
|
+
export default function ContainersPane() {
|
|
12
|
+
const { activePane, setActivePane } = useApplicationStore((state) => state);
|
|
13
|
+
const { containers, setContainers, activeContainer, setActiveContainer } = useContainerStore((state) => state);
|
|
14
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
15
|
+
const paneActive = activePane === "containers";
|
|
16
|
+
const scrollBoxRef = useRef<ScrollBoxRenderable>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!paneActive) return;
|
|
20
|
+
|
|
21
|
+
const docker = new Docker();
|
|
22
|
+
|
|
23
|
+
docker.watch((dockerContainers) => {
|
|
24
|
+
const transformed = dockerContainers.map(container => ({
|
|
25
|
+
name: container.Names[0].replace("/", ""),
|
|
26
|
+
status: container.Status,
|
|
27
|
+
state: container.State,
|
|
28
|
+
health: container.Health,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
setContainers(transformed);
|
|
32
|
+
|
|
33
|
+
if (transformed.length === 0) {
|
|
34
|
+
setActiveContainer(undefined);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get the CURRENT active container from store, not from stale closure
|
|
38
|
+
const currentActive = useContainerStore.getState().activeContainer;
|
|
39
|
+
|
|
40
|
+
if (currentActive) {
|
|
41
|
+
const updatedContainer = transformed.find((c) => c.name === currentActive.name);
|
|
42
|
+
if (updatedContainer) {
|
|
43
|
+
setActiveContainer(updatedContainer);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (transformed.length > 0) {
|
|
49
|
+
setActiveContainer(transformed[0]);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}, [paneActive]);
|
|
53
|
+
|
|
54
|
+
useKeyboard((key) => {
|
|
55
|
+
if (!paneActive) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (key.name === "left") {
|
|
60
|
+
setActivePane("volumes");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (key.name === "right") {
|
|
64
|
+
setActivePane("images");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (key.name === 'j' || key.name === 'down') {
|
|
68
|
+
const index = Math.min(selectedIndex + 1, containers.length - 1);
|
|
69
|
+
setSelectedIndex(index);
|
|
70
|
+
setActiveContainer(containers[index]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (key.name === 'k' || key.name === 'up') {
|
|
74
|
+
const index = Math.max(selectedIndex - 1, 0);
|
|
75
|
+
setActiveContainer(containers[index]);
|
|
76
|
+
setSelectedIndex(index);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Pane
|
|
82
|
+
title="Containers"
|
|
83
|
+
flexDirection="column"
|
|
84
|
+
width="100%"
|
|
85
|
+
active={paneActive}
|
|
86
|
+
>
|
|
87
|
+
<scrollbox
|
|
88
|
+
ref={scrollBoxRef}
|
|
89
|
+
scrollY={true}
|
|
90
|
+
stickyScroll={true}
|
|
91
|
+
stickyStart="bottom"
|
|
92
|
+
viewportOptions={{
|
|
93
|
+
flexGrow: 1
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{containers.map((item, index) => {
|
|
97
|
+
function getStateColor() {
|
|
98
|
+
if (paneActive && activeContainer?.name === item.name) {
|
|
99
|
+
return colors.backgroundPanel;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (item?.state === "running") {
|
|
103
|
+
if (item.status.includes("starting")) {
|
|
104
|
+
return termColors.orange11;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (item.status.includes("unhealthy")) {
|
|
108
|
+
return termColors.red11;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return termColors.green11;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (item?.state === "exited") {
|
|
115
|
+
return termColors.red11;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return termColors.blue11;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<box
|
|
123
|
+
key={index}
|
|
124
|
+
backgroundColor={paneActive && activeContainer?.name === item.name ? colors.primary : undefined}
|
|
125
|
+
flexDirection="row"
|
|
126
|
+
justifyContent="space-between"
|
|
127
|
+
paddingLeft={1}
|
|
128
|
+
paddingRight={1}
|
|
129
|
+
>
|
|
130
|
+
<text
|
|
131
|
+
content={item.name}
|
|
132
|
+
fg={paneActive && activeContainer?.name === item.name ? colors.backgroundPanel : colors.textMuted}
|
|
133
|
+
attributes={paneActive && activeContainer?.name === item.name && TextAttributes.BOLD}
|
|
134
|
+
/>
|
|
135
|
+
<text
|
|
136
|
+
content={item.state}
|
|
137
|
+
fg={getStateColor()}
|
|
138
|
+
/>
|
|
139
|
+
</box>
|
|
140
|
+
)
|
|
141
|
+
})}
|
|
142
|
+
{containers.length < 1 && <text fg={colors.textMuted}>No Containers</text>}
|
|
143
|
+
</scrollbox>
|
|
144
|
+
</Pane>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useKeyboard } from "@opentui/react";
|
|
3
|
+
import Pane from "./Pane";
|
|
4
|
+
import { colors } from "../utils/styling";
|
|
5
|
+
import { useImageStore } from "../stores/images";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
8
|
+
|
|
9
|
+
export default function ImagesPane() {
|
|
10
|
+
const { activePane, setActivePane } = useApplicationStore((state) => state);
|
|
11
|
+
const { images, setImages } = useImageStore();
|
|
12
|
+
const scrollBoxRef = useRef<ScrollBoxRenderable>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const process = Bun.spawn(
|
|
16
|
+
["docker", "images", "--format", "{{.Repository}}"],
|
|
17
|
+
{stdout: "pipe", stderr: "pipe"},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
async function read() {
|
|
21
|
+
try {
|
|
22
|
+
const reader = process.stdout.getReader();
|
|
23
|
+
|
|
24
|
+
while (true) {
|
|
25
|
+
const { done, value } = await reader.read();
|
|
26
|
+
if (done) break;
|
|
27
|
+
const decodedValues = new TextDecoder().decode(value);
|
|
28
|
+
const lines = decodedValues.split("\n").filter(Boolean);
|
|
29
|
+
setImages(lines);
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
read();
|
|
37
|
+
|
|
38
|
+
return () => process.kill();
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useKeyboard((key) => {
|
|
42
|
+
if (activePane !== "images") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (key.name === "left") {
|
|
47
|
+
setActivePane("containers");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (key.name === "right" || key.name === "tab") {
|
|
51
|
+
setActivePane("volumes");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Pane
|
|
57
|
+
title="Images"
|
|
58
|
+
width="100%"
|
|
59
|
+
active={activePane === "images"}
|
|
60
|
+
flexDirection="column"
|
|
61
|
+
>
|
|
62
|
+
<scrollbox
|
|
63
|
+
ref={scrollBoxRef}
|
|
64
|
+
scrollY={true}
|
|
65
|
+
stickyScroll={true}
|
|
66
|
+
stickyStart="top"
|
|
67
|
+
>
|
|
68
|
+
{images.map((item: string, index: number) => {
|
|
69
|
+
return (
|
|
70
|
+
<box key={index}>
|
|
71
|
+
<text
|
|
72
|
+
key={index}
|
|
73
|
+
content={item}
|
|
74
|
+
fg={colors.textMuted}
|
|
75
|
+
/>
|
|
76
|
+
</box>
|
|
77
|
+
)
|
|
78
|
+
})}
|
|
79
|
+
{images.length < 1 && <text fg={colors.textMuted}>No Images</text>}
|
|
80
|
+
</scrollbox>
|
|
81
|
+
</Pane>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
2
|
+
import { colors, termColors } from "../utils/styling";
|
|
3
|
+
import { RGBA, TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
4
|
+
import Pane from "./Pane";
|
|
5
|
+
import { useContainerStore } from "../stores/containers";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
import { Shimmer } from "./ui/Shimmer";
|
|
8
|
+
|
|
9
|
+
type LogEntry = {
|
|
10
|
+
text: string;
|
|
11
|
+
type: 'stdout' | 'stderr';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default function LogsPane() {
|
|
15
|
+
const { activePane } = useApplicationStore((state) => state);
|
|
16
|
+
const { activeContainer } = useContainerStore((state) => state);
|
|
17
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
18
|
+
const scrollBoxRef = useRef<ScrollBoxRenderable>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!activeContainer || activePane !== "containers") {
|
|
22
|
+
cleanup();
|
|
23
|
+
return;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const process = Bun.spawn([
|
|
27
|
+
"docker",
|
|
28
|
+
"logs",
|
|
29
|
+
"--follow",
|
|
30
|
+
"--tail",
|
|
31
|
+
"100",
|
|
32
|
+
activeContainer.name,
|
|
33
|
+
], {
|
|
34
|
+
stdout: "pipe",
|
|
35
|
+
stderr: "pipe",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function readStream(stream: ReadableStream, type: 'stdout' | 'stderr') {
|
|
39
|
+
const decoder = new TextDecoder();
|
|
40
|
+
const reader = stream.getReader();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
|
|
47
|
+
const text = decoder.decode(value, { stream: true });
|
|
48
|
+
// Split by newlines and filter out empty lines
|
|
49
|
+
const lines = text.split('\n').filter(line => line.trim().length > 0);
|
|
50
|
+
|
|
51
|
+
if (lines.length > 0) {
|
|
52
|
+
setLogs(prev => {
|
|
53
|
+
const newEntries = lines.map(line => ({
|
|
54
|
+
text: line,
|
|
55
|
+
type
|
|
56
|
+
}));
|
|
57
|
+
return [...prev, ...newEntries];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
readStream(process.stdout, 'stdout');
|
|
65
|
+
readStream(process.stderr, 'stderr');
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
process.kill();
|
|
69
|
+
cleanup();
|
|
70
|
+
}
|
|
71
|
+
}, [activePane, activeContainer?.name, activeContainer?.state])
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (scrollBoxRef.current && logs.length > 0) {
|
|
75
|
+
scrollBoxRef.current.scrollTo({ x: 0, y: scrollBoxRef.current.scrollHeight });
|
|
76
|
+
}
|
|
77
|
+
}, [logs]);
|
|
78
|
+
|
|
79
|
+
function cleanup() {
|
|
80
|
+
setLogs([]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const output = useMemo(() => {
|
|
84
|
+
if (logs.length === 0 && activeContainer?.state === "created") {
|
|
85
|
+
return <Shimmer text="Container starting..." color={RGBA.fromHex(colors.text)} />
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (logs.length === 0) {
|
|
89
|
+
return <text fg={colors.textMuted}>No logs available</text>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return logs.map((log, index) => (
|
|
93
|
+
<text
|
|
94
|
+
key={index}
|
|
95
|
+
>
|
|
96
|
+
{log.text}
|
|
97
|
+
</text>
|
|
98
|
+
));
|
|
99
|
+
}, [logs]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Pane
|
|
103
|
+
title="Logs"
|
|
104
|
+
flexDirection="column"
|
|
105
|
+
width="70%"
|
|
106
|
+
>
|
|
107
|
+
<box flexDirection="column" gap={1} height="100%">
|
|
108
|
+
<box flexDirection="row" gap={2} height="auto">
|
|
109
|
+
<box flexDirection="column" gap={1}>
|
|
110
|
+
<text fg={termColors.purple11} attributes={TextAttributes.BOLD}>Container</text>
|
|
111
|
+
<text>{activeContainer?.name || "None"}</text>
|
|
112
|
+
</box>
|
|
113
|
+
<box flexDirection="column">
|
|
114
|
+
<text fg={termColors.purple11} attributes={TextAttributes.BOLD}>Status</text>
|
|
115
|
+
<text>{activeContainer?.status || "None"}</text>
|
|
116
|
+
</box>
|
|
117
|
+
</box>
|
|
118
|
+
<scrollbox
|
|
119
|
+
ref={scrollBoxRef}
|
|
120
|
+
scrollY={true}
|
|
121
|
+
stickyScroll={true}
|
|
122
|
+
stickyStart="bottom"
|
|
123
|
+
height="100%"
|
|
124
|
+
>
|
|
125
|
+
<box flexDirection="column" height="auto">
|
|
126
|
+
{output}
|
|
127
|
+
</box>
|
|
128
|
+
</scrollbox>
|
|
129
|
+
</box>
|
|
130
|
+
</Pane>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { SplitBorder } from "./Border";
|
|
2
|
+
import type { BoxProps } from "@opentui/react";
|
|
3
|
+
import { padding, colors, termColors } from "../utils/styling";
|
|
4
|
+
import { TextAttributes } from "@opentui/core";
|
|
5
|
+
|
|
6
|
+
interface PaneProps extends BoxProps {
|
|
7
|
+
title?: string;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function Pane({
|
|
13
|
+
title,
|
|
14
|
+
active = false,
|
|
15
|
+
children,
|
|
16
|
+
...props
|
|
17
|
+
}: PaneProps) {
|
|
18
|
+
return (
|
|
19
|
+
<box
|
|
20
|
+
{...SplitBorder}
|
|
21
|
+
{...props}
|
|
22
|
+
borderColor={active ? termColors.purple11 : colors.backgroundPanel}
|
|
23
|
+
>
|
|
24
|
+
<box
|
|
25
|
+
flexGrow={1}
|
|
26
|
+
backgroundColor={colors.backgroundPanel}
|
|
27
|
+
{...padding}
|
|
28
|
+
>
|
|
29
|
+
{title && <text marginBottom={1} attributes={TextAttributes.BOLD}>{title}</text>}
|
|
30
|
+
{children}
|
|
31
|
+
</box>
|
|
32
|
+
</box>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useKeyboard } from "@opentui/react";
|
|
3
|
+
import Pane from "./Pane";
|
|
4
|
+
import { colors } from "../utils/styling";
|
|
5
|
+
import { useVolumeStore } from "../stores/volumes";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
8
|
+
|
|
9
|
+
export default function ImagesPane() {
|
|
10
|
+
const { activePane, setActivePane } = useApplicationStore((state) => state);
|
|
11
|
+
const { volumes, setVolumes } = useVolumeStore();
|
|
12
|
+
const scrollBoxRef = useRef<ScrollBoxRenderable>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const process = Bun.spawn(
|
|
16
|
+
["docker", "volume", "ls", "--format", "{{.Name}}"],
|
|
17
|
+
{stdout: "pipe", stderr: "pipe"},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
async function read() {
|
|
21
|
+
try {
|
|
22
|
+
const reader = process.stdout.getReader();
|
|
23
|
+
|
|
24
|
+
while (true) {
|
|
25
|
+
const { done, value } = await reader.read();
|
|
26
|
+
if (done) break;
|
|
27
|
+
const decodedValues = new TextDecoder().decode(value);
|
|
28
|
+
const lines = decodedValues.split("\n").filter(Boolean);
|
|
29
|
+
setVolumes(lines);
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
read();
|
|
37
|
+
|
|
38
|
+
return () => process.kill();
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useKeyboard((key) => {
|
|
42
|
+
if (activePane !== "volumes") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (key.name === "left") {
|
|
47
|
+
setActivePane("images");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (key.name === "right" || key.name === "tab") {
|
|
51
|
+
setActivePane("containers");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Pane
|
|
57
|
+
title="Volumes"
|
|
58
|
+
active={activePane === "volumes"}
|
|
59
|
+
width="100%"
|
|
60
|
+
flexDirection="column"
|
|
61
|
+
>
|
|
62
|
+
<scrollbox
|
|
63
|
+
ref={scrollBoxRef}
|
|
64
|
+
scrollY={true}
|
|
65
|
+
stickyScroll={true}
|
|
66
|
+
stickyStart="bottom"
|
|
67
|
+
viewportOptions={{
|
|
68
|
+
flexGrow: 1
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{volumes.map((item, index) => {
|
|
72
|
+
return <box>
|
|
73
|
+
<text
|
|
74
|
+
key={index}
|
|
75
|
+
content={`[*] ${item}`}
|
|
76
|
+
fg={colors.textMuted}
|
|
77
|
+
/>
|
|
78
|
+
</box>
|
|
79
|
+
})}
|
|
80
|
+
{volumes.length < 1 && <text fg={colors.textMuted}>No Volumes</text>}
|
|
81
|
+
</scrollbox>
|
|
82
|
+
</Pane>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { RGBA } from "@opentui/core"
|
|
2
|
+
import { useTimeline } from "@opentui/react"
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
|
|
5
|
+
export type ShimmerProps = {
|
|
6
|
+
text: string
|
|
7
|
+
color: RGBA
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DURATION = 2_500
|
|
11
|
+
|
|
12
|
+
export function Shimmer(props: ShimmerProps) {
|
|
13
|
+
const timeline = useTimeline({
|
|
14
|
+
duration: DURATION,
|
|
15
|
+
loop: true,
|
|
16
|
+
})
|
|
17
|
+
const characters = props.text.split("")
|
|
18
|
+
const color = props.color
|
|
19
|
+
|
|
20
|
+
const [shimmerValues, setShimmerValues] = useState<number[]>(() =>
|
|
21
|
+
characters.map(() => 0.4)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
characters.forEach((_, i) => {
|
|
26
|
+
const target = {
|
|
27
|
+
shimmer: shimmerValues[i],
|
|
28
|
+
setShimmer: (value: number) => {
|
|
29
|
+
setShimmerValues(prev => {
|
|
30
|
+
const newValues = [...prev]
|
|
31
|
+
newValues[i] = value
|
|
32
|
+
return newValues
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
timeline!.add(
|
|
38
|
+
target,
|
|
39
|
+
{
|
|
40
|
+
shimmer: 1,
|
|
41
|
+
duration: DURATION / (props.text.length + 1),
|
|
42
|
+
ease: "linear",
|
|
43
|
+
alternate: true,
|
|
44
|
+
loop: 2,
|
|
45
|
+
onUpdate: () => {
|
|
46
|
+
target.setShimmer(target.shimmer)
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
(i * (DURATION / (props.text.length + 1))) / 2,
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
}, [timeline, props.text.length])
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<text>
|
|
56
|
+
{characters.map((ch, i) => {
|
|
57
|
+
const shimmer = shimmerValues[i]
|
|
58
|
+
const fg = RGBA.fromInts(color.r * 255, color.g * 255, color.b * 255, shimmer * 255)
|
|
59
|
+
return <span key={i} style={{ fg }}>{ch}</span>
|
|
60
|
+
})}
|
|
61
|
+
</text>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { colors, padding } from "../utils/styling";
|
|
5
|
+
import { TextAttributes } from "@opentui/core";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
import { SplitBorder } from "../components/Border";
|
|
8
|
+
|
|
9
|
+
export default function BaseLayout({ children }: { children: React.ReactNode }) {
|
|
10
|
+
const { activePane, setActivePane } = useApplicationStore((state) => state);
|
|
11
|
+
const dimensions = useTerminalDimensions();
|
|
12
|
+
const [pwd, setPwd] = useState<string>("");
|
|
13
|
+
const version = "v0.1.3";
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
Bun.$`pwd`.quiet().then(result => setPwd(result.text()));
|
|
17
|
+
setActivePane("containers");
|
|
18
|
+
}, [setActivePane]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<box
|
|
22
|
+
width={dimensions.width}
|
|
23
|
+
height={dimensions.height}
|
|
24
|
+
backgroundColor={colors.background}
|
|
25
|
+
>
|
|
26
|
+
<box
|
|
27
|
+
flexDirection="row"
|
|
28
|
+
gap={1}
|
|
29
|
+
width="100%"
|
|
30
|
+
{...padding}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</box>
|
|
34
|
+
<box
|
|
35
|
+
height={1}
|
|
36
|
+
backgroundColor={colors.backgroundPanel}
|
|
37
|
+
flexDirection="row"
|
|
38
|
+
justifyContent="space-between"
|
|
39
|
+
flexShrink={0}
|
|
40
|
+
>
|
|
41
|
+
<box flexDirection="row">
|
|
42
|
+
<box flexDirection="row" backgroundColor={colors.backgroundElement} paddingLeft={1} paddingRight={1}>
|
|
43
|
+
<text fg={colors.textMuted}>open</text>
|
|
44
|
+
<text attributes={TextAttributes.BOLD}>docker </text>
|
|
45
|
+
<text fg={colors.textMuted}>{version}</text>
|
|
46
|
+
</box>
|
|
47
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
48
|
+
<text fg={colors.textMuted}>~{pwd}</text>
|
|
49
|
+
</box>
|
|
50
|
+
</box>
|
|
51
|
+
<box flexDirection="row" gap={1}>
|
|
52
|
+
{/* <text fg={colors.textMuted}>tab</text> */}
|
|
53
|
+
<box backgroundColor={colors.accent} paddingLeft={1} paddingRight={1} {...SplitBorder} borderColor={colors.backgroundPanel}>
|
|
54
|
+
<text fg={colors.backgroundPanel} attributes={TextAttributes.BOLD}>{activePane}</text>
|
|
55
|
+
</box>
|
|
56
|
+
</box>
|
|
57
|
+
</box>
|
|
58
|
+
</box>
|
|
59
|
+
)
|
|
60
|
+
}
|