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