322 lines
6.7 KiB
Go
322 lines
6.7 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.Focus()
|
||
return &TextInput{Model: ti, focused: true}
|
||
}
|
||
|
||
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 注册可聚焦组件(指定标识和切换顺序)
|
||
func (fm *FocusManager) Register(id string, comp Focusable) {
|
||
// 防御性检查:避免 components 为空导致 panic
|
||
if fm.components == nil {
|
||
fm.components = make(map[string]Focusable)
|
||
}
|
||
|
||
// 避免重复注册
|
||
if _, exists := fm.components[id]; exists {
|
||
return
|
||
}
|
||
|
||
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()
|
||
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
|
||
}
|
||
|
||
// 按 [业务逻辑顺序] 注册组件 (决定Tab切换的顺序)
|
||
var componentOrder []string
|
||
|
||
// 按页面类型定义不同的注册顺序
|
||
switch page {
|
||
case PageAgreement:
|
||
componentOrder = []string{"accept_btn", "reject_btn"}
|
||
case PageData:
|
||
componentOrder = []string{
|
||
"Homepage_input",
|
||
"Hostname_input",
|
||
"Country_input",
|
||
"Region_input",
|
||
"Timezone_input",
|
||
"DBPath_input",
|
||
"Software_input",
|
||
"next_btn",
|
||
"prev_btn",
|
||
}
|
||
case PagePublicNetwork:
|
||
componentOrder = []string{
|
||
"PublicInterface_input",
|
||
"PublicIPAddress_input",
|
||
"PublicNetmask_input",
|
||
"PublicGateway_input",
|
||
"next_btn",
|
||
"prev_btn",
|
||
}
|
||
case PageInternalNetwork:
|
||
componentOrder = []string{
|
||
"InternalInterface_input",
|
||
"InternalIPAddress_input",
|
||
"InternalNetmask_input",
|
||
"next_btn",
|
||
"prev_btn",
|
||
}
|
||
case PageDNS:
|
||
componentOrder = []string{
|
||
"Pri_DNS_input",
|
||
"Sec_DNS_input",
|
||
"next_btn",
|
||
"prev_btn",
|
||
}
|
||
case PageSummary:
|
||
componentOrder = []string{"confirm_btn", "cancel_btn"}
|
||
}
|
||
|
||
// 注册组件到焦点管理器(按顺序)
|
||
for _, compID := range componentOrder {
|
||
if comp, exists := pageComps[compID]; exists {
|
||
m.focusManager.Register(compID, comp)
|
||
}
|
||
}
|
||
}
|