Tui 重构代码逻辑
This commit is contained in:
@@ -1,194 +1,162 @@
|
||||
package wizard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bufio"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sunhpc/pkg/database"
|
||||
"sunhpc/pkg/utils"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
)
|
||||
|
||||
// saveConfig 保存配置到文件
|
||||
// 配置项映射:定义每个配置项对应的表名、键名
|
||||
var configMappings = []struct {
|
||||
table string
|
||||
key string
|
||||
getVal func(m *model) interface{} // 动态获取配置值的函数
|
||||
}{
|
||||
// attributes 表
|
||||
{"attributes", "license", func(m *model) any { return m.config.License }},
|
||||
{"attributes", "accepted", func(m *model) any { return m.config.AgreementAccepted }},
|
||||
{"attributes", "country", func(m *model) any { return m.config.Country }},
|
||||
{"attributes", "region", func(m *model) any { return m.config.Region }},
|
||||
{"attributes", "timezone", func(m *model) any { return m.config.Timezone }},
|
||||
{"attributes", "homepage", func(m *model) any { return m.config.HomePage }},
|
||||
{"attributes", "dbaddress", func(m *model) any { return m.config.DBAddress }},
|
||||
{"attributes", "software", func(m *model) any { return m.config.Software }},
|
||||
|
||||
// nodes 表
|
||||
{"nodes", "name", func(m *model) any { return m.config.Hostname }},
|
||||
|
||||
// 公网设置表
|
||||
{"public_network", "public_interface", func(m *model) any { return m.config.PublicInterface }},
|
||||
{"public_network", "ip_address", func(m *model) any { return m.config.PublicIPAddress }},
|
||||
{"public_network", "netmask", func(m *model) any { return m.config.PublicNetmask }},
|
||||
{"public_network", "gateway", func(m *model) any { return m.config.PublicGateway }},
|
||||
// 内网配置表
|
||||
{"internal_network", "internal_interface", func(m *model) any { return m.config.InternalInterface }},
|
||||
{"internal_network", "internal_ip", func(m *model) any { return m.config.InternalIPAddress }},
|
||||
{"internal_network", "internal_mask", func(m *model) any { return m.config.InternalNetmask }},
|
||||
// DNS配置表
|
||||
{"dns_config", "dns_primary", func(m *model) any { return m.config.DNSPrimary }},
|
||||
{"dns_config", "dns_secondary", func(m *model) any { return m.config.DNSSecondary }},
|
||||
}
|
||||
|
||||
// saveConfig 入口函数:保存所有配置到数据库
|
||||
func (m *model) saveConfig() error {
|
||||
configPath := GetConfigPath()
|
||||
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("创建配置目录失败:%w", err)
|
||||
}
|
||||
|
||||
// 序列化配置
|
||||
data, err := json.MarshalIndent(m.config, "", " ")
|
||||
conn, err := database.GetDB() // 假设database包已实现getDB()获取连接
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化配置失败:%w", err)
|
||||
return fmt.Errorf("获取数据库连接失败: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 写入文件
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("保存配置文件失败:%w", err)
|
||||
m.force = false // 初始化全量覆盖标识
|
||||
|
||||
// 遍历所有配置项,逐个处理
|
||||
for _, item := range configMappings {
|
||||
val := item.getVal(m)
|
||||
exists, err := m.checkExists(conn, item.table, item.key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查%s.%s是否存在失败: %w", item.table, item.key, err)
|
||||
}
|
||||
|
||||
// 根据存在性和用户选择处理
|
||||
if !exists {
|
||||
// 不存在则直接插入
|
||||
if err := m.upsertConfig(conn, item.table, item.key, val, false); err != nil {
|
||||
return fmt.Errorf("插入%s.%s失败: %w", item.table, item.key, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 已存在:判断是否全量覆盖
|
||||
if m.force {
|
||||
if err := m.upsertConfig(conn, item.table, item.key, val, true); err != nil {
|
||||
return fmt.Errorf("强制更新%s.%s失败: %w", item.table, item.key, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 询问用户操作
|
||||
choice, err := m.askUserChoice(item.table, item.key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取用户选择失败: %w", err)
|
||||
}
|
||||
|
||||
switch strings.ToLower(choice) {
|
||||
case "y", "yes":
|
||||
// 单条覆盖
|
||||
if err := m.upsertConfig(conn, item.table, item.key, val, true); err != nil {
|
||||
return fmt.Errorf("更新%s.%s失败: %w", item.table, item.key, err)
|
||||
}
|
||||
case "a", "all":
|
||||
// 全量覆盖,后续不再询问
|
||||
m.force = true
|
||||
if err := m.upsertConfig(conn, item.table, item.key, val, true); err != nil {
|
||||
return fmt.Errorf("全量更新%s.%s失败: %w", item.table, item.key, err)
|
||||
}
|
||||
case "n", "no":
|
||||
// 跳过当前项
|
||||
fmt.Printf("跳过%s.%s的更新\n", item.table, item.key)
|
||||
default:
|
||||
fmt.Printf("无效选择%s,跳过%s.%s的更新\n", choice, item.table, item.key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadConfig 从文件加载配置
|
||||
func loadConfig() (*Config, error) {
|
||||
configPath := GetConfigPath()
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
// checkExists 集中判断配置项是否存在(核心判断逻辑)
|
||||
func (m *model) checkExists(conn *sql.DB, table, key string) (bool, error) {
|
||||
var count int
|
||||
// 通用存在性检查SQL(假设所有表都有key字段作为主键)
|
||||
query := fmt.Sprintf("SELECT COUNT(1) FROM %s WHERE `key` = ?", table)
|
||||
err := conn.QueryRow(query, key).Scan(&count)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取配置文件失败:%w", err)
|
||||
// 表不存在也视为"不存在"(可选:根据实际需求调整,比如先建表)
|
||||
if strings.Contains(err.Error(), "table not found") {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("解析配置文件失败:%w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// 以下是 model.go 中调用的保存方法
|
||||
func (m *model) saveCurrentPage() {
|
||||
switch m.currentPage {
|
||||
case PageData:
|
||||
m.saveDataPage()
|
||||
case PagePublicNetwork:
|
||||
m.savePublicNetworkPage()
|
||||
case PageInternalNetwork:
|
||||
m.saveInternalNetworkPage()
|
||||
case PageDNS:
|
||||
m.saveDNSPage()
|
||||
// upsertConfig 统一处理插入/更新逻辑
|
||||
func (m *model) upsertConfig(conn *sql.DB, table, key string, val interface{}, update bool) error {
|
||||
var query string
|
||||
if !update {
|
||||
// 插入:假设表结构为(key, value)
|
||||
query = fmt.Sprintf("INSERT INTO %s (`key`, `value`) VALUES (?, ?)", table)
|
||||
} else {
|
||||
// 更新
|
||||
query = fmt.Sprintf("UPDATE %s SET `value` = ? WHERE `key` = ?", table)
|
||||
}
|
||||
|
||||
// 处理参数顺序(更新和插入的参数顺序不同)
|
||||
var args []interface{}
|
||||
if !update {
|
||||
args = []interface{}{key, val}
|
||||
} else {
|
||||
args = []interface{}{val, key}
|
||||
}
|
||||
|
||||
_, err := conn.Exec(query, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *model) saveDataPage() {
|
||||
if len(m.textInputs) >= 8 {
|
||||
m.config.HomePage = m.textInputs[0].Value()
|
||||
m.config.Hostname = m.textInputs[1].Value()
|
||||
m.config.Country = m.textInputs[2].Value()
|
||||
m.config.Region = m.textInputs[3].Value()
|
||||
m.config.Timezone = m.textInputs[4].Value()
|
||||
m.config.DBAddress = m.textInputs[5].Value()
|
||||
m.config.DataAddress = m.textInputs[6].Value()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) savePublicNetworkPage() {
|
||||
if len(m.textInputs) >= 4 {
|
||||
m.config.PublicInterface = m.textInputs[0].Value()
|
||||
m.config.PublicIPAddress = m.textInputs[1].Value()
|
||||
m.config.PublicNetmask = m.textInputs[2].Value()
|
||||
m.config.PublicGateway = m.textInputs[3].Value()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) saveInternalNetworkPage() {
|
||||
if len(m.textInputs) >= 3 {
|
||||
m.config.InternalInterface = m.textInputs[0].Value()
|
||||
m.config.InternalIPAddress = m.textInputs[1].Value()
|
||||
m.config.InternalNetmask = m.textInputs[2].Value()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) saveDNSPage() {
|
||||
if len(m.textInputs) >= 2 {
|
||||
m.config.DNSPrimary = m.textInputs[0].Value()
|
||||
m.config.DNSSecondary = m.textInputs[1].Value()
|
||||
}
|
||||
}
|
||||
|
||||
// initPageInputs 初始化当前页面的输入框
|
||||
func (m *model) initPageInputs() {
|
||||
m.textInputs = make([]textinput.Model, 0)
|
||||
|
||||
switch m.currentPage {
|
||||
case PageData:
|
||||
fields := []struct{ label, value string }{
|
||||
{"Homepage", m.config.HomePage},
|
||||
{"Hostname", m.config.Hostname},
|
||||
{"Country", m.config.Country},
|
||||
{"Region", m.config.Region},
|
||||
{"Timezone", m.config.Timezone},
|
||||
{"DB Path", m.config.DBAddress},
|
||||
{"Software", m.config.DataAddress},
|
||||
}
|
||||
for _, f := range fields {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = ""
|
||||
ti.Placeholder = f.label
|
||||
ti.SetValue(f.value)
|
||||
ti.Width = 50
|
||||
m.textInputs = append(m.textInputs, ti)
|
||||
}
|
||||
m.focusIndex = 0
|
||||
if len(m.textInputs) > 0 {
|
||||
m.textInputs[0].Focus()
|
||||
}
|
||||
m.inputLabels = []string{"Homepage", "Hostname", "Country", "Region", "Timezone", "DBPath", "Software"}
|
||||
|
||||
case PagePublicNetwork:
|
||||
fields := []struct{ label, value string }{
|
||||
{"Public Interface", m.config.PublicInterface},
|
||||
{"Public IP Address", m.config.PublicIPAddress},
|
||||
{"Public Netmask", m.config.PublicNetmask},
|
||||
{"Public Gateway", m.config.PublicGateway},
|
||||
}
|
||||
for _, f := range fields {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = ""
|
||||
ti.Placeholder = f.label
|
||||
ti.SetValue(f.value)
|
||||
ti.Width = 50
|
||||
m.textInputs = append(m.textInputs, ti)
|
||||
}
|
||||
m.focusIndex = 0
|
||||
if len(m.textInputs) > 0 {
|
||||
m.textInputs[0].Focus()
|
||||
}
|
||||
m.inputLabels = []string{"Public Interface", "Public IP Address", "Public Netmask", "Public Gateway"}
|
||||
|
||||
case PageInternalNetwork:
|
||||
fields := []struct{ label, value string }{
|
||||
{"Internal Interface", m.config.InternalInterface},
|
||||
{"Internal IP Address", m.config.InternalIPAddress},
|
||||
{"Internal Netmask", m.config.InternalNetmask},
|
||||
}
|
||||
for _, f := range fields {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = f.label
|
||||
ti.SetValue(f.value)
|
||||
ti.Width = 50
|
||||
m.textInputs = append(m.textInputs, ti)
|
||||
}
|
||||
m.focusIndex = 0
|
||||
if len(m.textInputs) > 0 {
|
||||
m.textInputs[0].Focus()
|
||||
}
|
||||
m.inputLabels = []string{"Internal Interface", "Internal IP", "Internal Mask"}
|
||||
|
||||
case PageDNS:
|
||||
fields := []struct{ label, value string }{
|
||||
{"Primary DNS", m.config.DNSPrimary},
|
||||
{"Secondary DNS", m.config.DNSSecondary},
|
||||
}
|
||||
for _, f := range fields {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = f.label
|
||||
ti.SetValue(f.value)
|
||||
ti.Width = 50
|
||||
m.textInputs = append(m.textInputs, ti)
|
||||
}
|
||||
m.focusIndex = 0
|
||||
if len(m.textInputs) > 0 {
|
||||
m.textInputs[0].Focus()
|
||||
}
|
||||
m.inputLabels = []string{"Pri DNS", "Sec DNS"}
|
||||
// askUserChoice 询问用户操作选择
|
||||
func (m *model) askUserChoice(table, key string) (string, error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Printf("配置项%s.%s已存在,选择操作(y/yes=覆盖, n/no=跳过, a/all=全量覆盖后续所有): ", table, key)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 去除空格和换行
|
||||
return strings.TrimSpace(input), nil
|
||||
}
|
||||
|
||||
// 获取系统网络接口
|
||||
|
||||
321
pkg/wizard/focused.go
Normal file
321
pkg/wizard/focused.go
Normal file
@@ -0,0 +1,321 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,26 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// PageType 页面类型
|
||||
type PageType int
|
||||
|
||||
// 总页码
|
||||
const TotalPages = 6
|
||||
|
||||
// Config 系统配置结构
|
||||
type Config struct {
|
||||
// 协议
|
||||
AgreementAccepted bool `json:"agreement_accepted"`
|
||||
License string `json:"license"`
|
||||
AgreementAccepted bool `json:"agreement_accepted"`
|
||||
|
||||
// 数据接收
|
||||
Hostname string `json:"hostname"`
|
||||
Country string `json:"country"`
|
||||
Region string `json:"region"`
|
||||
Timezone string `json:"timezone"`
|
||||
HomePage string `json:"homepage"`
|
||||
DBAddress string `json:"db_address"`
|
||||
DataAddress string `json:"data_address"`
|
||||
Hostname string `json:"hostname"`
|
||||
Country string `json:"country"`
|
||||
Region string `json:"region"`
|
||||
Timezone string `json:"timezone"`
|
||||
HomePage string `json:"homepage"`
|
||||
DBAddress string `json:"db_address"`
|
||||
Software string `json:"software"`
|
||||
|
||||
// 公网设置
|
||||
PublicInterface string `json:"public_interface"`
|
||||
@@ -37,9 +44,6 @@ type Config struct {
|
||||
DNSSecondary string `json:"dns_secondary"`
|
||||
}
|
||||
|
||||
// PageType 页面类型
|
||||
type PageType int
|
||||
|
||||
const (
|
||||
PageAgreement PageType = iota
|
||||
PageData
|
||||
@@ -49,29 +53,24 @@ const (
|
||||
PageSummary
|
||||
)
|
||||
|
||||
const (
|
||||
FocusTypeInput int = 0
|
||||
FocusTypePrev int = 1
|
||||
FocusTypeNext int = 2
|
||||
)
|
||||
|
||||
// model TUI 主模型
|
||||
type model struct {
|
||||
config Config
|
||||
currentPage PageType
|
||||
config Config // 全局配置
|
||||
currentPage PageType // 当前页面
|
||||
totalPages int
|
||||
networkInterfaces []string // 所有系统网络接口
|
||||
textInputs []textinput.Model
|
||||
inputLabels []string // 存储标签
|
||||
focusIndex int
|
||||
focusType int // 0=输入框, 1=上一步按钮, 2=下一步按钮
|
||||
agreementIdx int // 0=拒绝,1=接受
|
||||
textInputs []textinput.Model // 当前页面的输入框
|
||||
networkInterfaces []string // 所有系统网络接口
|
||||
width int
|
||||
height int
|
||||
err error
|
||||
quitting bool
|
||||
done bool
|
||||
force bool
|
||||
|
||||
// 核心1: 按页面分组存储所有组件(6个页面 + 6个map)
|
||||
pageComponents map[PageType]map[string]Focusable
|
||||
// 核心2:焦点管理器(每次切换页面时重置)
|
||||
focusManager *FocusManager
|
||||
}
|
||||
|
||||
// defaultConfig 返回默认配置
|
||||
@@ -97,13 +96,14 @@ func defaultConfig() Config {
|
||||
}
|
||||
|
||||
return Config{
|
||||
License: "This test license is for testing purposes only. Do not use it in production.",
|
||||
Hostname: "cluster.hpc.org",
|
||||
Country: "China",
|
||||
Region: "Beijing",
|
||||
Timezone: "Asia/Shanghai",
|
||||
HomePage: "www.sunhpc.com",
|
||||
DBAddress: "/var/lib/sunhpc/sunhpc.db",
|
||||
DataAddress: "/export/sunhpc",
|
||||
Software: "/export/sunhpc",
|
||||
PublicInterface: defaultPublicInterface,
|
||||
PublicIPAddress: "",
|
||||
PublicNetmask: "",
|
||||
@@ -119,18 +119,76 @@ func defaultConfig() Config {
|
||||
// initialModel 初始化模型
|
||||
func initialModel() model {
|
||||
cfg := defaultConfig()
|
||||
|
||||
// 1. 初始化所有页面组件(6个页面)
|
||||
pageComponents := make(map[PageType]map[string]Focusable)
|
||||
|
||||
// ------------------ 页面1:协议页面 --------------------
|
||||
page1Comps := make(map[string]Focusable)
|
||||
page1Comps["accept_btn"] = NewButton("接受协议")
|
||||
page1Comps["reject_btn"] = NewButton("拒绝协议")
|
||||
pageComponents[PageAgreement] = page1Comps
|
||||
|
||||
// ------------------ 页面2:基础信息页面 --------------------
|
||||
page2Comps := make(map[string]Focusable)
|
||||
page2Comps["Homepage_input"] = NewTextInput("Homepage", cfg.HomePage)
|
||||
page2Comps["Hostname_input"] = NewTextInput("Hostname", cfg.Hostname)
|
||||
page2Comps["Country_input"] = NewTextInput("Country", cfg.Country)
|
||||
page2Comps["Region_input"] = NewTextInput("Region", cfg.Region)
|
||||
page2Comps["Timezone_input"] = NewTextInput("Timezone", cfg.Timezone)
|
||||
page2Comps["DBPath_input"] = NewTextInput("DBPath", cfg.DBAddress)
|
||||
page2Comps["Software_input"] = NewTextInput("Software", cfg.Software)
|
||||
page2Comps["next_btn"] = NewButton("下一步")
|
||||
page2Comps["prev_btn"] = NewButton("上一步")
|
||||
pageComponents[PageData] = page2Comps
|
||||
|
||||
// ------------------ 页面3:公网网络页面 --------------------
|
||||
page3Comps := make(map[string]Focusable)
|
||||
page3Comps["PublicInterface_input"] = NewTextInput("PublicInterface", cfg.PublicInterface)
|
||||
page3Comps["PublicIPAddress_input"] = NewTextInput("PublicIPAddress", cfg.PublicIPAddress)
|
||||
page3Comps["PublicNetmask_input"] = NewTextInput("PublicNetmask", cfg.PublicNetmask)
|
||||
page3Comps["PublicGateway_input"] = NewTextInput("PublicGateway", cfg.PublicGateway)
|
||||
page3Comps["next_btn"] = NewButton("下一步")
|
||||
page3Comps["prev_btn"] = NewButton("上一步")
|
||||
pageComponents[PagePublicNetwork] = page3Comps
|
||||
|
||||
// ------------------ 页面4:内网网络页面 --------------------
|
||||
page4Comps := make(map[string]Focusable)
|
||||
page4Comps["InternalInterface_input"] = NewTextInput("InternalInterface", cfg.InternalInterface)
|
||||
page4Comps["InternalIPAddress_input"] = NewTextInput("InternalIPAddress", cfg.InternalIPAddress)
|
||||
page4Comps["InternalNetmask_input"] = NewTextInput("InternalNetmask", cfg.InternalNetmask)
|
||||
page4Comps["next_btn"] = NewButton("下一步")
|
||||
page4Comps["prev_btn"] = NewButton("上一步")
|
||||
pageComponents[PageInternalNetwork] = page4Comps
|
||||
|
||||
// ------------------ 页面5:DNS页面 --------------------
|
||||
page5Comps := make(map[string]Focusable)
|
||||
page5Comps["Pri_DNS_input"] = NewTextInput("Pri DNS", cfg.DNSPrimary)
|
||||
page5Comps["Sec_DNS_input"] = NewTextInput("Sec DNS", cfg.DNSSecondary)
|
||||
page5Comps["next_btn"] = NewButton("下一步")
|
||||
page5Comps["prev_btn"] = NewButton("上一步")
|
||||
pageComponents[PageDNS] = page5Comps
|
||||
|
||||
// ------------------ 页面6:Summary页面 --------------------
|
||||
page6Comps := make(map[string]Focusable)
|
||||
page6Comps["confirm_btn"] = NewButton("Confirm")
|
||||
page6Comps["cancel_btn"] = NewButton("Cancel")
|
||||
pageComponents[PageSummary] = page6Comps
|
||||
|
||||
// 创建焦点管理器(初始化聚焦页)
|
||||
fm := NewFocusManager(true)
|
||||
|
||||
// 初始化模型
|
||||
m := model{
|
||||
config: cfg,
|
||||
totalPages: 6,
|
||||
textInputs: make([]textinput.Model, 0),
|
||||
inputLabels: make([]string, 0),
|
||||
agreementIdx: 1,
|
||||
focusIndex: 0,
|
||||
focusType: 0, // 0=输入框, 1=上一步按钮, 2=下一步按钮
|
||||
width: 80,
|
||||
height: 24,
|
||||
config: cfg,
|
||||
totalPages: 6,
|
||||
currentPage: PageAgreement, // 初始化聚焦在协议页面
|
||||
pageComponents: pageComponents,
|
||||
focusManager: fm,
|
||||
}
|
||||
m.initPageInputs()
|
||||
|
||||
// 初始化当前页 (页1) 的焦点
|
||||
m.initPageFocus(m.currentPage)
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -141,8 +199,6 @@ func (m model) Init() tea.Cmd {
|
||||
|
||||
// Update 处理消息更新
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
@@ -150,205 +206,104 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
|
||||
case "esc":
|
||||
if m.currentPage > 0 {
|
||||
return m.prevPage()
|
||||
}
|
||||
// 1. 焦点切换(Tab/Shift+Tab)交给管理器处理
|
||||
case "tab", "shift+tab", "left", "right":
|
||||
cmd := m.focusManager.HandleInput(msg)
|
||||
return m, cmd
|
||||
|
||||
// 2. 回车键:处理当前焦点组件的点击/确认
|
||||
case "enter":
|
||||
return m.handleEnter()
|
||||
currentCompID := m.focusManager.currentFocusID
|
||||
switch currentCompID {
|
||||
// 页1:accept → 进入页2
|
||||
case "accept_btn":
|
||||
return m, m.switchPage(PageData)
|
||||
// 页1:reject → 退出程序
|
||||
case "reject_btn":
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
|
||||
case "tab", "shift+tab", "up", "down", "left", "right":
|
||||
return m.handleNavigation(msg)
|
||||
}
|
||||
// 通用上一页/下一页逻辑
|
||||
case "prev_btn":
|
||||
return m, m.switchPage(m.currentPage - 1)
|
||||
case "next_btn":
|
||||
return m, m.switchPage(m.currentPage + 1)
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
// 页6:确认配置 → 退出并保存
|
||||
case "confirm_btn":
|
||||
m.done = true
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
|
||||
// 动态调整容器宽度
|
||||
/*
|
||||
if msg.Width > 100 {
|
||||
containerStyle = containerStyle.Width(90)
|
||||
} else if msg.Width > 80 {
|
||||
containerStyle = containerStyle.Width(70)
|
||||
} else {
|
||||
containerStyle = containerStyle.Width(msg.Width - 10)
|
||||
case "cancel_btn":
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
*/
|
||||
|
||||
// ✅ 动态计算容器宽度(终端宽度的 80%)
|
||||
containerWidth := msg.Width * 80 / 100
|
||||
|
||||
// ✅ 重新设置容器样式宽度
|
||||
containerStyle = containerStyle.Width(containerWidth)
|
||||
|
||||
// 动态设置协议框宽度(容器宽度的 90%)
|
||||
agreementWidth := containerWidth * 80 / 100
|
||||
agreementBox = agreementBox.Width(agreementWidth)
|
||||
|
||||
// 动态设置输入框宽度
|
||||
inputWidth := containerWidth * 60 / 100
|
||||
if inputWidth < 40 {
|
||||
inputWidth = 40
|
||||
}
|
||||
inputBox = inputBox.Width(inputWidth)
|
||||
|
||||
// 动态设置总结框宽度
|
||||
summaryWidth := containerWidth * 90 / 100
|
||||
summaryBox = summaryBox.Width(summaryWidth)
|
||||
|
||||
return m, nil
|
||||
// 其他消息(窗口大小、输入框输入等)...
|
||||
}
|
||||
|
||||
// 更新当前焦点的输入框
|
||||
if len(m.textInputs) > 0 && m.focusIndex < len(m.textInputs) {
|
||||
var cmd tea.Cmd
|
||||
m.textInputs[m.focusIndex], cmd = m.textInputs[m.focusIndex].Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
// 处理当前焦点组件的内部更新(比如输入框打字、列表选值)
|
||||
currentComp, exists := m.focusManager.GetCurrent()
|
||||
if exists {
|
||||
// 不同组件的内部更新逻辑(示例)
|
||||
switch comp := currentComp.(type) {
|
||||
case *TextInput:
|
||||
// 输入框更新
|
||||
newTI, cmd := comp.Model.Update(msg)
|
||||
comp.Model = newTI
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
// 保存输入值到全局配置(示例:主机名)
|
||||
switch m.focusManager.currentFocusID {
|
||||
// 页2:基础信息
|
||||
case "Homepage_input":
|
||||
m.config.HomePage = comp.Value()
|
||||
case "Hostname_input":
|
||||
m.config.Hostname = comp.Value()
|
||||
case "Country_input":
|
||||
m.config.Country = comp.Value()
|
||||
case "Region_input":
|
||||
m.config.Region = comp.Value()
|
||||
case "Timezone_input":
|
||||
m.config.Timezone = comp.Value()
|
||||
case "DBPath_input":
|
||||
m.config.DBAddress = comp.Value()
|
||||
case "Software_input":
|
||||
m.config.Software = comp.Value()
|
||||
|
||||
// handleEnter 处理回车事件
|
||||
func (m *model) handleEnter() (tea.Model, tea.Cmd) {
|
||||
switch m.currentPage {
|
||||
case PageAgreement:
|
||||
if m.agreementIdx == 1 {
|
||||
m.config.AgreementAccepted = true
|
||||
return m.nextPage()
|
||||
} else {
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
// 页3:公网网络
|
||||
case "PublicInterface_input":
|
||||
m.config.PublicInterface = comp.Value()
|
||||
case "PublicIPAddress_input":
|
||||
m.config.PublicIPAddress = comp.Value()
|
||||
case "PublicNetmask_input":
|
||||
m.config.PublicNetmask = comp.Value()
|
||||
case "PublicGateway_input":
|
||||
m.config.PublicGateway = comp.Value()
|
||||
|
||||
case PageData, PagePublicNetwork, PageInternalNetwork, PageDNS:
|
||||
// 根据焦点类型执行不同操作
|
||||
switch m.focusType {
|
||||
case FocusTypeInput:
|
||||
// 在输入框上,保存并下一页
|
||||
m.saveCurrentPage()
|
||||
return m.nextPage()
|
||||
case FocusTypePrev:
|
||||
// 上一步按钮,返回上一页
|
||||
return m.prevPage()
|
||||
case FocusTypeNext:
|
||||
// 下一步按钮,切换到下一页
|
||||
m.saveCurrentPage()
|
||||
return m.nextPage()
|
||||
}
|
||||
// 页4:内网网络
|
||||
case "InternalInterface_input":
|
||||
m.config.InternalInterface = comp.Value()
|
||||
case "InternalIPAddress_input":
|
||||
m.config.InternalIPAddress = comp.Value()
|
||||
case "InternalNetmask_input":
|
||||
m.config.InternalNetmask = comp.Value()
|
||||
|
||||
// 页5:DNS
|
||||
case "Pri_DNS_input":
|
||||
m.config.DNSPrimary = comp.Value()
|
||||
case "Sec_DNS_input":
|
||||
m.config.DNSSecondary = comp.Value()
|
||||
|
||||
case PageSummary:
|
||||
switch m.focusIndex {
|
||||
case 0: // 执行
|
||||
m.done = true
|
||||
if err := m.saveConfig(); err != nil {
|
||||
m.err = err
|
||||
return m, nil
|
||||
}
|
||||
return m, tea.Quit
|
||||
case 1: // 取消
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
return m, cmd
|
||||
|
||||
case *List:
|
||||
// 列表更新
|
||||
newList, cmd := comp.Model.Update(msg)
|
||||
comp.Model = newList
|
||||
return m, cmd
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// handleNavigation 处理导航
|
||||
func (m *model) handleNavigation(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// debug
|
||||
//fmt.Fprintf(os.Stderr, "DEBUG: key=%s page=%d\n", msg.String(), m.currentPage)
|
||||
|
||||
switch m.currentPage {
|
||||
case PageAgreement:
|
||||
switch msg.String() {
|
||||
case "left", "right", "tab", "shift+tab", "up", "down":
|
||||
m.agreementIdx = 1 - m.agreementIdx
|
||||
}
|
||||
|
||||
case PageSummary:
|
||||
switch msg.String() {
|
||||
case "left", "right", "tab", "shift+tab":
|
||||
m.focusIndex = 1 - m.focusIndex
|
||||
}
|
||||
|
||||
default:
|
||||
// 输入框页面: 支持输入框和按钮之间切换
|
||||
// totalFocusable := len(m.textInputs) + 2
|
||||
|
||||
switch msg.String() {
|
||||
case "down", "tab":
|
||||
// 当前在输入框
|
||||
switch m.focusType {
|
||||
case FocusTypeInput:
|
||||
if m.focusIndex < len(m.textInputs)-1 {
|
||||
// 切换到下一个输入框
|
||||
m.textInputs[m.focusIndex].Blur()
|
||||
m.focusIndex++
|
||||
m.textInputs[m.focusIndex].Focus()
|
||||
} else {
|
||||
// 最后一个输入框,切换到“下一步”按钮
|
||||
m.textInputs[m.focusIndex].Blur()
|
||||
m.focusIndex = 0
|
||||
m.focusType = FocusTypeNext // 下一步按钮
|
||||
}
|
||||
case FocusTypePrev:
|
||||
// 当前在“上一步”按钮,切换到第一个输入框
|
||||
m.focusType = FocusTypeInput
|
||||
m.focusIndex = 0
|
||||
m.textInputs[0].Focus()
|
||||
case FocusTypeNext:
|
||||
// 当前在“下一步”按钮,切换到“上一步”按钮
|
||||
m.focusType = FocusTypePrev
|
||||
}
|
||||
case "up", "shift+tab":
|
||||
// 当前在输入框
|
||||
switch m.focusType {
|
||||
case FocusTypeInput:
|
||||
if m.focusIndex > 0 {
|
||||
// 切换到上一个输入框
|
||||
m.textInputs[m.focusIndex].Blur()
|
||||
m.focusIndex--
|
||||
m.textInputs[m.focusIndex].Focus()
|
||||
} else {
|
||||
// 第一个输入框,切换到“上一步”按钮
|
||||
m.textInputs[m.focusIndex].Blur()
|
||||
m.focusIndex = 0
|
||||
m.focusType = FocusTypePrev // 上一步按钮
|
||||
}
|
||||
case FocusTypeNext:
|
||||
// 当前在“下一步”按钮,切换到最后一个输入框
|
||||
m.focusType = FocusTypeInput
|
||||
m.focusIndex = len(m.textInputs) - 1
|
||||
m.textInputs[m.focusIndex].Focus()
|
||||
case FocusTypePrev:
|
||||
// 当前在“上一步”按钮,切换到“下一步”按钮
|
||||
m.focusType = FocusTypeNext
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// nextPage 下一页
|
||||
func (m *model) nextPage() (tea.Model, tea.Cmd) {
|
||||
if m.currentPage < PageSummary {
|
||||
m.currentPage++
|
||||
m.focusIndex = 0
|
||||
m.initPageInputs()
|
||||
}
|
||||
return m, textinput.Blink
|
||||
}
|
||||
|
||||
// prevPage 上一页
|
||||
func (m *model) prevPage() (tea.Model, tea.Cmd) {
|
||||
if m.currentPage > 0 {
|
||||
m.saveCurrentPage()
|
||||
m.currentPage--
|
||||
m.focusIndex = 0
|
||||
m.initPageInputs()
|
||||
}
|
||||
return m, textinput.Blink
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package wizard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var split_line = splitlineStyle.Render(
|
||||
"───────────────────────────────────────────────────────────────")
|
||||
|
||||
// View 渲染视图
|
||||
func (m model) View() string {
|
||||
if m.done {
|
||||
@@ -19,38 +21,39 @@ func (m model) View() string {
|
||||
return errorView(m.err)
|
||||
}
|
||||
|
||||
var page string
|
||||
var pageContent string
|
||||
switch m.currentPage {
|
||||
case PageAgreement:
|
||||
page = m.agreementView()
|
||||
pageContent = renderLicensePage(m)
|
||||
case PageData:
|
||||
page = m.dataView()
|
||||
pageContent = renderDataInfoPage(m)
|
||||
case PagePublicNetwork:
|
||||
page = m.publicNetworkView()
|
||||
pageContent = renderPublicNetworkPage(m)
|
||||
case PageInternalNetwork:
|
||||
page = m.internalNetworkView()
|
||||
pageContent = renderInternalNetworkPage(m)
|
||||
case PageDNS:
|
||||
page = m.dnsView()
|
||||
pageContent = renderDNSPage(m)
|
||||
case PageSummary:
|
||||
page = m.summaryView()
|
||||
pageContent = renderSummaryPage(m)
|
||||
|
||||
default:
|
||||
pageContent = appStyle.Render("无效页面")
|
||||
}
|
||||
|
||||
content := strings.Builder{}
|
||||
content.WriteString(page)
|
||||
content.WriteString("\n\n")
|
||||
content.WriteString(progressView(m.currentPage, m.totalPages))
|
||||
|
||||
return containerStyle.Render(content.String())
|
||||
return appStyle.Render(pageContent)
|
||||
}
|
||||
|
||||
// agreementView 协议页面
|
||||
func (m model) agreementView() string {
|
||||
func makeRow(label, value string) string {
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
labelStyle.Render(label+":"),
|
||||
valueStyle.Render(value),
|
||||
)
|
||||
}
|
||||
|
||||
func renderLicensePage(m model) string {
|
||||
title := titleStyle.Render("SunHPC Software License Agreement")
|
||||
|
||||
agreement := agreementBox.Render(`
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SunHPC License Agreement │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
licenseText := `
|
||||
───────────────────────────────────────────────────────────────
|
||||
1. License Grant
|
||||
This software grants you a non-exclusive, non-transferable
|
||||
license to use it.
|
||||
@@ -69,288 +72,265 @@ func (m model) agreementView() string {
|
||||
PLEASE READ THE ABOVE TERMS CAREFULLY AND CLICK "ACCEPT"
|
||||
TO AGREE AND FOLLOW THIS AGREEMENT.
|
||||
───────────────────────────────────────────────────────────────
|
||||
`)
|
||||
`
|
||||
|
||||
var acceptBtn, rejectBtn string
|
||||
if m.agreementIdx == 0 {
|
||||
rejectBtn = selectedButton.Render(">> Reject <<")
|
||||
acceptBtn = " Accept "
|
||||
} else {
|
||||
rejectBtn = " Reject "
|
||||
acceptBtn = selectedButton.Render(">> Accept <<")
|
||||
}
|
||||
|
||||
buttonGroup := lipgloss.JoinHorizontal(
|
||||
lipgloss.Center,
|
||||
acceptBtn, " ", rejectBtn)
|
||||
pageComps := m.pageComponents[PageAgreement]
|
||||
acceptBtn := pageComps["accept_btn"].View()
|
||||
rejectBtn := pageComps["reject_btn"].View()
|
||||
|
||||
// ✅ 添加调试信息(确认 agreementIdx 的值)
|
||||
// debugInfo := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).
|
||||
// Render(fmt.Sprintf("[DEBUG: idx=%d]", m.agreementIdx),)
|
||||
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||
pageContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
agreement, "",
|
||||
buttonGroup, "",
|
||||
// debugInfo, "", // ✅ 显示调试信息
|
||||
licenseTextStyle.Render(licenseText),
|
||||
lipgloss.JoinHorizontal(lipgloss.Center, acceptBtn, rejectBtn),
|
||||
|
||||
hint,
|
||||
)
|
||||
|
||||
return appStyle.Render(pageContent)
|
||||
}
|
||||
|
||||
// dataView 数据接收页面
|
||||
func (m model) dataView() string {
|
||||
title := titleStyle.Render("Cluster Information")
|
||||
// ---------------- 页2:基础信息页渲染 ----------------
|
||||
func renderDataInfoPage(m model) string {
|
||||
pageComps := m.pageComponents[PageData]
|
||||
|
||||
var inputs strings.Builder
|
||||
for i, ti := range m.textInputs {
|
||||
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
|
||||
input := inputBox.Render(info + ti.View())
|
||||
inputs.WriteString(input + "\n")
|
||||
}
|
||||
// 拼接基础信息表单
|
||||
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||
split_line,
|
||||
makeRow("Homepage", pageComps["Homepage_input"].View()),
|
||||
split_line,
|
||||
makeRow("Hostname", pageComps["Hostname_input"].View()),
|
||||
split_line,
|
||||
makeRow("Country", pageComps["Country_input"].View()),
|
||||
split_line,
|
||||
makeRow("Region", pageComps["Region_input"].View()),
|
||||
split_line,
|
||||
makeRow("Timezone", pageComps["Timezone_input"].View()),
|
||||
split_line,
|
||||
makeRow("DBPath", pageComps["DBPath_input"].View()),
|
||||
split_line,
|
||||
makeRow("Software", pageComps["Software_input"].View()),
|
||||
split_line,
|
||||
)
|
||||
|
||||
buttons := m.renderNavButtons()
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
inputs.String(), "",
|
||||
buttons, "",
|
||||
// 按钮区域
|
||||
btnArea := lipgloss.JoinHorizontal(
|
||||
lipgloss.Center,
|
||||
pageComps["next_btn"].View(),
|
||||
pageComps["prev_btn"].View(),
|
||||
)
|
||||
|
||||
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||
|
||||
// 页面整体
|
||||
pageContent := lipgloss.JoinVertical(
|
||||
lipgloss.Center,
|
||||
titleStyle.Render("基础信息配置(页2/6)"),
|
||||
formContent,
|
||||
btnArea,
|
||||
hint,
|
||||
)
|
||||
|
||||
return appStyle.Render(pageContent)
|
||||
}
|
||||
|
||||
// publicNetworkView 公网设置页面
|
||||
func (m model) publicNetworkView() string {
|
||||
title := titleStyle.Render("Public Network Configuration")
|
||||
func renderPublicNetworkPage(m model) string {
|
||||
pageComps := m.pageComponents[PagePublicNetwork]
|
||||
|
||||
// 拼接公网网络表单
|
||||
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||
split_line,
|
||||
makeRow("PublicInterface", pageComps["PublicInterface_input"].View()),
|
||||
split_line,
|
||||
makeRow("PublicIPAddress", pageComps["PublicIPAddress_input"].View()),
|
||||
split_line,
|
||||
makeRow("PublicNetmask", pageComps["PublicNetmask_input"].View()),
|
||||
split_line,
|
||||
makeRow("PublicGateway", pageComps["PublicGateway_input"].View()),
|
||||
split_line,
|
||||
)
|
||||
|
||||
// 按钮区域
|
||||
btnArea := lipgloss.JoinHorizontal(
|
||||
lipgloss.Center,
|
||||
pageComps["next_btn"].View(),
|
||||
pageComps["prev_btn"].View(),
|
||||
)
|
||||
|
||||
networkInterfaces := getNetworkInterfaces()
|
||||
autoDetect := infoStyle.Render(
|
||||
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
|
||||
"[*] Auto Detect Interfaces: " + strings.Join(networkInterfaces, ", "))
|
||||
|
||||
var inputs strings.Builder
|
||||
for i, ti := range m.textInputs {
|
||||
info := fmt.Sprintf("%-20s|", m.inputLabels[i])
|
||||
input := inputBox.Render(info + ti.View())
|
||||
inputs.WriteString(input + "\n")
|
||||
}
|
||||
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||
|
||||
buttons := m.renderNavButtons()
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
autoDetect, "",
|
||||
inputs.String(), "",
|
||||
buttons, "",
|
||||
// 页面整体
|
||||
pageContent := lipgloss.JoinVertical(
|
||||
lipgloss.Center,
|
||||
titleStyle.Render("公网网络配置(页3/6)"),
|
||||
autoDetect,
|
||||
formContent,
|
||||
btnArea,
|
||||
hint,
|
||||
)
|
||||
|
||||
return appStyle.Render(pageContent)
|
||||
}
|
||||
|
||||
// internalNetworkView 内网配置页面
|
||||
func (m model) internalNetworkView() string {
|
||||
title := titleStyle.Render("Internal Network Configuration")
|
||||
func renderInternalNetworkPage(m model) string {
|
||||
pageComps := m.pageComponents[PageInternalNetwork]
|
||||
|
||||
networkInterfaces := getNetworkInterfaces()
|
||||
autoDetect := infoStyle.Render(
|
||||
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
|
||||
// 拼接内网网络表单
|
||||
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||
split_line,
|
||||
makeRow("InternalInterface", pageComps["InternalInterface_input"].View()),
|
||||
split_line,
|
||||
makeRow("InternalIPAddress", pageComps["InternalIPAddress_input"].View()),
|
||||
split_line,
|
||||
makeRow("InternalNetmask", pageComps["InternalNetmask_input"].View()),
|
||||
split_line,
|
||||
)
|
||||
|
||||
var inputs strings.Builder
|
||||
for i, ti := range m.textInputs {
|
||||
info := fmt.Sprintf("%-20s|", m.inputLabels[i])
|
||||
input := inputBox.Render(info + ti.View())
|
||||
inputs.WriteString(input + "\n")
|
||||
}
|
||||
// 按钮区域
|
||||
btnArea := lipgloss.JoinHorizontal(
|
||||
lipgloss.Center,
|
||||
pageComps["next_btn"].View(),
|
||||
pageComps["prev_btn"].View(),
|
||||
)
|
||||
|
||||
buttons := m.renderNavButtons()
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
autoDetect, "",
|
||||
inputs.String(), "",
|
||||
buttons, "",
|
||||
// 页面整体
|
||||
pageContent := lipgloss.JoinVertical(
|
||||
lipgloss.Center,
|
||||
titleStyle.Render("内网网络配置(页4/6)"),
|
||||
formContent,
|
||||
btnArea,
|
||||
hint,
|
||||
)
|
||||
|
||||
return appStyle.Render(pageContent)
|
||||
}
|
||||
|
||||
// dnsView DNS 配置页面
|
||||
func (m model) dnsView() string {
|
||||
title := titleStyle.Render("DNS Configuration")
|
||||
func renderDNSPage(m model) string {
|
||||
pageComps := m.pageComponents[PageDNS]
|
||||
|
||||
var inputs strings.Builder
|
||||
for i, ti := range m.textInputs {
|
||||
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
|
||||
input := inputBox.Render(info + ti.View())
|
||||
inputs.WriteString(input + "\n")
|
||||
}
|
||||
// 拼接 DNS 表单
|
||||
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||
split_line,
|
||||
makeRow("Pri DNS", pageComps["Pri_DNS_input"].View()),
|
||||
split_line,
|
||||
makeRow("Sec DNS", pageComps["Sec_DNS_input"].View()),
|
||||
split_line,
|
||||
)
|
||||
|
||||
buttons := m.renderNavButtons()
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
// 按钮区域
|
||||
btnArea := lipgloss.JoinHorizontal(
|
||||
lipgloss.Center,
|
||||
pageComps["next_btn"].View(),
|
||||
pageComps["prev_btn"].View(),
|
||||
)
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
inputs.String(), "",
|
||||
buttons, "",
|
||||
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||
|
||||
// 页面整体
|
||||
pageContent := lipgloss.JoinVertical(
|
||||
lipgloss.Center,
|
||||
titleStyle.Render("DNS 配置(页5/6)"),
|
||||
formContent,
|
||||
btnArea,
|
||||
hint,
|
||||
)
|
||||
|
||||
return appStyle.Render(pageContent)
|
||||
}
|
||||
|
||||
// summaryView 总结页面
|
||||
func (m model) summaryView() string {
|
||||
title := titleStyle.Render("Summary")
|
||||
subtitle := subTitleStyle.Render("Please confirm the following configuration information")
|
||||
func renderSummaryPage(m model) string {
|
||||
pageComps := m.pageComponents[PageSummary]
|
||||
|
||||
summary := summaryBox.Render(fmt.Sprintf(`
|
||||
+----------------------------------------------------+
|
||||
Basic Information
|
||||
+----------------------------------------------------+
|
||||
Homepage : %-38s
|
||||
Hostname : %-35s
|
||||
Country : %-31s
|
||||
Region : %-31s
|
||||
Timezone : %-38s
|
||||
Homepage : %-38s
|
||||
+----------------------------------------------------+
|
||||
Database Configuration
|
||||
+----------------------------------------------------+
|
||||
Database Path : %-38s
|
||||
Software : %-33s
|
||||
+----------------------------------------------------+
|
||||
Public Network Configuration
|
||||
+----------------------------------------------------+
|
||||
Public Interface : %-38s
|
||||
Public IP : %-41s
|
||||
Public Netmask : %-38s
|
||||
Public Gateway : %-38s
|
||||
+----------------------------------------------------+
|
||||
Internal Network Configuration
|
||||
+----------------------------------------------------+
|
||||
Internal Interface: %-38s
|
||||
Internal IP : %-41s
|
||||
Internal Netmask : %-38s
|
||||
+----------------------------------------------------+
|
||||
DNS Configuration
|
||||
+----------------------------------------------------+
|
||||
Primary DNS : %-37s
|
||||
Secondary DNS : %-37s
|
||||
+----------------------------------------------------+
|
||||
`,
|
||||
m.config.HomePage,
|
||||
m.config.Hostname,
|
||||
m.config.Country,
|
||||
m.config.Region,
|
||||
m.config.Timezone,
|
||||
m.config.HomePage,
|
||||
m.config.DBAddress,
|
||||
m.config.DataAddress,
|
||||
m.config.PublicInterface,
|
||||
m.config.PublicIPAddress,
|
||||
m.config.PublicNetmask,
|
||||
m.config.PublicGateway,
|
||||
m.config.InternalInterface,
|
||||
m.config.InternalIPAddress,
|
||||
m.config.InternalNetmask,
|
||||
m.config.DNSPrimary,
|
||||
m.config.DNSSecondary,
|
||||
))
|
||||
// 拼接 Summary 表单
|
||||
formContent := lipgloss.JoinVertical(lipgloss.Center,
|
||||
split_line,
|
||||
makeRow("Hostname", m.config.Hostname),
|
||||
split_line,
|
||||
makeRow("Country", m.config.Country),
|
||||
split_line,
|
||||
makeRow("Region", m.config.Region),
|
||||
split_line,
|
||||
makeRow("Timezone", m.config.Timezone),
|
||||
split_line,
|
||||
makeRow("Homepage", m.config.HomePage),
|
||||
split_line,
|
||||
makeRow("DBPath", m.config.DBAddress),
|
||||
split_line,
|
||||
makeRow("Software", m.config.Software),
|
||||
split_line,
|
||||
makeRow("PublicInterface", m.config.PublicInterface),
|
||||
split_line,
|
||||
makeRow("PublicIPAddress", m.config.PublicIPAddress),
|
||||
split_line,
|
||||
makeRow("PublicNetmask", m.config.PublicNetmask),
|
||||
split_line,
|
||||
makeRow("PublicGateway", m.config.PublicGateway),
|
||||
split_line,
|
||||
makeRow("InternalInterface", m.config.InternalInterface),
|
||||
split_line,
|
||||
makeRow("InternalIPAddress", m.config.InternalIPAddress),
|
||||
split_line,
|
||||
makeRow("InternalNetmask", m.config.InternalNetmask),
|
||||
split_line,
|
||||
makeRow("Pri DNS", m.config.DNSPrimary),
|
||||
split_line,
|
||||
makeRow("Sec DNS", m.config.DNSSecondary),
|
||||
split_line,
|
||||
)
|
||||
|
||||
var buttons string
|
||||
if m.focusIndex == 0 {
|
||||
buttons = selectedButton.Render("[>] Start Initialization") + " " + normalButton.Render("[ ] Cancel")
|
||||
} else {
|
||||
buttons = normalButton.Render("[>] Start Initialization") + " " + selectedButton.Render("[ ] Cancel")
|
||||
}
|
||||
// 按钮区域
|
||||
btnArea := lipgloss.JoinHorizontal(
|
||||
lipgloss.Center,
|
||||
pageComps["confirm_btn"].View(),
|
||||
pageComps["cancel_btn"].View(),
|
||||
)
|
||||
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
hint := hintStyle.Render("Use Left/Right OR Tab Change,Enter Confirm")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
subtitle, "",
|
||||
summary, "",
|
||||
buttons, "",
|
||||
// 页面整体
|
||||
pageContent := lipgloss.JoinVertical(
|
||||
lipgloss.Center,
|
||||
titleStyle.Render("确认信息(页6/6)"),
|
||||
formContent,
|
||||
btnArea,
|
||||
hint,
|
||||
)
|
||||
|
||||
return appStyle.Render(pageContent)
|
||||
}
|
||||
|
||||
// progressView 进度条
|
||||
func progressView(current PageType, total int) string {
|
||||
progress := ""
|
||||
for i := 0; i < total; i++ {
|
||||
if i < int(current) {
|
||||
progress += "[+]"
|
||||
} else if i == int(current) {
|
||||
progress += "[-]"
|
||||
} else {
|
||||
progress += "[ ]"
|
||||
}
|
||||
if i < total-1 {
|
||||
progress += " "
|
||||
}
|
||||
}
|
||||
labels := []string{"License", "Data", "Network", "Network", "DNS", "Summary"}
|
||||
label := labelStyle.Render(labels[current])
|
||||
return progressStyle.Render(progress) + " " + label
|
||||
}
|
||||
|
||||
// successView 成功视图
|
||||
func successView() string {
|
||||
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
||||
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||
successTitle.Render("Initialization Completed!"), "",
|
||||
successMsg.Render("System configuration has been saved, and the system is initializing..."), "",
|
||||
hintStyle.Render("Press any key to exit"),
|
||||
))
|
||||
successMsg.Render(
|
||||
"System configuration has been saved, and the system is initializing..."), "",
|
||||
)
|
||||
return appStyle.Render(content)
|
||||
}
|
||||
|
||||
// quitView 退出视图
|
||||
func quitView() string {
|
||||
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
||||
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||
errorTitle.Render("Canceled"), "",
|
||||
errorMsg.Render("Initialization canceled, no configuration saved"),
|
||||
))
|
||||
)
|
||||
return appStyle.Render(content)
|
||||
}
|
||||
|
||||
// errorView 错误视图
|
||||
func errorView(err error) string {
|
||||
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
||||
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||
errorTitle.Render("Error"), "",
|
||||
errorMsg.Render(err.Error()), "",
|
||||
hintStyle.Render("Press Ctrl+C to exit"),
|
||||
))
|
||||
}
|
||||
|
||||
// navButtons 导航按钮
|
||||
func navButtons(m model, prev, next string) string {
|
||||
var btns string
|
||||
if m.currentPage == 0 {
|
||||
btns = normalButton.Render(next) + " " + selectedButton.Render(prev)
|
||||
} else {
|
||||
btns = selectedButton.Render(next) + " " + normalButton.Render(prev)
|
||||
}
|
||||
return btns
|
||||
}
|
||||
|
||||
func (m model) renderNavButtons() string {
|
||||
var prevBtn, nextBtn string
|
||||
|
||||
switch m.focusType {
|
||||
case FocusTypePrev:
|
||||
// 焦点在"上一步"
|
||||
nextBtn = normalButton.Render(" Next ")
|
||||
prevBtn = selectedButton.Render(" << Prev >>")
|
||||
case FocusTypeNext:
|
||||
// 焦点在"下一步"
|
||||
nextBtn = selectedButton.Render(" << Next >>")
|
||||
prevBtn = normalButton.Render(" Prev ")
|
||||
default:
|
||||
// 焦点在输入框
|
||||
nextBtn = normalButton.Render(" Next ")
|
||||
prevBtn = normalButton.Render(" Prev ")
|
||||
}
|
||||
|
||||
return lipgloss.JoinHorizontal(
|
||||
lipgloss.Center,
|
||||
nextBtn,
|
||||
" ",
|
||||
prevBtn,
|
||||
)
|
||||
return appStyle.Render(content)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ import "github.com/charmbracelet/lipgloss"
|
||||
var (
|
||||
primaryColor = lipgloss.Color("#7C3AED")
|
||||
secondaryColor = lipgloss.Color("#10B981")
|
||||
titleColor = lipgloss.Color("#8b19a2")
|
||||
errorColor = lipgloss.Color("#EF4444")
|
||||
warnColor = lipgloss.Color("#F59E0B")
|
||||
btnTextColor = lipgloss.Color("#666666") // 深灰色
|
||||
btnbordColor = lipgloss.Color("#3b4147")
|
||||
btnFocusColor = lipgloss.Color("#ffffff")
|
||||
|
||||
// 背景色设为无,让终端自己的背景色生效,避免黑块
|
||||
bgColor = lipgloss.Color("#1F2937")
|
||||
@@ -16,82 +20,92 @@ var (
|
||||
)
|
||||
|
||||
// 容器样式
|
||||
var containerStyle = lipgloss.NewStyle().
|
||||
Padding(2, 4).
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(primaryColor).
|
||||
//Background(bgColor). // 注释掉背景色,防止在某些终端出现黑块
|
||||
Foreground(textColor).
|
||||
//Width(80).
|
||||
Align(lipgloss.Center)
|
||||
var (
|
||||
// 基础布局样式
|
||||
appStyle = lipgloss.NewStyle().
|
||||
Padding(1, 1).
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(primaryColor).
|
||||
Foreground(textColor).
|
||||
Align(lipgloss.Center).
|
||||
Height(40)
|
||||
|
||||
// 标题样式
|
||||
var titleStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(primaryColor).
|
||||
MarginBottom(1)
|
||||
// 标题样式
|
||||
titleStyle = lipgloss.NewStyle().
|
||||
Foreground(titleColor).
|
||||
Padding(0, 1).
|
||||
Bold(true).
|
||||
Align(lipgloss.Center)
|
||||
|
||||
var subTitleStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor).
|
||||
MarginBottom(2)
|
||||
// 子标题/标签样式
|
||||
labelStyle = lipgloss.NewStyle().
|
||||
Width(30).
|
||||
Align(lipgloss.Right).
|
||||
PaddingRight(2)
|
||||
|
||||
// 按钮样式
|
||||
var normalButton = lipgloss.NewStyle().
|
||||
Padding(0, 2).
|
||||
Foreground(lipgloss.Color("#666666")) // 深灰色,更暗
|
||||
valueStyle = lipgloss.NewStyle().
|
||||
Foreground(textColor).
|
||||
Width(50)
|
||||
|
||||
var selectedButton = lipgloss.NewStyle().
|
||||
Bold(true)
|
||||
// 输入框/列表内容样式
|
||||
inputBoxStyle = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(btnbordColor).
|
||||
Padding(0, 1).
|
||||
Width(50)
|
||||
|
||||
// 输入框样式
|
||||
var inputBox = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(primaryColor).
|
||||
Padding(0, 1)
|
||||
// 按钮基础样式
|
||||
btnBaseStyle = lipgloss.NewStyle().
|
||||
Foreground(btnTextColor).
|
||||
Padding(0, 2).
|
||||
Margin(1, 1).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(btnbordColor)
|
||||
|
||||
var labelStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor).
|
||||
Width(12).
|
||||
Align(lipgloss.Right)
|
||||
// 按钮选中/聚焦样式
|
||||
btnSelectedStyle = lipgloss.NewStyle().
|
||||
Foreground(btnFocusColor).
|
||||
Padding(0, 2).
|
||||
Margin(1, 1).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(btnbordColor)
|
||||
|
||||
// 协议框样式
|
||||
var agreementBox = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(warnColor).
|
||||
Padding(1, 2).
|
||||
//Width(70).
|
||||
Align(lipgloss.Left)
|
||||
splitlineStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
// 总结框样式
|
||||
var summaryBox = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.DoubleBorder()).
|
||||
BorderForeground(primaryColor).
|
||||
Padding(0, 0).
|
||||
Foreground(textColor)
|
||||
// 错误提示样式
|
||||
errorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#ff5555")).
|
||||
Bold(true).
|
||||
Width(76)
|
||||
|
||||
// 进度条样式
|
||||
var progressStyle = lipgloss.NewStyle().Foreground(primaryColor)
|
||||
// 协议文本样式
|
||||
licenseTextStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#f8f8f2")).
|
||||
Width(76)
|
||||
|
||||
// 提示信息样式
|
||||
var hintStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor).
|
||||
Italic(true)
|
||||
// 提示文本样式
|
||||
hintStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
Width(76)
|
||||
|
||||
// 成功/错误样式
|
||||
var successTitle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(secondaryColor)
|
||||
infoStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(btnbordColor)
|
||||
|
||||
var successMsg = lipgloss.NewStyle().
|
||||
Foreground(textColor)
|
||||
// 成功/错误提示样式
|
||||
successTitle = lipgloss.NewStyle().
|
||||
Foreground(secondaryColor).
|
||||
Bold(true)
|
||||
|
||||
var errorTitle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(errorColor)
|
||||
successMsg = lipgloss.NewStyle().
|
||||
Foreground(textColor)
|
||||
|
||||
var errorMsg = lipgloss.NewStyle().
|
||||
Foreground(textColor)
|
||||
errorTitle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(errorColor)
|
||||
|
||||
var infoStyle = lipgloss.NewStyle().
|
||||
Foreground(primaryColor).
|
||||
Bold(true)
|
||||
errorMsg = lipgloss.NewStyle().
|
||||
Foreground(textColor)
|
||||
)
|
||||
|
||||
@@ -2,20 +2,12 @@ package wizard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Run 启动初始化向导
|
||||
func Run(force bool) error {
|
||||
// 检查是否已有配置
|
||||
if !force && ConfigExists() {
|
||||
fmt.Println("⚠️ 检测到已有配置文件")
|
||||
fmt.Println(" 使用 --force 参数强制重新初始化")
|
||||
fmt.Println(" 或运行 sunhpc init tui --force")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建程序实例
|
||||
p := tea.NewProgram(initialModel())
|
||||
@@ -27,20 +19,3 @@ func Run(force bool) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getConfigPath 获取配置文件路径
|
||||
func GetConfigPath() string {
|
||||
// 优先使用环境变量
|
||||
if path := os.Getenv("SUNHPC_CONFIG"); path != "" {
|
||||
return path
|
||||
}
|
||||
// 默认路径
|
||||
return "/etc/sunhpc/config.json"
|
||||
}
|
||||
|
||||
// configExists 检查配置文件是否存在
|
||||
func ConfigExists() bool {
|
||||
configPath := GetConfigPath()
|
||||
_, err := os.Stat(configPath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user