Skip to content

Commit ec0cbae

Browse files
author
Timo
committed
Reconstruction app_cli.go
1 parent 6c59311 commit ec0cbae

7 files changed

Lines changed: 883 additions & 1007 deletions

File tree

pkg/app/app_cli.go

Lines changed: 80 additions & 748 deletions
Large diffs are not rendered by default.

pkg/app/gui.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package app
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"os/signal"
9+
"strings"
10+
"syscall"
11+
"time"
12+
13+
"github.com/chzyer/readline"
14+
fzf "github.com/junegunn/fzf/src"
15+
"github.com/yaogh99123/dcli/pkg/utils"
16+
)
17+
18+
// runFzfSelect 是一个通用的 FZF 选择器封装
19+
func (app *App) runFzfSelect(header string, lines []string) string {
20+
if len(lines) == 0 {
21+
return ""
22+
}
23+
24+
// 1. 设置 fzf 选项
25+
fzfArgs := []string{
26+
"--height=40%",
27+
"--reverse",
28+
fmt.Sprintf("--header=%s", header),
29+
"--cycle",
30+
}
31+
32+
opts, err := fzf.ParseOptions(true, fzfArgs)
33+
if err != nil {
34+
return ""
35+
}
36+
37+
// 2. 准备输入通道 (带缓冲)
38+
inputChan := make(chan string, len(lines))
39+
for _, line := range lines {
40+
inputChan <- line
41+
}
42+
close(inputChan)
43+
opts.Input = inputChan
44+
45+
// 3. 准备输出通道
46+
outputChan := make(chan string, 1)
47+
opts.Output = outputChan
48+
49+
// 4. 运行 fzf
50+
// 在启动 fzf 前,必须先关闭 readline 实例以释放终端控制权
51+
if app.RLInstance != nil {
52+
app.RLInstance.Close()
53+
}
54+
55+
code, _ := fzf.Run(opts)
56+
57+
// fzf 运行结束后,重新初始化 readline 供主循环使用
58+
app.RLInstance, _ = readline.NewEx(&readline.Config{
59+
Prompt: fmt.Sprintf("%s请选择功能 [0-18,100]: %s", utils.ColorCyan, utils.ColorNC),
60+
InterruptPrompt: "^C",
61+
EOFPrompt: "exit",
62+
})
63+
64+
// 5. 处理结果 (130 为取消/Esc/Ctrl-C)
65+
if code == 130 {
66+
return ""
67+
}
68+
69+
// 6. 获取选中项
70+
select {
71+
case result := <-outputChan:
72+
return result
73+
case <-time.After(50 * time.Millisecond):
74+
}
75+
76+
return ""
77+
}
78+
79+
// parseFzfResult 是一个辅助函数,用于解析 FZF 返回的行(通常格式为 "ID: Text" 或 "Index. Name: Status")
80+
func (app *App) parseFzfResult(result string) string {
81+
if result == "" {
82+
return ""
83+
}
84+
85+
// 策略 1: 经典的 "ID: Text" 格式 (用于菜单)
86+
if strings.Contains(result, ": ") {
87+
parts := strings.Split(result, ":")
88+
if len(parts) > 0 {
89+
left := strings.TrimSpace(parts[0])
90+
// 策略 2: "1. Name" 格式 (用于服务搜索)
91+
dotIdx := strings.Index(left, ". ")
92+
if dotIdx != -1 {
93+
return strings.TrimSpace(left[dotIdx+2:])
94+
}
95+
return left
96+
}
97+
}
98+
99+
return strings.TrimSpace(result)
100+
}
101+
102+
func (app *App) runSubprocessWithQuitKey(cmd *exec.Cmd) error {
103+
cmd.Stdout = os.Stdout
104+
cmd.Stderr = os.Stderr
105+
106+
// 为子进程创建一个单独的管道,用于在特殊情况下强制杀掉它
107+
if err := cmd.Start(); err != nil {
108+
return err
109+
}
110+
111+
done := make(chan error, 1)
112+
go func() {
113+
done <- cmd.Wait()
114+
}()
115+
116+
// 捕获系统中断,防止主程序退出
117+
sigChan := make(chan os.Signal, 1)
118+
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
119+
defer signal.Stop(sigChan)
120+
121+
// 监听 'exit' 命令来退出
122+
quitChan := make(chan bool, 1)
123+
go func() {
124+
scanner := bufio.NewScanner(os.Stdin)
125+
for scanner.Scan() {
126+
text := strings.TrimSpace(scanner.Text())
127+
if text == "exit" {
128+
quitChan <- true
129+
return
130+
}
131+
// 如果只是按了回车或其他,不做任何操作,继续等待
132+
}
133+
}()
134+
135+
select {
136+
case err := <-done:
137+
if err != nil {
138+
fmt.Printf("\n%s指令执行出错: %v%s\n", utils.ColorRed, err, utils.ColorNC)
139+
}
140+
fmt.Printf("\n%s--- 执行完毕,输入 'exit' 返回主菜单 ---%s\n", utils.ColorBlue, utils.ColorNC)
141+
// 子进程虽然结束了,但我们要继续等待 quitChan 里的 'exit' 命令
142+
goto WAIT_LOOP
143+
case <-sigChan:
144+
// 收到 Ctrl+C,打印提示但不退出
145+
fmt.Printf("\n%s[提示] 请输入 'exit' 并回车以返回主菜单%s\n", utils.ColorYellow, utils.ColorNC)
146+
goto WAIT_LOOP
147+
case <-quitChan:
148+
// 收到 exit 命令,如果子进程还在跑,就杀掉它
149+
_ = cmd.Process.Signal(os.Interrupt)
150+
fmt.Printf("\n%s正在返回主菜单...%s\n", utils.ColorBlue, utils.ColorNC)
151+
select {
152+
case <-done:
153+
case <-time.After(500 * time.Millisecond):
154+
_ = cmd.Process.Kill()
155+
}
156+
return nil
157+
}
158+
159+
WAIT_LOOP:
160+
// 无论是因为子进程结束还是收到信号,都必须等到 quitChan 收到 'exit' 为止
161+
for {
162+
select {
163+
case <-sigChan:
164+
fmt.Printf("\n%s[提示] 必须输入 'exit' 才能退出当前界面%s\n", utils.ColorYellow, utils.ColorNC)
165+
case <-quitChan:
166+
return nil
167+
case <-done:
168+
// 这种情况下进程已经通过 done 退出了,不需要再处理,只需处理 quitChan
169+
}
170+
}
171+
}
172+
173+
// runInteractiveSubprocess 专门用于需要完全控制 Stdin 的场景(如 docker exec -it)
174+
func (app *App) runInteractiveSubprocess(cmd *exec.Cmd) error {
175+
cmd.Stdout = os.Stdout
176+
cmd.Stderr = os.Stderr
177+
cmd.Stdin = os.Stdin
178+
179+
// 捕获信号,但不做特殊处理,让它透传给子进程
180+
sigChan := make(chan os.Signal, 1)
181+
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
182+
defer signal.Stop(sigChan)
183+
184+
if err := cmd.Start(); err != nil {
185+
return err
186+
}
187+
188+
done := make(chan error, 1)
189+
go func() {
190+
done <- cmd.Wait()
191+
}()
192+
193+
select {
194+
case err := <-done:
195+
return err
196+
case <-sigChan:
197+
// 收到信号时,将信号发给子进程
198+
_ = cmd.Process.Signal(os.Interrupt)
199+
// 等待子进程退出
200+
select {
201+
case <-done:
202+
case <-time.After(1 * time.Second):
203+
_ = cmd.Process.Kill()
204+
}
205+
return nil
206+
}
207+
}
208+
209+
// ReadInput is a helper to read input using readline
210+
func (app *App) ReadInput(prompt string) string {
211+
if app.RLInstance == nil {
212+
var input string
213+
fmt.Print(prompt)
214+
_, _ = fmt.Scanln(&input)
215+
return input
216+
}
217+
218+
oldPrompt := app.RLInstance.Config.Prompt
219+
app.RLInstance.SetPrompt(prompt)
220+
defer app.RLInstance.SetPrompt(oldPrompt)
221+
222+
line, err := app.RLInstance.Readline()
223+
if err != nil {
224+
return ""
225+
}
226+
return strings.TrimSpace(line)
227+
}

