Tui fix focus
This commit is contained in:
1
go.mod
1
go.mod
@@ -20,6 +20,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -1,5 +1,7 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
|||||||
@@ -1,162 +1,226 @@
|
|||||||
package wizard
|
package wizard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"errors"
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"sunhpc/pkg/database"
|
"sunhpc/pkg/database"
|
||||||
|
"sunhpc/pkg/logger"
|
||||||
"sunhpc/pkg/utils"
|
"sunhpc/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 配置项映射:定义每个配置项对应的表名、键名
|
// 配置项映射:定义每个配置项对应的表名、键名
|
||||||
var configMappings = []struct {
|
type ConfigMapping struct {
|
||||||
table string
|
Title string `toml:"title"`
|
||||||
key string
|
Base struct {
|
||||||
getVal func(m *model) interface{} // 动态获取配置值的函数
|
ClusterName string `toml:"cluster_name"`
|
||||||
|
Country string `toml:"country"`
|
||||||
|
State string `toml:"state"`
|
||||||
|
City string `toml:"city"`
|
||||||
|
HomePage string `toml:"homepage"`
|
||||||
|
Contact string `toml:"contact"`
|
||||||
|
License string `toml:"license"`
|
||||||
|
BaseDir string `toml:"base_dir"`
|
||||||
|
WorkDir string `toml:"work_dir"`
|
||||||
|
DistroDir string `toml:"distro_dir"`
|
||||||
|
Partition string `toml:"partition"`
|
||||||
|
Distribution string `toml:"distribution"`
|
||||||
|
Timezone string `toml:"timezone"`
|
||||||
|
SafePort string `toml:"safe_port"`
|
||||||
|
SafeDirs string `toml:"safe_dirs"`
|
||||||
|
SafeSecurity string `toml:"safe_security"`
|
||||||
|
PluginDirs string `toml:"plugin_dirs"`
|
||||||
|
PluginPort string `toml:"plugin_port"`
|
||||||
|
GangliaAddr string `toml:"ganglia_addr"`
|
||||||
|
} `toml:"base"`
|
||||||
|
Pxelinux struct {
|
||||||
|
NextServer string `toml:"next_server"`
|
||||||
|
PxeFilename string `toml:"pxe_filename"`
|
||||||
|
PxeLinuxDir string `toml:"pxelinux_dir"`
|
||||||
|
BootArgs string `toml:"boot_args"`
|
||||||
|
} `toml:"pxelinux"`
|
||||||
|
Public struct {
|
||||||
|
PublicHostname string `toml:"public_hostname"`
|
||||||
|
PublicInterface string `toml:"public_interface"`
|
||||||
|
PublicAddress string `toml:"public_address"`
|
||||||
|
PublicNetmask string `toml:"public_netmask"`
|
||||||
|
PublicGateway string `toml:"public_gateway"`
|
||||||
|
PublicNetwork string `toml:"public_network"`
|
||||||
|
PublicDomain string `toml:"public_domain"`
|
||||||
|
PublicCIDR string `toml:"public_cidr"`
|
||||||
|
PublicDNS string `toml:"public_dns"`
|
||||||
|
PublicMac string `toml:"public_mac"`
|
||||||
|
PublicMTU string `toml:"public_mtu"`
|
||||||
|
PublicNTP string `toml:"public_ntp"`
|
||||||
|
} `toml:"public"`
|
||||||
|
Private struct {
|
||||||
|
PrivateHostname string `toml:"private_hostname"`
|
||||||
|
PrivateInterface string `toml:"private_interface"`
|
||||||
|
PrivateAddress string `toml:"private_address"`
|
||||||
|
PrivateNetmask string `toml:"private_netmask"`
|
||||||
|
PrivateNetwork string `toml:"private_network"`
|
||||||
|
PrivateDomain string `toml:"private_domain"`
|
||||||
|
PrivateCIDR string `toml:"private_cidr"`
|
||||||
|
PrivateMac string `toml:"private_mac"`
|
||||||
|
PrivateMTU string `toml:"private_mtu"`
|
||||||
|
} `toml:"private"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigWithDefault() *ConfigMapping {
|
||||||
|
return &ConfigMapping{
|
||||||
|
Title: "Cluster Configuration",
|
||||||
|
Base: struct {
|
||||||
|
ClusterName string `toml:"cluster_name"`
|
||||||
|
Country string `toml:"country"`
|
||||||
|
State string `toml:"state"`
|
||||||
|
City string `toml:"city"`
|
||||||
|
HomePage string `toml:"homepage"`
|
||||||
|
Contact string `toml:"contact"`
|
||||||
|
License string `toml:"license"`
|
||||||
|
BaseDir string `toml:"base_dir"`
|
||||||
|
WorkDir string `toml:"work_dir"`
|
||||||
|
DistroDir string `toml:"distro_dir"`
|
||||||
|
Partition string `toml:"partition"`
|
||||||
|
Distribution string `toml:"distribution"`
|
||||||
|
Timezone string `toml:"timezone"`
|
||||||
|
SafePort string `toml:"safe_port"`
|
||||||
|
SafeDirs string `toml:"safe_dirs"`
|
||||||
|
SafeSecurity string `toml:"safe_security"`
|
||||||
|
PluginDirs string `toml:"plugin_dirs"`
|
||||||
|
PluginPort string `toml:"plugin_port"`
|
||||||
|
GangliaAddr string `toml:"ganglia_addr"`
|
||||||
}{
|
}{
|
||||||
// attributes 表
|
ClusterName: "SunHPC_Cluster",
|
||||||
{"attributes", "license", func(m *model) any { return m.config.License }},
|
Country: "CN",
|
||||||
{"attributes", "accepted", func(m *model) any { return m.config.AgreementAccepted }},
|
State: "Beijing",
|
||||||
{"attributes", "country", func(m *model) any { return m.config.Country }},
|
City: "Beijing",
|
||||||
{"attributes", "region", func(m *model) any { return m.config.Region }},
|
HomePage: "https://www.sunhpc.com",
|
||||||
{"attributes", "timezone", func(m *model) any { return m.config.Timezone }},
|
Contact: "admin@sunhpc.com",
|
||||||
{"attributes", "homepage", func(m *model) any { return m.config.HomePage }},
|
License: "MIT",
|
||||||
{"attributes", "dbaddress", func(m *model) any { return m.config.DBAddress }},
|
BaseDir: "install",
|
||||||
{"attributes", "software", func(m *model) any { return m.config.Software }},
|
WorkDir: "/export",
|
||||||
|
DistroDir: "/export/sunhpc",
|
||||||
|
Partition: "default",
|
||||||
|
Distribution: "sunhpc-dist",
|
||||||
|
Timezone: "Asia/Shanghai",
|
||||||
|
SafePort: "372",
|
||||||
|
SafeDirs: "safe.d",
|
||||||
|
SafeSecurity: "safe-security",
|
||||||
|
PluginDirs: "/etc/sunhpc/plugin",
|
||||||
|
PluginPort: "12123",
|
||||||
|
GangliaAddr: "224.0.0.3",
|
||||||
|
},
|
||||||
|
Pxelinux: struct {
|
||||||
|
NextServer string `toml:"next_server"`
|
||||||
|
PxeFilename string `toml:"pxe_filename"`
|
||||||
|
PxeLinuxDir string `toml:"pxelinux_dir"`
|
||||||
|
BootArgs string `toml:"boot_args"`
|
||||||
|
}{
|
||||||
|
NextServer: "192.168.1.1",
|
||||||
|
PxeFilename: "pxelinux.0",
|
||||||
|
PxeLinuxDir: "/tftpboot/pxelinux",
|
||||||
|
BootArgs: "net.ifnames=0 biosdevname=0",
|
||||||
|
},
|
||||||
|
Public: struct {
|
||||||
|
PublicHostname string `toml:"public_hostname"`
|
||||||
|
PublicInterface string `toml:"public_interface"`
|
||||||
|
PublicAddress string `toml:"public_address"`
|
||||||
|
PublicNetmask string `toml:"public_netmask"`
|
||||||
|
PublicGateway string `toml:"public_gateway"`
|
||||||
|
PublicNetwork string `toml:"public_network"`
|
||||||
|
PublicDomain string `toml:"public_domain"`
|
||||||
|
PublicCIDR string `toml:"public_cidr"`
|
||||||
|
PublicDNS string `toml:"public_dns"`
|
||||||
|
PublicMac string `toml:"public_mac"`
|
||||||
|
PublicMTU string `toml:"public_mtu"`
|
||||||
|
PublicNTP string `toml:"public_ntp"`
|
||||||
|
}{
|
||||||
|
PublicHostname: "cluster.hpc.org",
|
||||||
|
PublicInterface: "eth0",
|
||||||
|
PublicAddress: "",
|
||||||
|
PublicNetmask: "",
|
||||||
|
PublicGateway: "",
|
||||||
|
PublicNetwork: "",
|
||||||
|
PublicDomain: "hpc.org",
|
||||||
|
PublicCIDR: "",
|
||||||
|
PublicDNS: "",
|
||||||
|
PublicMac: "00:11:22:33:44:55",
|
||||||
|
PublicMTU: "1500",
|
||||||
|
PublicNTP: "pool.ntp.org",
|
||||||
|
},
|
||||||
|
Private: struct {
|
||||||
|
PrivateHostname string `toml:"private_hostname"`
|
||||||
|
PrivateInterface string `toml:"private_interface"`
|
||||||
|
PrivateAddress string `toml:"private_address"`
|
||||||
|
PrivateNetmask string `toml:"private_netmask"`
|
||||||
|
PrivateNetwork string `toml:"private_network"`
|
||||||
|
PrivateDomain string `toml:"private_domain"`
|
||||||
|
PrivateCIDR string `toml:"private_cidr"`
|
||||||
|
PrivateMac string `toml:"private_mac"`
|
||||||
|
PrivateMTU string `toml:"private_mtu"`
|
||||||
|
}{
|
||||||
|
PrivateHostname: "sunhpc",
|
||||||
|
PrivateInterface: "eth1",
|
||||||
|
PrivateAddress: "172.16.9.254",
|
||||||
|
PrivateNetmask: "255.255.255.0",
|
||||||
|
PrivateNetwork: "172.16.9.0",
|
||||||
|
PrivateDomain: "example.com",
|
||||||
|
PrivateCIDR: "172.16.9.0/24",
|
||||||
|
PrivateMac: "00:11:22:33:44:66",
|
||||||
|
PrivateMTU: "1500",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// nodes 表
|
func loadConfig() (*ConfigMapping, error) {
|
||||||
{"nodes", "name", func(m *model) any { return m.config.Hostname }},
|
configs := NewConfigWithDefault()
|
||||||
|
cfgfile := "/etc/sunhpc/config.toml"
|
||||||
|
|
||||||
// 公网设置表
|
// 尝试解析配置文件
|
||||||
{"public_network", "public_interface", func(m *model) any { return m.config.PublicInterface }},
|
if _, err := toml.DecodeFile(cfgfile, configs); err != nil {
|
||||||
{"public_network", "ip_address", func(m *model) any { return m.config.PublicIPAddress }},
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
{"public_network", "netmask", func(m *model) any { return m.config.PublicNetmask }},
|
// 文件不存在,直接返回默认配置
|
||||||
{"public_network", "gateway", func(m *model) any { return m.config.PublicGateway }},
|
logger.Infof("Config file %s not exist, use default config", cfgfile)
|
||||||
// 内网配置表
|
return configs, nil
|
||||||
{"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 }},
|
logger.Debugf("[DEBUG] Parse config file %s failed: %v", cfgfile, err)
|
||||||
// DNS配置表
|
return nil, err
|
||||||
{"dns_config", "dns_primary", func(m *model) any { return m.config.DNSPrimary }},
|
}
|
||||||
{"dns_config", "dns_secondary", func(m *model) any { return m.config.DNSSecondary }},
|
|
||||||
|
logger.Infof("Load config file %s success", cfgfile)
|
||||||
|
return configs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveConfig 入口函数:保存所有配置到数据库
|
// saveConfig 入口函数:保存所有配置到数据库
|
||||||
func (m *model) saveConfig() error {
|
func (m *model) saveConfig() error {
|
||||||
|
|
||||||
conn, err := database.GetDB() // 假设database包已实现getDB()获取连接
|
conn, err := database.GetDB() // 假设database包已实现getDB()获取连接
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("获取数据库连接失败: %w", err)
|
logger.Errorf("Database connection failed: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
m.force = false // 初始化全量覆盖标识
|
m.force = false // 初始化全量覆盖标识
|
||||||
|
|
||||||
// 遍历所有配置项,逐个处理
|
config, err := loadConfig()
|
||||||
for _, item := range configMappings {
|
|
||||||
val := item.getVal(m)
|
|
||||||
exists, err := m.checkExists(conn, item.table, item.key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("检查%s.%s是否存在失败: %w", item.table, item.key, err)
|
logger.Debugf("[DEBUG] Load config file failed: %v", 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// 表不存在也视为"不存在"(可选:根据实际需求调整,比如先建表)
|
|
||||||
if strings.Contains(err.Error(), "table not found") {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return count > 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// askUserChoice 询问用户操作选择
|
t_value := m.config.PrivateIPAddress
|
||||||
func (m *model) askUserChoice(table, key string) (string, error) {
|
c_value := config.Private.PrivateAddress
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
fmt.Printf("配置项%s.%s已存在,选择操作(y/yes=覆盖, n/no=跳过, a/all=全量覆盖后续所有): ", table, key)
|
logger.Debugf("t_value: %s\n", t_value)
|
||||||
input, err := reader.ReadString('\n')
|
logger.Debugf("c_value: %s\n", c_value)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
logger.Debugf("config.Base.ClusterName: %s\n", config.Base.ClusterName)
|
||||||
}
|
|
||||||
// 去除空格和换行
|
return nil
|
||||||
return strings.TrimSpace(input), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取系统网络接口
|
// 获取系统网络接口
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ func NewTextInput(placeholder string, defaultValue string) *TextInput {
|
|||||||
ti := textinput.New()
|
ti := textinput.New()
|
||||||
ti.Placeholder = placeholder
|
ti.Placeholder = placeholder
|
||||||
ti.SetValue(defaultValue)
|
ti.SetValue(defaultValue)
|
||||||
ti.Focus()
|
ti.Blur()
|
||||||
return &TextInput{Model: ti, focused: true}
|
return &TextInput{Model: ti, focused: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TextInput) Focus() tea.Cmd {
|
func (t *TextInput) Focus() tea.Cmd {
|
||||||
@@ -135,9 +135,14 @@ func NewFocusManager(loop bool) *FocusManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register 注册可聚焦组件(指定标识和切换顺序)
|
/*
|
||||||
|
Register 注册可聚焦组件(指定标识和切换顺序)
|
||||||
|
ID 组件的唯一标识,用于后续切换和获取焦点
|
||||||
|
例如 "form1.ip_input"、"form1.next_btn"
|
||||||
|
*/
|
||||||
func (fm *FocusManager) Register(id string, comp Focusable) {
|
func (fm *FocusManager) Register(id string, comp Focusable) {
|
||||||
// 防御性检查:避免 components 为空导致 panic
|
|
||||||
|
// 防御性检查:避免 components 未初始化为nil导致 panic
|
||||||
if fm.components == nil {
|
if fm.components == nil {
|
||||||
fm.components = make(map[string]Focusable)
|
fm.components = make(map[string]Focusable)
|
||||||
}
|
}
|
||||||
@@ -147,6 +152,8 @@ func (fm *FocusManager) Register(id string, comp Focusable) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// id : accept_btn, form1.reject_btn
|
||||||
|
// comp: 接受协议按钮, 拒绝协议按钮
|
||||||
fm.components[id] = comp
|
fm.components[id] = comp
|
||||||
fm.order = append(fm.order, id)
|
fm.order = append(fm.order, id)
|
||||||
|
|
||||||
@@ -211,6 +218,7 @@ func (fm *FocusManager) Prev() tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//fm.components[fm.currentFocusID].Blur()
|
||||||
fm.components[fm.currentFocusID].Blur()
|
fm.components[fm.currentFocusID].Blur()
|
||||||
prevID := fm.order[prevIdx]
|
prevID := fm.order[prevIdx]
|
||||||
fm.currentFocusID = prevID
|
fm.currentFocusID = prevID
|
||||||
@@ -259,23 +267,22 @@ func (m *model) initPageFocus(page PageType) {
|
|||||||
|
|
||||||
m.focusManager = NewFocusManager(true)
|
m.focusManager = NewFocusManager(true)
|
||||||
|
|
||||||
// 获取当前页面的组件
|
|
||||||
pageComps, exists := m.pageComponents[page]
|
pageComps, exists := m.pageComponents[page]
|
||||||
if !exists {
|
if !exists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按 [业务逻辑顺序] 注册组件 (决定Tab切换的顺序)
|
|
||||||
var componentOrder []string
|
var componentOrder []string
|
||||||
|
var defaultFocusID string
|
||||||
|
|
||||||
// 按页面类型定义不同的注册顺序
|
|
||||||
switch page {
|
switch page {
|
||||||
case PageAgreement:
|
case PageAgreement:
|
||||||
componentOrder = []string{"accept_btn", "reject_btn"}
|
componentOrder = []string{"accept_btn", "reject_btn"}
|
||||||
|
defaultFocusID = "accept_btn"
|
||||||
case PageData:
|
case PageData:
|
||||||
componentOrder = []string{
|
componentOrder = []string{
|
||||||
"Homepage_input",
|
"Homepage_input",
|
||||||
"Hostname_input",
|
"ClusterName_input",
|
||||||
"Country_input",
|
"Country_input",
|
||||||
"Region_input",
|
"Region_input",
|
||||||
"Timezone_input",
|
"Timezone_input",
|
||||||
@@ -284,23 +291,28 @@ func (m *model) initPageFocus(page PageType) {
|
|||||||
"next_btn",
|
"next_btn",
|
||||||
"prev_btn",
|
"prev_btn",
|
||||||
}
|
}
|
||||||
|
defaultFocusID = "next_btn"
|
||||||
case PagePublicNetwork:
|
case PagePublicNetwork:
|
||||||
componentOrder = []string{
|
componentOrder = []string{
|
||||||
"PublicInterface_input",
|
"PublicInterface_input",
|
||||||
"PublicIPAddress_input",
|
"PublicIPAddress_input",
|
||||||
"PublicNetmask_input",
|
"PublicNetmask_input",
|
||||||
"PublicGateway_input",
|
"PublicGateway_input",
|
||||||
|
"PublicMTU_input",
|
||||||
"next_btn",
|
"next_btn",
|
||||||
"prev_btn",
|
"prev_btn",
|
||||||
}
|
}
|
||||||
|
defaultFocusID = "next_btn"
|
||||||
case PageInternalNetwork:
|
case PageInternalNetwork:
|
||||||
componentOrder = []string{
|
componentOrder = []string{
|
||||||
"InternalInterface_input",
|
"PrivateInterface_input",
|
||||||
"InternalIPAddress_input",
|
"PrivateIPAddress_input",
|
||||||
"InternalNetmask_input",
|
"PrivateNetmask_input",
|
||||||
|
"PrivateMTU_input",
|
||||||
"next_btn",
|
"next_btn",
|
||||||
"prev_btn",
|
"prev_btn",
|
||||||
}
|
}
|
||||||
|
defaultFocusID = "next_btn"
|
||||||
case PageDNS:
|
case PageDNS:
|
||||||
componentOrder = []string{
|
componentOrder = []string{
|
||||||
"Pri_DNS_input",
|
"Pri_DNS_input",
|
||||||
@@ -308,14 +320,25 @@ func (m *model) initPageFocus(page PageType) {
|
|||||||
"next_btn",
|
"next_btn",
|
||||||
"prev_btn",
|
"prev_btn",
|
||||||
}
|
}
|
||||||
|
defaultFocusID = "next_btn"
|
||||||
case PageSummary:
|
case PageSummary:
|
||||||
componentOrder = []string{"confirm_btn", "cancel_btn"}
|
componentOrder = []string{"confirm_btn", "cancel_btn"}
|
||||||
|
defaultFocusID = "confirm_btn"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册组件到焦点管理器(按顺序)
|
|
||||||
for _, compID := range componentOrder {
|
for _, compID := range componentOrder {
|
||||||
if comp, exists := pageComps[compID]; exists {
|
if comp, exists := pageComps[compID]; exists {
|
||||||
m.focusManager.Register(compID, comp)
|
m.focusManager.Register(compID, comp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if defaultFocusID != "" {
|
||||||
|
if currentComp, exists := m.focusManager.GetCurrent(); exists {
|
||||||
|
currentComp.Blur()
|
||||||
|
}
|
||||||
|
if targetComp, exists := pageComps[defaultFocusID]; exists {
|
||||||
|
m.focusManager.currentFocusID = defaultFocusID
|
||||||
|
targetComp.Focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Config struct {
|
|||||||
AgreementAccepted bool `json:"agreement_accepted"`
|
AgreementAccepted bool `json:"agreement_accepted"`
|
||||||
|
|
||||||
// 数据接收
|
// 数据接收
|
||||||
Hostname string `json:"hostname"`
|
ClusterName string `json:"cluster_name"`
|
||||||
Country string `json:"country"`
|
Country string `json:"country"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
@@ -33,11 +33,13 @@ type Config struct {
|
|||||||
PublicIPAddress string `json:"ip_address"`
|
PublicIPAddress string `json:"ip_address"`
|
||||||
PublicNetmask string `json:"netmask"`
|
PublicNetmask string `json:"netmask"`
|
||||||
PublicGateway string `json:"gateway"`
|
PublicGateway string `json:"gateway"`
|
||||||
|
PublicMTU string `json:"public_mtu"`
|
||||||
|
|
||||||
// 内网配置
|
// 内网配置
|
||||||
InternalInterface string `json:"internal_interface"`
|
PrivateInterface string `json:"private_interface"`
|
||||||
InternalIPAddress string `json:"internal_ip"`
|
PrivateIPAddress string `json:"private_ip"`
|
||||||
InternalNetmask string `json:"internal_mask"`
|
PrivateNetmask string `json:"private_mask"`
|
||||||
|
PrivateMTU string `json:"private_mtu"`
|
||||||
|
|
||||||
// DNS 配置
|
// DNS 配置
|
||||||
DNSPrimary string `json:"dns_primary"`
|
DNSPrimary string `json:"dns_primary"`
|
||||||
@@ -97,7 +99,7 @@ func defaultConfig() Config {
|
|||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
License: "This test license is for testing purposes only. Do not use it in production.",
|
License: "This test license is for testing purposes only. Do not use it in production.",
|
||||||
Hostname: "cluster.hpc.org",
|
ClusterName: "cluster.hpc.org",
|
||||||
Country: "China",
|
Country: "China",
|
||||||
Region: "Beijing",
|
Region: "Beijing",
|
||||||
Timezone: "Asia/Shanghai",
|
Timezone: "Asia/Shanghai",
|
||||||
@@ -108,9 +110,10 @@ func defaultConfig() Config {
|
|||||||
PublicIPAddress: "",
|
PublicIPAddress: "",
|
||||||
PublicNetmask: "",
|
PublicNetmask: "",
|
||||||
PublicGateway: "",
|
PublicGateway: "",
|
||||||
InternalInterface: defaultInternalInterface,
|
PrivateInterface: defaultInternalInterface,
|
||||||
InternalIPAddress: "172.16.9.254",
|
PrivateIPAddress: "172.16.9.254",
|
||||||
InternalNetmask: "255.255.255.0",
|
PrivateNetmask: "255.255.255.0",
|
||||||
|
PrivateMTU: "1500",
|
||||||
DNSPrimary: "8.8.8.8",
|
DNSPrimary: "8.8.8.8",
|
||||||
DNSSecondary: "8.8.4.4",
|
DNSSecondary: "8.8.4.4",
|
||||||
}
|
}
|
||||||
@@ -132,7 +135,7 @@ func initialModel() model {
|
|||||||
// ------------------ 页面2:基础信息页面 --------------------
|
// ------------------ 页面2:基础信息页面 --------------------
|
||||||
page2Comps := make(map[string]Focusable)
|
page2Comps := make(map[string]Focusable)
|
||||||
page2Comps["Homepage_input"] = NewTextInput("Homepage", cfg.HomePage)
|
page2Comps["Homepage_input"] = NewTextInput("Homepage", cfg.HomePage)
|
||||||
page2Comps["Hostname_input"] = NewTextInput("Hostname", cfg.Hostname)
|
page2Comps["ClusterName_input"] = NewTextInput("ClusterName", cfg.ClusterName)
|
||||||
page2Comps["Country_input"] = NewTextInput("Country", cfg.Country)
|
page2Comps["Country_input"] = NewTextInput("Country", cfg.Country)
|
||||||
page2Comps["Region_input"] = NewTextInput("Region", cfg.Region)
|
page2Comps["Region_input"] = NewTextInput("Region", cfg.Region)
|
||||||
page2Comps["Timezone_input"] = NewTextInput("Timezone", cfg.Timezone)
|
page2Comps["Timezone_input"] = NewTextInput("Timezone", cfg.Timezone)
|
||||||
@@ -148,15 +151,17 @@ func initialModel() model {
|
|||||||
page3Comps["PublicIPAddress_input"] = NewTextInput("PublicIPAddress", cfg.PublicIPAddress)
|
page3Comps["PublicIPAddress_input"] = NewTextInput("PublicIPAddress", cfg.PublicIPAddress)
|
||||||
page3Comps["PublicNetmask_input"] = NewTextInput("PublicNetmask", cfg.PublicNetmask)
|
page3Comps["PublicNetmask_input"] = NewTextInput("PublicNetmask", cfg.PublicNetmask)
|
||||||
page3Comps["PublicGateway_input"] = NewTextInput("PublicGateway", cfg.PublicGateway)
|
page3Comps["PublicGateway_input"] = NewTextInput("PublicGateway", cfg.PublicGateway)
|
||||||
|
page3Comps["PublicMTU_input"] = NewTextInput("PublicMTU", cfg.PublicMTU)
|
||||||
page3Comps["next_btn"] = NewButton("下一步")
|
page3Comps["next_btn"] = NewButton("下一步")
|
||||||
page3Comps["prev_btn"] = NewButton("上一步")
|
page3Comps["prev_btn"] = NewButton("上一步")
|
||||||
pageComponents[PagePublicNetwork] = page3Comps
|
pageComponents[PagePublicNetwork] = page3Comps
|
||||||
|
|
||||||
// ------------------ 页面4:内网网络页面 --------------------
|
// ------------------ 页面4:内网网络页面 --------------------
|
||||||
page4Comps := make(map[string]Focusable)
|
page4Comps := make(map[string]Focusable)
|
||||||
page4Comps["InternalInterface_input"] = NewTextInput("InternalInterface", cfg.InternalInterface)
|
page4Comps["PrivateInterface_input"] = NewTextInput("PrivateInterface", cfg.PrivateInterface)
|
||||||
page4Comps["InternalIPAddress_input"] = NewTextInput("InternalIPAddress", cfg.InternalIPAddress)
|
page4Comps["PrivateIPAddress_input"] = NewTextInput("PrivateIPAddress", cfg.PrivateIPAddress)
|
||||||
page4Comps["InternalNetmask_input"] = NewTextInput("InternalNetmask", cfg.InternalNetmask)
|
page4Comps["PrivateNetmask_input"] = NewTextInput("PrivateNetmask", cfg.PrivateNetmask)
|
||||||
|
page4Comps["PrivateMTU_input"] = NewTextInput("PrivateMTU", cfg.PrivateMTU)
|
||||||
page4Comps["next_btn"] = NewButton("下一步")
|
page4Comps["next_btn"] = NewButton("下一步")
|
||||||
page4Comps["prev_btn"] = NewButton("上一步")
|
page4Comps["prev_btn"] = NewButton("上一步")
|
||||||
pageComponents[PageInternalNetwork] = page4Comps
|
pageComponents[PageInternalNetwork] = page4Comps
|
||||||
@@ -199,11 +204,13 @@ 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) {
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
m.quitting = true
|
m.saveConfig()
|
||||||
|
//m.quitting = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
|
||||||
// 1. 焦点切换(Tab/Shift+Tab)交给管理器处理
|
// 1. 焦点切换(Tab/Shift+Tab)交给管理器处理
|
||||||
@@ -213,6 +220,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
// 2. 回车键:处理当前焦点组件的点击/确认
|
// 2. 回车键:处理当前焦点组件的点击/确认
|
||||||
case "enter":
|
case "enter":
|
||||||
|
|
||||||
currentCompID := m.focusManager.currentFocusID
|
currentCompID := m.focusManager.currentFocusID
|
||||||
switch currentCompID {
|
switch currentCompID {
|
||||||
// 页1:accept → 进入页2
|
// 页1:accept → 进入页2
|
||||||
@@ -232,7 +240,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
// 页6:确认配置 → 退出并保存
|
// 页6:确认配置 → 退出并保存
|
||||||
case "confirm_btn":
|
case "confirm_btn":
|
||||||
m.done = true
|
m.done = true
|
||||||
m.quitting = true
|
//m.quitting = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
|
||||||
case "cancel_btn":
|
case "cancel_btn":
|
||||||
@@ -258,8 +266,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
// 页2:基础信息
|
// 页2:基础信息
|
||||||
case "Homepage_input":
|
case "Homepage_input":
|
||||||
m.config.HomePage = comp.Value()
|
m.config.HomePage = comp.Value()
|
||||||
case "Hostname_input":
|
case "ClusterName_input":
|
||||||
m.config.Hostname = comp.Value()
|
m.config.ClusterName = comp.Value()
|
||||||
case "Country_input":
|
case "Country_input":
|
||||||
m.config.Country = comp.Value()
|
m.config.Country = comp.Value()
|
||||||
case "Region_input":
|
case "Region_input":
|
||||||
@@ -280,14 +288,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.config.PublicNetmask = comp.Value()
|
m.config.PublicNetmask = comp.Value()
|
||||||
case "PublicGateway_input":
|
case "PublicGateway_input":
|
||||||
m.config.PublicGateway = comp.Value()
|
m.config.PublicGateway = comp.Value()
|
||||||
|
case "PublicMTU_input":
|
||||||
|
m.config.PublicMTU = comp.Value()
|
||||||
|
|
||||||
// 页4:内网网络
|
// 页4:内网网络
|
||||||
case "InternalInterface_input":
|
case "PrivateInterface_input":
|
||||||
m.config.InternalInterface = comp.Value()
|
m.config.PrivateInterface = comp.Value()
|
||||||
case "InternalIPAddress_input":
|
case "PrivateIPAddress_input":
|
||||||
m.config.InternalIPAddress = comp.Value()
|
m.config.PrivateIPAddress = comp.Value()
|
||||||
case "InternalNetmask_input":
|
case "PrivateNetmask_input":
|
||||||
m.config.InternalNetmask = comp.Value()
|
m.config.PrivateNetmask = comp.Value()
|
||||||
|
case "PrivateMTU_input":
|
||||||
|
m.config.PrivateMTU = comp.Value()
|
||||||
|
|
||||||
// 页5:DNS
|
// 页5:DNS
|
||||||
case "Pri_DNS_input":
|
case "Pri_DNS_input":
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func renderDataInfoPage(m model) string {
|
|||||||
split_line,
|
split_line,
|
||||||
makeRow("Homepage", pageComps["Homepage_input"].View()),
|
makeRow("Homepage", pageComps["Homepage_input"].View()),
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("Hostname", pageComps["Hostname_input"].View()),
|
makeRow("ClusterName", pageComps["ClusterName_input"].View()),
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("Country", pageComps["Country_input"].View()),
|
makeRow("Country", pageComps["Country_input"].View()),
|
||||||
split_line,
|
split_line,
|
||||||
@@ -152,6 +152,8 @@ func renderPublicNetworkPage(m model) string {
|
|||||||
split_line,
|
split_line,
|
||||||
makeRow("PublicGateway", pageComps["PublicGateway_input"].View()),
|
makeRow("PublicGateway", pageComps["PublicGateway_input"].View()),
|
||||||
split_line,
|
split_line,
|
||||||
|
makeRow("PublicMTU", pageComps["PublicMTU_input"].View()),
|
||||||
|
split_line,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 按钮区域
|
// 按钮区域
|
||||||
@@ -186,11 +188,13 @@ func renderInternalNetworkPage(m model) string {
|
|||||||
// 拼接内网网络表单
|
// 拼接内网网络表单
|
||||||
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("InternalInterface", pageComps["InternalInterface_input"].View()),
|
makeRow("PrivateInterface", pageComps["PrivateInterface_input"].View()),
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("InternalIPAddress", pageComps["InternalIPAddress_input"].View()),
|
makeRow("PrivateIPAddress", pageComps["PrivateIPAddress_input"].View()),
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("InternalNetmask", pageComps["InternalNetmask_input"].View()),
|
makeRow("PrivateNetmask", pageComps["PrivateNetmask_input"].View()),
|
||||||
|
split_line,
|
||||||
|
makeRow("PrivateMTU", pageComps["PrivateMTU_input"].View()),
|
||||||
split_line,
|
split_line,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -254,7 +258,7 @@ func renderSummaryPage(m model) string {
|
|||||||
// 拼接 Summary 表单
|
// 拼接 Summary 表单
|
||||||
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("Hostname", m.config.Hostname),
|
makeRow("ClusterName", m.config.ClusterName),
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("Country", m.config.Country),
|
makeRow("Country", m.config.Country),
|
||||||
split_line,
|
split_line,
|
||||||
@@ -276,11 +280,13 @@ func renderSummaryPage(m model) string {
|
|||||||
split_line,
|
split_line,
|
||||||
makeRow("PublicGateway", m.config.PublicGateway),
|
makeRow("PublicGateway", m.config.PublicGateway),
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("InternalInterface", m.config.InternalInterface),
|
makeRow("PrivateInterface", m.config.PrivateInterface),
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("InternalIPAddress", m.config.InternalIPAddress),
|
makeRow("PrivateIPAddress", m.config.PrivateIPAddress),
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("InternalNetmask", m.config.InternalNetmask),
|
makeRow("PrivateNetmask", m.config.PrivateNetmask),
|
||||||
|
split_line,
|
||||||
|
makeRow("PrivateMTU", m.config.PrivateMTU),
|
||||||
split_line,
|
split_line,
|
||||||
makeRow("Pri DNS", m.config.DNSPrimary),
|
makeRow("Pri DNS", m.config.DNSPrimary),
|
||||||
split_line,
|
split_line,
|
||||||
|
|||||||
Reference in New Issue
Block a user