Files
sunhpc-go/pkg/wizard/focused.go
2026-02-27 22:52:15 +08:00

322 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}
}