add tui command

This commit is contained in:
2026-02-21 20:22:21 +08:00
parent fbe6aec707
commit ce9af9f7d0
11 changed files with 1194 additions and 239 deletions

192
pkg/wizard/config.go Normal file
View File

@@ -0,0 +1,192 @@
package wizard
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/charmbracelet/bubbles/textinput"
)
// saveConfig 保存配置到文件
func (m *model) saveConfig() error {
configPath := GetConfigPath()
// 确保目录存在
dir := filepath.Dir(configPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建配置目录失败:%w", err)
}
// 序列化配置
data, err := json.MarshalIndent(m.config, "", " ")
if err != nil {
return fmt.Errorf("序列化配置失败:%w", err)
}
// 写入文件
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("保存配置文件失败:%w", err)
}
return nil
}
// loadConfig 从文件加载配置
func loadConfig() (*Config, error) {
configPath := GetConfigPath()
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败:%w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败:%w", err)
}
return &cfg, nil
}
// 以下是 model.go 中调用的保存方法
func (m *model) saveCurrentPage() {
switch m.currentPage {
case PageData:
m.saveDataPage()
case PagePublicNetwork:
m.savePublicNetworkPage()
case PageInternalNetwork:
m.saveInternalNetworkPage()
case PageDNS:
m.saveDNSPage()
}
}
func (m *model) saveDataPage() {
if len(m.textInputs) >= 8 {
m.config.Hostname = m.textInputs[0].Value()
m.config.Country = m.textInputs[1].Value()
m.config.Region = m.textInputs[2].Value()
m.config.Timezone = m.textInputs[3].Value()
m.config.HomePage = m.textInputs[4].Value()
m.config.DBAddress = m.textInputs[5].Value()
m.config.DBName = m.textInputs[6].Value()
m.config.DataAddress = m.textInputs[7].Value()
}
}
func (m *model) savePublicNetworkPage() {
if len(m.textInputs) >= 4 {
m.config.PublicInterface = m.textInputs[0].Value()
m.config.IPAddress = m.textInputs[1].Value()
m.config.Netmask = m.textInputs[2].Value()
m.config.Gateway = m.textInputs[3].Value()
}
}
func (m *model) saveInternalNetworkPage() {
if len(m.textInputs) >= 3 {
m.config.InternalInterface = m.textInputs[0].Value()
m.config.InternalIP = m.textInputs[1].Value()
m.config.InternalMask = m.textInputs[2].Value()
}
}
func (m *model) saveDNSPage() {
if len(m.textInputs) >= 2 {
m.config.DNSPrimary = m.textInputs[0].Value()
m.config.DNSSecondary = m.textInputs[1].Value()
}
}
// initPageInputs 初始化当前页面的输入框
func (m *model) initPageInputs() {
m.textInputs = make([]textinput.Model, 0)
switch m.currentPage {
case PageData:
fields := []struct{ label, value string }{
{"主机名:", m.config.Hostname},
{"国家:", m.config.Country},
{"地区:", m.config.Region},
{"时区:", m.config.Timezone},
{"主页:", m.config.HomePage},
{"数据库地址:", m.config.DBAddress},
{"数据库名称:", m.config.DBName},
{"Data 地址:", m.config.DataAddress},
}
for _, f := range fields {
ti := textinput.New()
ti.Placeholder = ""
ti.Placeholder = f.label
//ti.Placeholder = "请输入" + f.label[:len(f.label)-1]
ti.SetValue(f.value)
ti.Width = 50
m.textInputs = append(m.textInputs, ti)
}
m.focusIndex = 0
if len(m.textInputs) > 0 {
m.textInputs[0].Focus()
}
m.inputLabels = []string{"Hostname", "Country", "Region", "Timezone", "Homepage", "DBPath", "DBName", "Software"}
case PagePublicNetwork:
fields := []struct{ label, value string }{
{"公网接口:", m.config.PublicInterface},
{"IP 地址:", m.config.IPAddress},
{"子网掩码:", m.config.Netmask},
{"网关:", m.config.Gateway},
}
for _, f := range fields {
ti := textinput.New()
ti.Placeholder = ""
ti.SetValue(f.value)
ti.Width = 50
m.textInputs = append(m.textInputs, ti)
}
m.focusIndex = 0
if len(m.textInputs) > 0 {
m.textInputs[0].Focus()
}
m.inputLabels = []string{"Wan iface", "IPAddress", "Netmask", "Gateway"}
case PageInternalNetwork:
fields := []struct{ label, value string }{
{"内网接口:", m.config.InternalInterface},
{"内网 IP:", m.config.InternalIP},
{"内网掩码:", m.config.InternalMask},
}
for _, f := range fields {
ti := textinput.New()
ti.Placeholder = ""
ti.SetValue(f.value)
ti.Width = 50
m.textInputs = append(m.textInputs, ti)
}
m.focusIndex = 0
if len(m.textInputs) > 0 {
m.textInputs[0].Focus()
}
m.inputLabels = []string{"Lan iface", "IPAddress", "Netmask"}
case PageDNS:
fields := []struct{ label, value string }{
{"主 DNS:", m.config.DNSPrimary},
{"备 DNS:", m.config.DNSSecondary},
}
for _, f := range fields {
ti := textinput.New()
ti.Placeholder = ""
ti.SetValue(f.value)
ti.Width = 50
m.textInputs = append(m.textInputs, ti)
}
m.focusIndex = 0
if len(m.textInputs) > 0 {
m.textInputs[0].Focus()
}
m.inputLabels = []string{"DNSPrimary", "DNSSecondary"}
}
}