pkg/manager/image.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package manager
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"strings"
7+
"time"
8+
9+
"github.com/docker/docker/api/types/image"
10+
"github.com/yaogh99123/dcli/pkg/commands"
11+
"github.com/yaogh99123/dcli/pkg/utils"
12+
)
13+
14+
// RunImageMenu handles image management CLI interactions
15+
func RunImageMenu(dockerCmd *commands.DockerCommand, readInput func(string) string, runInteractiveSubprocess func(*exec.Cmd) error) {
16+
for {
17+
images, err := dockerCmd.RefreshImages()
18+
if err != nil {
19+
fmt.Printf("%s错误: %v%s\n", utils.ColorRed, err, utils.ColorNC)
20+
return
21+
}
22+
23+
fmt.Printf("\033[H\033[2J") // 清屏
24+
fmt.Printf("%s========================================%s\n", utils.ColorBlue, utils.ColorNC)
25+
fmt.Printf("%s Docker 镜像管理%s\n", utils.ColorBlue, utils.ColorNC)
26+
fmt.Printf("%s========================================%s\n\n", utils.ColorBlue, utils.ColorNC)
27+
28+
// 打印表头
29+
fmt.Printf("%-50s %-12s %-12s %-12s %-10s\n", "IMAGE", "ID", "DISK USAGE", "CONTENT SIZE", "EXTRA")
30+
for i, img := range images {
31+
name := img.Name
32+
if len(name) > 48 {
33+
name = name[:45] + "..."
34+
}
35+
36+
extra := ""
37+
if img.Image.Containers == -1 {
38+
extra = "U" // Unused
39+
}
40+
41+
fmt.Printf("%2d. %-46s %-12s %-12s %-12s %-10s\n",
42+
i+1, name, img.ID[:12], img.GetDisplaySize(), img.GetDisplayContentSize(), extra)
43+
}
44+
45+
fmt.Printf("\n%s镜像操作%s\n", utils.ColorGreen, utils.ColorNC)
46+
fmt.Println("------------------------------")
47+
fmt.Println(" 1. 拉取镜像")
48+
fmt.Println(" 2. 删除指定镜像")
49+
fmt.Println(" 3. 删除所有镜像")
50+
fmt.Println("------------------------------")
51+
fmt.Println(" 0. 返回")
52+
fmt.Println("------------------------------")
53+
fmt.Print("请选择: ")
54+
55+
input := readInput("")
56+
input = strings.TrimSpace(input)
57+
58+
if input == "0" || input == "" {
59+
break
60+
}
61+
62+
switch input {
63+
case "1":
64+
fmt.Print("请输入要拉取的镜像名称 (如 nginx:latest): ")
65+
name := readInput("")
66+
name = strings.TrimSpace(name)
67+
if name != "" {
68+
fmt.Printf("%s正在拉取镜像 %s...%s\n", utils.ColorYellow, name, utils.ColorNC)
69+
cmd := exec.Command("docker", "pull", name)
70+
_ = runInteractiveSubprocess(cmd)
71+
}
72+
case "2":
73+
fmt.Print("请输入要删除的镜像索引: ")
74+
idxStr := readInput("")
75+
var idx int
76+
_, _ = fmt.Sscanf(strings.TrimSpace(idxStr), "%d", &idx)
77+
if idx > 0 && idx <= len(images) {
78+
img := images[idx-1]
79+
fmt.Printf("%s确定要删除镜像 %s (ID: %s) 吗? (y/n): %s", utils.ColorYellow, img.Name, img.ID[:12], utils.ColorNC)
80+
confirm := readInput("")
81+
if strings.ToLower(strings.TrimSpace(confirm)) == "y" {
82+
fmt.Printf("%s正在删除镜像...%s\n", utils.ColorYellow, utils.ColorNC)
83+
if err := img.Remove(image.RemoveOptions{Force: true}); err != nil {
84+
fmt.Printf("%s失败: %v%s\n", utils.ColorRed, err, utils.ColorNC)
85+
} else {
86+
fmt.Printf("%s成功%s\n", utils.ColorGreen, utils.ColorNC)
87+
}
88+
time.Sleep(1 * time.Second)
89+
}
90+
} else {
91+
fmt.Printf("%s无效的索引%s\n", utils.ColorRed, utils.ColorNC)
92+
time.Sleep(1 * time.Second)
93+
}
94+
case "3":
95+
fmt.Printf("%s危险: 这将删除所有未被使用的镜像! 是否继续? (y/n): %s", utils.ColorRed, utils.ColorNC)
96+
confirm := readInput("")
97+
if strings.ToLower(strings.TrimSpace(confirm)) == "y" {
98+
fmt.Printf("%s正在清理所有未使用镜像...%s\n", utils.ColorYellow, utils.ColorNC)
99+
cmd := exec.Command("docker", "image", "prune", "-af")
100+
_ = runInteractiveSubprocess(cmd)
101+
}
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)