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