333
pkg/wizard/model.go Normal file
View File

@@ -0,0 +1,333 @@
package wizard
import (
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)
// Config 系统配置结构
type Config struct {
// 协议
AgreementAccepted bool `json:"agreement_accepted"`
// 数据接收
Hostname string `json:"hostname"`
Country string `json:"country"`
Region string `json:"region"`
Timezone string `json:"timezone"`
HomePage string `json:"homepage"`
DBAddress string `json:"db_address"`
DBName string `json:"db_name"`
DataAddress string `json:"data_address"`
// 公网设置
PublicInterface string `json:"public_interface"`
IPAddress string `json:"ip_address"`
Netmask string `json:"netmask"`
Gateway string `json:"gateway"`
// 内网配置
InternalInterface string `json:"internal_interface"`
InternalIP string `json:"internal_ip"`
InternalMask string `json:"internal_mask"`
// DNS 配置
DNSPrimary string `json:"dns_primary"`
DNSSecondary string `json:"dns_secondary"`
}
// PageType 页面类型
type PageType int
const (
PageAgreement PageType = iota
PageData
PagePublicNetwork
PageInternalNetwork
PageDNS
PageSummary
)
const (
FocusTypeInput int = 0
FocusTypePrev int = 1
FocusTypeNext int = 2
)
// model TUI 主模型
type model struct {
config Config
currentPage PageType
totalPages int
textInputs []textinput.Model
inputLabels []string // 存储标签
focusIndex int
focusType int // 0=输入框, 1=上一步按钮, 2=下一步按钮
agreementIdx int // 0=拒绝1=接受
width int
height int
err error
quitting bool
done bool
force bool
}
// defaultConfig 返回默认配置
func defaultConfig() Config {
return Config{
Hostname: "sunhpc01",
Country: "China",
Region: "Beijing",
Timezone: "Asia/Shanghai",
HomePage: "https://sunhpc.example.com",
DBAddress: "127.0.0.1",
DBName: "sunhpc_db",
DataAddress: "/data/sunhpc",
PublicInterface: "eth0",
InternalInterface: "eth1",
IPAddress: "192.168.1.100",
Netmask: "255.255.255.0",
Gateway: "192.168.1.1",
InternalIP: "10.0.0.100",
InternalMask: "255.255.255.0",
DNSPrimary: "8.8.8.8",
DNSSecondary: "8.8.4.4",
}
}
// initialModel 初始化模型
func initialModel() model {
cfg := defaultConfig()
m := model{
config: cfg,
totalPages: 6,
textInputs: make([]textinput.Model, 0),
inputLabels: make([]string, 0),
agreementIdx: 1,
focusIndex: 0,
focusType: 0, // 0=输入框, 1=上一步按钮, 2=下一步按钮
width: 80,
height: 24,
}
m.initPageInputs()
return m
}
// Init 初始化命令
func (m model) Init() tea.Cmd {
return textinput.Blink
}
// Update 处理消息更新
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.quitting = true
return m, tea.Quit
case "esc":
if m.currentPage > 0 {
return m.prevPage()
}
case "enter":
return m.handleEnter()
case "tab", "shift+tab", "up", "down", "left", "right":
return m.handleNavigation(msg)
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
// 动态调整容器宽度
/*
if msg.Width > 100 {
containerStyle = containerStyle.Width(90)
} else if msg.Width > 80 {
containerStyle = containerStyle.Width(70)
} else {
containerStyle = containerStyle.Width(msg.Width - 10)
}
*/
// ✅ 动态计算容器宽度(终端宽度的 80%
containerWidth := msg.Width * 80 / 100
// ✅ 重新设置容器样式宽度
containerStyle = containerStyle.Width(containerWidth)
// 动态设置协议框宽度(容器宽度的 90%
agreementWidth := containerWidth * 80 / 100
agreementBox = agreementBox.Width(agreementWidth)
// 动态设置输入框宽度
inputWidth := containerWidth * 60 / 100
if inputWidth < 40 {
inputWidth = 40
}
inputBox = inputBox.Width(inputWidth)
// 动态设置总结框宽度
summaryWidth := containerWidth * 90 / 100
summaryBox = summaryBox.Width(summaryWidth)
return m, nil
}
// 更新当前焦点的输入框
if len(m.textInputs) > 0 && m.focusIndex < len(m.textInputs) {
var cmd tea.Cmd
m.textInputs[m.focusIndex], cmd = m.textInputs[m.focusIndex].Update(msg)
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}
// handleEnter 处理回车事件
func (m *model) handleEnter() (tea.Model, tea.Cmd) {
switch m.currentPage {
case PageAgreement:
if m.agreementIdx == 1 {
m.config.AgreementAccepted = true
return m.nextPage()
} else {
m.quitting = true
return m, tea.Quit
}
case PageData, PagePublicNetwork, PageInternalNetwork, PageDNS:
// 根据焦点类型执行不同操作
switch m.focusType {
case FocusTypeInput:
// 在输入框上,保存并下一页
m.saveCurrentPage()
return m.nextPage()
case FocusTypePrev:
// 上一步按钮,返回上一页
return m.prevPage()
case FocusTypeNext:
// 下一步按钮,切换到下一页
m.saveCurrentPage()
return m.nextPage()
}
case PageSummary:
switch m.focusIndex {
case 0: // 执行
m.done = true
if err := m.saveConfig(); err != nil {
m.err = err
return m, nil
}
return m, tea.Quit
case 1: // 取消
m.quitting = true
return m, tea.Quit
}
}
return m, nil
}
// handleNavigation 处理导航
func (m *model) handleNavigation(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// debug
//fmt.Fprintf(os.Stderr, "DEBUG: key=%s page=%d\n", msg.String(), m.currentPage)
switch m.currentPage {
case PageAgreement:
switch msg.String() {
case "left", "right", "tab", "shift+tab", "up", "down":
m.agreementIdx = 1 - m.agreementIdx
}
case PageSummary:
switch msg.String() {
case "left", "right", "tab", "shift+tab":
m.focusIndex = 1 - m.focusIndex
}
default:
// 输入框页面: 支持输入框和按钮之间切换
// totalFocusable := len(m.textInputs) + 2
switch msg.String() {
case "down", "tab":
// 当前在输入框
switch m.focusType {
case FocusTypeInput:
if m.focusIndex < len(m.textInputs)-1 {
// 切换到下一个输入框
m.textInputs[m.focusIndex].Blur()
m.focusIndex++
m.textInputs[m.focusIndex].Focus()
} else {
// 最后一个输入框,切换到“下一步”按钮
m.textInputs[m.focusIndex].Blur()
m.focusIndex = 0
m.focusType = FocusTypeNext // 下一步按钮
}
case FocusTypePrev:
// 当前在“上一步”按钮,切换到第一个输入框
m.focusType = FocusTypeInput
m.focusIndex = 0
m.textInputs[0].Focus()
case FocusTypeNext:
// 当前在“下一步”按钮,切换到“上一步”按钮
m.focusType = FocusTypePrev
}
case "up", "shift+tab":
// 当前在输入框
switch m.focusType {
case FocusTypeInput:
if m.focusIndex > 0 {
// 切换到上一个输入框
m.textInputs[m.focusIndex].Blur()
m.focusIndex--
m.textInputs[m.focusIndex].Focus()
} else {
// 第一个输入框,切换到“上一步”按钮
m.textInputs[m.focusIndex].Blur()
m.focusIndex = 0
m.focusType = FocusTypePrev // 上一步按钮
}
case FocusTypeNext:
// 当前在“下一步”按钮,切换到最后一个输入框
m.focusType = FocusTypeInput
m.focusIndex = len(m.textInputs) - 1
m.textInputs[m.focusIndex].Focus()
case FocusTypePrev:
// 当前在“上一步”按钮,切换到“下一步”按钮
m.focusType = FocusTypeNext
}
}
}
return m, nil
}
// nextPage 下一页
func (m *model) nextPage() (tea.Model, tea.Cmd) {
if m.currentPage < PageSummary {
m.currentPage++
m.focusIndex = 0
m.initPageInputs()
}
return m, textinput.Blink
}
// prevPage 上一页
func (m *model) prevPage() (tea.Model, tea.Cmd) {
if m.currentPage > 0 {
m.saveCurrentPage()
m.currentPage--
m.focusIndex = 0
m.initPageInputs()
}
return m, textinput.Blink
}

