2026-02-27 22:52:15 +08:00
|
|
|
|
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)
|
2026-02-28 19:29:17 +08:00
|
|
|
|
ti.Blur()
|
|
|
|
|
|
return &TextInput{Model: ti, focused: false}
|
2026-02-27 22:52:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 19:29:17 +08:00
|
|
|
|
/*
|
|
|
|
|
|
Register 注册可聚焦组件(指定标识和切换顺序)
|
|
|
|
|
|
ID 组件的唯一标识,用于后续切换和获取焦点
|
|
|
|
|
|
例如 "form1.ip_input"、"form1.next_btn"
|
|
|
|
|
|
*/
|
2026-02-27 22:52:15 +08:00
|
|
|
|
func (fm *FocusManager) Register(id string, comp Focusable) {
|
2026-02-28 19:29:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 防御性检查:避免 components 未初始化为nil导致 panic
|
2026-02-27 22:52:15 +08:00
|
|
|
|
if fm.components == nil {
|
|
|
|
|
|
fm.components = make(map[string]Focusable)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 避免重复注册
|
|
|
|
|
|
if _, exists := fm.components[id]; exists {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 19:29:17 +08:00
|
|
|
|
// id : accept_btn, form1.reject_btn
|
|
|
|
|
|
// comp: 接受协议按钮, 拒绝协议按钮
|
2026-02-27 22:52:15 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 19:29:17 +08:00
|
|
|
|
//fm.components[fm.currentFocusID].Blur()
|
2026-02-27 22:52:15 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var componentOrder []string
|
2026-02-28 19:29:17 +08:00
|
|
|
|
var defaultFocusID string
|
2026-02-27 22:52:15 +08:00
|
|
|
|
|
|
|
|
|
|
switch page {
|
|
|
|
|
|
case PageAgreement:
|
|
|
|
|
|
componentOrder = []string{"accept_btn", "reject_btn"}
|
2026-02-28 19:29:17 +08:00
|
|
|
|
defaultFocusID = "accept_btn"
|
2026-02-27 22:52:15 +08:00
|
|
|
|
case PageData:
|
|
|
|
|
|
componentOrder = []string{
|
|
|
|
|
|
"Homepage_input",
|
2026-02-28 19:29:17 +08:00
|
|
|
|
"ClusterName_input",
|
2026-02-27 22:52:15 +08:00
|
|
|
|
"Country_input",
|
2026-03-06 00:29:34 +08:00
|
|
|
|
"State_input",
|
|
|
|
|
|
"City_input",
|
|
|
|
|
|
"Contact_input",
|
2026-02-27 22:52:15 +08:00
|
|
|
|
"Timezone_input",
|
2026-03-06 00:29:34 +08:00
|
|
|
|
"DistroDir_input",
|
2026-02-27 22:52:15 +08:00
|
|
|
|
"next_btn",
|
|
|
|
|
|
"prev_btn",
|
|
|
|
|
|
}
|
2026-02-28 19:29:17 +08:00
|
|
|
|
defaultFocusID = "next_btn"
|
2026-02-27 22:52:15 +08:00
|
|
|
|
case PagePublicNetwork:
|
|
|
|
|
|
componentOrder = []string{
|
2026-03-06 00:29:34 +08:00
|
|
|
|
"PublicHostname_input",
|
2026-02-27 22:52:15 +08:00
|
|
|
|
"PublicInterface_input",
|
|
|
|
|
|
"PublicIPAddress_input",
|
|
|
|
|
|
"PublicNetmask_input",
|
|
|
|
|
|
"PublicGateway_input",
|
2026-03-06 00:29:34 +08:00
|
|
|
|
"PublicDomain_input",
|
2026-02-28 19:29:17 +08:00
|
|
|
|
"PublicMTU_input",
|
2026-02-27 22:52:15 +08:00
|
|
|
|
"next_btn",
|
|
|
|
|
|
"prev_btn",
|
|
|
|
|
|
}
|
2026-02-28 19:29:17 +08:00
|
|
|
|
defaultFocusID = "next_btn"
|
2026-02-27 22:52:15 +08:00
|
|
|
|
case PageInternalNetwork:
|
|
|
|
|
|
componentOrder = []string{
|
2026-03-06 00:29:34 +08:00
|
|
|
|
"PrivateHostname_input",
|
2026-02-28 19:29:17 +08:00
|
|
|
|
"PrivateInterface_input",
|
|
|
|
|
|
"PrivateIPAddress_input",
|
|
|
|
|
|
"PrivateNetmask_input",
|
2026-03-06 00:29:34 +08:00
|
|
|
|
"PrivateDomain_input",
|
2026-02-28 19:29:17 +08:00
|
|
|
|
"PrivateMTU_input",
|
2026-02-27 22:52:15 +08:00
|
|
|
|
"next_btn",
|
|
|
|
|
|
"prev_btn",
|
|
|
|
|
|
}
|
2026-02-28 19:29:17 +08:00
|
|
|
|
defaultFocusID = "next_btn"
|
2026-02-27 22:52:15 +08:00
|
|
|
|
case PageDNS:
|
|
|
|
|
|
componentOrder = []string{
|
|
|
|
|
|
"Pri_DNS_input",
|
|
|
|
|
|
"Sec_DNS_input",
|
|
|
|
|
|
"next_btn",
|
|
|
|
|
|
"prev_btn",
|
|
|
|
|
|
}
|
2026-02-28 19:29:17 +08:00
|
|
|
|
defaultFocusID = "next_btn"
|
2026-02-27 22:52:15 +08:00
|
|
|
|
case PageSummary:
|
|
|
|
|
|
componentOrder = []string{"confirm_btn", "cancel_btn"}
|
2026-02-28 19:29:17 +08:00
|
|
|
|
defaultFocusID = "confirm_btn"
|
2026-02-27 22:52:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, compID := range componentOrder {
|
|
|
|
|
|
if comp, exists := pageComps[compID]; exists {
|
|
|
|
|
|
m.focusManager.Register(compID, comp)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-28 19:29:17 +08:00
|
|
|
|
|
|
|
|
|
|
if defaultFocusID != "" {
|
|
|
|
|
|
if currentComp, exists := m.focusManager.GetCurrent(); exists {
|
|
|
|
|
|
currentComp.Blur()
|
|
|
|
|
|
}
|
|
|
|
|
|
if targetComp, exists := pageComps[defaultFocusID]; exists {
|
|
|
|
|
|
m.focusManager.currentFocusID = defaultFocusID
|
|
|
|
|
|
targetComp.Focus()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-27 22:52:15 +08:00
|
|
|
|
}
|