add tui command

This commit is contained in:
2026-02-21 20:22:21 +08:00
parent fbe6aec707
commit ce9af9f7d0
11 changed files with 1194 additions and 239 deletions

333
pkg/wizard/model.go Normal file
View File

@@ -0,0 +1,333 @@
package wizard
import (
"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"`
DBName string `json:"db_name"`
DataAddress string `json:"data_address"`
// 公网设置
PublicInterface string `json:"public_interface"`
IPAddress string `json:"ip_address"`
Netmask string `json:"netmask"`
Gateway string `json:"gateway"`
// 内网配置
InternalInterface string `json:"internal_interface"`
InternalIP string `json:"internal_ip"`
InternalMask string `json:"internal_mask"`
// 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 {
config Config
currentPage PageType
totalPages int
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
}
// defaultConfig 返回默认配置
func defaultConfig() Config {
return Config{
Hostname: "sunhpc01",
Country: "China",
Region: "Beijing",
Timezone: "Asia/Shanghai",
HomePage: "https://sunhpc.example.com",
DBAddress: "127.0.0.1",
DBName: "sunhpc_db",
DataAddress: "/data/sunhpc",
PublicInterface: "eth0",
InternalInterface: "eth1",
IPAddress: "192.168.1.100",
Netmask: "255.255.255.0",
Gateway: "192.168.1.1",
InternalIP: "10.0.0.100",
InternalMask: "255.255.255.0",
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
}