366
pkg/wizard/pages.go Normal file
View File

@@ -0,0 +1,366 @@
package wizard
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// View 渲染视图
func (m model) View() string {
if m.done {
return successView()
}
if m.quitting {
return quitView()
}
if m.err != nil {
return errorView(m.err)
}
var page string
switch m.currentPage {
case PageAgreement:
page = m.agreementView()
case PageData:
page = m.dataView()
case PagePublicNetwork:
page = m.publicNetworkView()
case PageInternalNetwork:
page = m.internalNetworkView()
case PageDNS:
page = m.dnsView()
case PageSummary:
page = m.summaryView()
}
content := strings.Builder{}
content.WriteString(page)
content.WriteString("\n\n")
content.WriteString(progressView(m.currentPage, m.totalPages))
return containerStyle.Render(content.String())
}
// agreementView 协议页面
func (m model) agreementView() string {
title := titleStyle.Render("SunHPC 系统初始化向导")
subtitle := subTitleStyle.Render("请先阅读并同意以下协议")
agreement := agreementBox.Render(`
┌─────────────────────────────────────────────────────────────┐
│ SunHPC 软件许可协议 │
└─────────────────────────────────────────────────────────────┘
1. 许可授予
本软件授予您非独占、不可转让的使用许可。
2. 使用限制
- 不得用于非法目的
- 不得反向工程或反编译
- 不得移除版权标识
3. 免责声明
本软件按"原样"提供,不提供任何明示或暗示的保证。
4. 责任限制
在任何情况下,作者不对因使用本软件造成的任何损失负责。
5. 协议终止
如违反本协议条款,许可将自动终止。
───────────────────────────────────────────────────────────────
请仔细阅读以上条款,点击"接受"表示您同意并遵守本协议。
───────────────────────────────────────────────────────────────
`)
var acceptBtn, rejectBtn string
if m.agreementIdx == 0 {
rejectBtn = selectedButton.Render(">> 拒绝 <<")
acceptBtn = selectedButton.Render(" 同意 ")
} else {
rejectBtn = selectedButton.Render(" 拒绝 ")
acceptBtn = selectedButton.Render(">> 同意 <<")
}
buttonGroup := lipgloss.JoinHorizontal(
lipgloss.Center,
acceptBtn, " ", rejectBtn)
// ✅ 添加调试信息(确认 agreementIdx 的值)
// debugInfo := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).
// Render(fmt.Sprintf("[DEBUG: idx=%d]", m.agreementIdx),)
hint := hintStyle.Render("使用 <- -> 或 Tab 选择,Enter 确认")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
subtitle, "",
agreement, "",
buttonGroup, "",
// debugInfo, "", // ✅ 显示调试信息
hint,
)
}
// dataView 数据接收页面
func (m model) dataView() string {
title := titleStyle.Render("集群基础配置")
subtitle := subTitleStyle.Render("请填写系统基本信息")
var inputs strings.Builder
for i, ti := range m.textInputs {
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
input := inputBox.Render(info + ti.View())
inputs.WriteString(input + "\n")
}
buttons := m.renderNavButtons()
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
subtitle, "",
inputs.String(), "",
buttons, "",
hint,
)
}
// publicNetworkView 公网设置页面
func (m model) publicNetworkView() string {
title := titleStyle.Render("公网配置")
subtitle := subTitleStyle.Render("请配置网络接口信息")
autoDetect := infoStyle.Render("[*] 自动检测网络接口: eth0, eth1, ens33")
var inputs strings.Builder
for i, ti := range m.textInputs {
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
input := inputBox.Render(info + ti.View())
inputs.WriteString(input + "\n")
}
buttons := m.renderNavButtons()
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
subtitle, "",
autoDetect, "",
inputs.String(), "",
buttons, "",
hint,
)
}
// internalNetworkView 内网配置页面
func (m model) internalNetworkView() string {
title := titleStyle.Render("内网配置")
subtitle := subTitleStyle.Render("请配置内网信息")
var inputs strings.Builder
for i, ti := range m.textInputs {
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
input := inputBox.Render(info + ti.View())
inputs.WriteString(input + "\n")
}
buttons := m.renderNavButtons()
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
subtitle, "",
inputs.String(), "",
buttons, "",
hint,
)
}
// dnsView DNS 配置页面
func (m model) dnsView() string {
title := titleStyle.Render("DNS 配置")
subtitle := subTitleStyle.Render("请配置 DNS 服务器")
var inputs strings.Builder
for i, ti := range m.textInputs {
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
input := inputBox.Render(info + ti.View())
inputs.WriteString(input + "\n")
}
buttons := m.renderNavButtons()
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
subtitle, "",
inputs.String(), "",
buttons, "",
hint,
)
}
// summaryView 总结页面
func (m model) summaryView() string {
title := titleStyle.Render("配置总结")
subtitle := subTitleStyle.Render("请确认以下配置信息")
summary := summaryBox.Render(fmt.Sprintf(`
+---------------------------------------------+
基本信息
+---------------------------------------------+
主机名:%-35s
国 家: %-31s
地 区:%-31s
时 区:%-38s
主 页:%-38s
+---------------------------------------------+
数据库
+---------------------------------------------+
地 址:%-38s
名 称:%-38s
软 件:%-33s
+---------------------------------------------+
公网配置
+---------------------------------------------+
接 口:%-38s
地 址: %-41s
掩 码:%-38s
网 关:%-38s
+---------------------------------------------+
内网配置
+---------------------------------------------+
接 口:%-38s
地 址: %-41s
掩 码:%-38s
+---------------------------------------------+
DNS
+---------------------------------------------+
主 DNS: %-37s
备 DNS: %-37s
+---------------------------------------------+
`,
m.config.Hostname,
m.config.Country,
m.config.Region,
m.config.Timezone,
m.config.HomePage,
m.config.DBAddress,
m.config.DBName,
m.config.DataAddress,
m.config.PublicInterface,
m.config.IPAddress,
m.config.Netmask,
m.config.Gateway,
m.config.InternalInterface,
m.config.InternalIP,
m.config.InternalMask,
m.config.DNSPrimary,
m.config.DNSSecondary,
))
var buttons string
if m.focusIndex == 0 {
buttons = selectedButton.Render("[>] 执行初始化") + " " + normalButton.Render("[ ] 取消")
} else {
buttons = normalButton.Render("[>] 执行初始化") + " " + selectedButton.Render("[ ] 取消")
}
hint := hintStyle.Render("使用 <- -> 或 Tab 选择Enter 确认")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
subtitle, "",
summary, "",
buttons, "",
hint,
)
}
// progressView 进度条
func progressView(current PageType, total int) string {
progress := ""
for i := 0; i < total; i++ {
if i < int(current) {
progress += "[+]"
} else if i == int(current) {
progress += "[-]"
} else {
progress += "[ ]"
}
if i < total-1 {
progress += " "
}
}
labels := []string{"协议", "数据", "公网", "内网", "DNS", "总结"}
label := labelStyle.Render(labels[current])
return progressStyle.Render(progress) + " " + label
}
// successView 成功视图
func successView() string {
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
successTitle.Render("初始化完成!"), "",
successMsg.Render("系统配置已保存,正在初始化..."), "",
hintStyle.Render("按任意键退出"),
))
}
// quitView 退出视图
func quitView() string {
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
errorTitle.Render("已取消"), "",
errorMsg.Render("初始化已取消,未保存任何配置"),
))
}
// errorView 错误视图
func errorView(err error) string {
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
errorTitle.Render("错误"), "",
errorMsg.Render(err.Error()), "",
hintStyle.Render("按 Ctrl+C 退出"),
))
}
// navButtons 导航按钮
func navButtons(m model, prev, next string) string {
var btns string
if m.currentPage == 0 {
btns = normalButton.Render(prev) + " " + selectedButton.Render(next)
} else {
btns = selectedButton.Render(prev) + " " + normalButton.Render(next)
}
return btns
}
func (m model) renderNavButtons() string {
var prevBtn, nextBtn string
switch m.focusType {
case FocusTypePrev:
// 焦点在"上一步"
prevBtn = selectedButton.Render("<< 上一步 >>")
nextBtn = normalButton.Render("下一步 >>")
case FocusTypeNext:
// 焦点在"下一步"
prevBtn = normalButton.Render("<< 上一步")
nextBtn = selectedButton.Render("<< 下一步 >>")
default:
// 焦点在输入框
prevBtn = normalButton.Render("<< 上一步")
nextBtn = normalButton.Render("下一步 >>")
}
return lipgloss.JoinHorizontal(
lipgloss.Center,
prevBtn,
" ",
nextBtn,
)
}

