|
| 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 | +} |
0 commit comments