Tui 重构代码逻辑

This commit is contained in:
2026-02-27 22:52:15 +08:00
parent 3a5f5ddd5d
commit d4e214fe23
8 changed files with 934 additions and 718 deletions

1
go.mod
View File

@@ -44,6 +44,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect

2
go.sum
View File

@@ -78,6 +78,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=

View File

@@ -1,194 +1,162 @@
package wizard
import (
"encoding/json"
"bufio"
"database/sql"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"sunhpc/pkg/database"
"sunhpc/pkg/utils"
"github.com/charmbracelet/bubbles/textinput"
)
// saveConfig 保存配置到文件
// 配置项映射:定义每个配置项对应的表名、键名
var configMappings = []struct {
table string
key string
getVal func(m *model) interface{} // 动态获取配置值的函数
}{
// attributes 表
{"attributes", "license", func(m *model) any { return m.config.License }},
{"attributes", "accepted", func(m *model) any { return m.config.AgreementAccepted }},
{"attributes", "country", func(m *model) any { return m.config.Country }},
{"attributes", "region", func(m *model) any { return m.config.Region }},
{"attributes", "timezone", func(m *model) any { return m.config.Timezone }},
{"attributes", "homepage", func(m *model) any { return m.config.HomePage }},
{"attributes", "dbaddress", func(m *model) any { return m.config.DBAddress }},
{"attributes", "software", func(m *model) any { return m.config.Software }},
// nodes 表
{"nodes", "name", func(m *model) any { return m.config.Hostname }},
// 公网设置表
{"public_network", "public_interface", func(m *model) any { return m.config.PublicInterface }},
{"public_network", "ip_address", func(m *model) any { return m.config.PublicIPAddress }},
{"public_network", "netmask", func(m *model) any { return m.config.PublicNetmask }},
{"public_network", "gateway", func(m *model) any { return m.config.PublicGateway }},
// 内网配置表
{"internal_network", "internal_interface", func(m *model) any { return m.config.InternalInterface }},
{"internal_network", "internal_ip", func(m *model) any { return m.config.InternalIPAddress }},
{"internal_network", "internal_mask", func(m *model) any { return m.config.InternalNetmask }},
// DNS配置表
{"dns_config", "dns_primary", func(m *model) any { return m.config.DNSPrimary }},
{"dns_config", "dns_secondary", func(m *model) any { return m.config.DNSSecondary }},
}
// 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, "", " ")
conn, err := database.GetDB() // 假设database包已实现getDB()获取连接
if err != nil {
return fmt.Errorf("序列化配置失败:%w", err)
return fmt.Errorf("获取数据库连接失败: %w", err)
}
defer conn.Close()
m.force = false // 初始化全量覆盖标识
// 遍历所有配置项,逐个处理
for _, item := range configMappings {
val := item.getVal(m)
exists, err := m.checkExists(conn, item.table, item.key)
if err != nil {
return fmt.Errorf("检查%s.%s是否存在失败: %w", item.table, item.key, err)
}
// 写入文件
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("保存配置文件失败:%w", err)
// 根据存在性和用户选择处理
if !exists {
// 不存在则直接插入
if err := m.upsertConfig(conn, item.table, item.key, val, false); err != nil {
return fmt.Errorf("插入%s.%s失败: %w", item.table, item.key, err)
}
continue
}
// 已存在:判断是否全量覆盖
if m.force {
if err := m.upsertConfig(conn, item.table, item.key, val, true); err != nil {
return fmt.Errorf("强制更新%s.%s失败: %w", item.table, item.key, err)
}
continue
}
// 询问用户操作
choice, err := m.askUserChoice(item.table, item.key)
if err != nil {
return fmt.Errorf("获取用户选择失败: %w", err)
}
switch strings.ToLower(choice) {
case "y", "yes":
// 单条覆盖
if err := m.upsertConfig(conn, item.table, item.key, val, true); err != nil {
return fmt.Errorf("更新%s.%s失败: %w", item.table, item.key, err)
}
case "a", "all":
// 全量覆盖,后续不再询问
m.force = true
if err := m.upsertConfig(conn, item.table, item.key, val, true); err != nil {
return fmt.Errorf("全量更新%s.%s失败: %w", item.table, item.key, err)
}
case "n", "no":
// 跳过当前项
fmt.Printf("跳过%s.%s的更新\n", item.table, item.key)
default:
fmt.Printf("无效选择%s跳过%s.%s的更新\n", choice, item.table, item.key)
}
}
return nil
}
// loadConfig 从文件加载配置
func loadConfig() (*Config, error) {
configPath := GetConfigPath()
data, err := os.ReadFile(configPath)
// checkExists 集中判断配置项是否存在(核心判断逻辑)
func (m *model) checkExists(conn *sql.DB, table, key string) (bool, error) {
var count int
// 通用存在性检查SQL假设所有表都有key字段作为主键
query := fmt.Sprintf("SELECT COUNT(1) FROM %s WHERE `key` = ?", table)
err := conn.QueryRow(query, key).Scan(&count)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败:%w", err)
// 表不存在也视为"不存在"(可选:根据实际需求调整,比如先建表)
if strings.Contains(err.Error(), "table not found") {
return false, nil
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败:%w", err)
return false, err
}
return &cfg, nil
return count > 0, 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()
// upsertConfig 统一处理插入/更新逻辑
func (m *model) upsertConfig(conn *sql.DB, table, key string, val interface{}, update bool) error {
var query string
if !update {
// 插入:假设表结构为(key, value)
query = fmt.Sprintf("INSERT INTO %s (`key`, `value`) VALUES (?, ?)", table)
} else {
// 更新
query = fmt.Sprintf("UPDATE %s SET `value` = ? WHERE `key` = ?", table)
}
// 处理参数顺序(更新和插入的参数顺序不同)
var args []interface{}
if !update {
args = []interface{}{key, val}
} else {
args = []interface{}{val, key}
}
_, err := conn.Exec(query, args...)
return err
}
func (m *model) saveDataPage() {
if len(m.textInputs) >= 8 {
m.config.HomePage = m.textInputs[0].Value()
m.config.Hostname = m.textInputs[1].Value()
m.config.Country = m.textInputs[2].Value()
m.config.Region = m.textInputs[3].Value()
m.config.Timezone = m.textInputs[4].Value()
m.config.DBAddress = m.textInputs[5].Value()
m.config.DataAddress = m.textInputs[6].Value()
}
}
func (m *model) savePublicNetworkPage() {
if len(m.textInputs) >= 4 {
m.config.PublicInterface = m.textInputs[0].Value()
m.config.PublicIPAddress = m.textInputs[1].Value()
m.config.PublicNetmask = m.textInputs[2].Value()
m.config.PublicGateway = m.textInputs[3].Value()
}
}
func (m *model) saveInternalNetworkPage() {
if len(m.textInputs) >= 3 {
m.config.InternalInterface = m.textInputs[0].Value()
m.config.InternalIPAddress = m.textInputs[1].Value()
m.config.InternalNetmask = 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 }{
{"Homepage", m.config.HomePage},
{"Hostname", m.config.Hostname},
{"Country", m.config.Country},
{"Region", m.config.Region},
{"Timezone", m.config.Timezone},
{"DB Path", m.config.DBAddress},
{"Software", m.config.DataAddress},
}
for _, f := range fields {
ti := textinput.New()
ti.Placeholder = ""
ti.Placeholder = f.label
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{"Homepage", "Hostname", "Country", "Region", "Timezone", "DBPath", "Software"}
case PagePublicNetwork:
fields := []struct{ label, value string }{
{"Public Interface", m.config.PublicInterface},
{"Public IP Address", m.config.PublicIPAddress},
{"Public Netmask", m.config.PublicNetmask},
{"Public Gateway", m.config.PublicGateway},
}
for _, f := range fields {
ti := textinput.New()
ti.Placeholder = ""
ti.Placeholder = f.label
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{"Public Interface", "Public IP Address", "Public Netmask", "Public Gateway"}
case PageInternalNetwork:
fields := []struct{ label, value string }{
{"Internal Interface", m.config.InternalInterface},
{"Internal IP Address", m.config.InternalIPAddress},
{"Internal Netmask", m.config.InternalNetmask},
}
for _, f := range fields {
ti := textinput.New()
ti.Placeholder = f.label
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{"Internal Interface", "Internal IP", "Internal Mask"}
case PageDNS:
fields := []struct{ label, value string }{
{"Primary DNS", m.config.DNSPrimary},
{"Secondary DNS", m.config.DNSSecondary},
}
for _, f := range fields {
ti := textinput.New()
ti.Placeholder = f.label
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{"Pri DNS", "Sec DNS"}
// askUserChoice 询问用户操作选择
func (m *model) askUserChoice(table, key string) (string, error) {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("配置项%s.%s已存在选择操作(y/yes=覆盖, n/no=跳过, a/all=全量覆盖后续所有): ", table, key)
input, err := reader.ReadString('\n')
if err != nil {
return "", err
}
// 去除空格和换行
return strings.TrimSpace(input), nil
}
// 获取系统网络接口

321
pkg/wizard/focused.go Normal file
View File

@@ -0,0 +1,321 @@
package wizard
import (
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// Focusable 定义可聚焦组件的通用接口
type Focusable interface {
// Focus 激活焦点(比如输入框闪烁光标、按钮高亮)
Focus() tea.Cmd
// Blur 失活焦点(取消高亮/闪烁)
Blur()
// IsFocused 判断是否处于焦点状态
IsFocused() bool
// View 渲染组件(和 bubbletea 统一)
View() string
}
// --------------- 为常用组件实现 Focusable 接口 ---------------
// TextInput 适配 bubbles/textinput
type TextInput struct {
textinput.Model
focused bool
}
func NewTextInput(placeholder string, defaultValue string) *TextInput {
ti := textinput.New()
ti.Placeholder = placeholder
ti.SetValue(defaultValue)
ti.Focus()
return &TextInput{Model: ti, focused: true}
}
func (t *TextInput) Focus() tea.Cmd {
t.focused = true
return t.Model.Focus()
}
func (t *TextInput) Blur() {
t.focused = false
t.Model.Blur()
}
func (t *TextInput) IsFocused() bool {
return t.focused
}
// Button 适配 bubbles/button
type Button struct {
label string
focused bool
buttonBlur lipgloss.Style
buttonFocus lipgloss.Style
}
func NewButton(label string) *Button {
return &Button{
label: label,
focused: false,
buttonBlur: btnBaseStyle,
buttonFocus: btnSelectedStyle,
}
}
func (b *Button) Focus() tea.Cmd {
b.focused = true
b.buttonBlur = b.buttonFocus
return nil
}
func (b *Button) Blur() {
b.focused = false
b.buttonBlur = btnBaseStyle
}
func (b *Button) IsFocused() bool {
return b.focused
}
func (b *Button) View() string {
if b.focused {
return b.buttonFocus.Render(b.label)
}
return b.buttonBlur.Render(b.label)
}
// List 适配 bubbles/list
type List struct {
list.Model
focused bool
}
func NewList(items []list.Item) List {
l := list.New(items, list.NewDefaultDelegate(), 0, 0)
l.SetShowHelp(false)
return List{Model: l, focused: false}
}
func (l *List) Focus() tea.Cmd {
l.focused = true
return nil
}
func (l *List) Blur() {
l.focused = false
}
func (l *List) IsFocused() bool {
return l.focused
}
// FocusManager 焦点管理器
type FocusManager struct {
// 所有可聚焦组件key=唯一标识,比如 "form1.ip_input"、"form1.next_btn"
components map[string]Focusable
// 组件切换顺序(按这个顺序切换焦点)
order []string
// 当前焦点组件的标识
currentFocusID string
// 是否循环切换(到最后一个后回到第一个)
loop bool
}
// NewFocusManager 创建焦点管理器
func NewFocusManager(loop bool) *FocusManager {
return &FocusManager{
components: make(map[string]Focusable),
order: make([]string, 0),
currentFocusID: "",
loop: loop,
}
}
// Register 注册可聚焦组件(指定标识和切换顺序)
func (fm *FocusManager) Register(id string, comp Focusable) {
// 防御性检查:避免 components 为空导致 panic
if fm.components == nil {
fm.components = make(map[string]Focusable)
}
// 避免重复注册
if _, exists := fm.components[id]; exists {
return
}
fm.components[id] = comp
fm.order = append(fm.order, id)
// 如果是第一个注册的组件,默认聚焦
if fm.currentFocusID == "" {
fm.currentFocusID = id
comp.Focus()
}
}
// Next 切换到下一个组件
func (fm *FocusManager) Next() tea.Cmd {
if len(fm.order) == 0 {
return nil
}
// 1. 找到当前组件的索引
currentIdx := -1
for i, id := range fm.order {
if id == fm.currentFocusID {
currentIdx = i
break
}
}
// 2. 计算下一个索引
nextIdx := currentIdx + 1
if fm.loop && nextIdx >= len(fm.order) {
nextIdx = 0
}
if nextIdx >= len(fm.order) {
return nil // 不循环则到最后一个停止
}
// 3. 切换焦点(当前组件失活,下一个激活)
fm.components[fm.currentFocusID].Blur()
nextID := fm.order[nextIdx]
fm.currentFocusID = nextID
return fm.components[nextID].Focus()
}
// Prev 切换到上一个组件
func (fm *FocusManager) Prev() tea.Cmd {
if len(fm.order) == 0 {
return nil
}
currentIdx := -1
for i, id := range fm.order {
if id == fm.currentFocusID {
currentIdx = i
break
}
}
prevIdx := currentIdx - 1
if fm.loop && prevIdx < 0 {
prevIdx = len(fm.order) - 1
}
if prevIdx < 0 {
return nil
}
fm.components[fm.currentFocusID].Blur()
prevID := fm.order[prevIdx]
fm.currentFocusID = prevID
return fm.components[prevID].Focus()
}
// GetCurrent 获取当前焦点组件
func (fm *FocusManager) GetCurrent() (Focusable, bool) {
comp, exists := fm.components[fm.currentFocusID]
return comp, exists
}
// HandleInput 统一处理焦点切换输入(比如 Tab/Shift+Tab
func (fm *FocusManager) HandleInput(msg tea.KeyMsg) tea.Cmd {
switch msg.String() {
case "tab": // Tab 下一个
return fm.Next()
case "shift+tab": // Shift+Tab 上一个
return fm.Prev()
case "left": // Left 上一个
return fm.Prev()
case "right": // Right 下一个
return fm.Next()
default:
return nil
}
}
func (m *model) switchPage(targetPage PageType) tea.Cmd {
// 边界检查(不能超出 1-6 页面)
if targetPage < PageAgreement || targetPage > PageSummary {
return nil
}
// 更新当前页面
m.currentPage = targetPage
// 初始化新页面的焦点
m.initPageFocus(targetPage)
// 返回空指令(或返回第一个组件的Focus命令)
return nil
}
func (m *model) initPageFocus(page PageType) {
m.focusManager = NewFocusManager(true)
// 获取当前页面的组件
pageComps, exists := m.pageComponents[page]
if !exists {
return
}
// 按 [业务逻辑顺序] 注册组件 (决定Tab切换的顺序)
var componentOrder []string
// 按页面类型定义不同的注册顺序
switch page {
case PageAgreement:
componentOrder = []string{"accept_btn", "reject_btn"}
case PageData:
componentOrder = []string{
"Homepage_input",
"Hostname_input",
"Country_input",
"Region_input",
"Timezone_input",
"DBPath_input",
"Software_input",
"next_btn",
"prev_btn",
}
case PagePublicNetwork:
componentOrder = []string{
"PublicInterface_input",
"PublicIPAddress_input",
"PublicNetmask_input",
"PublicGateway_input",
"next_btn",
"prev_btn",
}
case PageInternalNetwork:
componentOrder = []string{
"InternalInterface_input",
"InternalIPAddress_input",
"InternalNetmask_input",
"next_btn",
"prev_btn",
}
case PageDNS:
componentOrder = []string{
"Pri_DNS_input",
"Sec_DNS_input",
"next_btn",
"prev_btn",
}
case PageSummary:
componentOrder = []string{"confirm_btn", "cancel_btn"}
}
// 注册组件到焦点管理器(按顺序)
for _, compID := range componentOrder {
if comp, exists := pageComps[compID]; exists {
m.focusManager.Register(compID, comp)
}
}
}

View File

@@ -7,9 +7,16 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
// PageType 页面类型
type PageType int
// 总页码
const TotalPages = 6
// Config 系统配置结构
type Config struct {
// 协议
License string `json:"license"`
AgreementAccepted bool `json:"agreement_accepted"`
// 数据接收
@@ -19,7 +26,7 @@ type Config struct {
Timezone string `json:"timezone"`
HomePage string `json:"homepage"`
DBAddress string `json:"db_address"`
DataAddress string `json:"data_address"`
Software string `json:"software"`
// 公网设置
PublicInterface string `json:"public_interface"`
@@ -37,9 +44,6 @@ type Config struct {
DNSSecondary string `json:"dns_secondary"`
}
// PageType 页面类型
type PageType int
const (
PageAgreement PageType = iota
PageData
@@ -49,29 +53,24 @@ const (
PageSummary
)
const (
FocusTypeInput int = 0
FocusTypePrev int = 1
FocusTypeNext int = 2
)
// model TUI 主模型
type model struct {
config Config
currentPage PageType
config Config // 全局配置
currentPage PageType // 当前页面
totalPages int
textInputs []textinput.Model // 当前页面的输入框
networkInterfaces []string // 所有系统网络接口
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
// 核心1: 按页面分组存储所有组件(6个页面 + 6个map)
pageComponents map[PageType]map[string]Focusable
// 核心2焦点管理器(每次切换页面时重置)
focusManager *FocusManager
}
// defaultConfig 返回默认配置
@@ -97,13 +96,14 @@ func defaultConfig() Config {
}
return Config{
License: "This test license is for testing purposes only. Do not use it in production.",
Hostname: "cluster.hpc.org",
Country: "China",
Region: "Beijing",
Timezone: "Asia/Shanghai",
HomePage: "www.sunhpc.com",
DBAddress: "/var/lib/sunhpc/sunhpc.db",
DataAddress: "/export/sunhpc",
Software: "/export/sunhpc",
PublicInterface: defaultPublicInterface,
PublicIPAddress: "",
PublicNetmask: "",
@@ -119,18 +119,76 @@ func defaultConfig() Config {
// initialModel 初始化模型
func initialModel() model {
cfg := defaultConfig()
// 1. 初始化所有页面组件(6个页面)
pageComponents := make(map[PageType]map[string]Focusable)
// ------------------ 页面1协议页面 --------------------
page1Comps := make(map[string]Focusable)
page1Comps["accept_btn"] = NewButton("接受协议")
page1Comps["reject_btn"] = NewButton("拒绝协议")
pageComponents[PageAgreement] = page1Comps
// ------------------ 页面2基础信息页面 --------------------
page2Comps := make(map[string]Focusable)
page2Comps["Homepage_input"] = NewTextInput("Homepage", cfg.HomePage)
page2Comps["Hostname_input"] = NewTextInput("Hostname", cfg.Hostname)
page2Comps["Country_input"] = NewTextInput("Country", cfg.Country)
page2Comps["Region_input"] = NewTextInput("Region", cfg.Region)
page2Comps["Timezone_input"] = NewTextInput("Timezone", cfg.Timezone)
page2Comps["DBPath_input"] = NewTextInput("DBPath", cfg.DBAddress)
page2Comps["Software_input"] = NewTextInput("Software", cfg.Software)
page2Comps["next_btn"] = NewButton("下一步")
page2Comps["prev_btn"] = NewButton("上一步")
pageComponents[PageData] = page2Comps
// ------------------ 页面3公网网络页面 --------------------
page3Comps := make(map[string]Focusable)
page3Comps["PublicInterface_input"] = NewTextInput("PublicInterface", cfg.PublicInterface)
page3Comps["PublicIPAddress_input"] = NewTextInput("PublicIPAddress", cfg.PublicIPAddress)
page3Comps["PublicNetmask_input"] = NewTextInput("PublicNetmask", cfg.PublicNetmask)
page3Comps["PublicGateway_input"] = NewTextInput("PublicGateway", cfg.PublicGateway)
page3Comps["next_btn"] = NewButton("下一步")
page3Comps["prev_btn"] = NewButton("上一步")
pageComponents[PagePublicNetwork] = page3Comps
// ------------------ 页面4内网网络页面 --------------------
page4Comps := make(map[string]Focusable)
page4Comps["InternalInterface_input"] = NewTextInput("InternalInterface", cfg.InternalInterface)
page4Comps["InternalIPAddress_input"] = NewTextInput("InternalIPAddress", cfg.InternalIPAddress)
page4Comps["InternalNetmask_input"] = NewTextInput("InternalNetmask", cfg.InternalNetmask)
page4Comps["next_btn"] = NewButton("下一步")
page4Comps["prev_btn"] = NewButton("上一步")
pageComponents[PageInternalNetwork] = page4Comps
// ------------------ 页面5DNS页面 --------------------
page5Comps := make(map[string]Focusable)
page5Comps["Pri_DNS_input"] = NewTextInput("Pri DNS", cfg.DNSPrimary)
page5Comps["Sec_DNS_input"] = NewTextInput("Sec DNS", cfg.DNSSecondary)
page5Comps["next_btn"] = NewButton("下一步")
page5Comps["prev_btn"] = NewButton("上一步")
pageComponents[PageDNS] = page5Comps
// ------------------ 页面6Summary页面 --------------------
page6Comps := make(map[string]Focusable)
page6Comps["confirm_btn"] = NewButton("Confirm")
page6Comps["cancel_btn"] = NewButton("Cancel")
pageComponents[PageSummary] = page6Comps
// 创建焦点管理器(初始化聚焦页)
fm := NewFocusManager(true)
// 初始化模型
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,
currentPage: PageAgreement, // 初始化聚焦在协议页面
pageComponents: pageComponents,
focusManager: fm,
}
m.initPageInputs()
// 初始化当前页 (页1) 的焦点
m.initPageFocus(m.currentPage)
return m
}
@@ -141,8 +199,6 @@ func (m model) Init() tea.Cmd {
// 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() {
@@ -150,205 +206,104 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.quitting = true
return m, tea.Quit
case "esc":
if m.currentPage > 0 {
return m.prevPage()
}
// 1. 焦点切换Tab/Shift+Tab交给管理器处理
case "tab", "shift+tab", "left", "right":
cmd := m.focusManager.HandleInput(msg)
return m, cmd
// 2. 回车键:处理当前焦点组件的点击/确认
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 {
currentCompID := m.focusManager.currentFocusID
switch currentCompID {
// 页1accept → 进入页2
case "accept_btn":
return m, m.switchPage(PageData)
// 页1reject → 退出程序
case "reject_btn":
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 "prev_btn":
return m, m.switchPage(m.currentPage - 1)
case "next_btn":
return m, m.switchPage(m.currentPage + 1)
case PageSummary:
switch m.focusIndex {
case 0: // 执行
// 页6确认配置 → 退出并保存
case "confirm_btn":
m.done = true
if err := m.saveConfig(); err != nil {
m.err = err
return m, nil
}
m.quitting = true
return m, tea.Quit
case 1: // 取消
case "cancel_btn":
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
}
// 处理当前焦点组件的内部更新(比如输入框打字、列表选值)
currentComp, exists := m.focusManager.GetCurrent()
if exists {
// 不同组件的内部更新逻辑(示例)
switch comp := currentComp.(type) {
case *TextInput:
// 输入框更新
newTI, cmd := comp.Model.Update(msg)
comp.Model = newTI
default:
// 输入框页面: 支持输入框和按钮之间切换
// totalFocusable := len(m.textInputs) + 2
// 保存输入值到全局配置(示例:主机名)
switch m.focusManager.currentFocusID {
// 页2基础信息
case "Homepage_input":
m.config.HomePage = comp.Value()
case "Hostname_input":
m.config.Hostname = comp.Value()
case "Country_input":
m.config.Country = comp.Value()
case "Region_input":
m.config.Region = comp.Value()
case "Timezone_input":
m.config.Timezone = comp.Value()
case "DBPath_input":
m.config.DBAddress = comp.Value()
case "Software_input":
m.config.Software = comp.Value()
// 页3公网网络
case "PublicInterface_input":
m.config.PublicInterface = comp.Value()
case "PublicIPAddress_input":
m.config.PublicIPAddress = comp.Value()
case "PublicNetmask_input":
m.config.PublicNetmask = comp.Value()
case "PublicGateway_input":
m.config.PublicGateway = comp.Value()
// 页4内网网络
case "InternalInterface_input":
m.config.InternalInterface = comp.Value()
case "InternalIPAddress_input":
m.config.InternalIPAddress = comp.Value()
case "InternalNetmask_input":
m.config.InternalNetmask = comp.Value()
// 页5DNS
case "Pri_DNS_input":
m.config.DNSPrimary = comp.Value()
case "Sec_DNS_input":
m.config.DNSSecondary = comp.Value()
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, cmd
case *List:
// 列表更新
newList, cmd := comp.Model.Update(msg)
comp.Model = newList
return m, cmd
}
}
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
}

View File

@@ -1,12 +1,14 @@
package wizard
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
var split_line = splitlineStyle.Render(
"───────────────────────────────────────────────────────────────")
// View 渲染视图
func (m model) View() string {
if m.done {
@@ -19,38 +21,39 @@ func (m model) View() string {
return errorView(m.err)
}
var page string
var pageContent string
switch m.currentPage {
case PageAgreement:
page = m.agreementView()
pageContent = renderLicensePage(m)
case PageData:
page = m.dataView()
pageContent = renderDataInfoPage(m)
case PagePublicNetwork:
page = m.publicNetworkView()
pageContent = renderPublicNetworkPage(m)
case PageInternalNetwork:
page = m.internalNetworkView()
pageContent = renderInternalNetworkPage(m)
case PageDNS:
page = m.dnsView()
pageContent = renderDNSPage(m)
case PageSummary:
page = m.summaryView()
pageContent = renderSummaryPage(m)
default:
pageContent = appStyle.Render("无效页面")
}
content := strings.Builder{}
content.WriteString(page)
content.WriteString("\n\n")
content.WriteString(progressView(m.currentPage, m.totalPages))
return containerStyle.Render(content.String())
return appStyle.Render(pageContent)
}
// agreementView 协议页面
func (m model) agreementView() string {
func makeRow(label, value string) string {
return lipgloss.JoinHorizontal(lipgloss.Left,
labelStyle.Render(label+":"),
valueStyle.Render(value),
)
}
func renderLicensePage(m model) string {
title := titleStyle.Render("SunHPC Software License Agreement")
agreement := agreementBox.Render(`
─────────────────────────────────────────────────────────────
│ SunHPC License Agreement │
└─────────────────────────────────────────────────────────────┘
licenseText := `
───────────────────────────────────────────────────────────────
1. License Grant
This software grants you a non-exclusive, non-transferable
license to use it.
@@ -69,288 +72,265 @@ func (m model) agreementView() string {
PLEASE READ THE ABOVE TERMS CAREFULLY AND CLICK "ACCEPT"
TO AGREE AND FOLLOW THIS AGREEMENT.
───────────────────────────────────────────────────────────────
`)
`
var acceptBtn, rejectBtn string
if m.agreementIdx == 0 {
rejectBtn = selectedButton.Render(">> Reject <<")
acceptBtn = " Accept "
} else {
rejectBtn = " Reject "
acceptBtn = selectedButton.Render(">> Accept <<")
}
buttonGroup := lipgloss.JoinHorizontal(
lipgloss.Center,
acceptBtn, " ", rejectBtn)
pageComps := m.pageComponents[PageAgreement]
acceptBtn := pageComps["accept_btn"].View()
rejectBtn := pageComps["reject_btn"].View()
// ✅ 添加调试信息(确认 agreementIdx 的值)
// debugInfo := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).
// Render(fmt.Sprintf("[DEBUG: idx=%d]", m.agreementIdx),)
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
return lipgloss.JoinVertical(lipgloss.Center,
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
pageContent := lipgloss.JoinVertical(lipgloss.Center,
title, "",
agreement, "",
buttonGroup, "",
// debugInfo, "", // ✅ 显示调试信息
licenseTextStyle.Render(licenseText),
lipgloss.JoinHorizontal(lipgloss.Center, acceptBtn, rejectBtn),
hint,
)
return appStyle.Render(pageContent)
}
// dataView 数据接收页面
func (m model) dataView() string {
title := titleStyle.Render("Cluster Information")
// ---------------- 页2基础信息页渲染 ----------------
func renderDataInfoPage(m model) string {
pageComps := m.pageComponents[PageData]
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")
}
// 拼接基础信息表单
formContent := lipgloss.JoinVertical(lipgloss.Center,
split_line,
makeRow("Homepage", pageComps["Homepage_input"].View()),
split_line,
makeRow("Hostname", pageComps["Hostname_input"].View()),
split_line,
makeRow("Country", pageComps["Country_input"].View()),
split_line,
makeRow("Region", pageComps["Region_input"].View()),
split_line,
makeRow("Timezone", pageComps["Timezone_input"].View()),
split_line,
makeRow("DBPath", pageComps["DBPath_input"].View()),
split_line,
makeRow("Software", pageComps["Software_input"].View()),
split_line,
)
buttons := m.renderNavButtons()
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
inputs.String(), "",
buttons, "",
// 按钮区域
btnArea := lipgloss.JoinHorizontal(
lipgloss.Center,
pageComps["next_btn"].View(),
pageComps["prev_btn"].View(),
)
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
// 页面整体
pageContent := lipgloss.JoinVertical(
lipgloss.Center,
titleStyle.Render("基础信息配置(页2/6)"),
formContent,
btnArea,
hint,
)
return appStyle.Render(pageContent)
}
// publicNetworkView 公网设置页面
func (m model) publicNetworkView() string {
title := titleStyle.Render("Public Network Configuration")
func renderPublicNetworkPage(m model) string {
pageComps := m.pageComponents[PagePublicNetwork]
// 拼接公网网络表单
formContent := lipgloss.JoinVertical(lipgloss.Center,
split_line,
makeRow("PublicInterface", pageComps["PublicInterface_input"].View()),
split_line,
makeRow("PublicIPAddress", pageComps["PublicIPAddress_input"].View()),
split_line,
makeRow("PublicNetmask", pageComps["PublicNetmask_input"].View()),
split_line,
makeRow("PublicGateway", pageComps["PublicGateway_input"].View()),
split_line,
)
// 按钮区域
btnArea := lipgloss.JoinHorizontal(
lipgloss.Center,
pageComps["next_btn"].View(),
pageComps["prev_btn"].View(),
)
networkInterfaces := getNetworkInterfaces()
autoDetect := infoStyle.Render(
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
"[*] Auto Detect Interfaces: " + strings.Join(networkInterfaces, ", "))
var inputs strings.Builder
for i, ti := range m.textInputs {
info := fmt.Sprintf("%-20s|", m.inputLabels[i])
input := inputBox.Render(info + ti.View())
inputs.WriteString(input + "\n")
}
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
buttons := m.renderNavButtons()
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
autoDetect, "",
inputs.String(), "",
buttons, "",
// 页面整体
pageContent := lipgloss.JoinVertical(
lipgloss.Center,
titleStyle.Render("公网网络配置(页3/6)"),
autoDetect,
formContent,
btnArea,
hint,
)
return appStyle.Render(pageContent)
}
// internalNetworkView 内网配置页面
func (m model) internalNetworkView() string {
title := titleStyle.Render("Internal Network Configuration")
func renderInternalNetworkPage(m model) string {
pageComps := m.pageComponents[PageInternalNetwork]
networkInterfaces := getNetworkInterfaces()
autoDetect := infoStyle.Render(
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
// 拼接内网网络表单
formContent := lipgloss.JoinVertical(lipgloss.Center,
split_line,
makeRow("InternalInterface", pageComps["InternalInterface_input"].View()),
split_line,
makeRow("InternalIPAddress", pageComps["InternalIPAddress_input"].View()),
split_line,
makeRow("InternalNetmask", pageComps["InternalNetmask_input"].View()),
split_line,
)
var inputs strings.Builder
for i, ti := range m.textInputs {
info := fmt.Sprintf("%-20s|", m.inputLabels[i])
input := inputBox.Render(info + ti.View())
inputs.WriteString(input + "\n")
}
// 按钮区域
btnArea := lipgloss.JoinHorizontal(
lipgloss.Center,
pageComps["next_btn"].View(),
pageComps["prev_btn"].View(),
)
buttons := m.renderNavButtons()
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
autoDetect, "",
inputs.String(), "",
buttons, "",
// 页面整体
pageContent := lipgloss.JoinVertical(
lipgloss.Center,
titleStyle.Render("内网网络配置(页4/6)"),
formContent,
btnArea,
hint,
)
return appStyle.Render(pageContent)
}
// dnsView DNS 配置页面
func (m model) dnsView() string {
title := titleStyle.Render("DNS Configuration")
func renderDNSPage(m model) string {
pageComps := m.pageComponents[PageDNS]
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")
}
// 拼接 DNS 表单
formContent := lipgloss.JoinVertical(lipgloss.Center,
split_line,
makeRow("Pri DNS", pageComps["Pri_DNS_input"].View()),
split_line,
makeRow("Sec DNS", pageComps["Sec_DNS_input"].View()),
split_line,
)
buttons := m.renderNavButtons()
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
// 按钮区域
btnArea := lipgloss.JoinHorizontal(
lipgloss.Center,
pageComps["next_btn"].View(),
pageComps["prev_btn"].View(),
)
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
inputs.String(), "",
buttons, "",
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
// 页面整体
pageContent := lipgloss.JoinVertical(
lipgloss.Center,
titleStyle.Render("DNS 配置(页5/6)"),
formContent,
btnArea,
hint,
)
return appStyle.Render(pageContent)
}
// summaryView 总结页面
func (m model) summaryView() string {
title := titleStyle.Render("Summary")
subtitle := subTitleStyle.Render("Please confirm the following configuration information")
func renderSummaryPage(m model) string {
pageComps := m.pageComponents[PageSummary]
summary := summaryBox.Render(fmt.Sprintf(`
+----------------------------------------------------+
Basic Information
+----------------------------------------------------+
Homepage : %-38s
Hostname : %-35s
Country : %-31s
Region : %-31s
Timezone : %-38s
Homepage : %-38s
+----------------------------------------------------+
Database Configuration
+----------------------------------------------------+
Database Path : %-38s
Software : %-33s
+----------------------------------------------------+
Public Network Configuration
+----------------------------------------------------+
Public Interface : %-38s
Public IP : %-41s
Public Netmask : %-38s
Public Gateway : %-38s
+----------------------------------------------------+
Internal Network Configuration
+----------------------------------------------------+
Internal Interface: %-38s
Internal IP : %-41s
Internal Netmask : %-38s
+----------------------------------------------------+
DNS Configuration
+----------------------------------------------------+
Primary DNS : %-37s
Secondary DNS : %-37s
+----------------------------------------------------+
`,
m.config.HomePage,
m.config.Hostname,
m.config.Country,
m.config.Region,
m.config.Timezone,
m.config.HomePage,
m.config.DBAddress,
m.config.DataAddress,
m.config.PublicInterface,
m.config.PublicIPAddress,
m.config.PublicNetmask,
m.config.PublicGateway,
m.config.InternalInterface,
m.config.InternalIPAddress,
m.config.InternalNetmask,
m.config.DNSPrimary,
m.config.DNSSecondary,
))
// 拼接 Summary 表单
formContent := lipgloss.JoinVertical(lipgloss.Center,
split_line,
makeRow("Hostname", m.config.Hostname),
split_line,
makeRow("Country", m.config.Country),
split_line,
makeRow("Region", m.config.Region),
split_line,
makeRow("Timezone", m.config.Timezone),
split_line,
makeRow("Homepage", m.config.HomePage),
split_line,
makeRow("DBPath", m.config.DBAddress),
split_line,
makeRow("Software", m.config.Software),
split_line,
makeRow("PublicInterface", m.config.PublicInterface),
split_line,
makeRow("PublicIPAddress", m.config.PublicIPAddress),
split_line,
makeRow("PublicNetmask", m.config.PublicNetmask),
split_line,
makeRow("PublicGateway", m.config.PublicGateway),
split_line,
makeRow("InternalInterface", m.config.InternalInterface),
split_line,
makeRow("InternalIPAddress", m.config.InternalIPAddress),
split_line,
makeRow("InternalNetmask", m.config.InternalNetmask),
split_line,
makeRow("Pri DNS", m.config.DNSPrimary),
split_line,
makeRow("Sec DNS", m.config.DNSSecondary),
split_line,
)
var buttons string
if m.focusIndex == 0 {
buttons = selectedButton.Render("[>] Start Initialization") + " " + normalButton.Render("[ ] Cancel")
} else {
buttons = normalButton.Render("[>] Start Initialization") + " " + selectedButton.Render("[ ] Cancel")
}
// 按钮区域
btnArea := lipgloss.JoinHorizontal(
lipgloss.Center,
pageComps["confirm_btn"].View(),
pageComps["cancel_btn"].View(),
)
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
return lipgloss.JoinVertical(lipgloss.Center,
title, "",
subtitle, "",
summary, "",
buttons, "",
// 页面整体
pageContent := lipgloss.JoinVertical(
lipgloss.Center,
titleStyle.Render("确认信息(页6/6)"),
formContent,
btnArea,
hint,
)
return appStyle.Render(pageContent)
}
// 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{"License", "Data", "Network", "Network", "DNS", "Summary"}
label := labelStyle.Render(labels[current])
return progressStyle.Render(progress) + " " + label
}
// successView 成功视图
func successView() string {
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
content := lipgloss.JoinVertical(lipgloss.Center,
successTitle.Render("Initialization Completed!"), "",
successMsg.Render("System configuration has been saved, and the system is initializing..."), "",
hintStyle.Render("Press any key to exit"),
))
successMsg.Render(
"System configuration has been saved, and the system is initializing..."), "",
)
return appStyle.Render(content)
}
// quitView 退出视图
func quitView() string {
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
content := lipgloss.JoinVertical(lipgloss.Center,
errorTitle.Render("Canceled"), "",
errorMsg.Render("Initialization canceled, no configuration saved"),
))
)
return appStyle.Render(content)
}
// errorView 错误视图
func errorView(err error) string {
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
content := lipgloss.JoinVertical(lipgloss.Center,
errorTitle.Render("Error"), "",
errorMsg.Render(err.Error()), "",
hintStyle.Render("Press Ctrl+C to exit"),
))
}
// navButtons 导航按钮
func navButtons(m model, prev, next string) string {
var btns string
if m.currentPage == 0 {
btns = normalButton.Render(next) + " " + selectedButton.Render(prev)
} else {
btns = selectedButton.Render(next) + " " + normalButton.Render(prev)
}
return btns
}
func (m model) renderNavButtons() string {
var prevBtn, nextBtn string
switch m.focusType {
case FocusTypePrev:
// 焦点在"上一步"
nextBtn = normalButton.Render(" Next ")
prevBtn = selectedButton.Render(" << Prev >>")
case FocusTypeNext:
// 焦点在"下一步"
nextBtn = selectedButton.Render(" << Next >>")
prevBtn = normalButton.Render(" Prev ")
default:
// 焦点在输入框
nextBtn = normalButton.Render(" Next ")
prevBtn = normalButton.Render(" Prev ")
}
return lipgloss.JoinHorizontal(
lipgloss.Center,
nextBtn,
" ",
prevBtn,
)
return appStyle.Render(content)
}

View File

@@ -6,8 +6,12 @@ import "github.com/charmbracelet/lipgloss"
var (
primaryColor = lipgloss.Color("#7C3AED")
secondaryColor = lipgloss.Color("#10B981")
titleColor = lipgloss.Color("#8b19a2")
errorColor = lipgloss.Color("#EF4444")
warnColor = lipgloss.Color("#F59E0B")
btnTextColor = lipgloss.Color("#666666") // 深灰色
btnbordColor = lipgloss.Color("#3b4147")
btnFocusColor = lipgloss.Color("#ffffff")
// 背景色设为无,让终端自己的背景色生效,避免黑块
bgColor = lipgloss.Color("#1F2937")
@@ -16,82 +20,92 @@ var (
)
// 容器样式
var containerStyle = lipgloss.NewStyle().
Padding(2, 4).
var (
// 基础布局样式
appStyle = lipgloss.NewStyle().
Padding(1, 1).
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(primaryColor).
//Background(bgColor). // 注释掉背景色,防止在某些终端出现黑块
Foreground(textColor).
//Width(80).
Align(lipgloss.Center).
Height(40)
// 标题样式
titleStyle = lipgloss.NewStyle().
Foreground(titleColor).
Padding(0, 1).
Bold(true).
Align(lipgloss.Center)
// 标题样式
var titleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(primaryColor).
MarginBottom(1)
// 标题/标签样式
labelStyle = lipgloss.NewStyle().
Width(30).
Align(lipgloss.Right).
PaddingRight(2)
var subTitleStyle = lipgloss.NewStyle().
Foreground(mutedColor).
MarginBottom(2)
valueStyle = lipgloss.NewStyle().
Foreground(textColor).
Width(50)
// 按钮样式
var normalButton = lipgloss.NewStyle().
// 输入框/列表内容样式
inputBoxStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(btnbordColor).
Padding(0, 1).
Width(50)
// 按钮基础样式
btnBaseStyle = lipgloss.NewStyle().
Foreground(btnTextColor).
Padding(0, 2).
Foreground(lipgloss.Color("#666666")) // 深灰色,更暗
Margin(1, 1).
Border(lipgloss.RoundedBorder()).
BorderForeground(btnbordColor)
var selectedButton = lipgloss.NewStyle().
// 按钮选中/聚焦样式
btnSelectedStyle = lipgloss.NewStyle().
Foreground(btnFocusColor).
Padding(0, 2).
Margin(1, 1).
Border(lipgloss.RoundedBorder()).
BorderForeground(btnbordColor)
splitlineStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#888888"))
// 错误提示样式
errorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#ff5555")).
Bold(true).
Width(76)
// 协议文本样式
licenseTextStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#f8f8f2")).
Width(76)
// 提示文本样式
hintStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#888888")).
Width(76)
infoStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#888888")).
Border(lipgloss.RoundedBorder()).
BorderForeground(btnbordColor)
// 成功/错误提示样式
successTitle = lipgloss.NewStyle().
Foreground(secondaryColor).
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).
successMsg = lipgloss.NewStyle().
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().
errorTitle = lipgloss.NewStyle().
Bold(true).
Foreground(errorColor)
var errorMsg = lipgloss.NewStyle().
errorMsg = lipgloss.NewStyle().
Foreground(textColor)
var infoStyle = lipgloss.NewStyle().
Foreground(primaryColor).
Bold(true)
)

View File

@@ -2,20 +2,12 @@ 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())
@@ -27,20 +19,3 @@ func Run(force bool) error {
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
}