99
pkg/wizard/styles.go Normal file
View File

@@ -0,0 +1,99 @@
package wizard
import "github.com/charmbracelet/lipgloss"
// 颜色定义
var (
primaryColor = lipgloss.Color("#7C3AED")
secondaryColor = lipgloss.Color("#10B981")
errorColor = lipgloss.Color("#EF4444")
warnColor = lipgloss.Color("#F59E0B")
// 背景色设为无,让终端自己的背景色生效,避免黑块
bgColor = lipgloss.Color("#1F2937")
textColor = lipgloss.Color("#FFFFFF")
mutedColor = lipgloss.Color("#B0B0B0")
)
// 容器样式
var containerStyle = lipgloss.NewStyle().
Padding(2, 4).
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(primaryColor).
//Background(bgColor). // 注释掉背景色,防止在某些终端出现黑块
Foreground(textColor).
//Width(80).
Align(lipgloss.Center)
// 标题样式
var titleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(primaryColor).
MarginBottom(1)
var subTitleStyle = lipgloss.NewStyle().
Foreground(mutedColor).
MarginBottom(2)
// 按钮样式
var normalButton = lipgloss.NewStyle().
Padding(0, 2).
Foreground(lipgloss.Color("#666666")) // 深灰色,更暗
var selectedButton = lipgloss.NewStyle().
Padding(0, 2).
Foreground(lipgloss.Color("#3d4747ff")). // 亮绿色
Bold(true)
// 输入框样式
var inputBox = lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(primaryColor).
Padding(0, 1)
var labelStyle = lipgloss.NewStyle().
Foreground(mutedColor).
Width(12).
Align(lipgloss.Right)
// 协议框样式
var agreementBox = lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(warnColor).
Padding(1, 2).
//Width(70).
Align(lipgloss.Left)
// 总结框样式
var summaryBox = lipgloss.NewStyle().
BorderStyle(lipgloss.DoubleBorder()).
BorderForeground(primaryColor).
Padding(0, 0).
Foreground(textColor)
// 进度条样式
var progressStyle = lipgloss.NewStyle().Foreground(primaryColor)
// 提示信息样式
var hintStyle = lipgloss.NewStyle().
Foreground(mutedColor).
Italic(true)
// 成功/错误样式
var successTitle = lipgloss.NewStyle().
Bold(true).
Foreground(secondaryColor)
var successMsg = lipgloss.NewStyle().
Foreground(textColor)
var errorTitle = lipgloss.NewStyle().
Bold(true).
Foreground(errorColor)
var errorMsg = lipgloss.NewStyle().
Foreground(textColor)
var infoStyle = lipgloss.NewStyle().
Foreground(primaryColor).
Bold(true)

46
pkg/wizard/wizard.go Normal file
View File

@@ -0,0 +1,46 @@
package wizard
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
// Run 启动初始化向导
func Run(force bool) error {
// 检查是否已有配置
if !force && ConfigExists() {
fmt.Println("⚠️ 检测到已有配置文件")
fmt.Println(" 使用 --force 参数强制重新初始化")
fmt.Println(" 或运行 sunhpc init tui --force")
return nil
}
// 创建程序实例
p := tea.NewProgram(initialModel())
// 运行程序
if _, err := p.Run(); err != nil {
return fmt.Errorf("初始化向导运行失败:%w", err)
}
return nil
}
// getConfigPath 获取配置文件路径
func GetConfigPath() string {
// 优先使用环境变量
if path := os.Getenv("SUNHPC_CONFIG"); path != "" {
return path
}
// 默认路径
return "/etc/sunhpc/config.json"
}
// configExists 检查配置文件是否存在
func ConfigExists() bool {
configPath := GetConfigPath()
_, err := os.Stat(configPath)
return err == nil
}