Compare commits
2 Commits
a3917c5a15
...
ce9af9f7d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
ce9af9f7d0
|
|||
|
fbe6aec707
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sunhpc
|
||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
// - db/*/*.yaml : 匹配data/一级子目录下的所有yaml文件.
|
// - db/*/*.yaml : 匹配data/一级子目录下的所有yaml文件.
|
||||||
// - 如需递归匹配子目录(如data/db/sub/*.yaml),用 data/**/*.yaml(Go.18+)
|
// - 如需递归匹配子目录(如data/db/sub/*.yaml),用 data/**/*.yaml(Go.18+)
|
||||||
//
|
//
|
||||||
//go:embed db/*.yaml firewall/*.yaml
|
//go:embed frontend/*.yaml firewall/*.yaml
|
||||||
var ConfigFS embed.FS
|
var ConfigFS embed.FS
|
||||||
|
|
||||||
// GetConfigFile 获取指定目录下的的单个配置文件内容
|
// GetConfigFile 获取指定目录下的的单个配置文件内容
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
# 基础数据配置文件
|
|
||||||
version: 1.0
|
|
||||||
description: "SunHPC 基础数据配置"
|
|
||||||
|
|
||||||
# 节点基础数据
|
|
||||||
nodes:
|
|
||||||
- name: frontend
|
|
||||||
cpus: 4
|
|
||||||
memory: 8192
|
|
||||||
disk: 100
|
|
||||||
rack: null
|
|
||||||
rank: null
|
|
||||||
arch: x86_64
|
|
||||||
os: linux
|
|
||||||
runaction: os
|
|
||||||
installaction: os
|
|
||||||
status: active
|
|
||||||
description: "管理节点"
|
|
||||||
|
|
||||||
# 属性基础数据
|
|
||||||
attributes:
|
|
||||||
# 国家地区
|
|
||||||
- node_name: frontend # 通过节点名称关联
|
|
||||||
attr: country
|
|
||||||
value: CN
|
|
||||||
shadow: ""
|
|
||||||
- node_name: frontend
|
|
||||||
attr: state
|
|
||||||
value: Liaoning
|
|
||||||
shadow: ""
|
|
||||||
- node_name: frontend
|
|
||||||
attr: city
|
|
||||||
value: Shenyang
|
|
||||||
shadow: ""
|
|
||||||
|
|
||||||
# 网络配置
|
|
||||||
- node_name: frontend
|
|
||||||
attr: network_type
|
|
||||||
value: management
|
|
||||||
shadow: ""
|
|
||||||
- node_name: frontend
|
|
||||||
attr: ip_address
|
|
||||||
value: 192.168.1.100
|
|
||||||
shadow: ""
|
|
||||||
- node_name: frontend
|
|
||||||
attr: subnet_mask
|
|
||||||
value: 255.255.255.0
|
|
||||||
shadow: ""
|
|
||||||
|
|
||||||
# 硬件信息
|
|
||||||
- node_name: frontend
|
|
||||||
attr: manufacturer
|
|
||||||
value: Dell
|
|
||||||
shadow: ""
|
|
||||||
- node_name: frontend
|
|
||||||
attr: model
|
|
||||||
value: PowerEdge R740
|
|
||||||
shadow: ""
|
|
||||||
|
|
||||||
# 系统配置
|
|
||||||
- node_name: frontend
|
|
||||||
attr: timezone
|
|
||||||
value: Asia/Shanghai
|
|
||||||
shadow: ""
|
|
||||||
- node_name: frontend
|
|
||||||
attr: language
|
|
||||||
value: zh_CN.UTF-8
|
|
||||||
shadow: ""
|
|
||||||
- node_name: frontend
|
|
||||||
attr: kernel_version
|
|
||||||
value: "5.10.0"
|
|
||||||
shadow: ""
|
|
||||||
|
|
||||||
# 软件基础数据
|
|
||||||
software:
|
|
||||||
- name: openssl
|
|
||||||
version: "1.1.1k"
|
|
||||||
vendor: OpenSSL
|
|
||||||
install_method: source
|
|
||||||
is_installed: 0
|
|
||||||
description: "加密库"
|
|
||||||
|
|
||||||
- name: slurm
|
|
||||||
version: "23.02"
|
|
||||||
vendor: SchedMD
|
|
||||||
install_method: source
|
|
||||||
is_installed: 0
|
|
||||||
description: "作业调度系统"
|
|
||||||
|
|
||||||
- name: openmpi
|
|
||||||
version: "4.1.5"
|
|
||||||
vendor: OpenMPI
|
|
||||||
install_method: source
|
|
||||||
is_installed: 0
|
|
||||||
description: "MPI 并行计算库"
|
|
||||||
|
|
||||||
# 网络基础数据
|
|
||||||
networks:
|
|
||||||
- node_name: frontend
|
|
||||||
interface: eth0
|
|
||||||
ip_address: 192.168.1.100
|
|
||||||
netmask: 255.255.255.0
|
|
||||||
gateway: 192.168.1.1
|
|
||||||
type: management
|
|
||||||
mac: "00:11:22:33:44:55"
|
|
||||||
|
|
||||||
# 分区基础数据
|
|
||||||
partitions:
|
|
||||||
- node_name: frontend
|
|
||||||
device: /dev/sda1
|
|
||||||
mount_point: /boot
|
|
||||||
size: 1024
|
|
||||||
fs_type: ext4
|
|
||||||
- node_name: frontend
|
|
||||||
device: /dev/sda2
|
|
||||||
mount_point: /
|
|
||||||
size: 102400
|
|
||||||
fs_type: ext4
|
|
||||||
- node_name: frontend
|
|
||||||
device: /dev/sda3
|
|
||||||
mount_point: /home
|
|
||||||
size: 51200
|
|
||||||
fs_type: ext4
|
|
||||||
293
data/confs/frontend/config.yaml
Normal file
293
data/confs/frontend/config.yaml
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# 数据中心/集群基础配置
|
||||||
|
metadata:
|
||||||
|
version: "1.0"
|
||||||
|
last_updated: "2024-01-01"
|
||||||
|
description: "数据中心基础设施配置"
|
||||||
|
|
||||||
|
# 集群配置
|
||||||
|
cluster:
|
||||||
|
name: "sunhpc-cluster"
|
||||||
|
type: "control"
|
||||||
|
osname: "Rocky Linux"
|
||||||
|
osversion: "9.7"
|
||||||
|
location:
|
||||||
|
country: "China"
|
||||||
|
city: "Beijing"
|
||||||
|
timezone:
|
||||||
|
name: "Asia/Shanghai"
|
||||||
|
offset: "+08:00"
|
||||||
|
ntp_servers:
|
||||||
|
- "ntp1.aliyun.com"
|
||||||
|
- "ntp2.tencent.com"
|
||||||
|
- "pool.ntp.org"
|
||||||
|
environment:
|
||||||
|
type: "production" # production/staging/development
|
||||||
|
region: "华北"
|
||||||
|
availability_zone: "AZ-01"
|
||||||
|
network:
|
||||||
|
domain: "sunhpc.local"
|
||||||
|
dns:
|
||||||
|
primary: "8.8.8.8"
|
||||||
|
secondary: "114.114.114.114"
|
||||||
|
wan:
|
||||||
|
- interface: "eth0"
|
||||||
|
address: "202.96.128.86"
|
||||||
|
netmask: "255.255.255.0"
|
||||||
|
gateway: "202.96.128.1"
|
||||||
|
mtu: 1500
|
||||||
|
type: "public"
|
||||||
|
description: "public network"
|
||||||
|
lan:
|
||||||
|
- interface: "eth1"
|
||||||
|
address: "192.168.1.100"
|
||||||
|
netmask: "255.255.255.0"
|
||||||
|
gateway: ""
|
||||||
|
mtu: 1500
|
||||||
|
type: "management"
|
||||||
|
description: "management network"
|
||||||
|
disks:
|
||||||
|
- device: "/dev/sda"
|
||||||
|
model: "PowerVault ME484"
|
||||||
|
type: "ssd"
|
||||||
|
size: "50TB"
|
||||||
|
vendor: "Dell"
|
||||||
|
serial: "1234567890"
|
||||||
|
status: "online"
|
||||||
|
|
||||||
|
partition:
|
||||||
|
- name: "sda1"
|
||||||
|
usage: "boot partition"
|
||||||
|
mount: "/boot"
|
||||||
|
size: "16GB"
|
||||||
|
fstype: "ext4"
|
||||||
|
filesystem: "ext4"
|
||||||
|
uuid: "12345678-90ab-cdef-1234-567890abcdef"
|
||||||
|
|
||||||
|
- name: "sda2"
|
||||||
|
usage: "root partition"
|
||||||
|
mount: "/"
|
||||||
|
size: "100GB"
|
||||||
|
fstype: "ext4"
|
||||||
|
filesystem: "ext4"
|
||||||
|
uuid: "12345678-90ab-cdef-1234-567890abcdef"
|
||||||
|
options: "defaults,noatime"
|
||||||
|
|
||||||
|
- name: "sda3"
|
||||||
|
usage: "home partition"
|
||||||
|
mount: "/home"
|
||||||
|
size: "50TB"
|
||||||
|
fstype: "xfs"
|
||||||
|
filesystem: "ext4"
|
||||||
|
uuid: "12345678-90ab-cdef-1234-567890abcdef"
|
||||||
|
|
||||||
|
- name: "sda4"
|
||||||
|
usage: "var partition"
|
||||||
|
mount: "/var"
|
||||||
|
size: "150GB"
|
||||||
|
fstype: "xfs"
|
||||||
|
filesystem: "xfs"
|
||||||
|
uuid: "12345678-90ab-cdef-1234-567890abcdef"
|
||||||
|
|
||||||
|
- device: "/dev/sdb"
|
||||||
|
model: "PowerVault ME484"
|
||||||
|
type: "ssd"
|
||||||
|
size: "50TB"
|
||||||
|
vendor: "Dell"
|
||||||
|
serial: "1234567890"
|
||||||
|
status: "online"
|
||||||
|
|
||||||
|
partition:
|
||||||
|
- name: "sdb1"
|
||||||
|
usage: "data partition"
|
||||||
|
mount: "/data"
|
||||||
|
size: "50TB"
|
||||||
|
fstype: "xfs"
|
||||||
|
filesystem: "xfs"
|
||||||
|
uuid: "12345678-90ab-cdef-1234-567890abcdef"
|
||||||
|
|
||||||
|
|
||||||
|
firewall:
|
||||||
|
global_policies:
|
||||||
|
- name: "默认策略"
|
||||||
|
input: "drop"
|
||||||
|
output: "accept"
|
||||||
|
forward: "drop"
|
||||||
|
|
||||||
|
zones:
|
||||||
|
- name: "public"
|
||||||
|
interfaces: ["eth0", "eth1"]
|
||||||
|
services_allowed: ["ssh", "http", "https"]
|
||||||
|
source_ranges: ["0.0.0.0/0"]
|
||||||
|
|
||||||
|
- name: "internal"
|
||||||
|
interfaces: ["eth2"]
|
||||||
|
services_allowed: ["ssh", "mysql", "redis", "mongodb", "nfs", "samba"]
|
||||||
|
source_ranges: ["192.168.0.0/16", "10.0.0.0/8"]
|
||||||
|
|
||||||
|
- name: "storage"
|
||||||
|
interfaces: ["eth3"]
|
||||||
|
services_allowed: ["iscsi", "nfs", "smb"]
|
||||||
|
source_ranges: ["172.16.0.0/12"]
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- name: "允许Ping"
|
||||||
|
protocol: "icmp"
|
||||||
|
action: "accept"
|
||||||
|
source: "any"
|
||||||
|
destination: "any"
|
||||||
|
|
||||||
|
- name: "限制SSH访问"
|
||||||
|
protocol: "tcp"
|
||||||
|
port: 22
|
||||||
|
action: "accept"
|
||||||
|
source: "192.168.1.0/24"
|
||||||
|
destination: "any"
|
||||||
|
|
||||||
|
# 全局服务配置
|
||||||
|
services:
|
||||||
|
common_services:
|
||||||
|
- name: "sshd"
|
||||||
|
port: 22
|
||||||
|
protocol: "tcp"
|
||||||
|
enabled: true
|
||||||
|
description: "SSH远程登录服务"
|
||||||
|
|
||||||
|
- name: "ntpd"
|
||||||
|
port: 123
|
||||||
|
protocol: "udp"
|
||||||
|
enabled: true
|
||||||
|
description: "时间同步服务"
|
||||||
|
|
||||||
|
- name: "rsyslog"
|
||||||
|
port: 514
|
||||||
|
protocol: "udp"
|
||||||
|
enabled: true
|
||||||
|
description: "日志收集服务"
|
||||||
|
|
||||||
|
monitoring_services:
|
||||||
|
- name: "prometheus"
|
||||||
|
port: 9090
|
||||||
|
protocol: "tcp"
|
||||||
|
enabled: true
|
||||||
|
description: "监控数据采集"
|
||||||
|
|
||||||
|
- name: "grafana"
|
||||||
|
port: 3000
|
||||||
|
protocol: "tcp"
|
||||||
|
enabled: true
|
||||||
|
description: "监控数据可视化"
|
||||||
|
|
||||||
|
- name: "node_exporter"
|
||||||
|
port: 9100
|
||||||
|
protocol: "tcp"
|
||||||
|
enabled: true
|
||||||
|
description: "节点指标采集"
|
||||||
|
|
||||||
|
database_services:
|
||||||
|
- name: "mysql"
|
||||||
|
port: 3306
|
||||||
|
protocol: "tcp"
|
||||||
|
enabled: true
|
||||||
|
version: "8.0"
|
||||||
|
description: "关系型数据库"
|
||||||
|
|
||||||
|
- name: "redis"
|
||||||
|
port: 6379
|
||||||
|
protocol: "tcp"
|
||||||
|
enabled: true
|
||||||
|
version: "6.2"
|
||||||
|
description: "缓存数据库"
|
||||||
|
|
||||||
|
- name: "mongodb"
|
||||||
|
port: 27017
|
||||||
|
protocol: "tcp"
|
||||||
|
enabled: true
|
||||||
|
version: "5.0"
|
||||||
|
description: "文档数据库"
|
||||||
|
|
||||||
|
# 节点列表
|
||||||
|
nodes:
|
||||||
|
# 计算节点
|
||||||
|
compute_nodes:
|
||||||
|
- name: "compute-01"
|
||||||
|
hostname: "compute01.example.local"
|
||||||
|
role: "compute"
|
||||||
|
status: "active"
|
||||||
|
|
||||||
|
basic_info:
|
||||||
|
timezone: "Asia/Shanghai"
|
||||||
|
cpu: "Intel Xeon Gold 6248R 3.0GHz (48核)"
|
||||||
|
memory: "512GB DDR4"
|
||||||
|
os: "CentOS 7.9"
|
||||||
|
kernel: "3.10.0-1160"
|
||||||
|
virtualization: "KVM"
|
||||||
|
|
||||||
|
network:
|
||||||
|
interfaces:
|
||||||
|
- name: "eth0"
|
||||||
|
ip_address: "192.168.1.11"
|
||||||
|
mac_address: "00:0c:29:xx:xx:01"
|
||||||
|
network_type: "management"
|
||||||
|
speed: "1Gbps"
|
||||||
|
|
||||||
|
disk:
|
||||||
|
- device: "/dev/sda"
|
||||||
|
size: "480GB"
|
||||||
|
type: "SSD"
|
||||||
|
mount_point: "/"
|
||||||
|
filesystem: "xfs"
|
||||||
|
usage: "系统盘"
|
||||||
|
|
||||||
|
- device: "/dev/sdb"
|
||||||
|
size: "3.6TB"
|
||||||
|
type: "NVMe"
|
||||||
|
mount_point: "/data/local"
|
||||||
|
filesystem: "xfs"
|
||||||
|
usage: "本地数据盘"
|
||||||
|
|
||||||
|
- device: "/dev/sdc"
|
||||||
|
size: "10TB"
|
||||||
|
type: "HDD"
|
||||||
|
mount_point: "/data/shared"
|
||||||
|
filesystem: "xfs"
|
||||||
|
usage: "共享存储挂载"
|
||||||
|
|
||||||
|
services:
|
||||||
|
enabled:
|
||||||
|
- "sshd"
|
||||||
|
- "ntpd"
|
||||||
|
- "docker"
|
||||||
|
- "kubelet"
|
||||||
|
- "node_exporter"
|
||||||
|
disabled:
|
||||||
|
- "firewalld"
|
||||||
|
- "postfix"
|
||||||
|
|
||||||
|
firewall:
|
||||||
|
enabled: true
|
||||||
|
rules:
|
||||||
|
- port: 22
|
||||||
|
protocol: "tcp"
|
||||||
|
source: "192.168.1.0/24"
|
||||||
|
action: "accept"
|
||||||
|
- port: 10250
|
||||||
|
protocol: "tcp"
|
||||||
|
source: "10.10.0.0/16"
|
||||||
|
action: "accept"
|
||||||
|
|
||||||
|
hardware:
|
||||||
|
manufacturer: "Dell"
|
||||||
|
model: "PowerEdge R740xd"
|
||||||
|
serial_number: "ABC123XYZ"
|
||||||
|
warranty_expiry: "2025-12-31"
|
||||||
|
|
||||||
|
location:
|
||||||
|
rack: "RACK-01"
|
||||||
|
position: "01U"
|
||||||
|
power_consumption: "500W"
|
||||||
|
|
||||||
|
- name: "compute-02"
|
||||||
|
hostname: "compute02.example.local"
|
||||||
|
role: "compute"
|
||||||
|
status: "active"
|
||||||
|
# ... 类似配置,IP地址递增
|
||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
// - db/*/*.yaml : 匹配data/一级子目录下的所有yaml文件.
|
// - db/*/*.yaml : 匹配data/一级子目录下的所有yaml文件.
|
||||||
// - 如需递归匹配子目录(如data/db/sub/*.yaml),用 data/**/*.yaml(Go.18+)
|
// - 如需递归匹配子目录(如data/db/sub/*.yaml),用 data/**/*.yaml(Go.18+)
|
||||||
//
|
//
|
||||||
//go:embed db/*.yaml services/*.yaml firewall/*.yaml
|
//go:embed services/*.yaml
|
||||||
var ConfigFS embed.FS
|
var ConfigFS embed.FS
|
||||||
|
|
||||||
// GetConfigFile 获取指定目录下的的单个配置文件内容
|
// GetConfigFile 获取指定目录下的的单个配置文件内容
|
||||||
|
|||||||
41
go.mod
41
go.mod
@@ -3,32 +3,55 @@ module sunhpc
|
|||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
github.com/gdamore/tcell/v2 v2.13.8
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.34
|
||||||
|
github.com/rivo/tview v0.42.0
|
||||||
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
|
go.uber.org/zap v1.27.1
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
github.com/gdamore/encoding v1.0.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
go.uber.org/zap v1.27.1 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
golang.org/x/term v0.37.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
100
go.sum
100
go.sum
@@ -1,14 +1,42 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||||
|
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||||
|
github.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3RlfU=
|
||||||
|
github.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
@@ -21,17 +49,30 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
|
||||||
|
github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
@@ -56,18 +97,57 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -2,18 +2,14 @@ package initcmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sunhpc/internal/middler/auth"
|
"sunhpc/internal/middler/auth"
|
||||||
"sunhpc/pkg/logger"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewConfigCmd 创建 "init config" 命令
|
// NewConfigCmd 创建 "init config" 命令
|
||||||
func NewInitCfgCmd() *cobra.Command {
|
func NewInitCfgCmd() *cobra.Command {
|
||||||
var (
|
var (
|
||||||
force bool
|
output string
|
||||||
path string
|
|
||||||
verbose bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -24,23 +20,19 @@ func NewInitCfgCmd() *cobra.Command {
|
|||||||
|
|
||||||
示例:
|
示例:
|
||||||
sunhpc init config # 生成默认配置文件
|
sunhpc init config # 生成默认配置文件
|
||||||
sunhpc init config -f # 强制覆盖已有配置文件
|
sunhpc init config -o /etc/sunhpc/sunhpc.yaml # 指定路径
|
||||||
sunhpc init config -p /etc/sunhpc/sunhpc.yaml # 指定路径
|
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := auth.RequireRoot(); err != nil {
|
if err := auth.RequireRoot(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("✅ 配置文件已生成", zap.String("path", path))
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义局部 flags
|
// 定义局部 flags
|
||||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "强制覆盖已有配置文件")
|
cmd.Flags().StringVarP(&output, "output", "o", "", "指定配置文件路径")
|
||||||
cmd.Flags().StringVarP(&path, "path", "p", "", "指定配置文件路径")
|
|
||||||
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "显示详细日志")
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func NewInitCmd() *cobra.Command {
|
|||||||
|
|
||||||
cmd.AddCommand(NewInitDBCmd())
|
cmd.AddCommand(NewInitDBCmd())
|
||||||
cmd.AddCommand(NewInitCfgCmd())
|
cmd.AddCommand(NewInitCfgCmd())
|
||||||
|
cmd.AddCommand(NewInitTuiCmd())
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
33
internal/cli/init/tui.go
Normal file
33
internal/cli/init/tui.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package initcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sunhpc/internal/middler/auth"
|
||||||
|
|
||||||
|
"sunhpc/pkg/wizard"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewInitTuiCmd() *cobra.Command {
|
||||||
|
var force bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "tui",
|
||||||
|
Short: "初始化TUI",
|
||||||
|
Long: `初始化SunHPC TUI,创建所有表结构和默认数据。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
sunhpc init tui # 初始化TUI
|
||||||
|
sunhpc init tui --force # 强制重新初始化`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := auth.RequireRoot(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wizard.Run(force)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&force, "force", "f", false, "强制重新初始化")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
192
pkg/wizard/config.go
Normal file
192
pkg/wizard/config.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package wizard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("序列化配置失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("保存配置文件失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig 从文件加载配置
|
||||||
|
func loadConfig() (*Config, error) {
|
||||||
|
configPath := GetConfigPath()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取配置文件失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析配置文件失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) saveDataPage() {
|
||||||
|
if len(m.textInputs) >= 8 {
|
||||||
|
m.config.Hostname = m.textInputs[0].Value()
|
||||||
|
m.config.Country = m.textInputs[1].Value()
|
||||||
|
m.config.Region = m.textInputs[2].Value()
|
||||||
|
m.config.Timezone = m.textInputs[3].Value()
|
||||||
|
m.config.HomePage = m.textInputs[4].Value()
|
||||||
|
m.config.DBAddress = m.textInputs[5].Value()
|
||||||
|
m.config.DBName = m.textInputs[6].Value()
|
||||||
|
m.config.DataAddress = m.textInputs[7].Value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) savePublicNetworkPage() {
|
||||||
|
if len(m.textInputs) >= 4 {
|
||||||
|
m.config.PublicInterface = m.textInputs[0].Value()
|
||||||
|
m.config.IPAddress = m.textInputs[1].Value()
|
||||||
|
m.config.Netmask = m.textInputs[2].Value()
|
||||||
|
m.config.Gateway = m.textInputs[3].Value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) saveInternalNetworkPage() {
|
||||||
|
if len(m.textInputs) >= 3 {
|
||||||
|
m.config.InternalInterface = m.textInputs[0].Value()
|
||||||
|
m.config.InternalIP = m.textInputs[1].Value()
|
||||||
|
m.config.InternalMask = 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 }{
|
||||||
|
{"主机名:", m.config.Hostname},
|
||||||
|
{"国家:", m.config.Country},
|
||||||
|
{"地区:", m.config.Region},
|
||||||
|
{"时区:", m.config.Timezone},
|
||||||
|
{"主页:", m.config.HomePage},
|
||||||
|
{"数据库地址:", m.config.DBAddress},
|
||||||
|
{"数据库名称:", m.config.DBName},
|
||||||
|
{"Data 地址:", m.config.DataAddress},
|
||||||
|
}
|
||||||
|
for _, f := range fields {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = ""
|
||||||
|
ti.Placeholder = f.label
|
||||||
|
//ti.Placeholder = "请输入" + f.label[:len(f.label)-1]
|
||||||
|
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{"Hostname", "Country", "Region", "Timezone", "Homepage", "DBPath", "DBName", "Software"}
|
||||||
|
|
||||||
|
case PagePublicNetwork:
|
||||||
|
fields := []struct{ label, value string }{
|
||||||
|
{"公网接口:", m.config.PublicInterface},
|
||||||
|
{"IP 地址:", m.config.IPAddress},
|
||||||
|
{"子网掩码:", m.config.Netmask},
|
||||||
|
{"网关:", m.config.Gateway},
|
||||||
|
}
|
||||||
|
for _, f := range fields {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = ""
|
||||||
|
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{"Wan iface", "IPAddress", "Netmask", "Gateway"}
|
||||||
|
|
||||||
|
case PageInternalNetwork:
|
||||||
|
fields := []struct{ label, value string }{
|
||||||
|
{"内网接口:", m.config.InternalInterface},
|
||||||
|
{"内网 IP:", m.config.InternalIP},
|
||||||
|
{"内网掩码:", m.config.InternalMask},
|
||||||
|
}
|
||||||
|
for _, f := range fields {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = ""
|
||||||
|
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{"Lan iface", "IPAddress", "Netmask"}
|
||||||
|
|
||||||
|
case PageDNS:
|
||||||
|
fields := []struct{ label, value string }{
|
||||||
|
{"主 DNS:", m.config.DNSPrimary},
|
||||||
|
{"备 DNS:", m.config.DNSSecondary},
|
||||||
|
}
|
||||||
|
for _, f := range fields {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = ""
|
||||||
|
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{"DNSPrimary", "DNSSecondary"}
|
||||||
|
}
|
||||||
|
}
|
||||||
333
pkg/wizard/model.go
Normal file
333
pkg/wizard/model.go
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
package wizard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config 系统配置结构
|
||||||
|
type Config struct {
|
||||||
|
// 协议
|
||||||
|
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"`
|
||||||
|
DBName string `json:"db_name"`
|
||||||
|
DataAddress string `json:"data_address"`
|
||||||
|
|
||||||
|
// 公网设置
|
||||||
|
PublicInterface string `json:"public_interface"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
Netmask string `json:"netmask"`
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
|
|
||||||
|
// 内网配置
|
||||||
|
InternalInterface string `json:"internal_interface"`
|
||||||
|
InternalIP string `json:"internal_ip"`
|
||||||
|
InternalMask string `json:"internal_mask"`
|
||||||
|
|
||||||
|
// DNS 配置
|
||||||
|
DNSPrimary string `json:"dns_primary"`
|
||||||
|
DNSSecondary string `json:"dns_secondary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageType 页面类型
|
||||||
|
type PageType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PageAgreement PageType = iota
|
||||||
|
PageData
|
||||||
|
PagePublicNetwork
|
||||||
|
PageInternalNetwork
|
||||||
|
PageDNS
|
||||||
|
PageSummary
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FocusTypeInput int = 0
|
||||||
|
FocusTypePrev int = 1
|
||||||
|
FocusTypeNext int = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// model TUI 主模型
|
||||||
|
type model struct {
|
||||||
|
config Config
|
||||||
|
currentPage PageType
|
||||||
|
totalPages int
|
||||||
|
textInputs []textinput.Model
|
||||||
|
inputLabels []string // 存储标签
|
||||||
|
focusIndex int
|
||||||
|
focusType int // 0=输入框, 1=上一步按钮, 2=下一步按钮
|
||||||
|
agreementIdx int // 0=拒绝,1=接受
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
err error
|
||||||
|
quitting bool
|
||||||
|
done bool
|
||||||
|
force bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultConfig 返回默认配置
|
||||||
|
func defaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Hostname: "sunhpc01",
|
||||||
|
Country: "China",
|
||||||
|
Region: "Beijing",
|
||||||
|
Timezone: "Asia/Shanghai",
|
||||||
|
HomePage: "https://sunhpc.example.com",
|
||||||
|
DBAddress: "127.0.0.1",
|
||||||
|
DBName: "sunhpc_db",
|
||||||
|
DataAddress: "/data/sunhpc",
|
||||||
|
PublicInterface: "eth0",
|
||||||
|
InternalInterface: "eth1",
|
||||||
|
IPAddress: "192.168.1.100",
|
||||||
|
Netmask: "255.255.255.0",
|
||||||
|
Gateway: "192.168.1.1",
|
||||||
|
InternalIP: "10.0.0.100",
|
||||||
|
InternalMask: "255.255.255.0",
|
||||||
|
DNSPrimary: "8.8.8.8",
|
||||||
|
DNSSecondary: "8.8.4.4",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialModel 初始化模型
|
||||||
|
func initialModel() model {
|
||||||
|
cfg := defaultConfig()
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
m.initPageInputs()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init 初始化命令
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return textinput.Blink
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
case "ctrl+c":
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case "esc":
|
||||||
|
if m.currentPage > 0 {
|
||||||
|
return m.prevPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "enter":
|
||||||
|
return m.handleEnter()
|
||||||
|
|
||||||
|
case "tab", "shift+tab", "up", "down", "left", "right":
|
||||||
|
return m.handleNavigation(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
|
||||||
|
// 动态调整容器宽度
|
||||||
|
/*
|
||||||
|
if msg.Width > 100 {
|
||||||
|
containerStyle = containerStyle.Width(90)
|
||||||
|
} else if msg.Width > 80 {
|
||||||
|
containerStyle = containerStyle.Width(70)
|
||||||
|
} else {
|
||||||
|
containerStyle = containerStyle.Width(msg.Width - 10)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ✅ 动态计算容器宽度(终端宽度的 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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
|
||||||
|
}
|
||||||
366
pkg/wizard/pages.go
Normal file
366
pkg/wizard/pages.go
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
package wizard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// View 渲染视图
|
||||||
|
func (m model) View() string {
|
||||||
|
if m.done {
|
||||||
|
return successView()
|
||||||
|
}
|
||||||
|
if m.quitting {
|
||||||
|
return quitView()
|
||||||
|
}
|
||||||
|
if m.err != nil {
|
||||||
|
return errorView(m.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var page string
|
||||||
|
switch m.currentPage {
|
||||||
|
case PageAgreement:
|
||||||
|
page = m.agreementView()
|
||||||
|
case PageData:
|
||||||
|
page = m.dataView()
|
||||||
|
case PagePublicNetwork:
|
||||||
|
page = m.publicNetworkView()
|
||||||
|
case PageInternalNetwork:
|
||||||
|
page = m.internalNetworkView()
|
||||||
|
case PageDNS:
|
||||||
|
page = m.dnsView()
|
||||||
|
case PageSummary:
|
||||||
|
page = m.summaryView()
|
||||||
|
}
|
||||||
|
|
||||||
|
content := strings.Builder{}
|
||||||
|
content.WriteString(page)
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
content.WriteString(progressView(m.currentPage, m.totalPages))
|
||||||
|
|
||||||
|
return containerStyle.Render(content.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// agreementView 协议页面
|
||||||
|
func (m model) agreementView() string {
|
||||||
|
title := titleStyle.Render("SunHPC 系统初始化向导")
|
||||||
|
subtitle := subTitleStyle.Render("请先阅读并同意以下协议")
|
||||||
|
|
||||||
|
agreement := agreementBox.Render(`
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ SunHPC 软件许可协议 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
1. 许可授予
|
||||||
|
本软件授予您非独占、不可转让的使用许可。
|
||||||
|
|
||||||
|
2. 使用限制
|
||||||
|
- 不得用于非法目的
|
||||||
|
- 不得反向工程或反编译
|
||||||
|
- 不得移除版权标识
|
||||||
|
|
||||||
|
3. 免责声明
|
||||||
|
本软件按"原样"提供,不提供任何明示或暗示的保证。
|
||||||
|
|
||||||
|
4. 责任限制
|
||||||
|
在任何情况下,作者不对因使用本软件造成的任何损失负责。
|
||||||
|
|
||||||
|
5. 协议终止
|
||||||
|
如违反本协议条款,许可将自动终止。
|
||||||
|
|
||||||
|
───────────────────────────────────────────────────────────────
|
||||||
|
请仔细阅读以上条款,点击"接受"表示您同意并遵守本协议。
|
||||||
|
───────────────────────────────────────────────────────────────
|
||||||
|
`)
|
||||||
|
|
||||||
|
var acceptBtn, rejectBtn string
|
||||||
|
if m.agreementIdx == 0 {
|
||||||
|
rejectBtn = selectedButton.Render(">> 拒绝 <<")
|
||||||
|
acceptBtn = selectedButton.Render(" 同意 ")
|
||||||
|
} else {
|
||||||
|
rejectBtn = selectedButton.Render(" 拒绝 ")
|
||||||
|
acceptBtn = selectedButton.Render(">> 同意 <<")
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonGroup := lipgloss.JoinHorizontal(
|
||||||
|
lipgloss.Center,
|
||||||
|
acceptBtn, " ", rejectBtn)
|
||||||
|
|
||||||
|
// ✅ 添加调试信息(确认 agreementIdx 的值)
|
||||||
|
// debugInfo := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).
|
||||||
|
// Render(fmt.Sprintf("[DEBUG: idx=%d]", m.agreementIdx),)
|
||||||
|
|
||||||
|
hint := hintStyle.Render("使用 <- -> 或 Tab 选择,Enter 确认")
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
title, "",
|
||||||
|
subtitle, "",
|
||||||
|
agreement, "",
|
||||||
|
buttonGroup, "",
|
||||||
|
// debugInfo, "", // ✅ 显示调试信息
|
||||||
|
hint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dataView 数据接收页面
|
||||||
|
func (m model) dataView() string {
|
||||||
|
title := titleStyle.Render("集群基础配置")
|
||||||
|
subtitle := subTitleStyle.Render("请填写系统基本信息")
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons := m.renderNavButtons()
|
||||||
|
|
||||||
|
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
title, "",
|
||||||
|
subtitle, "",
|
||||||
|
inputs.String(), "",
|
||||||
|
buttons, "",
|
||||||
|
hint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// publicNetworkView 公网设置页面
|
||||||
|
func (m model) publicNetworkView() string {
|
||||||
|
title := titleStyle.Render("公网配置")
|
||||||
|
subtitle := subTitleStyle.Render("请配置网络接口信息")
|
||||||
|
|
||||||
|
autoDetect := infoStyle.Render("[*] 自动检测网络接口: eth0, eth1, ens33")
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons := m.renderNavButtons()
|
||||||
|
|
||||||
|
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
title, "",
|
||||||
|
subtitle, "",
|
||||||
|
autoDetect, "",
|
||||||
|
inputs.String(), "",
|
||||||
|
buttons, "",
|
||||||
|
hint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// internalNetworkView 内网配置页面
|
||||||
|
func (m model) internalNetworkView() string {
|
||||||
|
title := titleStyle.Render("内网配置")
|
||||||
|
subtitle := subTitleStyle.Render("请配置内网信息")
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons := m.renderNavButtons()
|
||||||
|
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
title, "",
|
||||||
|
subtitle, "",
|
||||||
|
inputs.String(), "",
|
||||||
|
buttons, "",
|
||||||
|
hint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsView DNS 配置页面
|
||||||
|
func (m model) dnsView() string {
|
||||||
|
title := titleStyle.Render("DNS 配置")
|
||||||
|
subtitle := subTitleStyle.Render("请配置 DNS 服务器")
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons := m.renderNavButtons()
|
||||||
|
|
||||||
|
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
title, "",
|
||||||
|
subtitle, "",
|
||||||
|
inputs.String(), "",
|
||||||
|
buttons, "",
|
||||||
|
hint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// summaryView 总结页面
|
||||||
|
func (m model) summaryView() string {
|
||||||
|
title := titleStyle.Render("配置总结")
|
||||||
|
subtitle := subTitleStyle.Render("请确认以下配置信息")
|
||||||
|
|
||||||
|
summary := summaryBox.Render(fmt.Sprintf(`
|
||||||
|
+---------------------------------------------+
|
||||||
|
基本信息
|
||||||
|
+---------------------------------------------+
|
||||||
|
主机名:%-35s
|
||||||
|
国 家: %-31s
|
||||||
|
地 区:%-31s
|
||||||
|
时 区:%-38s
|
||||||
|
主 页:%-38s
|
||||||
|
+---------------------------------------------+
|
||||||
|
数据库
|
||||||
|
+---------------------------------------------+
|
||||||
|
地 址:%-38s
|
||||||
|
名 称:%-38s
|
||||||
|
软 件:%-33s
|
||||||
|
+---------------------------------------------+
|
||||||
|
公网配置
|
||||||
|
+---------------------------------------------+
|
||||||
|
接 口:%-38s
|
||||||
|
地 址: %-41s
|
||||||
|
掩 码:%-38s
|
||||||
|
网 关:%-38s
|
||||||
|
+---------------------------------------------+
|
||||||
|
内网配置
|
||||||
|
+---------------------------------------------+
|
||||||
|
接 口:%-38s
|
||||||
|
地 址: %-41s
|
||||||
|
掩 码:%-38s
|
||||||
|
+---------------------------------------------+
|
||||||
|
DNS
|
||||||
|
+---------------------------------------------+
|
||||||
|
主 DNS: %-37s
|
||||||
|
备 DNS: %-37s
|
||||||
|
+---------------------------------------------+
|
||||||
|
`,
|
||||||
|
m.config.Hostname,
|
||||||
|
m.config.Country,
|
||||||
|
m.config.Region,
|
||||||
|
m.config.Timezone,
|
||||||
|
m.config.HomePage,
|
||||||
|
m.config.DBAddress,
|
||||||
|
m.config.DBName,
|
||||||
|
m.config.DataAddress,
|
||||||
|
m.config.PublicInterface,
|
||||||
|
m.config.IPAddress,
|
||||||
|
m.config.Netmask,
|
||||||
|
m.config.Gateway,
|
||||||
|
m.config.InternalInterface,
|
||||||
|
m.config.InternalIP,
|
||||||
|
m.config.InternalMask,
|
||||||
|
m.config.DNSPrimary,
|
||||||
|
m.config.DNSSecondary,
|
||||||
|
))
|
||||||
|
|
||||||
|
var buttons string
|
||||||
|
if m.focusIndex == 0 {
|
||||||
|
buttons = selectedButton.Render("[>] 执行初始化") + " " + normalButton.Render("[ ] 取消")
|
||||||
|
} else {
|
||||||
|
buttons = normalButton.Render("[>] 执行初始化") + " " + selectedButton.Render("[ ] 取消")
|
||||||
|
}
|
||||||
|
|
||||||
|
hint := hintStyle.Render("使用 <- -> 或 Tab 选择,Enter 确认")
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
title, "",
|
||||||
|
subtitle, "",
|
||||||
|
summary, "",
|
||||||
|
buttons, "",
|
||||||
|
hint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{"协议", "数据", "公网", "内网", "DNS", "总结"}
|
||||||
|
label := labelStyle.Render(labels[current])
|
||||||
|
return progressStyle.Render(progress) + " " + label
|
||||||
|
}
|
||||||
|
|
||||||
|
// successView 成功视图
|
||||||
|
func successView() string {
|
||||||
|
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
successTitle.Render("初始化完成!"), "",
|
||||||
|
successMsg.Render("系统配置已保存,正在初始化..."), "",
|
||||||
|
hintStyle.Render("按任意键退出"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// quitView 退出视图
|
||||||
|
func quitView() string {
|
||||||
|
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
errorTitle.Render("已取消"), "",
|
||||||
|
errorMsg.Render("初始化已取消,未保存任何配置"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorView 错误视图
|
||||||
|
func errorView(err error) string {
|
||||||
|
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
errorTitle.Render("错误"), "",
|
||||||
|
errorMsg.Render(err.Error()), "",
|
||||||
|
hintStyle.Render("按 Ctrl+C 退出"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// navButtons 导航按钮
|
||||||
|
func navButtons(m model, prev, next string) string {
|
||||||
|
var btns string
|
||||||
|
if m.currentPage == 0 {
|
||||||
|
btns = normalButton.Render(prev) + " " + selectedButton.Render(next)
|
||||||
|
} else {
|
||||||
|
btns = selectedButton.Render(prev) + " " + normalButton.Render(next)
|
||||||
|
}
|
||||||
|
return btns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) renderNavButtons() string {
|
||||||
|
var prevBtn, nextBtn string
|
||||||
|
|
||||||
|
switch m.focusType {
|
||||||
|
case FocusTypePrev:
|
||||||
|
// 焦点在"上一步"
|
||||||
|
prevBtn = selectedButton.Render("<< 上一步 >>")
|
||||||
|
nextBtn = normalButton.Render("下一步 >>")
|
||||||
|
case FocusTypeNext:
|
||||||
|
// 焦点在"下一步"
|
||||||
|
prevBtn = normalButton.Render("<< 上一步")
|
||||||
|
nextBtn = selectedButton.Render("<< 下一步 >>")
|
||||||
|
default:
|
||||||
|
// 焦点在输入框
|
||||||
|
prevBtn = normalButton.Render("<< 上一步")
|
||||||
|
nextBtn = normalButton.Render("下一步 >>")
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinHorizontal(
|
||||||
|
lipgloss.Center,
|
||||||
|
prevBtn,
|
||||||
|
" ",
|
||||||
|
nextBtn,
|
||||||
|
)
|
||||||
|
}
|
||||||
99
pkg/wizard/styles.go
Normal file
99
pkg/wizard/styles.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package wizard
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
// 颜色定义
|
||||||
|
var (
|
||||||
|
primaryColor = lipgloss.Color("#7C3AED")
|
||||||
|
secondaryColor = lipgloss.Color("#10B981")
|
||||||
|
errorColor = lipgloss.Color("#EF4444")
|
||||||
|
warnColor = lipgloss.Color("#F59E0B")
|
||||||
|
|
||||||
|
// 背景色设为无,让终端自己的背景色生效,避免黑块
|
||||||
|
bgColor = lipgloss.Color("#1F2937")
|
||||||
|
textColor = lipgloss.Color("#FFFFFF")
|
||||||
|
mutedColor = lipgloss.Color("#B0B0B0")
|
||||||
|
)
|
||||||
|
|
||||||
|
// 容器样式
|
||||||
|
var containerStyle = lipgloss.NewStyle().
|
||||||
|
Padding(2, 4).
|
||||||
|
BorderStyle(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(primaryColor).
|
||||||
|
//Background(bgColor). // 注释掉背景色,防止在某些终端出现黑块
|
||||||
|
Foreground(textColor).
|
||||||
|
//Width(80).
|
||||||
|
Align(lipgloss.Center)
|
||||||
|
|
||||||
|
// 标题样式
|
||||||
|
var titleStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(primaryColor).
|
||||||
|
MarginBottom(1)
|
||||||
|
|
||||||
|
var subTitleStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(mutedColor).
|
||||||
|
MarginBottom(2)
|
||||||
|
|
||||||
|
// 按钮样式
|
||||||
|
var normalButton = lipgloss.NewStyle().
|
||||||
|
Padding(0, 2).
|
||||||
|
Foreground(lipgloss.Color("#666666")) // 深灰色,更暗
|
||||||
|
|
||||||
|
var selectedButton = lipgloss.NewStyle().
|
||||||
|
Padding(0, 2).
|
||||||
|
Foreground(lipgloss.Color("#3d4747ff")). // 亮绿色
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
// 输入框样式
|
||||||
|
var inputBox = lipgloss.NewStyle().
|
||||||
|
BorderStyle(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(primaryColor).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
var labelStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(mutedColor).
|
||||||
|
Width(12).
|
||||||
|
Align(lipgloss.Right)
|
||||||
|
|
||||||
|
// 协议框样式
|
||||||
|
var agreementBox = lipgloss.NewStyle().
|
||||||
|
BorderStyle(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(warnColor).
|
||||||
|
Padding(1, 2).
|
||||||
|
//Width(70).
|
||||||
|
Align(lipgloss.Left)
|
||||||
|
|
||||||
|
// 总结框样式
|
||||||
|
var summaryBox = lipgloss.NewStyle().
|
||||||
|
BorderStyle(lipgloss.DoubleBorder()).
|
||||||
|
BorderForeground(primaryColor).
|
||||||
|
Padding(0, 0).
|
||||||
|
Foreground(textColor)
|
||||||
|
|
||||||
|
// 进度条样式
|
||||||
|
var progressStyle = lipgloss.NewStyle().Foreground(primaryColor)
|
||||||
|
|
||||||
|
// 提示信息样式
|
||||||
|
var hintStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(mutedColor).
|
||||||
|
Italic(true)
|
||||||
|
|
||||||
|
// 成功/错误样式
|
||||||
|
var successTitle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(secondaryColor)
|
||||||
|
|
||||||
|
var successMsg = lipgloss.NewStyle().
|
||||||
|
Foreground(textColor)
|
||||||
|
|
||||||
|
var errorTitle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(errorColor)
|
||||||
|
|
||||||
|
var errorMsg = lipgloss.NewStyle().
|
||||||
|
Foreground(textColor)
|
||||||
|
|
||||||
|
var infoStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(primaryColor).
|
||||||
|
Bold(true)
|
||||||
46
pkg/wizard/wizard.go
Normal file
46
pkg/wizard/wizard.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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())
|
||||||
|
|
||||||
|
// 运行程序
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
return fmt.Errorf("初始化向导运行失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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