2
0
mirror of https://github.com/acepanel/helper.git synced 2026-02-04 06:43:15 +08:00
Files
helper/internal/ui/app.go
2026-01-24 16:41:32 +08:00

987 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ui
import (
"context"
"errors"
"fmt"
"os/exec"
"strings"
"time"
"github.com/acepanel/helper/pkg/config"
"github.com/acepanel/helper/pkg/embed"
"github.com/charmbracelet/bubbles/progress"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/leonelquinteros/gotext"
"github.com/acepanel/helper/internal/service"
"github.com/acepanel/helper/pkg/i18n"
"github.com/acepanel/helper/pkg/types"
)
// ViewState 视图状态
type ViewState int
const (
ViewLanguageSelect ViewState = iota
ViewMainMenu
ViewInstall
ViewUninstall
ViewMount
)
// MenuChoice 菜单选项
type MenuChoice int
const (
MenuInstall MenuChoice = iota
MenuUninstall
MenuMount
MenuExit
)
// 消息类型
type (
progressMsg types.Progress
verboseMsg string // verbose 模式命令输出
installCompleteMsg struct {
err error
info string
}
uninstallCompleteMsg struct{ err error }
mountCompleteMsg struct{ err error }
countdownTickMsg struct{ remaining int }
disksLoadedMsg struct {
disks []types.DiskInfo
err error
}
)
// App 主应用
type App struct {
state ViewState
width int
height int
program *tea.Program
installer service.Installer
uninstaller service.Uninstaller
mounter service.Mounter
// 语言选择
langForm *huh.Form
langChoice string
// 主菜单
menuForm *huh.Form
menuChoice MenuChoice
// 安装
installForm *huh.Form
installConfirmed bool
setupPath string
installSpinner spinner.Model
installProgress progress.Model
installStep string
installPercent float64
installLogs []string
installErr error
installRunning bool
installDone bool
installInfo string // 安装完成后的面板信息
// verbose 模式
verboseLogs []string
verboseViewport viewport.Model
// 卸载
uninstallCountdown int
uninstallSkipCount int // 连按enter跳过倒计时的计数
uninstallForm *huh.Form
uninstallConfirm bool
uninstallSpinner spinner.Model
uninstallStep string
uninstallLogs []string
uninstallErr error
uninstallRunning bool
uninstallDone bool
uninstallState int // 0=warning, 1=countdown, 2=confirm, 3=running, 4=done
// 磁盘分区
disks []types.DiskInfo
mountDiskForm *huh.Form
mountPointForm *huh.Form
mountFSForm *huh.Form
mountConfirmForm *huh.Form
mountConfirmed bool
mountConfig types.MountConfig
mountSpinner spinner.Model
mountStep string
mountLogs []string
mountErr error
mountRunning bool
mountDone bool
mountState int // 0=loading, 1=selectDisk, 2=selectMount, 3=selectFS, 4=confirm, 5=running, 6=done
}
// NewApp 创建应用
func NewApp(installer service.Installer, uninstaller service.Uninstaller, mounter service.Mounter) *App {
app := &App{
state: ViewLanguageSelect,
langChoice: "zh_CN",
installer: installer,
uninstaller: uninstaller,
mounter: mounter,
setupPath: "/opt/ace",
mountConfig: types.MountConfig{
MountPoint: "/opt/ace",
FSType: types.FSTypeExt4,
},
}
// 初始化 spinner
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(ColorPrimary)
app.installSpinner = s
app.uninstallSpinner = s
app.mountSpinner = s
// 初始化进度条
app.installProgress = progress.New(progress.WithDefaultGradient())
// 初始化 verbose viewport
app.verboseViewport = viewport.New(80, 8)
app.verboseViewport.Style = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(ColorMuted).
Padding(0, 1)
return app
}
func (a *App) SetProgram(p *tea.Program) {
a.program = p
}
func (a *App) Init() tea.Cmd {
a.langForm = huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select Language / 选择语言").
Options(
huh.NewOption("简体中文", "zh_CN"),
huh.NewOption("繁體中文", "zh_TW"),
huh.NewOption("English", "en_US"),
).
Value(&a.langChoice),
),
).WithTheme(huh.ThemeDracula())
return a.langForm.Init()
}
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
a.width = msg.Width
a.height = msg.Height
a.installProgress.Width = msg.Width - 10
case tea.KeyMsg:
if msg.String() == "ctrl+c" {
return a, tea.Quit
}
}
switch a.state {
case ViewLanguageSelect:
return a.updateLanguageSelect(msg)
case ViewMainMenu:
return a.updateMainMenu(msg)
case ViewInstall:
return a.updateInstall(msg)
case ViewUninstall:
return a.updateUninstall(msg)
case ViewMount:
return a.updateMount(msg)
}
return a, nil
}
func (a *App) View() string {
switch a.state {
case ViewLanguageSelect:
return a.viewLanguageSelect()
case ViewMainMenu:
return a.viewMainMenu()
case ViewInstall:
return a.viewInstall()
case ViewUninstall:
return a.viewUninstall()
case ViewMount:
return a.viewMount()
}
return ""
}
// ========== 语言选择 ==========
func (a *App) updateLanguageSelect(msg tea.Msg) (tea.Model, tea.Cmd) {
form, cmd := a.langForm.Update(msg)
if f, ok := form.(*huh.Form); ok {
a.langForm = f
}
if a.langForm.State == huh.StateCompleted {
locale := gotext.NewLocaleFSWithPath(a.langChoice, embed.LocalesFS, "locales")
locale.AddDomain("helper")
i18n.T = locale
a.state = ViewMainMenu
return a, a.initMainMenu()
}
return a, cmd
}
func (a *App) viewLanguageSelect() string {
return RenderLogo() + "\n" + a.langForm.View()
}
// ========== 主菜单 ==========
func (a *App) initMainMenu() tea.Cmd {
a.menuForm = huh.NewForm(
huh.NewGroup(
huh.NewSelect[MenuChoice]().
Title(i18n.T.Get("Select Operation")).
Options(
huh.NewOption(i18n.T.Get("Install Panel"), MenuInstall),
huh.NewOption(i18n.T.Get("Uninstall Panel"), MenuUninstall),
huh.NewOption(i18n.T.Get("Disk Partition"), MenuMount),
huh.NewOption(i18n.T.Get("Exit"), MenuExit),
).
Value(&a.menuChoice),
),
).WithTheme(huh.ThemeDracula())
return a.menuForm.Init()
}
func (a *App) updateMainMenu(msg tea.Msg) (tea.Model, tea.Cmd) {
form, cmd := a.menuForm.Update(msg)
if f, ok := form.(*huh.Form); ok {
a.menuForm = f
}
if a.menuForm.State == huh.StateCompleted {
switch a.menuChoice {
case MenuInstall:
a.state = ViewInstall
a.installRunning = false
a.installDone = false
a.installErr = nil
a.installLogs = nil
return a, a.initInstall()
case MenuUninstall:
a.state = ViewUninstall
a.uninstallState = 0
a.uninstallRunning = false
a.uninstallDone = false
a.uninstallErr = nil
a.uninstallLogs = nil
a.uninstallCountdown = 60
return a, nil
case MenuMount:
a.state = ViewMount
a.mountState = 0
a.mountRunning = false
a.mountDone = false
a.mountErr = nil
a.mountLogs = nil
return a, tea.Batch(a.mountSpinner.Tick, a.loadDisks())
case MenuExit:
return a, tea.Quit
}
}
return a, cmd
}
func (a *App) viewMainMenu() string {
return RenderLogo() + "\n" + a.menuForm.View()
}
// ========== 安装面板 ==========
func (a *App) initInstall() tea.Cmd {
a.installForm = huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(i18n.T.Get("Confirm Installation")).
Description(i18n.T.Get("Panel will be installed to %s", a.setupPath)).
Affirmative(i18n.T.Get("Yes")).
Negative(i18n.T.Get("No")).
Value(&a.installConfirmed),
),
).WithTheme(huh.ThemeDracula())
return a.installForm.Init()
}
func (a *App) updateInstall(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "esc" && !a.installRunning {
a.state = ViewMainMenu
return a, a.initMainMenu()
}
if msg.String() == "enter" && a.installDone {
a.state = ViewMainMenu
return a, a.initMainMenu()
}
case progressMsg:
a.installStep = msg.Step
a.installPercent = msg.Percent
if msg.Message != "" {
a.installLogs = append(a.installLogs, msg.Message)
if len(a.installLogs) > 8 {
a.installLogs = a.installLogs[1:]
}
}
return a, nil
case verboseMsg:
a.verboseLogs = append(a.verboseLogs, string(msg))
// 保留最近 100 条
if len(a.verboseLogs) > 100 {
a.verboseLogs = a.verboseLogs[len(a.verboseLogs)-100:]
}
a.verboseViewport.SetContent(strings.Join(a.verboseLogs, "\n"))
a.verboseViewport.GotoBottom()
return a, nil
case installCompleteMsg:
a.installRunning = false
a.installDone = true
a.installErr = msg.err
a.installInfo = msg.info
return a, nil
case spinner.TickMsg:
if a.installRunning {
var cmd tea.Cmd
a.installSpinner, cmd = a.installSpinner.Update(msg)
return a, cmd
}
}
if !a.installRunning && !a.installDone {
form, cmd := a.installForm.Update(msg)
if f, ok := form.(*huh.Form); ok {
a.installForm = f
}
if a.installForm.State == huh.StateCompleted {
if a.installConfirmed {
a.installRunning = true
a.verboseLogs = nil // 清空 verbose 日志
return a, tea.Batch(a.installSpinner.Tick, a.startInstall())
}
a.state = ViewMainMenu
return a, a.initMainMenu()
}
return a, cmd
}
return a, nil
}
func (a *App) startInstall() tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
progressCh := make(chan types.Progress, 10)
go func() {
for p := range progressCh {
if a.program != nil {
a.program.Send(progressMsg(p))
}
}
}()
// 设置 verbose 回调
if config.Global.Verbose {
a.installer.SetVerboseCallback(func(cmd string) {
if a.program != nil {
a.program.Send(verboseMsg("$ " + cmd))
}
})
}
cfg := &types.InstallConfig{
SetupPath: a.setupPath,
AutoSwap: true,
}
err := a.installer.Install(ctx, cfg, progressCh)
close(progressCh)
time.Sleep(100 * time.Millisecond)
// 安装成功后获取面板信息
var info string
if err == nil {
cmd := exec.CommandContext(ctx, "/usr/local/sbin/acepanel", "info")
output, _ := cmd.Output()
info = string(output)
}
return installCompleteMsg{err: err, info: info}
}
}
func (a *App) viewInstall() string {
var sb strings.Builder
sb.WriteString(RenderLogo())
sb.WriteString("\n")
sb.WriteString(RenderTitle(i18n.T.Get("Install Panel")))
sb.WriteString("\n")
if a.installDone {
if a.installErr != nil {
sb.WriteString(RenderError(i18n.T.Get("Installation failed")))
sb.WriteString("\n\n")
sb.WriteString(ErrorBoxStyle.Render(a.installErr.Error()))
} else {
sb.WriteString(RenderSuccess(i18n.T.Get("Installation successful")))
if a.installInfo != "" {
sb.WriteString("\n\n")
sb.WriteString(InfoBoxStyle.Render(strings.TrimSpace(a.installInfo)))
}
}
sb.WriteString("\n\n")
sb.WriteString(RenderHelp("Enter", i18n.T.Get("Back")))
} else if a.installRunning {
sb.WriteString(fmt.Sprintf("%s %s\n\n", a.installSpinner.View(), a.installStep))
sb.WriteString(a.installProgress.ViewAs(a.installPercent))
sb.WriteString("\n\n")
for _, log := range a.installLogs {
sb.WriteString(LogStyle.Render(log))
sb.WriteString("\n")
}
// verbose 模式显示命令日志
if config.Global.Verbose && len(a.verboseLogs) > 0 {
sb.WriteString("\n")
sb.WriteString(MutedStyle.Render("Commands:"))
sb.WriteString("\n")
sb.WriteString(a.verboseViewport.View())
}
} else {
sb.WriteString(a.installForm.View())
}
return sb.String()
}
// ========== 卸载面板 ==========
func (a *App) updateUninstall(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
if !a.uninstallRunning {
a.state = ViewMainMenu
return a, a.initMainMenu()
}
case "enter":
if a.uninstallState == 0 {
a.uninstallState = 1
a.uninstallSkipCount = 0
return a, a.tickCountdown()
}
if a.uninstallState == 1 {
// 倒计时期间连按10次enter可跳过
a.uninstallSkipCount++
if a.uninstallSkipCount >= 10 {
a.uninstallState = 2
a.initUninstallConfirm()
return a, a.uninstallForm.Init()
}
}
if a.uninstallDone {
a.state = ViewMainMenu
return a, a.initMainMenu()
}
}
case countdownTickMsg:
a.uninstallCountdown = msg.remaining
if a.uninstallCountdown <= 0 {
a.uninstallState = 2
a.initUninstallConfirm()
return a, a.uninstallForm.Init()
}
return a, a.tickCountdown()
case progressMsg:
a.uninstallStep = msg.Step
if msg.Message != "" {
a.uninstallLogs = append(a.uninstallLogs, msg.Message)
if len(a.uninstallLogs) > 8 {
a.uninstallLogs = a.uninstallLogs[1:]
}
}
return a, nil
case verboseMsg:
a.verboseLogs = append(a.verboseLogs, string(msg))
if len(a.verboseLogs) > 100 {
a.verboseLogs = a.verboseLogs[len(a.verboseLogs)-100:]
}
a.verboseViewport.SetContent(strings.Join(a.verboseLogs, "\n"))
a.verboseViewport.GotoBottom()
return a, nil
case uninstallCompleteMsg:
a.uninstallRunning = false
a.uninstallDone = true
a.uninstallState = 4
a.uninstallErr = msg.err
return a, nil
case spinner.TickMsg:
if a.uninstallRunning {
var cmd tea.Cmd
a.uninstallSpinner, cmd = a.uninstallSpinner.Update(msg)
return a, cmd
}
}
if a.uninstallState == 2 {
form, cmd := a.uninstallForm.Update(msg)
if f, ok := form.(*huh.Form); ok {
a.uninstallForm = f
}
if a.uninstallForm.State == huh.StateCompleted {
if a.uninstallConfirm {
a.uninstallState = 3
a.uninstallRunning = true
a.verboseLogs = nil // 清空 verbose 日志
return a, tea.Batch(a.uninstallSpinner.Tick, a.startUninstall())
}
a.state = ViewMainMenu
return a, a.initMainMenu()
}
return a, cmd
}
return a, nil
}
func (a *App) initUninstallConfirm() {
a.uninstallConfirm = false
a.uninstallForm = huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(i18n.T.Get("Confirm Uninstallation")).
Description(i18n.T.Get("Are you sure you want to uninstall the panel?")).
Affirmative(i18n.T.Get("Yes")).
Negative(i18n.T.Get("No")).
Value(&a.uninstallConfirm),
),
).WithTheme(huh.ThemeDracula())
}
func (a *App) tickCountdown() tea.Cmd {
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
return countdownTickMsg{remaining: a.uninstallCountdown - 1}
})
}
func (a *App) startUninstall() tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
// 设置 verbose 回调
if config.Global.Verbose {
a.uninstaller.SetVerboseCallback(func(cmd string) {
if a.program != nil {
a.program.Send(verboseMsg("$ " + cmd))
}
})
}
err := a.uninstaller.Uninstall(ctx, a.setupPath, func(step, message string) {
if a.program != nil {
a.program.Send(progressMsg{Step: step, Message: message})
}
})
return uninstallCompleteMsg{err: err}
}
}
func (a *App) viewUninstall() string {
var sb strings.Builder
sb.WriteString(RenderLogo())
sb.WriteString("\n")
sb.WriteString(RenderTitle(i18n.T.Get("Uninstall Panel")))
sb.WriteString("\n")
switch a.uninstallState {
case 0: // warning
sb.WriteString(WarningBoxStyle.Render(
RenderWarning(i18n.T.Get("High-risk operation")) + "\n\n" +
i18n.T.Get("Please backup all data before uninstalling.") + "\n" +
i18n.T.Get("All data will be cleared and cannot be recovered!"),
))
sb.WriteString("\n\n")
sb.WriteString(RenderHelp("Enter", i18n.T.Get("Continue"), "Esc", i18n.T.Get("Back")))
case 1: // countdown
sb.WriteString(WarningBoxStyle.Render(
fmt.Sprintf("%s\n\n%s %d %s",
i18n.T.Get("For safety, please wait before proceeding"),
i18n.T.Get("Waiting:"),
a.uninstallCountdown,
i18n.T.Get("seconds"),
),
))
sb.WriteString("\n\n")
if a.uninstallSkipCount > 0 {
sb.WriteString(MutedStyle.Render(fmt.Sprintf("Press Enter %d more times to skip", 10-a.uninstallSkipCount)))
sb.WriteString("\n")
}
sb.WriteString(RenderHelp("Esc", i18n.T.Get("Cancel"), "Enter×10", i18n.T.Get("Skip")))
case 2: // confirm
sb.WriteString(a.uninstallForm.View())
case 3: // running
sb.WriteString(fmt.Sprintf("%s %s\n\n", a.uninstallSpinner.View(), a.uninstallStep))
for _, log := range a.uninstallLogs {
sb.WriteString(LogStyle.Render(log))
sb.WriteString("\n")
}
// verbose 模式显示命令日志
if config.Global.Verbose && len(a.verboseLogs) > 0 {
sb.WriteString("\n")
sb.WriteString(MutedStyle.Render("Commands:"))
sb.WriteString("\n")
sb.WriteString(a.verboseViewport.View())
}
case 4: // done
if a.uninstallErr != nil {
sb.WriteString(RenderError(i18n.T.Get("Uninstallation failed")))
sb.WriteString("\n\n")
sb.WriteString(ErrorBoxStyle.Render(a.uninstallErr.Error()))
} else {
sb.WriteString(RenderSuccess(i18n.T.Get("Uninstallation successful")))
sb.WriteString("\n\n")
sb.WriteString(i18n.T.Get("Thank you for using AcePanel."))
}
sb.WriteString("\n\n")
sb.WriteString(RenderHelp("Enter", i18n.T.Get("Back")))
}
return sb.String()
}
// ========== 磁盘分区 ==========
func (a *App) loadDisks() tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
disks, err := a.mounter.ListDisks(ctx)
return disksLoadedMsg{disks: disks, err: err}
}
}
func (a *App) updateMount(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
if !a.mountRunning {
a.state = ViewMainMenu
return a, a.initMainMenu()
}
case "enter":
if a.mountDone {
a.state = ViewMainMenu
return a, a.initMainMenu()
}
}
case disksLoadedMsg:
if msg.err != nil {
a.mountDone = true
a.mountErr = msg.err
return a, nil
}
if len(msg.disks) == 0 {
a.mountDone = true
a.mountErr = errors.New(i18n.T.Get("No available disks found"))
return a, nil
}
a.disks = msg.disks
a.mountState = 1
a.initDiskForm()
return a, a.mountDiskForm.Init()
case progressMsg:
a.mountStep = msg.Step
if msg.Message != "" {
a.mountLogs = append(a.mountLogs, msg.Message)
if len(a.mountLogs) > 8 {
a.mountLogs = a.mountLogs[1:]
}
}
return a, nil
case verboseMsg:
a.verboseLogs = append(a.verboseLogs, string(msg))
if len(a.verboseLogs) > 100 {
a.verboseLogs = a.verboseLogs[len(a.verboseLogs)-100:]
}
a.verboseViewport.SetContent(strings.Join(a.verboseLogs, "\n"))
a.verboseViewport.GotoBottom()
return a, nil
case mountCompleteMsg:
a.mountRunning = false
a.mountDone = true
a.mountErr = msg.err
return a, nil
case spinner.TickMsg:
if a.mountState == 0 || a.mountRunning {
var cmd tea.Cmd
a.mountSpinner, cmd = a.mountSpinner.Update(msg)
return a, cmd
}
}
switch a.mountState {
case 1: // select disk
form, cmd := a.mountDiskForm.Update(msg)
if f, ok := form.(*huh.Form); ok {
a.mountDiskForm = f
}
if a.mountDiskForm.State == huh.StateCompleted {
a.mountState = 2
a.initMountPointForm()
return a, a.mountPointForm.Init()
}
return a, cmd
case 2: // select mount point
form, cmd := a.mountPointForm.Update(msg)
if f, ok := form.(*huh.Form); ok {
a.mountPointForm = f
}
if a.mountPointForm.State == huh.StateCompleted {
a.mountState = 3
a.initFSForm()
return a, a.mountFSForm.Init()
}
return a, cmd
case 3: // select fs
form, cmd := a.mountFSForm.Update(msg)
if f, ok := form.(*huh.Form); ok {
a.mountFSForm = f
}
if a.mountFSForm.State == huh.StateCompleted {
a.mountState = 4
a.initMountConfirmForm()
return a, a.mountConfirmForm.Init()
}
return a, cmd
case 4: // confirm
form, cmd := a.mountConfirmForm.Update(msg)
if f, ok := form.(*huh.Form); ok {
a.mountConfirmForm = f
}
if a.mountConfirmForm.State == huh.StateCompleted {
if a.mountConfirmed {
a.mountState = 5
a.mountRunning = true
a.verboseLogs = nil // 清空 verbose 日志
return a, tea.Batch(a.mountSpinner.Tick, a.startMount())
}
a.state = ViewMainMenu
return a, a.initMainMenu()
}
return a, cmd
}
return a, nil
}
func (a *App) initDiskForm() {
options := make([]huh.Option[string], len(a.disks))
for i, disk := range a.disks {
options[i] = huh.NewOption(fmt.Sprintf("%s (%s)", disk.Name, disk.Size), disk.Name)
}
a.mountDiskForm = huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title(i18n.T.Get("Select Disk")).
Description(i18n.T.Get("Select a disk to partition and mount")).
Options(options...).
Value(&a.mountConfig.Disk),
),
).WithTheme(huh.ThemeDracula())
}
func (a *App) initMountPointForm() {
a.mountPointForm = huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title(i18n.T.Get("Mount Point")).
Description(i18n.T.Get("Enter the mount point path (e.g. /opt/ace)")).
Value(&a.mountConfig.MountPoint).
Validate(func(s string) error {
if len(s) == 0 || s[0] != '/' {
return errors.New(i18n.T.Get("Please enter an absolute path"))
}
return nil
}),
),
).WithTheme(huh.ThemeDracula())
}
func (a *App) initFSForm() {
a.mountFSForm = huh.NewForm(
huh.NewGroup(
huh.NewSelect[types.FSType]().
Title(i18n.T.Get("File System")).
Description(i18n.T.Get("Select the file system type")).
Options(
huh.NewOption(fmt.Sprintf("ext4 (%s)", i18n.T.Get("Recommended")), types.FSTypeExt4),
huh.NewOption("xfs", types.FSTypeXFS),
).
Value(&a.mountConfig.FSType),
),
).WithTheme(huh.ThemeDracula())
}
func (a *App) initMountConfirmForm() {
a.mountConfirmed = false
a.mountConfirmForm = huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(i18n.T.Get("Confirm Operation")).
Description(i18n.T.Get("Disk: %s, Mount Point: %s, File System: %s",
a.mountConfig.Disk,
a.mountConfig.MountPoint,
a.mountConfig.FSType,
)).
Affirmative(i18n.T.Get("Yes")).
Negative(i18n.T.Get("No")).
Value(&a.mountConfirmed),
),
).WithTheme(huh.ThemeDracula())
}
func (a *App) startMount() tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
// 设置 verbose 回调
if config.Global.Verbose {
a.mounter.SetVerboseCallback(func(cmd string) {
if a.program != nil {
a.program.Send(verboseMsg("$ " + cmd))
}
})
}
err := a.mounter.Mount(ctx, &a.mountConfig, func(step, message string) {
if a.program != nil {
a.program.Send(progressMsg{Step: step, Message: message})
}
})
return mountCompleteMsg{err: err}
}
}
func (a *App) viewMount() string {
var sb strings.Builder
sb.WriteString(RenderLogo())
sb.WriteString("\n")
sb.WriteString(RenderTitle(i18n.T.Get("Disk Partition")))
sb.WriteString("\n")
if a.mountDone {
if a.mountErr != nil {
sb.WriteString(RenderError(i18n.T.Get("Operation failed")))
sb.WriteString("\n\n")
sb.WriteString(ErrorBoxStyle.Render(a.mountErr.Error()))
} else {
sb.WriteString(RenderSuccess(i18n.T.Get("Partition and mount successful")))
sb.WriteString("\n\n")
sb.WriteString(BoxStyle.Render(fmt.Sprintf(
"%s: /dev/%s1\n%s: %s\n%s: %s",
i18n.T.Get("Device"), a.mountConfig.Disk,
i18n.T.Get("Mount Point"), a.mountConfig.MountPoint,
i18n.T.Get("File System"), a.mountConfig.FSType,
)))
}
sb.WriteString("\n\n")
sb.WriteString(RenderHelp("Enter", i18n.T.Get("Back")))
return sb.String()
}
switch a.mountState {
case 0: // loading
sb.WriteString(fmt.Sprintf("%s %s", a.mountSpinner.View(), i18n.T.Get("Loading disk list...")))
case 1: // select disk
sb.WriteString(i18n.T.Get("Available disks:"))
sb.WriteString("\n\n")
for _, disk := range a.disks {
sb.WriteString(fmt.Sprintf(" • %s (%s)\n", disk.Name, disk.Size))
}
sb.WriteString("\n")
sb.WriteString(a.mountDiskForm.View())
case 2: // select mount point
sb.WriteString(a.mountPointForm.View())
case 3: // select fs
sb.WriteString(a.mountFSForm.View())
case 4: // confirm
sb.WriteString(WarningBoxStyle.Render(i18n.T.Get("Warning: This will erase all data on the disk!")))
sb.WriteString("\n\n")
sb.WriteString(a.mountConfirmForm.View())
case 5: // running
sb.WriteString(fmt.Sprintf("%s %s\n\n", a.mountSpinner.View(), a.mountStep))
for _, log := range a.mountLogs {
sb.WriteString(LogStyle.Render(log))
sb.WriteString("\n")
}
// verbose 模式显示命令日志
if config.Global.Verbose && len(a.verboseLogs) > 0 {
sb.WriteString("\n")
sb.WriteString(MutedStyle.Render("Commands:"))
sb.WriteString("\n")
sb.WriteString(a.verboseViewport.View())
}
}
return sb.String()
}