# Fyne + Go 命令执行与输出捕获教程

## 1. Fyne 快速入门

### 安装 Fyne

```bash
go get fyne.io/fyne/v2@latest
```

### 最小 Fyne 窗口

```go
package main

import (
    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/widget"
)

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Hello Fyne")

    myWindow.SetContent(widget.NewLabel("Hello, World!"))
    myWindow.Resize(fyne.NewSize(400, 300))
    myWindow.ShowAndRun()
}
```

### 常用控件

| 控件 | 说明 |
|------|------|
| `widget.NewLabel(text)` | 文本标签 |
| `widget.NewButton(text, callback)` | 按钮 |
| `widget.NewEntry()` | 单行输入框 |
| `widget.NewMultiLineEntry()` | 多行输入框 |
| `widget.NewCheck(text, callback)` | 复选框 |
| `widget.NewSelect(options, callback)` | 下拉选择 |
| `widget.NewProgressBar()` | 进度条 |

### 布局容器

| 容器 | 说明 |
|------|------|
| `container.NewVBox(...)` | 垂直排列 |
| `container.NewHBox(...)` | 水平排列 |
| `container.NewBorder(top, bottom, left, right, center)` | 边框布局 |
| `container.NewGridWithColumns(cols, ...)` | 网格布局 |

---

## 2. Go 执行外部命令并获取输出

### 核心包: `os/exec`

Go 标准库 `os/exec` 包提供了执行外部命令的功能。

### 2.1 基础方法

#### 获取命令输出 (一次性)

```go
package main

import (
    "fmt"
    "os/exec"
)

func main() {
    // cmd := exec.Command("cmd", "/c", "dir")   // Windows
    cmd := exec.Command("ls", "-l")              // Linux/macOS

    output, err := cmd.Output() // 返回 []byte
    if err != nil {
        fmt.Println("执行出错:", err)
        return
    }
    fmt.Println(string(output))
}
```

#### 合并 stdout + stderr

```go
output, err := cmd.CombinedOutput() // stdout + stderr 合并
```

#### 分别获取 stdout 和 stderr

```go
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

err := cmd.Run()
fmt.Println("stdout:", stdout.String())
fmt.Println("stderr:", stderr.String())
```

### 2.2 实时读取输出 (逐行)

适用于长时间运行的命令或需要实时显示进度的场景。

```go
func runCommandWithLiveOutput(name string, args ...string) error {
    cmd := exec.Command(name, args...)

    stdout, err := cmd.StdoutPipe()
    if err != nil {
        return err
    }
    stderr, err := cmd.StderrPipe()
    if err != nil {
        return err
    }

    if err := cmd.Start(); err != nil {
        return err
    }

    // 并发读取 stdout
    go func() {
        scanner := bufio.NewScanner(stdout)
        for scanner.Scan() {
            fmt.Println("[OUT]", scanner.Text())
        }
    }()

    // 并发读取 stderr
    go func() {
        scanner := bufio.NewScanner(stderr)
        for scanner.Scan() {
            fmt.Println("[ERR]", scanner.Text())
        }
    }()

    return cmd.Wait()
}
```

### 2.3 设置超时

```go
func runWithTimeout(timeout time.Duration, name string, args ...string) (string, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    cmd := exec.CommandContext(ctx, name, args...)
    output, err := cmd.Output()
    if ctx.Err() == context.DeadlineExceeded {
        return "", fmt.Errorf("命令执行超时 (%v)", timeout)
    }
    return string(output), err
}
```

### 2.4 设置工作目录和环境变量

```go
cmd := exec.Command("make", "build")
cmd.Dir = "/path/to/project"              // 设置工作目录
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") // 追加环境变量
```

### 2.5 Windows 特殊处理

Windows 下 `cmd` 命令需要 `/c` 前缀：

```go
// Windows
cmd := exec.Command("cmd", "/c", "dir")
// 或者使用 PowerShell
cmd := exec.Command("powershell", "-Command", "Get-ChildItem")
```

通用跨平台封装：

```go
func shellCommand(command string) *exec.Cmd {
    if runtime.GOOS == "windows" {
        return exec.Command("cmd", "/c", command)
    }
    return exec.Command("sh", "-c", command)
}
```

---

## 3. Fyne + 命令执行完整示例

### 3.1 简单的命令执行 GUI

```go
package main

import (
    "bytes"
    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/container"
    "fyne.io/fyne/v2/widget"
    "os/exec"
    "runtime"
)

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("命令执行器")
    myWindow.Resize(fyne.NewSize(600, 400))

    cmdEntry := widget.NewEntry()
    cmdEntry.SetPlaceHolder("输入命令，如: ping 127.0.0.1")

    outputLabel := widget.NewMultiLineEntry()
    outputLabel.Disable() // 只读
    outputLabel.Wrapping = fyne.TextWrapWord

    runBtn := widget.NewButton("执行", func() {
        command := cmdEntry.Text
        if command == "" {
            return
        }

        var cmd *exec.Cmd
        if runtime.GOOS == "windows" {
            cmd = exec.Command("cmd", "/c", command)
        } else {
            cmd = exec.Command("sh", "-c", command)
        }

        var out bytes.Buffer
        var errOut bytes.Buffer
        cmd.Stdout = &out
        cmd.Stderr = &errOut

        err := cmd.Run()
        if err != nil {
            outputLabel.SetText("错误: " + err.Error() + "\n" + errOut.String())
            return
        }
        outputLabel.SetText(out.String())
    })

    content := container.NewVBox(
        widget.NewLabel("输入要执行的命令:"),
        cmdEntry,
        runBtn,
        widget.NewLabel("输出:"),
        outputLabel,
    )

    myWindow.SetContent(content)
    myWindow.ShowAndRun()
}
```

