350 lines
7.4 KiB
Go
350 lines
7.4 KiB
Go
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()
|
||
}
|
||
}
|
||
}
|