Tui 重构代码逻辑
This commit is contained in:
1
go.mod
1
go.mod
@@ -44,6 +44,7 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // 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/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
|||||||
2
go.sum
2
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/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 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
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 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
|
|||||||
@@ -1,194 +1,162 @@
|
|||||||
package wizard
|
package wizard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"bufio"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"strings"
|
||||||
|
"sunhpc/pkg/database"
|
||||||
"sunhpc/pkg/utils"
|
"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 {
|
func (m *model) saveConfig() error {
|
||||||
configPath := GetConfigPath()
|
conn, err := database.GetDB() // 假设database包已实现getDB()获取连接
|
||||||
|
|
||||||
// 确保目录存在
|
|
||||||
dir := filepath.Dir(configPath)
|
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("创建配置目录失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 序列化配置
|
|
||||||
data, err := json.MarshalIndent(m.config, "", " ")
|
|
||||||
if err != nil {
|
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 {
|
if !exists {
|
||||||
return fmt.Errorf("保存配置文件失败:%w", err)
|
// 不存在则直接插入
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadConfig 从文件加载配置
|
// checkExists 集中判断配置项是否存在(核心判断逻辑)
|
||||||
func loadConfig() (*Config, error) {
|
func (m *model) checkExists(conn *sql.DB, table, key string) (bool, error) {
|
||||||
configPath := GetConfigPath()
|
var count int
|
||||||
|
// 通用存在性检查SQL(假设所有表都有key字段作为主键)
|
||||||
data, err := os.ReadFile(configPath)
|
query := fmt.Sprintf("SELECT COUNT(1) FROM %s WHERE `key` = ?", table)
|
||||||
|
err := conn.QueryRow(query, key).Scan(&count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("读取配置文件失败:%w", err)
|
// 表不存在也视为"不存在"(可选:根据实际需求调整,比如先建表)
|
||||||
|
if strings.Contains(err.Error(), "table not found") {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg Config
|
// upsertConfig 统一处理插入/更新逻辑
|
||||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
func (m *model) upsertConfig(conn *sql.DB, table, key string, val interface{}, update bool) error {
|
||||||
return nil, fmt.Errorf("解析配置文件失败:%w", err)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cfg, nil
|
// 处理参数顺序(更新和插入的参数顺序不同)
|
||||||
|
var args []interface{}
|
||||||
|
if !update {
|
||||||
|
args = []interface{}{key, val}
|
||||||
|
} else {
|
||||||
|
args = []interface{}{val, key}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以下是 model.go 中调用的保存方法
|
_, err := conn.Exec(query, args...)
|
||||||
func (m *model) saveCurrentPage() {
|
return err
|
||||||
switch m.currentPage {
|
|
||||||
case PageData:
|
|
||||||
m.saveDataPage()
|
|
||||||
case PagePublicNetwork:
|
|
||||||
m.savePublicNetworkPage()
|
|
||||||
case PageInternalNetwork:
|
|
||||||
m.saveInternalNetworkPage()
|
|
||||||
case PageDNS:
|
|
||||||
m.saveDNSPage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) saveDataPage() {
|
// askUserChoice 询问用户操作选择
|
||||||
if len(m.textInputs) >= 8 {
|
func (m *model) askUserChoice(table, key string) (string, error) {
|
||||||
m.config.HomePage = m.textInputs[0].Value()
|
reader := bufio.NewReader(os.Stdin)
|
||||||
m.config.Hostname = m.textInputs[1].Value()
|
fmt.Printf("配置项%s.%s已存在,选择操作(y/yes=覆盖, n/no=跳过, a/all=全量覆盖后续所有): ", table, key)
|
||||||
m.config.Country = m.textInputs[2].Value()
|
input, err := reader.ReadString('\n')
|
||||||
m.config.Region = m.textInputs[3].Value()
|
if err != nil {
|
||||||
m.config.Timezone = m.textInputs[4].Value()
|
return "", err
|
||||||
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"}
|
|
||||||
}
|
}
|
||||||
|
// 去除空格和换行
|
||||||
|
return strings.TrimSpace(input), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取系统网络接口
|
// 获取系统网络接口
|
||||||
|
|||||||
321
pkg/wizard/focused.go
Normal file
321
pkg/wizard/focused.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,16 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PageType 页面类型
|
||||||
|
type PageType int
|
||||||
|
|
||||||
|
// 总页码
|
||||||
|
const TotalPages = 6
|
||||||
|
|
||||||
// Config 系统配置结构
|
// Config 系统配置结构
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// 协议
|
// 协议
|
||||||
|
License string `json:"license"`
|
||||||
AgreementAccepted bool `json:"agreement_accepted"`
|
AgreementAccepted bool `json:"agreement_accepted"`
|
||||||
|
|
||||||
// 数据接收
|
// 数据接收
|
||||||
@@ -19,7 +26,7 @@ type Config struct {
|
|||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
HomePage string `json:"homepage"`
|
HomePage string `json:"homepage"`
|
||||||
DBAddress string `json:"db_address"`
|
DBAddress string `json:"db_address"`
|
||||||
DataAddress string `json:"data_address"`
|
Software string `json:"software"`
|
||||||
|
|
||||||
// 公网设置
|
// 公网设置
|
||||||
PublicInterface string `json:"public_interface"`
|
PublicInterface string `json:"public_interface"`
|
||||||
@@ -37,9 +44,6 @@ type Config struct {
|
|||||||
DNSSecondary string `json:"dns_secondary"`
|
DNSSecondary string `json:"dns_secondary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageType 页面类型
|
|
||||||
type PageType int
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PageAgreement PageType = iota
|
PageAgreement PageType = iota
|
||||||
PageData
|
PageData
|
||||||
@@ -49,29 +53,24 @@ const (
|
|||||||
PageSummary
|
PageSummary
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
FocusTypeInput int = 0
|
|
||||||
FocusTypePrev int = 1
|
|
||||||
FocusTypeNext int = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// model TUI 主模型
|
// model TUI 主模型
|
||||||
type model struct {
|
type model struct {
|
||||||
config Config
|
config Config // 全局配置
|
||||||
currentPage PageType
|
currentPage PageType // 当前页面
|
||||||
totalPages int
|
totalPages int
|
||||||
|
textInputs []textinput.Model // 当前页面的输入框
|
||||||
networkInterfaces []string // 所有系统网络接口
|
networkInterfaces []string // 所有系统网络接口
|
||||||
textInputs []textinput.Model
|
|
||||||
inputLabels []string // 存储标签
|
|
||||||
focusIndex int
|
|
||||||
focusType int // 0=输入框, 1=上一步按钮, 2=下一步按钮
|
|
||||||
agreementIdx int // 0=拒绝,1=接受
|
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
err error
|
err error
|
||||||
quitting bool
|
quitting bool
|
||||||
done bool
|
done bool
|
||||||
force bool
|
force bool
|
||||||
|
|
||||||
|
// 核心1: 按页面分组存储所有组件(6个页面 + 6个map)
|
||||||
|
pageComponents map[PageType]map[string]Focusable
|
||||||
|
// 核心2:焦点管理器(每次切换页面时重置)
|
||||||
|
focusManager *FocusManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultConfig 返回默认配置
|
// defaultConfig 返回默认配置
|
||||||
@@ -97,13 +96,14 @@ func defaultConfig() Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
|
License: "This test license is for testing purposes only. Do not use it in production.",
|
||||||
Hostname: "cluster.hpc.org",
|
Hostname: "cluster.hpc.org",
|
||||||
Country: "China",
|
Country: "China",
|
||||||
Region: "Beijing",
|
Region: "Beijing",
|
||||||
Timezone: "Asia/Shanghai",
|
Timezone: "Asia/Shanghai",
|
||||||
HomePage: "www.sunhpc.com",
|
HomePage: "www.sunhpc.com",
|
||||||
DBAddress: "/var/lib/sunhpc/sunhpc.db",
|
DBAddress: "/var/lib/sunhpc/sunhpc.db",
|
||||||
DataAddress: "/export/sunhpc",
|
Software: "/export/sunhpc",
|
||||||
PublicInterface: defaultPublicInterface,
|
PublicInterface: defaultPublicInterface,
|
||||||
PublicIPAddress: "",
|
PublicIPAddress: "",
|
||||||
PublicNetmask: "",
|
PublicNetmask: "",
|
||||||
@@ -119,18 +119,76 @@ func defaultConfig() Config {
|
|||||||
// initialModel 初始化模型
|
// initialModel 初始化模型
|
||||||
func initialModel() model {
|
func initialModel() model {
|
||||||
cfg := defaultConfig()
|
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{
|
m := model{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
totalPages: 6,
|
totalPages: 6,
|
||||||
textInputs: make([]textinput.Model, 0),
|
currentPage: PageAgreement, // 初始化聚焦在协议页面
|
||||||
inputLabels: make([]string, 0),
|
pageComponents: pageComponents,
|
||||||
agreementIdx: 1,
|
focusManager: fm,
|
||||||
focusIndex: 0,
|
|
||||||
focusType: 0, // 0=输入框, 1=上一步按钮, 2=下一步按钮
|
|
||||||
width: 80,
|
|
||||||
height: 24,
|
|
||||||
}
|
}
|
||||||
m.initPageInputs()
|
|
||||||
|
// 初始化当前页 (页1) 的焦点
|
||||||
|
m.initPageFocus(m.currentPage)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,8 +199,6 @@ func (m model) Init() tea.Cmd {
|
|||||||
|
|
||||||
// Update 处理消息更新
|
// Update 处理消息更新
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
@@ -150,205 +206,104 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.quitting = true
|
m.quitting = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
|
||||||
case "esc":
|
// 1. 焦点切换(Tab/Shift+Tab)交给管理器处理
|
||||||
if m.currentPage > 0 {
|
case "tab", "shift+tab", "left", "right":
|
||||||
return m.prevPage()
|
cmd := m.focusManager.HandleInput(msg)
|
||||||
}
|
return m, cmd
|
||||||
|
|
||||||
|
// 2. 回车键:处理当前焦点组件的点击/确认
|
||||||
case "enter":
|
case "enter":
|
||||||
return m.handleEnter()
|
currentCompID := m.focusManager.currentFocusID
|
||||||
|
switch currentCompID {
|
||||||
case "tab", "shift+tab", "up", "down", "left", "right":
|
// 页1:accept → 进入页2
|
||||||
return m.handleNavigation(msg)
|
case "accept_btn":
|
||||||
}
|
return m, m.switchPage(PageData)
|
||||||
|
// 页1:reject → 退出程序
|
||||||
case tea.WindowSizeMsg:
|
case "reject_btn":
|
||||||
m.width = msg.Width
|
|
||||||
m.height = msg.Height
|
|
||||||
|
|
||||||
// 动态调整容器宽度
|
|
||||||
/*
|
|
||||||
if msg.Width > 100 {
|
|
||||||
containerStyle = containerStyle.Width(90)
|
|
||||||
} else if msg.Width > 80 {
|
|
||||||
containerStyle = containerStyle.Width(70)
|
|
||||||
} else {
|
|
||||||
containerStyle = containerStyle.Width(msg.Width - 10)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ✅ 动态计算容器宽度(终端宽度的 80%)
|
|
||||||
containerWidth := msg.Width * 80 / 100
|
|
||||||
|
|
||||||
// ✅ 重新设置容器样式宽度
|
|
||||||
containerStyle = containerStyle.Width(containerWidth)
|
|
||||||
|
|
||||||
// 动态设置协议框宽度(容器宽度的 90%)
|
|
||||||
agreementWidth := containerWidth * 80 / 100
|
|
||||||
agreementBox = agreementBox.Width(agreementWidth)
|
|
||||||
|
|
||||||
// 动态设置输入框宽度
|
|
||||||
inputWidth := containerWidth * 60 / 100
|
|
||||||
if inputWidth < 40 {
|
|
||||||
inputWidth = 40
|
|
||||||
}
|
|
||||||
inputBox = inputBox.Width(inputWidth)
|
|
||||||
|
|
||||||
// 动态设置总结框宽度
|
|
||||||
summaryWidth := containerWidth * 90 / 100
|
|
||||||
summaryBox = summaryBox.Width(summaryWidth)
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新当前焦点的输入框
|
|
||||||
if len(m.textInputs) > 0 && m.focusIndex < len(m.textInputs) {
|
|
||||||
var cmd tea.Cmd
|
|
||||||
m.textInputs[m.focusIndex], cmd = m.textInputs[m.focusIndex].Update(msg)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleEnter 处理回车事件
|
|
||||||
func (m *model) handleEnter() (tea.Model, tea.Cmd) {
|
|
||||||
switch m.currentPage {
|
|
||||||
case PageAgreement:
|
|
||||||
if m.agreementIdx == 1 {
|
|
||||||
m.config.AgreementAccepted = true
|
|
||||||
return m.nextPage()
|
|
||||||
} else {
|
|
||||||
m.quitting = true
|
m.quitting = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
|
||||||
|
|
||||||
case PageData, PagePublicNetwork, PageInternalNetwork, PageDNS:
|
// 通用上一页/下一页逻辑
|
||||||
// 根据焦点类型执行不同操作
|
case "prev_btn":
|
||||||
switch m.focusType {
|
return m, m.switchPage(m.currentPage - 1)
|
||||||
case FocusTypeInput:
|
case "next_btn":
|
||||||
// 在输入框上,保存并下一页
|
return m, m.switchPage(m.currentPage + 1)
|
||||||
m.saveCurrentPage()
|
|
||||||
return m.nextPage()
|
|
||||||
case FocusTypePrev:
|
|
||||||
// 上一步按钮,返回上一页
|
|
||||||
return m.prevPage()
|
|
||||||
case FocusTypeNext:
|
|
||||||
// 下一步按钮,切换到下一页
|
|
||||||
m.saveCurrentPage()
|
|
||||||
return m.nextPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
case PageSummary:
|
// 页6:确认配置 → 退出并保存
|
||||||
switch m.focusIndex {
|
case "confirm_btn":
|
||||||
case 0: // 执行
|
|
||||||
m.done = true
|
m.done = true
|
||||||
if err := m.saveConfig(); err != nil {
|
m.quitting = true
|
||||||
m.err = err
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case 1: // 取消
|
|
||||||
|
case "cancel_btn":
|
||||||
m.quitting = true
|
m.quitting = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
// 其他消息(窗口大小、输入框输入等)...
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleNavigation 处理导航
|
// 处理当前焦点组件的内部更新(比如输入框打字、列表选值)
|
||||||
func (m *model) handleNavigation(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
currentComp, exists := m.focusManager.GetCurrent()
|
||||||
// debug
|
if exists {
|
||||||
//fmt.Fprintf(os.Stderr, "DEBUG: key=%s page=%d\n", msg.String(), m.currentPage)
|
// 不同组件的内部更新逻辑(示例)
|
||||||
|
switch comp := currentComp.(type) {
|
||||||
|
case *TextInput:
|
||||||
|
// 输入框更新
|
||||||
|
newTI, cmd := comp.Model.Update(msg)
|
||||||
|
comp.Model = newTI
|
||||||
|
|
||||||
switch m.currentPage {
|
// 保存输入值到全局配置(示例:主机名)
|
||||||
case PageAgreement:
|
switch m.focusManager.currentFocusID {
|
||||||
switch msg.String() {
|
// 页2:基础信息
|
||||||
case "left", "right", "tab", "shift+tab", "up", "down":
|
case "Homepage_input":
|
||||||
m.agreementIdx = 1 - m.agreementIdx
|
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()
|
||||||
|
|
||||||
case PageSummary:
|
// 页3:公网网络
|
||||||
switch msg.String() {
|
case "PublicInterface_input":
|
||||||
case "left", "right", "tab", "shift+tab":
|
m.config.PublicInterface = comp.Value()
|
||||||
m.focusIndex = 1 - m.focusIndex
|
case "PublicIPAddress_input":
|
||||||
}
|
m.config.PublicIPAddress = comp.Value()
|
||||||
|
case "PublicNetmask_input":
|
||||||
|
m.config.PublicNetmask = comp.Value()
|
||||||
|
case "PublicGateway_input":
|
||||||
|
m.config.PublicGateway = comp.Value()
|
||||||
|
|
||||||
default:
|
// 页4:内网网络
|
||||||
// 输入框页面: 支持输入框和按钮之间切换
|
case "InternalInterface_input":
|
||||||
// totalFocusable := len(m.textInputs) + 2
|
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()
|
||||||
|
|
||||||
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
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package wizard
|
package wizard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var split_line = splitlineStyle.Render(
|
||||||
|
"───────────────────────────────────────────────────────────────")
|
||||||
|
|
||||||
// View 渲染视图
|
// View 渲染视图
|
||||||
func (m model) View() string {
|
func (m model) View() string {
|
||||||
if m.done {
|
if m.done {
|
||||||
@@ -19,38 +21,39 @@ func (m model) View() string {
|
|||||||
return errorView(m.err)
|
return errorView(m.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var page string
|
var pageContent string
|
||||||
switch m.currentPage {
|
switch m.currentPage {
|
||||||
case PageAgreement:
|
case PageAgreement:
|
||||||
page = m.agreementView()
|
pageContent = renderLicensePage(m)
|
||||||
case PageData:
|
case PageData:
|
||||||
page = m.dataView()
|
pageContent = renderDataInfoPage(m)
|
||||||
case PagePublicNetwork:
|
case PagePublicNetwork:
|
||||||
page = m.publicNetworkView()
|
pageContent = renderPublicNetworkPage(m)
|
||||||
case PageInternalNetwork:
|
case PageInternalNetwork:
|
||||||
page = m.internalNetworkView()
|
pageContent = renderInternalNetworkPage(m)
|
||||||
case PageDNS:
|
case PageDNS:
|
||||||
page = m.dnsView()
|
pageContent = renderDNSPage(m)
|
||||||
case PageSummary:
|
case PageSummary:
|
||||||
page = m.summaryView()
|
pageContent = renderSummaryPage(m)
|
||||||
|
|
||||||
|
default:
|
||||||
|
pageContent = appStyle.Render("无效页面")
|
||||||
|
}
|
||||||
|
return appStyle.Render(pageContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
content := strings.Builder{}
|
func makeRow(label, value string) string {
|
||||||
content.WriteString(page)
|
return lipgloss.JoinHorizontal(lipgloss.Left,
|
||||||
content.WriteString("\n\n")
|
labelStyle.Render(label+":"),
|
||||||
content.WriteString(progressView(m.currentPage, m.totalPages))
|
valueStyle.Render(value),
|
||||||
|
)
|
||||||
return containerStyle.Render(content.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// agreementView 协议页面
|
func renderLicensePage(m model) string {
|
||||||
func (m model) agreementView() string {
|
|
||||||
title := titleStyle.Render("SunHPC Software License Agreement")
|
title := titleStyle.Render("SunHPC Software License Agreement")
|
||||||
|
|
||||||
agreement := agreementBox.Render(`
|
licenseText := `
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
───────────────────────────────────────────────────────────────
|
||||||
│ SunHPC License Agreement │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
1. License Grant
|
1. License Grant
|
||||||
This software grants you a non-exclusive, non-transferable
|
This software grants you a non-exclusive, non-transferable
|
||||||
license to use it.
|
license to use it.
|
||||||
@@ -69,288 +72,265 @@ func (m model) agreementView() string {
|
|||||||
PLEASE READ THE ABOVE TERMS CAREFULLY AND CLICK "ACCEPT"
|
PLEASE READ THE ABOVE TERMS CAREFULLY AND CLICK "ACCEPT"
|
||||||
TO AGREE AND FOLLOW THIS AGREEMENT.
|
TO AGREE AND FOLLOW THIS AGREEMENT.
|
||||||
───────────────────────────────────────────────────────────────
|
───────────────────────────────────────────────────────────────
|
||||||
`)
|
`
|
||||||
|
|
||||||
var acceptBtn, rejectBtn string
|
pageComps := m.pageComponents[PageAgreement]
|
||||||
if m.agreementIdx == 0 {
|
acceptBtn := pageComps["accept_btn"].View()
|
||||||
rejectBtn = selectedButton.Render(">> Reject <<")
|
rejectBtn := pageComps["reject_btn"].View()
|
||||||
acceptBtn = " Accept "
|
|
||||||
} else {
|
|
||||||
rejectBtn = " Reject "
|
|
||||||
acceptBtn = selectedButton.Render(">> Accept <<")
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonGroup := lipgloss.JoinHorizontal(
|
|
||||||
lipgloss.Center,
|
|
||||||
acceptBtn, " ", rejectBtn)
|
|
||||||
|
|
||||||
// ✅ 添加调试信息(确认 agreementIdx 的值)
|
// ✅ 添加调试信息(确认 agreementIdx 的值)
|
||||||
// debugInfo := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).
|
// debugInfo := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).
|
||||||
// Render(fmt.Sprintf("[DEBUG: idx=%d]", m.agreementIdx),)
|
// Render(fmt.Sprintf("[DEBUG: idx=%d]", m.agreementIdx),)
|
||||||
|
|
||||||
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,
|
pageContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
title, "",
|
title, "",
|
||||||
agreement, "",
|
licenseTextStyle.Render(licenseText),
|
||||||
buttonGroup, "",
|
lipgloss.JoinHorizontal(lipgloss.Center, acceptBtn, rejectBtn),
|
||||||
// debugInfo, "", // ✅ 显示调试信息
|
|
||||||
hint,
|
hint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return appStyle.Render(pageContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dataView 数据接收页面
|
// ---------------- 页2:基础信息页渲染 ----------------
|
||||||
func (m model) dataView() string {
|
func renderDataInfoPage(m model) string {
|
||||||
title := titleStyle.Render("Cluster Information")
|
pageComps := m.pageComponents[PageData]
|
||||||
|
|
||||||
var inputs strings.Builder
|
// 拼接基础信息表单
|
||||||
for i, ti := range m.textInputs {
|
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
|
split_line,
|
||||||
input := inputBox.Render(info + ti.View())
|
makeRow("Homepage", pageComps["Homepage_input"].View()),
|
||||||
inputs.WriteString(input + "\n")
|
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")
|
btnArea := lipgloss.JoinHorizontal(
|
||||||
return lipgloss.JoinVertical(lipgloss.Center,
|
lipgloss.Center,
|
||||||
title, "",
|
pageComps["next_btn"].View(),
|
||||||
inputs.String(), "",
|
pageComps["prev_btn"].View(),
|
||||||
buttons, "",
|
)
|
||||||
|
|
||||||
|
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||||
|
|
||||||
|
// 页面整体
|
||||||
|
pageContent := lipgloss.JoinVertical(
|
||||||
|
lipgloss.Center,
|
||||||
|
titleStyle.Render("基础信息配置(页2/6)"),
|
||||||
|
formContent,
|
||||||
|
btnArea,
|
||||||
hint,
|
hint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return appStyle.Render(pageContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// publicNetworkView 公网设置页面
|
func renderPublicNetworkPage(m model) string {
|
||||||
func (m model) publicNetworkView() string {
|
pageComps := m.pageComponents[PagePublicNetwork]
|
||||||
title := titleStyle.Render("Public Network Configuration")
|
|
||||||
|
// 拼接公网网络表单
|
||||||
|
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()
|
networkInterfaces := getNetworkInterfaces()
|
||||||
autoDetect := infoStyle.Render(
|
autoDetect := infoStyle.Render(
|
||||||
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
|
"[*] Auto Detect Interfaces: " + strings.Join(networkInterfaces, ", "))
|
||||||
|
|
||||||
var inputs strings.Builder
|
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||||
for i, ti := range m.textInputs {
|
|
||||||
info := fmt.Sprintf("%-20s|", m.inputLabels[i])
|
|
||||||
input := inputBox.Render(info + ti.View())
|
|
||||||
inputs.WriteString(input + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons := m.renderNavButtons()
|
// 页面整体
|
||||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
pageContent := lipgloss.JoinVertical(
|
||||||
|
lipgloss.Center,
|
||||||
return lipgloss.JoinVertical(lipgloss.Center,
|
titleStyle.Render("公网网络配置(页3/6)"),
|
||||||
title, "",
|
autoDetect,
|
||||||
autoDetect, "",
|
formContent,
|
||||||
inputs.String(), "",
|
btnArea,
|
||||||
buttons, "",
|
|
||||||
hint,
|
hint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return appStyle.Render(pageContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// internalNetworkView 内网配置页面
|
func renderInternalNetworkPage(m model) string {
|
||||||
func (m model) internalNetworkView() string {
|
pageComps := m.pageComponents[PageInternalNetwork]
|
||||||
title := titleStyle.Render("Internal Network Configuration")
|
|
||||||
|
|
||||||
networkInterfaces := getNetworkInterfaces()
|
// 拼接内网网络表单
|
||||||
autoDetect := infoStyle.Render(
|
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
|
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 {
|
btnArea := lipgloss.JoinHorizontal(
|
||||||
info := fmt.Sprintf("%-20s|", m.inputLabels[i])
|
lipgloss.Center,
|
||||||
input := inputBox.Render(info + ti.View())
|
pageComps["next_btn"].View(),
|
||||||
inputs.WriteString(input + "\n")
|
pageComps["prev_btn"].View(),
|
||||||
}
|
)
|
||||||
|
|
||||||
buttons := m.renderNavButtons()
|
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Center,
|
// 页面整体
|
||||||
title, "",
|
pageContent := lipgloss.JoinVertical(
|
||||||
autoDetect, "",
|
lipgloss.Center,
|
||||||
inputs.String(), "",
|
titleStyle.Render("内网网络配置(页4/6)"),
|
||||||
buttons, "",
|
formContent,
|
||||||
|
btnArea,
|
||||||
hint,
|
hint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return appStyle.Render(pageContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dnsView DNS 配置页面
|
func renderDNSPage(m model) string {
|
||||||
func (m model) dnsView() string {
|
pageComps := m.pageComponents[PageDNS]
|
||||||
title := titleStyle.Render("DNS Configuration")
|
|
||||||
|
|
||||||
var inputs strings.Builder
|
// 拼接 DNS 表单
|
||||||
for i, ti := range m.textInputs {
|
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
|
split_line,
|
||||||
input := inputBox.Render(info + ti.View())
|
makeRow("Pri DNS", pageComps["Pri_DNS_input"].View()),
|
||||||
inputs.WriteString(input + "\n")
|
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,
|
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||||
title, "",
|
|
||||||
inputs.String(), "",
|
// 页面整体
|
||||||
buttons, "",
|
pageContent := lipgloss.JoinVertical(
|
||||||
|
lipgloss.Center,
|
||||||
|
titleStyle.Render("DNS 配置(页5/6)"),
|
||||||
|
formContent,
|
||||||
|
btnArea,
|
||||||
hint,
|
hint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return appStyle.Render(pageContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// summaryView 总结页面
|
func renderSummaryPage(m model) string {
|
||||||
func (m model) summaryView() string {
|
pageComps := m.pageComponents[PageSummary]
|
||||||
title := titleStyle.Render("Summary")
|
|
||||||
subtitle := subTitleStyle.Render("Please confirm the following configuration information")
|
|
||||||
|
|
||||||
summary := summaryBox.Render(fmt.Sprintf(`
|
// 拼接 Summary 表单
|
||||||
+----------------------------------------------------+
|
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
Basic Information
|
split_line,
|
||||||
+----------------------------------------------------+
|
makeRow("Hostname", m.config.Hostname),
|
||||||
Homepage : %-38s
|
split_line,
|
||||||
Hostname : %-35s
|
makeRow("Country", m.config.Country),
|
||||||
Country : %-31s
|
split_line,
|
||||||
Region : %-31s
|
makeRow("Region", m.config.Region),
|
||||||
Timezone : %-38s
|
split_line,
|
||||||
Homepage : %-38s
|
makeRow("Timezone", m.config.Timezone),
|
||||||
+----------------------------------------------------+
|
split_line,
|
||||||
Database Configuration
|
makeRow("Homepage", m.config.HomePage),
|
||||||
+----------------------------------------------------+
|
split_line,
|
||||||
Database Path : %-38s
|
makeRow("DBPath", m.config.DBAddress),
|
||||||
Software : %-33s
|
split_line,
|
||||||
+----------------------------------------------------+
|
makeRow("Software", m.config.Software),
|
||||||
Public Network Configuration
|
split_line,
|
||||||
+----------------------------------------------------+
|
makeRow("PublicInterface", m.config.PublicInterface),
|
||||||
Public Interface : %-38s
|
split_line,
|
||||||
Public IP : %-41s
|
makeRow("PublicIPAddress", m.config.PublicIPAddress),
|
||||||
Public Netmask : %-38s
|
split_line,
|
||||||
Public Gateway : %-38s
|
makeRow("PublicNetmask", m.config.PublicNetmask),
|
||||||
+----------------------------------------------------+
|
split_line,
|
||||||
Internal Network Configuration
|
makeRow("PublicGateway", m.config.PublicGateway),
|
||||||
+----------------------------------------------------+
|
split_line,
|
||||||
Internal Interface: %-38s
|
makeRow("InternalInterface", m.config.InternalInterface),
|
||||||
Internal IP : %-41s
|
split_line,
|
||||||
Internal Netmask : %-38s
|
makeRow("InternalIPAddress", m.config.InternalIPAddress),
|
||||||
+----------------------------------------------------+
|
split_line,
|
||||||
DNS Configuration
|
makeRow("InternalNetmask", m.config.InternalNetmask),
|
||||||
+----------------------------------------------------+
|
split_line,
|
||||||
Primary DNS : %-37s
|
makeRow("Pri DNS", m.config.DNSPrimary),
|
||||||
Secondary DNS : %-37s
|
split_line,
|
||||||
+----------------------------------------------------+
|
makeRow("Sec DNS", m.config.DNSSecondary),
|
||||||
`,
|
split_line,
|
||||||
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,
|
|
||||||
))
|
|
||||||
|
|
||||||
var buttons string
|
// 按钮区域
|
||||||
if m.focusIndex == 0 {
|
btnArea := lipgloss.JoinHorizontal(
|
||||||
buttons = selectedButton.Render("[>] Start Initialization") + " " + normalButton.Render("[ ] Cancel")
|
lipgloss.Center,
|
||||||
} else {
|
pageComps["confirm_btn"].View(),
|
||||||
buttons = normalButton.Render("[>] Start Initialization") + " " + selectedButton.Render("[ ] Cancel")
|
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, "",
|
pageContent := lipgloss.JoinVertical(
|
||||||
subtitle, "",
|
lipgloss.Center,
|
||||||
summary, "",
|
titleStyle.Render("确认信息(页6/6)"),
|
||||||
buttons, "",
|
formContent,
|
||||||
|
btnArea,
|
||||||
hint,
|
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 {
|
func successView() string {
|
||||||
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
successTitle.Render("Initialization Completed!"), "",
|
successTitle.Render("Initialization Completed!"), "",
|
||||||
successMsg.Render("System configuration has been saved, and the system is initializing..."), "",
|
successMsg.Render(
|
||||||
hintStyle.Render("Press any key to exit"),
|
"System configuration has been saved, and the system is initializing..."), "",
|
||||||
))
|
)
|
||||||
|
return appStyle.Render(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// quitView 退出视图
|
|
||||||
func quitView() string {
|
func quitView() string {
|
||||||
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
errorTitle.Render("Canceled"), "",
|
errorTitle.Render("Canceled"), "",
|
||||||
errorMsg.Render("Initialization canceled, no configuration saved"),
|
errorMsg.Render("Initialization canceled, no configuration saved"),
|
||||||
))
|
)
|
||||||
|
return appStyle.Render(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// errorView 错误视图
|
|
||||||
func errorView(err error) string {
|
func errorView(err error) string {
|
||||||
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
errorTitle.Render("Error"), "",
|
errorTitle.Render("Error"), "",
|
||||||
errorMsg.Render(err.Error()), "",
|
errorMsg.Render(err.Error()), "",
|
||||||
hintStyle.Render("Press Ctrl+C to exit"),
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import "github.com/charmbracelet/lipgloss"
|
|||||||
var (
|
var (
|
||||||
primaryColor = lipgloss.Color("#7C3AED")
|
primaryColor = lipgloss.Color("#7C3AED")
|
||||||
secondaryColor = lipgloss.Color("#10B981")
|
secondaryColor = lipgloss.Color("#10B981")
|
||||||
|
titleColor = lipgloss.Color("#8b19a2")
|
||||||
errorColor = lipgloss.Color("#EF4444")
|
errorColor = lipgloss.Color("#EF4444")
|
||||||
warnColor = lipgloss.Color("#F59E0B")
|
warnColor = lipgloss.Color("#F59E0B")
|
||||||
|
btnTextColor = lipgloss.Color("#666666") // 深灰色
|
||||||
|
btnbordColor = lipgloss.Color("#3b4147")
|
||||||
|
btnFocusColor = lipgloss.Color("#ffffff")
|
||||||
|
|
||||||
// 背景色设为无,让终端自己的背景色生效,避免黑块
|
// 背景色设为无,让终端自己的背景色生效,避免黑块
|
||||||
bgColor = lipgloss.Color("#1F2937")
|
bgColor = lipgloss.Color("#1F2937")
|
||||||
@@ -16,82 +20,92 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 容器样式
|
// 容器样式
|
||||||
var containerStyle = lipgloss.NewStyle().
|
var (
|
||||||
Padding(2, 4).
|
// 基础布局样式
|
||||||
|
appStyle = lipgloss.NewStyle().
|
||||||
|
Padding(1, 1).
|
||||||
BorderStyle(lipgloss.RoundedBorder()).
|
BorderStyle(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(primaryColor).
|
BorderForeground(primaryColor).
|
||||||
//Background(bgColor). // 注释掉背景色,防止在某些终端出现黑块
|
|
||||||
Foreground(textColor).
|
Foreground(textColor).
|
||||||
//Width(80).
|
Align(lipgloss.Center).
|
||||||
Align(lipgloss.Center)
|
Height(40)
|
||||||
|
|
||||||
// 标题样式
|
// 标题样式
|
||||||
var titleStyle = lipgloss.NewStyle().
|
titleStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(titleColor).
|
||||||
|
Padding(0, 1).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(primaryColor).
|
Align(lipgloss.Center)
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
var subTitleStyle = lipgloss.NewStyle().
|
// 子标题/标签样式
|
||||||
Foreground(mutedColor).
|
labelStyle = lipgloss.NewStyle().
|
||||||
MarginBottom(2)
|
Width(30).
|
||||||
|
Align(lipgloss.Right).
|
||||||
|
PaddingRight(2)
|
||||||
|
|
||||||
// 按钮样式
|
valueStyle = lipgloss.NewStyle().
|
||||||
var normalButton = lipgloss.NewStyle().
|
Foreground(textColor).
|
||||||
|
Width(50)
|
||||||
|
|
||||||
|
// 输入框/列表内容样式
|
||||||
|
inputBoxStyle = lipgloss.NewStyle().
|
||||||
|
BorderStyle(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(btnbordColor).
|
||||||
|
Padding(0, 1).
|
||||||
|
Width(50)
|
||||||
|
|
||||||
|
// 按钮基础样式
|
||||||
|
btnBaseStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(btnTextColor).
|
||||||
Padding(0, 2).
|
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)
|
Bold(true)
|
||||||
|
|
||||||
// 输入框样式
|
successMsg = lipgloss.NewStyle().
|
||||||
var inputBox = lipgloss.NewStyle().
|
|
||||||
BorderStyle(lipgloss.RoundedBorder()).
|
|
||||||
BorderForeground(primaryColor).
|
|
||||||
Padding(0, 1)
|
|
||||||
|
|
||||||
var labelStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(mutedColor).
|
|
||||||
Width(12).
|
|
||||||
Align(lipgloss.Right)
|
|
||||||
|
|
||||||
// 协议框样式
|
|
||||||
var agreementBox = lipgloss.NewStyle().
|
|
||||||
BorderStyle(lipgloss.RoundedBorder()).
|
|
||||||
BorderForeground(warnColor).
|
|
||||||
Padding(1, 2).
|
|
||||||
//Width(70).
|
|
||||||
Align(lipgloss.Left)
|
|
||||||
|
|
||||||
// 总结框样式
|
|
||||||
var summaryBox = lipgloss.NewStyle().
|
|
||||||
BorderStyle(lipgloss.DoubleBorder()).
|
|
||||||
BorderForeground(primaryColor).
|
|
||||||
Padding(0, 0).
|
|
||||||
Foreground(textColor)
|
Foreground(textColor)
|
||||||
|
|
||||||
// 进度条样式
|
errorTitle = lipgloss.NewStyle().
|
||||||
var progressStyle = lipgloss.NewStyle().Foreground(primaryColor)
|
|
||||||
|
|
||||||
// 提示信息样式
|
|
||||||
var hintStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(mutedColor).
|
|
||||||
Italic(true)
|
|
||||||
|
|
||||||
// 成功/错误样式
|
|
||||||
var successTitle = lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(secondaryColor)
|
|
||||||
|
|
||||||
var successMsg = lipgloss.NewStyle().
|
|
||||||
Foreground(textColor)
|
|
||||||
|
|
||||||
var errorTitle = lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(errorColor)
|
Foreground(errorColor)
|
||||||
|
|
||||||
var errorMsg = lipgloss.NewStyle().
|
errorMsg = lipgloss.NewStyle().
|
||||||
Foreground(textColor)
|
Foreground(textColor)
|
||||||
|
)
|
||||||
var infoStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(primaryColor).
|
|
||||||
Bold(true)
|
|
||||||
|
|||||||
@@ -2,20 +2,12 @@ package wizard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run 启动初始化向导
|
// Run 启动初始化向导
|
||||||
func Run(force bool) error {
|
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())
|
p := tea.NewProgram(initialModel())
|
||||||
@@ -27,20 +19,3 @@ func Run(force bool) error {
|
|||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user