### 3.2 带实时输出的进阶版本

```go
package main

import (
    "bufio"
    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/container"
    "fyne.io/fyne/v2/widget"
    "os/exec"
    "runtime"
    "sync"
)

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("实时命令输出")
    myWindow.Resize(fyne.NewSize(700, 500))

    cmdEntry := widget.NewEntry()
    cmdEntry.SetPlaceHolder("输入命令...")

    output := widget.NewMultiLineEntry()
    output.Disable()
    output.Wrapping = fyne.TextWrapWord

    var mu sync.Mutex

    appendOutput := func(text string) {
        mu.Lock()
        defer mu.Unlock()
        current := output.Text
        if len(current) > 0 {
            current += "\n"
        }
        output.SetText(current + text)
    }

    runBtn := widget.NewButton("执行", func() {
        command := cmdEntry.Text
        if command == "" {
            return
        }

        var cmd *exec.Cmd
        if runtime.GOOS == "windows" {
            cmd = exec.Command("cmd", "/c", command)
        } else {
            cmd = exec.Command("sh", "-c", command)
        }

        stdout, _ := cmd.StdoutPipe()
        stderr, _ := cmd.StderrPipe()

        cmd.Start()

        go func() {
            scanner := bufio.NewScanner(stdout)
            for scanner.Scan() {
                appendOutput(scanner.Text())
            }
        }()
        go func() {
            scanner := bufio.NewScanner(stderr)
            for scanner.Scan() {
                appendOutput("[ERR] " + scanner.Text())
            }
        }()

        cmd.Wait()
    })

    clearBtn := widget.NewButton("清空", func() {
        output.SetText("")
    })

    content := container.NewBorder(
        container.NewVBox(
            widget.NewLabelWithStyle("命令执行器 (实时输出)", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
            widget.NewSeparator(),
            cmdEntry,
            container.NewHBox(runBtn, clearBtn),
        ),
        nil, nil, nil,
        output,
    )

    myWindow.SetContent(content)
    myWindow.ShowAndRun()
}
```

### 3.3 带进度条的长时间任务

```go
package main

import (
    "context"
    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/container"
    "fyne.io/fyne/v2/widget"
    "os/exec"
    "time"
)

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("带超时的命令执行")
    myWindow.Resize(fyne.NewSize(500, 300))

    progress := widget.NewProgressBar()
    statusLabel := widget.NewLabel("就绪")

    go func() {
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()

        cmd := exec.CommandContext(ctx, "ping", "-n", "5", "127.0.0.1") // Windows
        // cmd := exec.CommandContext(ctx, "ping", "-c", "5", "127.0.0.1") // Linux/macOS

        // 模拟进度更新
        go func() {
            for i := 0; i <= 100; i++ {
                progress.SetValue(float64(i) / 100.0)
                time.Sleep(100 * time.Millisecond)
            }
        }()

        output, err := cmd.Output()
        if ctx.Err() == context.DeadlineExceeded {
            statusLabel.SetText("执行超时!")
            return
        }
        if err != nil {
            statusLabel.SetText("错误: " + err.Error())
            return
        }
        statusLabel.SetText("完成! 输出: " + string(output))
    }()

    content := container.NewVBox(
        widget.NewLabel("任务进度:"),
        progress,
        statusLabel,
    )

    myWindow.SetContent(content)
    myWindow.ShowAndRun()
}
```

---

## 4. `os/exec` 关键方法对比

| 方法 | 用途 | 特点 |
|------|------|------|
| `cmd.Run()` | 执行命令并等待完成 | stdout/stderr 需手动设置 |
| `cmd.Output()` | 执行并返回 stdout | stderr 为空时出错 |
| `cmd.CombinedOutput()` | 执行并返回 stdout+stderr | stderr 合并到输出 |
| `cmd.Start()` + `cmd.Wait()` | 手动控制启停 | 可配合 Pipe 实现实时输出 |
| `cmd.StdoutPipe()` | 获取 stdout 管道 | 需在 Start 前调用 |
| `cmd.StderrPipe()` | 获取 stderr 管道 | 需在 Start 前调用 |
| `exec.CommandContext()` | 带 context 的命令 | 支持超时/取消 |

---

## 5. 常见问题

### Q: `cmd.Output()` 报 "exit status 1"
**A:** 命令执行失败且 stderr 有内容时，`Output()` 会直接报错。改用 `CombinedOutput()` 或手动设置 `cmd.Stdout` / `cmd.Stderr`。

### Q: 如何执行 `cd` 命令？
**A:** `cd` 是 shell 内置命令，不能用 `exec.Command` 直接执行。应使用 `cmd.Dir` 设置工作目录。

### Q: Fyne 编译后文件太大？
**A:** 使用 `go build -ldflags="-s -w"` 压缩，或使用 `fyne package` 打包。

### Q: Fyne 不支持 Windows 7？
**A:** Fyne v2.4+ 需要 Windows 10 1809+。

---

## 6. 参考资料

- [Fyne 官方文档](https://developer.fyne.io/)
- [Go os/exec 文档](https://pkg.go.dev/os/exec)
- [Fyne GitHub](https://github.com/fyne-io/fyne)
