From d4e214fe237ccf70485253a73596aa3c87ec9b1f Mon Sep 17 00:00:00 2001 From: kelvin Date: Fri, 27 Feb 2026 22:52:15 +0800 Subject: [PATCH] =?UTF-8?q?Tui=20=E9=87=8D=E6=9E=84=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 1 + go.sum | 2 + pkg/wizard/config.go | 298 ++++++++++++--------------- pkg/wizard/focused.go | 321 +++++++++++++++++++++++++++++ pkg/wizard/model.go | 399 ++++++++++++++++-------------------- pkg/wizard/pages.go | 464 ++++++++++++++++++++---------------------- pkg/wizard/styles.go | 142 +++++++------ pkg/wizard/wizard.go | 25 --- 8 files changed, 934 insertions(+), 718 deletions(-) create mode 100644 pkg/wizard/focused.go diff --git a/go.mod b/go.mod index a0338dc..2531fde 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a394511..61d1ff2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/wizard/config.go b/pkg/wizard/config.go index f3bdde0..92dae59 100644 --- a/pkg/wizard/config.go +++ b/pkg/wizard/config.go @@ -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() - // 写入文件 - if err := os.WriteFile(configPath, data, 0644); err != nil { - return fmt.Errorf("保存配置文件失败:%w", err) + 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 !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 + } + return false, err } - - var cfg Config - if err := json.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("解析配置文件失败:%w", 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 } // 获取系统网络接口 diff --git a/pkg/wizard/focused.go b/pkg/wizard/focused.go new file mode 100644 index 0000000..fc2f3ee --- /dev/null +++ b/pkg/wizard/focused.go @@ -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) + } + } +} diff --git a/pkg/wizard/model.go b/pkg/wizard/model.go index 9faf11e..30798f8 100644 --- a/pkg/wizard/model.go +++ b/pkg/wizard/model.go @@ -7,19 +7,26 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +// PageType 页面类型 +type PageType int + +// 总页码 +const TotalPages = 6 + // Config 系统配置结构 type Config struct { // 协议 - AgreementAccepted bool `json:"agreement_accepted"` + License string `json:"license"` + 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"` - DataAddress string `json:"data_address"` + 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"` + 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 - networkInterfaces []string // 所有系统网络接口 - textInputs []textinput.Model - inputLabels []string // 存储标签 - focusIndex int - focusType int // 0=输入框, 1=上一步按钮, 2=下一步按钮 - agreementIdx int // 0=拒绝,1=接受 + textInputs []textinput.Model // 当前页面的输入框 + networkInterfaces []string // 所有系统网络接口 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 + + // ------------------ 页面5:DNS页面 -------------------- + 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 + + // ------------------ 页面6:Summary页面 -------------------- + 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, + config: cfg, + totalPages: 6, + 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() + currentCompID := m.focusManager.currentFocusID + switch currentCompID { + // 页1:accept → 进入页2 + case "accept_btn": + return m, m.switchPage(PageData) + // 页1:reject → 退出程序 + case "reject_btn": + m.quitting = true + return m, tea.Quit - case "tab", "shift+tab", "up", "down", "left", "right": - return m.handleNavigation(msg) - } + // 通用上一页/下一页逻辑 + case "prev_btn": + return m, m.switchPage(m.currentPage - 1) + case "next_btn": + return m, m.switchPage(m.currentPage + 1) - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height + // 页6:确认配置 → 退出并保存 + case "confirm_btn": + m.done = true + m.quitting = true + return m, tea.Quit - // 动态调整容器宽度 - /* - if msg.Width > 100 { - containerStyle = containerStyle.Width(90) - } else if msg.Width > 80 { - containerStyle = containerStyle.Width(70) - } else { - containerStyle = containerStyle.Width(msg.Width - 10) + case "cancel_btn": + m.quitting = true + return m, tea.Quit } - */ - - // ✅ 动态计算容器宽度(终端宽度的 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) - } + // 处理当前焦点组件的内部更新(比如输入框打字、列表选值) + currentComp, exists := m.focusManager.GetCurrent() + if exists { + // 不同组件的内部更新逻辑(示例) + switch comp := currentComp.(type) { + case *TextInput: + // 输入框更新 + newTI, cmd := comp.Model.Update(msg) + comp.Model = newTI - return m, tea.Batch(cmds...) -} + // 保存输入值到全局配置(示例:主机名) + 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() -// 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 - } + // 页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() - 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() - } + // 页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() + + // 页5:DNS + case "Pri_DNS_input": + m.config.DNSPrimary = comp.Value() + case "Sec_DNS_input": + m.config.DNSSecondary = comp.Value() - 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, cmd + + case *List: + // 列表更新 + newList, cmd := comp.Model.Update(msg) + comp.Model = newList + return m, cmd } } 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 -} diff --git a/pkg/wizard/pages.go b/pkg/wizard/pages.go index 93c91cf..8af73b1 100644 --- a/pkg/wizard/pages.go +++ b/pkg/wizard/pages.go @@ -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) } diff --git a/pkg/wizard/styles.go b/pkg/wizard/styles.go index b7bcd94..47e36e0 100644 --- a/pkg/wizard/styles.go +++ b/pkg/wizard/styles.go @@ -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). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(primaryColor). - //Background(bgColor). // 注释掉背景色,防止在某些终端出现黑块 - Foreground(textColor). - //Width(80). - Align(lipgloss.Center) +var ( + // 基础布局样式 + appStyle = lipgloss.NewStyle(). + Padding(1, 1). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(primaryColor). + Foreground(textColor). + Align(lipgloss.Center). + Height(40) -// 标题样式 -var titleStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(primaryColor). - MarginBottom(1) + // 标题样式 + titleStyle = lipgloss.NewStyle(). + Foreground(titleColor). + Padding(0, 1). + Bold(true). + Align(lipgloss.Center) -var subTitleStyle = lipgloss.NewStyle(). - Foreground(mutedColor). - MarginBottom(2) + // 子标题/标签样式 + labelStyle = lipgloss.NewStyle(). + Width(30). + Align(lipgloss.Right). + PaddingRight(2) -// 按钮样式 -var normalButton = lipgloss.NewStyle(). - Padding(0, 2). - Foreground(lipgloss.Color("#666666")) // 深灰色,更暗 + valueStyle = lipgloss.NewStyle(). + Foreground(textColor). + Width(50) -var selectedButton = lipgloss.NewStyle(). - Bold(true) + // 输入框/列表内容样式 + inputBoxStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(btnbordColor). + Padding(0, 1). + Width(50) -// 输入框样式 -var inputBox = lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(primaryColor). - Padding(0, 1) + // 按钮基础样式 + btnBaseStyle = lipgloss.NewStyle(). + Foreground(btnTextColor). + Padding(0, 2). + Margin(1, 1). + Border(lipgloss.RoundedBorder()). + BorderForeground(btnbordColor) -var labelStyle = lipgloss.NewStyle(). - Foreground(mutedColor). - Width(12). - Align(lipgloss.Right) + // 按钮选中/聚焦样式 + btnSelectedStyle = lipgloss.NewStyle(). + Foreground(btnFocusColor). + Padding(0, 2). + Margin(1, 1). + Border(lipgloss.RoundedBorder()). + BorderForeground(btnbordColor) -// 协议框样式 -var agreementBox = lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(warnColor). - Padding(1, 2). - //Width(70). - Align(lipgloss.Left) + splitlineStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#888888")) -// 总结框样式 -var summaryBox = lipgloss.NewStyle(). - BorderStyle(lipgloss.DoubleBorder()). - BorderForeground(primaryColor). - Padding(0, 0). - Foreground(textColor) + // 错误提示样式 + errorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#ff5555")). + Bold(true). + Width(76) -// 进度条样式 -var progressStyle = lipgloss.NewStyle().Foreground(primaryColor) + // 协议文本样式 + licenseTextStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#f8f8f2")). + Width(76) -// 提示信息样式 -var hintStyle = lipgloss.NewStyle(). - Foreground(mutedColor). - Italic(true) + // 提示文本样式 + hintStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#888888")). + Width(76) -// 成功/错误样式 -var successTitle = lipgloss.NewStyle(). - Bold(true). - Foreground(secondaryColor) + infoStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#888888")). + Border(lipgloss.RoundedBorder()). + BorderForeground(btnbordColor) -var successMsg = lipgloss.NewStyle(). - Foreground(textColor) + // 成功/错误提示样式 + successTitle = lipgloss.NewStyle(). + Foreground(secondaryColor). + Bold(true) -var errorTitle = lipgloss.NewStyle(). - Bold(true). - Foreground(errorColor) + successMsg = lipgloss.NewStyle(). + Foreground(textColor) -var errorMsg = lipgloss.NewStyle(). - Foreground(textColor) + errorTitle = lipgloss.NewStyle(). + Bold(true). + Foreground(errorColor) -var infoStyle = lipgloss.NewStyle(). - Foreground(primaryColor). - Bold(true) + errorMsg = lipgloss.NewStyle(). + Foreground(textColor) +) diff --git a/pkg/wizard/wizard.go b/pkg/wizard/wizard.go index 5f15fec..07d5d99 100644 --- a/pkg/wizard/wizard.go +++ b/pkg/wizard/wizard.go @@ -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 -}