add tui command
This commit is contained in:
@@ -290,222 +290,4 @@ nodes:
|
||||
hostname: "compute02.example.local"
|
||||
role: "compute"
|
||||
status: "active"
|
||||
# ... 类似配置,IP地址递增
|
||||
|
||||
# 存储节点
|
||||
storage_nodes:
|
||||
- name: "storage-01"
|
||||
hostname: "storage01.example.local"
|
||||
role: "storage"
|
||||
status: "active"
|
||||
|
||||
basic_info:
|
||||
timezone: "Asia/Shanghai"
|
||||
cpu: "Intel Xeon Silver 4210 2.2GHz (20核)"
|
||||
memory: "128GB DDR4"
|
||||
os: "CentOS 7.9"
|
||||
storage_software: "Ceph"
|
||||
|
||||
network:
|
||||
interfaces:
|
||||
- name: "eth0"
|
||||
ip_address: "192.168.1.21"
|
||||
network_type: "management"
|
||||
speed: "1Gbps"
|
||||
|
||||
- name: "eth1"
|
||||
ip_address: "172.16.1.21"
|
||||
network_type: "storage_frontend"
|
||||
speed: "10Gbps"
|
||||
|
||||
- name: "eth2"
|
||||
ip_address: "172.16.2.21"
|
||||
network_type: "storage_backend"
|
||||
speed: "25Gbps"
|
||||
|
||||
- name: "eth3"
|
||||
ip_address: "172.16.3.21"
|
||||
network_type: "cluster"
|
||||
speed: "10Gbps"
|
||||
|
||||
disk:
|
||||
- device: "/dev/sda"
|
||||
size: "240GB"
|
||||
type: "SSD"
|
||||
mount_point: "/"
|
||||
filesystem: "xfs"
|
||||
usage: "系统盘"
|
||||
|
||||
- device: "/dev/sdb"
|
||||
size: "480GB"
|
||||
type: "SSD"
|
||||
mount_point: "/var/lib/ceph/osd/ceph-0"
|
||||
filesystem: "xfs"
|
||||
usage: "OSD (日志/WAL)"
|
||||
|
||||
- device: "/dev/sdc"
|
||||
size: "8TB"
|
||||
type: "HDD"
|
||||
mount_point: "/var/lib/ceph/osd/ceph-1"
|
||||
filesystem: "xfs"
|
||||
usage: "OSD (数据)"
|
||||
|
||||
- device: "/dev/sdd"
|
||||
size: "8TB"
|
||||
type: "HDD"
|
||||
mount_point: "/var/lib/ceph/osd/ceph-2"
|
||||
filesystem: "xfs"
|
||||
usage: "OSD (数据)"
|
||||
|
||||
services:
|
||||
enabled:
|
||||
- "sshd"
|
||||
- "ntpd"
|
||||
- "ceph-mon"
|
||||
- "ceph-mgr"
|
||||
- "ceph-osd"
|
||||
|
||||
ceph_config:
|
||||
cluster_name: "ceph-prod"
|
||||
fsid: "12345678-1234-1234-1234-123456789012"
|
||||
mon_hosts:
|
||||
- "192.168.1.21"
|
||||
- "192.168.1.22"
|
||||
- "192.168.1.23"
|
||||
|
||||
- name: "storage-02"
|
||||
# ... 类似配置
|
||||
|
||||
# 其他节点
|
||||
other_nodes:
|
||||
# 管理节点
|
||||
- name: "management-01"
|
||||
hostname: "mgmt01.example.local"
|
||||
role: "management"
|
||||
status: "active"
|
||||
|
||||
basic_info:
|
||||
timezone: "Asia/Shanghai"
|
||||
cpu: "Intel Xeon Bronze 3204 1.9GHz (6核)"
|
||||
memory: "64GB DDR4"
|
||||
os: "CentOS 7.9"
|
||||
|
||||
network:
|
||||
interfaces:
|
||||
- name: "eth0"
|
||||
ip_address: "192.168.1.31"
|
||||
network_type: "management"
|
||||
speed: "1Gbps"
|
||||
|
||||
services:
|
||||
enabled:
|
||||
- "sshd"
|
||||
- "ntpd"
|
||||
- "ansible"
|
||||
- "salt-master"
|
||||
- "jumpserver"
|
||||
|
||||
# 网关节点
|
||||
- name: "gateway-01"
|
||||
hostname: "gw01.example.local"
|
||||
role: "gateway"
|
||||
status: "active"
|
||||
|
||||
basic_info:
|
||||
timezone: "Asia/Shanghai"
|
||||
cpu: "Intel Xeon E-2234 3.6GHz (4核)"
|
||||
memory: "32GB DDR4"
|
||||
os: "pfSense 2.5.2"
|
||||
|
||||
network:
|
||||
interfaces:
|
||||
- name: "wan"
|
||||
ip_address: "202.96.128.86"
|
||||
network_type: "external"
|
||||
speed: "1Gbps"
|
||||
|
||||
- name: "lan"
|
||||
ip_address: "192.168.1.254"
|
||||
network_type: "internal"
|
||||
speed: "1Gbps"
|
||||
|
||||
- name: "dmz"
|
||||
ip_address: "192.168.100.254"
|
||||
network_type: "dmz"
|
||||
speed: "1Gbps"
|
||||
|
||||
services:
|
||||
enabled:
|
||||
- "ssh"
|
||||
- "dnsmasq"
|
||||
- "nginx"
|
||||
- "haproxy"
|
||||
- "keepalived"
|
||||
|
||||
# 监控节点
|
||||
- name: "monitoring-01"
|
||||
hostname: "mon01.example.local"
|
||||
role: "monitoring"
|
||||
status: "active"
|
||||
|
||||
basic_info:
|
||||
timezone: "Asia/Shanghai"
|
||||
cpu: "Intel Xeon Silver 4208 2.1GHz (8核)"
|
||||
memory: "64GB DDR4"
|
||||
os: "Ubuntu 20.04 LTS"
|
||||
|
||||
services:
|
||||
enabled:
|
||||
- "prometheus"
|
||||
- "grafana"
|
||||
- "alertmanager"
|
||||
- "elasticsearch"
|
||||
- "kibana"
|
||||
- "filebeat"
|
||||
|
||||
|
||||
|
||||
# 节点基础数据
|
||||
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: ""
|
||||
# 软件基础数据
|
||||
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 并行计算库"
|
||||
# ... 类似配置,IP地址递增
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// - db/*/*.yaml : 匹配data/一级子目录下的所有yaml文件.
|
||||
// - 如需递归匹配子目录(如data/db/sub/*.yaml),用 data/**/*.yaml(Go.18+)
|
||||
//
|
||||
//go:embed db/*.yaml services/*.yaml firewall/*.yaml
|
||||
//go:embed services/*.yaml
|
||||
var ConfigFS embed.FS
|
||||
|
||||
// GetConfigFile 获取指定目录下的的单个配置文件内容
|
||||
|
||||
41
go.mod
41
go.mod
@@ -3,32 +3,55 @@ module sunhpc
|
||||
go 1.25.5
|
||||
|
||||
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/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
|
||||
)
|
||||
|
||||
require (
|
||||
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/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/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-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/rivo/uniseg v0.4.7 // 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/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // 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/zap v1.27.1 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.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/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/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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
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/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
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/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/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
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/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
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-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/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/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/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/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
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/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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -14,6 +14,7 @@ func NewInitCmd() *cobra.Command {
|
||||
|
||||
cmd.AddCommand(NewInitDBCmd())
|
||||
cmd.AddCommand(NewInitCfgCmd())
|
||||
cmd.AddCommand(NewInitTuiCmd())
|
||||
|
||||
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