opendocker 0.1.4 → 0.1.6
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/dist/main.js +1 -1
- package/package.json +2 -5
- package/.github/FUNDING.yml +0 -15
- package/AGENTS.md +0 -22
- package/TODO.md +0 -53
- package/bun.lock +0 -225
- package/bunfig.toml +0 -2
- package/docker-compose.yml +0 -24
- package/scripts/build.ts +0 -139
- package/src/assets/images/banner.jpg +0 -0
- package/src/components/Border.tsx +0 -19
- package/src/components/ContainersPane.tsx +0 -146
- package/src/components/ImagesPane.tsx +0 -83
- package/src/components/LogsPane.tsx +0 -132
- package/src/components/Pane.tsx +0 -34
- package/src/components/VolumesPanes.tsx +0 -84
- package/src/components/ui/Shimmer.tsx +0 -64
- package/src/layouts/BaseLayout.tsx +0 -60
- package/src/lib/docker.ts +0 -68
- package/src/main.tsx +0 -43
- package/src/stores/application.ts +0 -11
- package/src/stores/containers.ts +0 -22
- package/src/stores/images.ts +0 -11
- package/src/stores/volumes.ts +0 -11
- package/src/utils/styling.ts +0 -198
- package/tsconfig.json +0 -15
|
@@ -1,146 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
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
|
-
}
|
package/src/components/Pane.tsx
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,60 +0,0 @@
|
|
|
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.4";
|
|
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
|
-
}
|
package/src/lib/docker.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export class Docker {
|
|
3
|
-
private socket = "/var/run/docker.sock";
|
|
4
|
-
|
|
5
|
-
private async request(path: string) {
|
|
6
|
-
try {
|
|
7
|
-
const process = Bun.spawn([
|
|
8
|
-
"curl", "-s", "--unix-socket", this.socket,
|
|
9
|
-
`http://localhost${path}`
|
|
10
|
-
], { stdout: "pipe", stderr: "pipe" });
|
|
11
|
-
|
|
12
|
-
const output = await new Response(process.stdout).text();
|
|
13
|
-
process.kill();
|
|
14
|
-
|
|
15
|
-
return JSON.parse(output);
|
|
16
|
-
} catch (error) {
|
|
17
|
-
console.error('Docker socket request failed:', error);
|
|
18
|
-
throw error;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async getContainers() {
|
|
23
|
-
return this.request("/v1.41/containers/json?all=true");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async watch(callback: (containers: any[]) => void) {
|
|
27
|
-
try {
|
|
28
|
-
let containers = await this.getContainers();
|
|
29
|
-
callback(containers);
|
|
30
|
-
|
|
31
|
-
const process = Bun.spawn([
|
|
32
|
-
"curl", "-s", "--no-buffer", "--unix-socket", this.socket,
|
|
33
|
-
"http://localhost/v1.41/events"
|
|
34
|
-
], { stdout: "pipe", stderr: "pipe" });
|
|
35
|
-
|
|
36
|
-
const reader = process.stdout.getReader();
|
|
37
|
-
const decoder = new TextDecoder();
|
|
38
|
-
let buffer = "";
|
|
39
|
-
|
|
40
|
-
while (true) {
|
|
41
|
-
const { value, done } = await reader.read();
|
|
42
|
-
if (done) break;
|
|
43
|
-
|
|
44
|
-
buffer += decoder.decode(value, { stream: true });
|
|
45
|
-
const lines = buffer.split('\n');
|
|
46
|
-
buffer = lines.pop() || "";
|
|
47
|
-
|
|
48
|
-
for (const line of lines) {
|
|
49
|
-
if (line.trim()) {
|
|
50
|
-
try {
|
|
51
|
-
const event = JSON.parse(line);
|
|
52
|
-
if (event.Type === "container") {
|
|
53
|
-
containers = await this.getContainers();
|
|
54
|
-
callback(containers);
|
|
55
|
-
}
|
|
56
|
-
} catch (parseError) {
|
|
57
|
-
console.error('Failed to parse event:', line, parseError);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.error('Docker watch failed', error);
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|