feat: 增加类GUI显示,增强参数解析

This commit is contained in:
spiritlhl
2026-04-16 12:13:06 +08:00
parent 4e868a384a
commit 04725fef54
6 changed files with 896 additions and 56 deletions

14
go.mod
View File

@@ -3,6 +3,8 @@ module github.com/oneclickvirt/ecs
go 1.25.4 go 1.25.4
require ( require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/imroc/req/v3 v3.54.0 github.com/imroc/req/v3 v3.54.0
github.com/oneclickvirt/UnlockTests v0.0.35-20260207053956 github.com/oneclickvirt/UnlockTests v0.0.35-20260207053956
github.com/oneclickvirt/backtrace v0.0.8-20251109090457 github.com/oneclickvirt/backtrace v0.0.8-20251109090457
@@ -25,8 +27,14 @@ require (
github.com/StackExchange/wmi v1.2.1 // indirect github.com/StackExchange/wmi v1.2.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect github.com/ebitengine/purego v0.8.4 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
@@ -48,15 +56,20 @@ require (
github.com/koron/go-ssdp v0.0.4 // indirect github.com/koron/go-ssdp v0.0.4 // indirect
github.com/libp2p/go-nat v0.2.0 // indirect github.com/libp2p/go-nat v0.2.0 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/miekg/dns v1.1.61 // indirect github.com/miekg/dns v1.1.61 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // 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/nxtrace/NTrace-core v1.5.0 // indirect github.com/nxtrace/NTrace-core v1.5.0 // indirect
github.com/oneclickvirt/dd v0.0.2-20250808062818 // indirect github.com/oneclickvirt/dd v0.0.2-20250808062818 // indirect
github.com/oneclickvirt/fio v0.0.2-20250808045755 // indirect github.com/oneclickvirt/fio v0.0.2-20250808045755 // indirect
@@ -94,6 +107,7 @@ require (
github.com/tklauser/numcpus v0.9.0 // indirect github.com/tklauser/numcpus v0.9.0 // indirect
github.com/tsosunchia/powclient v0.2.0 // indirect github.com/tsosunchia/powclient v0.2.0 // indirect
github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f // indirect github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect

31
go.sum
View File

@@ -6,6 +6,20 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
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/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
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.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
@@ -16,6 +30,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 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/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -75,12 +91,16 @@ github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk=
github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk=
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
@@ -94,6 +114,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/nxtrace/NTrace-core v1.5.0 h1:n+a/FObw/+CcqvhuSQiWcm1q+ODtfo7Wt3VmaIx504I= github.com/nxtrace/NTrace-core v1.5.0 h1:n+a/FObw/+CcqvhuSQiWcm1q+ODtfo7Wt3VmaIx504I=
github.com/nxtrace/NTrace-core v1.5.0/go.mod h1:/jME48iJ7QaVTzsrTPQyTJ+yExhjeWjax2L6uBd4ckk= github.com/nxtrace/NTrace-core v1.5.0/go.mod h1:/jME48iJ7QaVTzsrTPQyTJ+yExhjeWjax2L6uBd4ckk=
github.com/oneclickvirt/UnlockTests v0.0.35-20260207053956 h1:yccGrw/sYOHZMaFJghPVN3Xn6JyTOXsEQc9v0I92k3M= github.com/oneclickvirt/UnlockTests v0.0.35-20260207053956 h1:yccGrw/sYOHZMaFJghPVN3Xn6JyTOXsEQc9v0I92k3M=
@@ -216,6 +242,8 @@ github.com/tsosunchia/powclient v0.2.0 h1:BDrI3O69CbzarbD+CnnY10Kuwn8xlmtQR0m5tB
github.com/tsosunchia/powclient v0.2.0/go.mod h1:fkb7tTW+HMH3ZWZzQUgwvvFKMj/8Ys+C8Sm/uGQzDA0= github.com/tsosunchia/powclient v0.2.0/go.mod h1:fkb7tTW+HMH3ZWZzQUgwvvFKMj/8Ys+C8Sm/uGQzDA0=
github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f h1:glX3VZCYwW1/OmFxOjazfCtBLxXB3YNZk9LF2lYx+Lw= github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f h1:glX3VZCYwW1/OmFxOjazfCtBLxXB3YNZk9LF2lYx+Lw=
github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f/go.mod h1:gh//RKyt2Gesx3eOj3ulzrSQ60ySj2UA4qnOdrtarvg= github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f/go.mod h1:gh//RKyt2Gesx3eOj3ulzrSQ60ySj2UA4qnOdrtarvg=
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -238,6 +266,8 @@ golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -267,6 +297,7 @@ golang.org/x/sys v0.0.0-20201008064518-c1f3e3309c71/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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-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-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -27,7 +27,7 @@ import (
) )
var ( var (
ecsVersion = "v0.1.117" // 融合怪版本号 ecsVersion = "v0.1.118" // 融合怪版本号
configs = params.NewConfig(ecsVersion) // 全局配置实例 configs = params.NewConfig(ecsVersion) // 全局配置实例
userSetFlags = make(map[string]bool) // 用于跟踪哪些参数是用户显式设置的 userSetFlags = make(map[string]bool) // 用于跟踪哪些参数是用户显式设置的
) )

View File

@@ -21,7 +21,7 @@ func GetMenuChoice(language string) string {
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigChan) defer signal.Stop(sigChan)
go func() { go func() {
select { select {
case <-sigChan: case <-sigChan:
@@ -35,7 +35,7 @@ func GetMenuChoice(language string) string {
return return
} }
}() }()
for { for {
var input string var input string
if language == "zh" { if language == "zh" {
@@ -46,7 +46,7 @@ func GetMenuChoice(language string) string {
fmt.Scanln(&input) fmt.Scanln(&input)
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
input = strings.TrimRight(input, "\n") input = strings.TrimRight(input, "\n")
re := regexp.MustCompile(`^\d+$`) re := regexp.MustCompile(`^\d+$`)
if re.MatchString(input) { if re.MatchString(input) {
inChoice := input inChoice := input
@@ -125,13 +125,13 @@ func PrintMenuOptions(preCheck utils.NetCheckResult, config *params.Config) {
} }
fmt.Printf("使用统计: %s\n", statsInfo) fmt.Printf("使用统计: %s\n", statsInfo)
} }
fmt.Println("1. 融合怪完全体(能测全测)") fmt.Println("1. 融合怪完全体(能测全测)")
fmt.Println("2. 极简版(系统信息+CPU+内存+磁盘+测速节点5个)") fmt.Println("2. 极简版(系统信息+CPU+内存+磁盘+测速节点5个)")
fmt.Println("3. 精简版(系统信息+CPU+内存+磁盘+跨国平台解锁+路由+测速节点5个)") fmt.Println("3. 精简版(系统信息+CPU+内存+磁盘+跨国平台解锁+路由+测速节点5个)")
fmt.Println("4. 精简网络版(系统信息+CPU+内存+磁盘+回程+路由+测速节点5个)") fmt.Println("4. 精简网络版(系统信息+CPU+内存+磁盘+回程+路由+测速节点5个)")
fmt.Println("5. 精简解锁版(系统信息+CPU+内存+磁盘IO+跨国平台解锁+测速节点5个)") fmt.Println("5. 精简解锁版(系统信息+CPU+内存+磁盘IO+跨国平台解锁+测速节点5个)")
fmt.Println("6. 网络单项(IP质量检测+上游及三网回程+广州三网回程详细路由+全国延迟+TGDC+网站延迟+测速节点11个)") fmt.Println("6. 网络单项(IP质量检测+上游及三网回程+广州三网回程详细路由+全国延迟+TGDC+网站延迟+测速节点11个)")
fmt.Println("7. 解锁单项(跨国平台解锁)") fmt.Println("7. 解锁单项(跨国平台解锁)")
fmt.Println("8. 硬件单项(系统信息+CPU+dd磁盘测试+fio磁盘测试)") fmt.Println("8. 硬件单项(系统信息+CPU+dd磁盘测试+fio磁盘测试)")
fmt.Println("9. IP质量检测(15个数据库的IP质量检测+邮件端口检测)") fmt.Println("9. IP质量检测(15个数据库的IP质量检测+邮件端口检测)")
fmt.Println("10. 三网回程线路检测+三网回程详细路由(北京上海广州成都)+全国延迟+TGDC+网站延迟") fmt.Println("10. 三网回程线路检测+三网回程详细路由(北京上海广州成都)+全国延迟+TGDC+网站延迟")
@@ -145,20 +145,20 @@ func PrintMenuOptions(preCheck utils.NetCheckResult, config *params.Config) {
} }
fmt.Printf("%s\n", statsInfo) fmt.Printf("%s\n", statsInfo)
} }
fmt.Println("1. VPS Fusion Monster Test (Full Test)") fmt.Println("1. VPS Fusion Monster Test (Full Test)")
fmt.Println("2. Minimal Test Suite (System Info + CPU + Memory + Disk + 5 Speed Test Nodes)") fmt.Println("2. Minimal Test Suite (System Info + CPU + Memory + Disk + 5 Speed Test Nodes)")
fmt.Println("3. Standard Test Suite (System Info + CPU + Memory + Disk + International Platform Unlock + Routing + 5 Speed Test Nodes)") fmt.Println("3. Standard Test Suite (System Info + CPU + Memory + Disk + International Platform Unlock + Routing + 5 Speed Test Nodes)")
fmt.Println("4. Network-Focused Test Suite (System Info + CPU + Memory + Disk + Backtrace + Routing + 5 Speed Test Nodes)") fmt.Println("4. Network-Focused Test Suite (System Info + CPU + Memory + Disk + Backtrace + Routing + 5 Speed Test Nodes)")
fmt.Println("5. Unlock-Focused Test Suite (System Info + CPU + Memory + Disk IO + International Platform Unlock + 5 Speed Test Nodes)") fmt.Println("5. Unlock-Focused Test Suite (System Info + CPU + Memory + Disk IO + International Platform Unlock + 5 Speed Test Nodes)")
fmt.Println("6. Network-Only Test (IP Quality Test + Upstream & 3-Network Backtrace + Guangzhou 3-Network Detailed Routing + National Latency + TGDC + Websites + 11 Speed Test Nodes)") fmt.Println("6. Network-Only Test (IP Quality Test + Upstream & 3-Network Backtrace + Guangzhou 3-Network Detailed Routing + National Latency + TGDC + Websites + 11 Speed Test Nodes)")
fmt.Println("7. Unlock-Only Test (International Platform Unlock)") fmt.Println("7. Unlock-Only Test (International Platform Unlock)")
fmt.Println("8. Hardware-Only Test (System Info + CPU + Memory + dd Disk Test + fio Disk Test)") fmt.Println("8. Hardware-Only Test (System Info + CPU + Memory + dd Disk Test + fio Disk Test)")
fmt.Println("9. IP Quality Test (IP Test with 15 Databases + Email Port Test)") fmt.Println("9. IP Quality Test (IP Test with 15 Databases + Email Port Test)")
fmt.Println("0. Exit Program") fmt.Println("0. Exit Program")
} }
} }
// HandleMenuMode handles menu selection // HandleMenuMode handles menu selection using the interactive TUI
func HandleMenuMode(preCheck utils.NetCheckResult, config *params.Config) { func HandleMenuMode(preCheck utils.NetCheckResult, config *params.Config) {
savedParams := config.SaveUserSetParams() savedParams := config.SaveUserSetParams()
config.BasicStatus = false config.BasicStatus = false
@@ -174,63 +174,47 @@ func HandleMenuMode(preCheck utils.NetCheckResult, config *params.Config) {
config.TgdcTestStatus = false config.TgdcTestStatus = false
config.WebTestStatus = false config.WebTestStatus = false
config.AutoChangeDiskMethod = true config.AutoChangeDiskMethod = true
PrintMenuOptions(preCheck, config)
Loop: result := RunTuiMenu(preCheck, config)
for { if result.quit {
config.Choice = GetMenuChoice(config.Language) os.Exit(0)
switch config.Choice { }
// Update language if changed by TUI selection
config.Language = result.language
if result.custom {
applyCustomResult(result, preCheck, config)
if config.SpeedTestStatus {
config.OnlyChinaTest = utils.CheckChina(config.EnableLogger, config.Language)
}
} else {
config.Choice = result.choice
switch result.choice {
case "0": case "0":
os.Exit(0) os.Exit(0)
case "1": case "1":
SetFullTestStatus(preCheck, config) SetFullTestStatus(preCheck, config)
config.OnlyChinaTest = utils.CheckChina(config.EnableLogger, config.Language) config.OnlyChinaTest = utils.CheckChina(config.EnableLogger, config.Language)
break Loop
case "2": case "2":
SetMinimalTestStatus(preCheck, config) SetMinimalTestStatus(preCheck, config)
break Loop
case "3": case "3":
SetStandardTestStatus(preCheck, config) SetStandardTestStatus(preCheck, config)
break Loop
case "4": case "4":
SetNetworkFocusedTestStatus(preCheck, config) SetNetworkFocusedTestStatus(preCheck, config)
break Loop
case "5": case "5":
SetUnlockFocusedTestStatus(preCheck, config) SetUnlockFocusedTestStatus(preCheck, config)
break Loop
case "6": case "6":
if !preCheck.Connected {
fmt.Println("Can not test without network connection!")
return
}
SetNetworkOnlyTestStatus(config) SetNetworkOnlyTestStatus(config)
break Loop
case "7": case "7":
if !preCheck.Connected {
fmt.Println("Can not test without network connection!")
return
}
SetUnlockOnlyTestStatus(config) SetUnlockOnlyTestStatus(config)
break Loop
case "8": case "8":
SetHardwareOnlyTestStatus(preCheck, config) SetHardwareOnlyTestStatus(preCheck, config)
break Loop
case "9": case "9":
if !preCheck.Connected {
fmt.Println("Can not test without network connection!")
return
}
SetIPQualityTestStatus(config) SetIPQualityTestStatus(config)
break Loop
case "10": case "10":
if !preCheck.Connected {
fmt.Println("Can not test without network connection!")
return
}
config.Nt3Location = "ALL" config.Nt3Location = "ALL"
SetRouteTestStatus(config) SetRouteTestStatus(config)
break Loop
default:
PrintInvalidChoice(config.Language)
} }
} }
config.RestoreUserSetParams(savedParams) config.RestoreUserSetParams(savedParams)

762
internal/menu/tui.go Normal file
View File

@@ -0,0 +1,762 @@
package menu
import (
"fmt"
"os"
"strings"
"sync"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/oneclickvirt/ecs/internal/params"
"github.com/oneclickvirt/ecs/utils"
)
// --- Styles ---
var (
tTitleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("39"))
tInfoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
tWarnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214"))
tSelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("120")).Bold(true)
tNormStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
tDimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
tHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
tSectStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true)
tChkOnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("120"))
tChkOffStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
tBtnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("0")).Background(lipgloss.Color("120")).Bold(true).Padding(0, 2)
tBtnDimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252")).Background(lipgloss.Color("238")).Padding(0, 2)
)
// --- Types ---
type menuPhase int
const (
phaseLang menuPhase = iota
phaseMain
phaseCustom
)
type mainMenuItem struct {
id string
zh string
en string
needNet bool
}
type testToggle struct {
nameZh string
nameEn string
enabled bool
needNet bool
}
type advSetting struct {
nameZh string
nameEn string
options []string
current int
}
type tuiResult struct {
choice string
language string
quit bool
custom bool
toggles []testToggle
advanced []advSetting
}
type tuiModel struct {
phase menuPhase
config *params.Config
preCheck utils.NetCheckResult
langPreset bool
// Language selection
langCursor int
// Main menu
mainCursor int
mainItems []mainMenuItem
// Custom mode
customCursor int
toggles []testToggle
advanced []advSetting
customTotal int // toggles + advanced + 1 (confirm button)
// Pre-loaded info
statsTotal int
statsDaily int
hasStats bool
cmpVersion int
newVersion string
// Result
result tuiResult
width int
height int
}
// --- Default data ---
func defaultMainItems() []mainMenuItem {
return []mainMenuItem{
{id: "1", zh: "融合怪完全体(能测全测)", en: "Full Test (All Available Tests)", needNet: false},
{id: "2", zh: "极简版(系统信息+CPU+内存+磁盘+测速节点5个)", en: "Minimal Suite (SysInfo+CPU+Mem+Disk+5 Speed Nodes)", needNet: false},
{id: "3", zh: "精简版(系统信息+CPU+内存+磁盘+解锁+路由+测速节点5个)", en: "Standard Suite (SysInfo+CPU+Mem+Disk+Unlock+Route+5 Speed Nodes)", needNet: false},
{id: "4", zh: "精简网络版(系统信息+CPU+内存+磁盘+回程+路由+测速节点5个)", en: "Network Suite (SysInfo+CPU+Mem+Disk+Backtrace+Route+5 Speed Nodes)", needNet: false},
{id: "5", zh: "精简解锁版(系统信息+CPU+内存+磁盘IO+解锁+测速节点5个)", en: "Unlock Suite (SysInfo+CPU+Mem+DiskIO+Unlock+5 Speed Nodes)", needNet: false},
{id: "6", zh: "网络单项(IP质量+回程+路由+延迟+TGDC+网站+测速节点11个)", en: "Network Only (IPQuality+Backtrace+Route+Latency+TGDC+Web+11 Speed Nodes)", needNet: true},
{id: "7", zh: "解锁单项(跨国平台解锁)", en: "Unlock Only (International Platform Unlock)", needNet: true},
{id: "8", zh: "硬件单项(系统信息+CPU+内存+dd磁盘+fio磁盘)", en: "Hardware Only (SysInfo+CPU+Mem+DD Disk+FIO Disk)", needNet: false},
{id: "9", zh: "IP质量检测(15个数据库+邮件端口检测)", en: "IP Quality (15 Databases + Email Port Test)", needNet: true},
{id: "10", zh: "三网回程线路+路由+延迟+TGDC+网站延迟", en: "3-Net Backtrace+Route+Latency+TGDC+Websites", needNet: true},
{id: "custom", zh: ">>> 自定义测试(自由选择测试项和高级设置)", en: ">>> Custom Test (Choose Tests & Advanced Settings)", needNet: false},
{id: "0", zh: "退出程序", en: "Exit Program", needNet: false},
}
}
func defaultTestToggles() []testToggle {
return []testToggle{
{nameZh: "基础系统信息", nameEn: "Basic System Info", enabled: true, needNet: false},
{nameZh: "CPU测试", nameEn: "CPU Test", enabled: true, needNet: false},
{nameZh: "内存测试", nameEn: "Memory Test", enabled: true, needNet: false},
{nameZh: "磁盘测试", nameEn: "Disk Test", enabled: true, needNet: false},
{nameZh: "跨国平台解锁", nameEn: "Streaming Unlock", enabled: false, needNet: true},
{nameZh: "IP质量检测", nameEn: "IP Quality Check", enabled: false, needNet: true},
{nameZh: "邮件端口检测", nameEn: "Email Port Check", enabled: false, needNet: true},
{nameZh: "回程路由", nameEn: "Backtrace Route", enabled: false, needNet: true},
{nameZh: "NT3路由", nameEn: "NT3 Route", enabled: false, needNet: true},
{nameZh: "测速", nameEn: "Speed Test", enabled: false, needNet: true},
{nameZh: "Ping测试", nameEn: "Ping Test", enabled: false, needNet: true},
{nameZh: "TGDC测试", nameEn: "TGDC Test", enabled: false, needNet: true},
{nameZh: "网站延迟", nameEn: "Website Latency", enabled: false, needNet: true},
}
}
func defaultAdvSettings(config *params.Config) []advSetting {
adv := []advSetting{
{nameZh: "CPU测试方法", nameEn: "CPU Method", options: []string{"sysbench", "geekbench", "winsat"}, current: 0},
{nameZh: "CPU线程模式", nameEn: "CPU Thread", options: []string{"multi", "single"}, current: 0},
{nameZh: "内存测试方法", nameEn: "Memory Method", options: []string{"stream", "sysbench", "dd", "winsat", "auto"}, current: 0},
{nameZh: "磁盘测试方法", nameEn: "Disk Method", options: []string{"fio", "dd", "winsat"}, current: 0},
{nameZh: "NT3测试位置", nameEn: "NT3 Location", options: []string{"GZ", "SH", "BJ", "CD", "ALL"}, current: 0},
{nameZh: "NT3测试类型", nameEn: "NT3 Type", options: []string{"ipv4", "ipv6", "both"}, current: 0},
{nameZh: "测速节点数/运营商", nameEn: "Speed Nodes/ISP", options: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, current: 1},
}
// Set current values from config
setAdvCurrent := func(idx int, val string) {
for j, opt := range adv[idx].options {
if opt == val {
adv[idx].current = j
return
}
}
}
setAdvCurrent(0, config.CpuTestMethod)
setAdvCurrent(1, config.CpuTestThreadMode)
setAdvCurrent(2, config.MemoryTestMethod)
setAdvCurrent(3, config.DiskTestMethod)
setAdvCurrent(4, config.Nt3Location)
setAdvCurrent(5, config.Nt3CheckType)
if config.SpNum >= 1 && config.SpNum <= 10 {
adv[6].current = config.SpNum - 1
}
return adv
}
func newTuiModel(preCheck utils.NetCheckResult, config *params.Config, langPreset bool, statsTotal, statsDaily int, hasStats bool, cmpVersion int, newVersion string) tuiModel {
toggles := defaultTestToggles()
advanced := defaultAdvSettings(config)
m := tuiModel{
config: config,
preCheck: preCheck,
langPreset: langPreset,
mainItems: defaultMainItems(),
toggles: toggles,
advanced: advanced,
customTotal: len(toggles) + len(advanced) + 1,
statsTotal: statsTotal,
statsDaily: statsDaily,
hasStats: hasStats,
cmpVersion: cmpVersion,
newVersion: newVersion,
width: 80,
height: 24,
}
if langPreset {
m.phase = phaseMain
m.result.language = config.Language
} else {
m.phase = phaseLang
}
return m
}
// --- Bubbletea Interface ---
func (m tuiModel) Init() tea.Cmd {
return nil
}
func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
return m, nil
case tea.KeyMsg:
switch m.phase {
case phaseLang:
return m.updateLang(msg)
case phaseMain:
return m.updateMain(msg)
case phaseCustom:
return m.updateCustom(msg)
}
}
return m, nil
}
func (m tuiModel) View() string {
switch m.phase {
case phaseLang:
return m.viewLang()
case phaseMain:
return m.viewMain()
case phaseCustom:
return m.viewCustom()
}
return ""
}
// --- Language Selection ---
func (m tuiModel) updateLang(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "up", "k":
if m.langCursor > 0 {
m.langCursor--
}
case "down", "j":
if m.langCursor < 1 {
m.langCursor++
}
case "1":
m.result.language = "zh"
m.phase = phaseMain
case "2":
m.result.language = "en"
m.phase = phaseMain
case "enter":
if m.langCursor == 0 {
m.result.language = "zh"
} else {
m.result.language = "en"
}
m.phase = phaseMain
case "q", "ctrl+c":
m.result.quit = true
return m, tea.Quit
}
return m, nil
}
func (m tuiModel) viewLang() string {
var s strings.Builder
s.WriteString("\n")
s.WriteString(tTitleStyle.Render(" VPS融合怪测试 / VPS Fusion Monster Test"))
s.WriteString("\n\n")
s.WriteString(tInfoStyle.Render(" 请选择语言 / Please select language:"))
s.WriteString("\n\n")
langs := []struct {
label string
desc string
}{
{"1. 中文", "Chinese"},
{"2. English", "英文"},
}
for i, l := range langs {
cursor := " "
style := tNormStyle
if m.langCursor == i {
cursor = " > "
style = tSelStyle
}
s.WriteString(fmt.Sprintf("%s%s %s\n", cursor, style.Render(l.label), tDimStyle.Render(l.desc)))
}
s.WriteString("\n")
s.WriteString(tHelpStyle.Render(" ↑/↓ Navigate Enter Confirm 1/2 Quick-Select q Quit"))
s.WriteString("\n")
return s.String()
}
// --- Main Menu ---
func (m tuiModel) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
key := msg.String()
switch key {
case "up", "k":
if m.mainCursor > 0 {
m.mainCursor--
}
case "down", "j":
if m.mainCursor < len(m.mainItems)-1 {
m.mainCursor++
}
case "home":
m.mainCursor = 0
case "end":
m.mainCursor = len(m.mainItems) - 1
case "enter":
item := m.mainItems[m.mainCursor]
if item.needNet && !m.preCheck.Connected {
return m, nil // can't select network-dependent items without connection
}
if item.id == "custom" {
m.phase = phaseCustom
m.customCursor = 0
return m, nil
}
m.result.choice = item.id
return m, tea.Quit
case "q", "ctrl+c":
m.result.quit = true
return m, tea.Quit
default:
// Number quick-select
for i, item := range m.mainItems {
if key == item.id {
if item.needNet && !m.preCheck.Connected {
return m, nil
}
if item.id == "custom" {
m.phase = phaseCustom
m.customCursor = 0
return m, nil
}
m.mainCursor = i
m.result.choice = item.id
return m, tea.Quit
}
}
}
return m, nil
}
func (m tuiModel) viewMain() string {
lang := m.result.language
var s strings.Builder
s.WriteString("\n")
// Title
if lang == "zh" {
s.WriteString(tTitleStyle.Render(fmt.Sprintf(" VPS融合怪 %s", m.config.EcsVersion)))
} else {
s.WriteString(tTitleStyle.Render(fmt.Sprintf(" VPS Fusion Monster %s", m.config.EcsVersion)))
}
s.WriteString("\n")
// Version warning
if m.preCheck.Connected && m.cmpVersion == -1 {
if lang == "zh" {
s.WriteString(tWarnStyle.Render(fmt.Sprintf(" ! 检测到新版本 %s 如有必要请更新!", m.newVersion)))
} else {
s.WriteString(tWarnStyle.Render(fmt.Sprintf(" ! New version %s detected, update if necessary!", m.newVersion)))
}
s.WriteString("\n")
}
// Stats
if m.preCheck.Connected && m.hasStats {
if lang == "zh" {
s.WriteString(tInfoStyle.Render(fmt.Sprintf(" 总使用量: %s | 今日使用: %s",
utils.FormatGoecsNumber(m.statsTotal), utils.FormatGoecsNumber(m.statsDaily))))
} else {
s.WriteString(tInfoStyle.Render(fmt.Sprintf(" Total Usage: %s | Daily Usage: %s",
utils.FormatGoecsNumber(m.statsTotal), utils.FormatGoecsNumber(m.statsDaily))))
}
s.WriteString("\n")
}
s.WriteString("\n")
// Section header
if lang == "zh" {
s.WriteString(tSectStyle.Render(" 请选择测试方案:"))
} else {
s.WriteString(tSectStyle.Render(" Select Test Suite:"))
}
s.WriteString("\n\n")
// Menu items
for i, item := range m.mainItems {
cursor := " "
style := tNormStyle
if m.mainCursor == i {
cursor = " > "
style = tSelStyle
}
var label string
if lang == "zh" {
label = item.zh
} else {
label = item.en
}
// Number prefix
prefix := ""
switch {
case item.id == "custom":
prefix = ""
case item.id == "0":
prefix = " 0. "
default:
prefix = fmt.Sprintf("%2s. ", item.id)
}
// Disabled indicator for network-dependent items when no connection
suffix := ""
if item.needNet && !m.preCheck.Connected {
style = tDimStyle
if lang == "zh" {
suffix = " [需要网络]"
} else {
suffix = " [No Network]"
}
}
s.WriteString(fmt.Sprintf("%s%s%s\n", cursor, style.Render(prefix+label), tDimStyle.Render(suffix)))
}
s.WriteString("\n")
if lang == "zh" {
s.WriteString(tHelpStyle.Render(" ↑/↓/j/k 移动 Enter 确认 数字 快速选择 q 退出"))
} else {
s.WriteString(tHelpStyle.Render(" Up/Down/j/k Navigate Enter Confirm Number Quick-Select q Quit"))
}
s.WriteString("\n")
return s.String()
}
// --- Custom Mode ---
func (m tuiModel) updateCustom(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
key := msg.String()
switch key {
case "up", "k":
if m.customCursor > 0 {
m.customCursor--
}
case "down", "j":
if m.customCursor < m.customTotal-1 {
m.customCursor++
}
case "home":
m.customCursor = 0
case "end":
m.customCursor = m.customTotal - 1
case " ":
if m.customCursor < len(m.toggles) {
// Toggle test item
t := &m.toggles[m.customCursor]
if t.needNet && !m.preCheck.Connected {
break // can't enable without network
}
t.enabled = !t.enabled
} else if m.customCursor < len(m.toggles)+len(m.advanced) {
// Cycle advanced setting forward
idx := m.customCursor - len(m.toggles)
a := &m.advanced[idx]
a.current = (a.current + 1) % len(a.options)
}
case "right", "l":
if m.customCursor >= len(m.toggles) && m.customCursor < len(m.toggles)+len(m.advanced) {
idx := m.customCursor - len(m.toggles)
a := &m.advanced[idx]
a.current = (a.current + 1) % len(a.options)
}
case "left", "h":
if m.customCursor >= len(m.toggles) && m.customCursor < len(m.toggles)+len(m.advanced) {
idx := m.customCursor - len(m.toggles)
a := &m.advanced[idx]
a.current = (a.current - 1 + len(a.options)) % len(a.options)
}
case "a":
// Toggle all test items
allEnabled := true
for _, t := range m.toggles {
if !t.enabled && (!t.needNet || m.preCheck.Connected) {
allEnabled = false
break
}
}
for i := range m.toggles {
if m.toggles[i].needNet && !m.preCheck.Connected {
continue
}
m.toggles[i].enabled = !allEnabled
}
case "enter":
if m.customCursor == m.customTotal-1 {
// Confirm button
m.result.custom = true
m.result.choice = "custom"
m.result.toggles = m.toggles
m.result.advanced = m.advanced
return m, tea.Quit
}
// Toggle current item (same as space)
if m.customCursor < len(m.toggles) {
t := &m.toggles[m.customCursor]
if t.needNet && !m.preCheck.Connected {
break
}
t.enabled = !t.enabled
} else if m.customCursor < len(m.toggles)+len(m.advanced) {
idx := m.customCursor - len(m.toggles)
a := &m.advanced[idx]
a.current = (a.current + 1) % len(a.options)
}
case "esc":
m.phase = phaseMain
return m, nil
case "q", "ctrl+c":
m.result.quit = true
return m, tea.Quit
}
return m, nil
}
func (m tuiModel) viewCustom() string {
lang := m.result.language
var s strings.Builder
s.WriteString("\n")
if lang == "zh" {
s.WriteString(tTitleStyle.Render(" 自定义测试配置"))
} else {
s.WriteString(tTitleStyle.Render(" Custom Test Configuration"))
}
s.WriteString("\n\n")
// Test toggles
if lang == "zh" {
s.WriteString(tSectStyle.Render(" 测试项目 (空格切换, a 全选/全不选):"))
} else {
s.WriteString(tSectStyle.Render(" Test Items (Space toggle, a Select all/none):"))
}
s.WriteString("\n\n")
for i, t := range m.toggles {
cursor := " "
if m.customCursor == i {
cursor = " > "
}
var check string
if t.enabled {
check = tChkOnStyle.Render("[x]")
} else {
check = tChkOffStyle.Render("[ ]")
}
var name string
if lang == "zh" {
name = t.nameZh
} else {
name = t.nameEn
}
style := tNormStyle
if m.customCursor == i {
style = tSelStyle
}
if t.needNet && !m.preCheck.Connected {
style = tDimStyle
}
s.WriteString(fmt.Sprintf("%s%s %s\n", cursor, check, style.Render(name)))
}
// Advanced settings
s.WriteString("\n")
if lang == "zh" {
s.WriteString(tSectStyle.Render(" 高级设置 (空格/←/→ 切换选项):"))
} else {
s.WriteString(tSectStyle.Render(" Advanced Settings (Space/Left/Right cycle):"))
}
s.WriteString("\n\n")
for i, a := range m.advanced {
idx := len(m.toggles) + i
cursor := " "
if m.customCursor == idx {
cursor = " > "
}
var name string
if lang == "zh" {
name = a.nameZh
} else {
name = a.nameEn
}
style := tNormStyle
if m.customCursor == idx {
style = tSelStyle
}
// Render current value with arrows
val := a.options[a.current]
var optStr string
if m.customCursor == idx {
optStr = tSelStyle.Render(fmt.Sprintf("< %s >", val))
} else {
optStr = tDimStyle.Render(fmt.Sprintf("< %s >", val))
}
s.WriteString(fmt.Sprintf("%s%-22s %s\n", cursor, style.Render(name+":"), optStr))
}
// Confirm button
s.WriteString("\n")
confirmIdx := m.customTotal - 1
if m.customCursor == confirmIdx {
if lang == "zh" {
s.WriteString(fmt.Sprintf(" %s\n", tBtnStyle.Render(">> 开始测试 <<")))
} else {
s.WriteString(fmt.Sprintf(" %s\n", tBtnStyle.Render(">> Start Test <<")))
}
} else {
if lang == "zh" {
s.WriteString(fmt.Sprintf(" %s\n", tBtnDimStyle.Render(">> 开始测试 <<")))
} else {
s.WriteString(fmt.Sprintf(" %s\n", tBtnDimStyle.Render(">> Start Test <<")))
}
}
s.WriteString("\n")
if lang == "zh" {
s.WriteString(tHelpStyle.Render(" ↑/↓ 移动 空格/Enter 切换 ←/→ 调整 a 全选 Esc 返回 q 退出"))
} else {
s.WriteString(tHelpStyle.Render(" Up/Down Move Space/Enter Toggle Left/Right Adjust a All Esc Back q Quit"))
}
s.WriteString("\n")
return s.String()
}
// --- Public Interface ---
// RunTuiMenu runs the interactive TUI menu and returns the result
func RunTuiMenu(preCheck utils.NetCheckResult, config *params.Config) tuiResult {
// Pre-load stats
var statsTotal, statsDaily int
var hasStats bool
var cmpVersion int
var newVersion string
if preCheck.Connected {
var wg sync.WaitGroup
var stats *utils.StatsResponse
var statsErr error
var githubInfo *utils.GitHubRelease
var githubErr error
wg.Add(2)
go func() {
defer wg.Done()
stats, statsErr = utils.GetGoescStats()
}()
go func() {
defer wg.Done()
githubInfo, githubErr = utils.GetLatestEcsRelease()
}()
wg.Wait()
if statsErr == nil {
statsTotal = stats.Total
statsDaily = stats.Daily
hasStats = true
}
if githubErr == nil {
cmpVersion = utils.CompareVersions(config.EcsVersion, githubInfo.TagName)
newVersion = githubInfo.TagName
}
}
langPreset := config.UserSetFlags["lang"] || config.UserSetFlags["l"]
m := newTuiModel(preCheck, config, langPreset, statsTotal, statsDaily, hasStats, cmpVersion, newVersion)
p := tea.NewProgram(m, tea.WithAltScreen())
finalModel, err := p.Run()
if err != nil {
fmt.Printf("Error running menu: %v\n", err)
os.Exit(1)
}
return finalModel.(tuiModel).result
}
// applyCustomResult applies custom mode selections to config
func applyCustomResult(result tuiResult, preCheck utils.NetCheckResult, config *params.Config) {
// Apply test toggles (order must match defaultTestToggles)
for i, t := range result.toggles {
enabled := t.enabled
if t.needNet && !preCheck.Connected {
enabled = false
}
switch i {
case 0:
config.BasicStatus = enabled
case 1:
config.CpuTestStatus = enabled
case 2:
config.MemoryTestStatus = enabled
case 3:
config.DiskTestStatus = enabled
case 4:
config.UtTestStatus = enabled
case 5:
config.SecurityTestStatus = enabled
case 6:
config.EmailTestStatus = enabled
case 7:
config.BacktraceStatus = enabled
case 8:
config.Nt3Status = enabled
case 9:
config.SpeedTestStatus = enabled
case 10:
config.PingTestStatus = enabled
case 11:
config.TgdcTestStatus = enabled
case 12:
config.WebTestStatus = enabled
}
}
// Apply advanced settings (order must match defaultAdvSettings)
if len(result.advanced) >= 7 {
config.CpuTestMethod = result.advanced[0].options[result.advanced[0].current]
config.CpuTestThreadMode = result.advanced[1].options[result.advanced[1].current]
config.MemoryTestMethod = result.advanced[2].options[result.advanced[2].current]
config.DiskTestMethod = result.advanced[3].options[result.advanced[3].current]
config.Nt3Location = result.advanced[4].options[result.advanced[4].current]
config.Nt3CheckType = result.advanced[5].options[result.advanced[5].current]
spIdx := result.advanced[6].current
config.SpNum = spIdx + 1
}
// Set OnlyIpInfoCheck if no hardware tests are enabled
if !config.BasicStatus && !config.CpuTestStatus && !config.MemoryTestStatus && !config.DiskTestStatus {
config.OnlyIpInfoCheck = true
}
config.AutoChangeDiskMethod = true
}

View File

@@ -3,6 +3,7 @@ package params
import ( import (
"flag" "flag"
"fmt" "fmt"
"strings"
) )
// Config holds all configuration parameters // Config holds all configuration parameters
@@ -64,7 +65,7 @@ func NewConfig(version string) *Config {
MemoryTestMethod: "stream", MemoryTestMethod: "stream",
DiskTestMethod: "fio", DiskTestMethod: "fio",
DiskTestPath: "", DiskTestPath: "",
DiskMultiCheck: false, DiskMultiCheck: false,
Nt3CheckType: "ipv4", Nt3CheckType: "ipv4",
SpNum: 2, SpNum: 2,
Width: 82, Width: 82,
@@ -92,8 +93,56 @@ func NewConfig(version string) *Config {
} }
} }
// normalizeBoolArgs preprocesses args so that bool flags written as
// "-flag true" or "-flag false" (space-separated) are converted to
// "-flag=true" / "-flag=false" that the standard flag package understands.
// This also strips any duplicate spaces that may appear between tokens when
// args have been assembled by shell scripts or other callers.
func normalizeBoolArgs(args []string) []string {
// All known boolean flag names (without leading dash).
boolFlags := map[string]bool{
"h": true, "help": true, "v": true, "version": true,
"menu": true, "basic": true, "cpu": true, "memory": true,
"disk": true, "ut": true, "security": true, "email": true,
"backtrace": true, "nt3": true, "speed": true, "ping": true,
"tgdc": true, "web": true, "log": true, "upload": true,
"diskmc": true,
}
out := make([]string, 0, len(args))
i := 0
for i < len(args) {
arg := args[i]
// Skip empty tokens that can appear from split on multiple spaces.
if arg == "" {
i++
continue
}
// Detect flag tokens: -flag or --flag (without embedded =).
if strings.HasPrefix(arg, "-") && !strings.Contains(arg, "=") {
name := strings.TrimLeft(arg, "-")
if boolFlags[name] {
// Peek at next token: if it is "true" or "false", merge.
if i+1 < len(args) {
next := strings.ToLower(strings.TrimSpace(args[i+1]))
if next == "true" || next == "false" {
out = append(out, arg+"="+next)
i += 2
continue
}
}
}
}
out = append(out, arg)
i++
}
return out
}
// ParseFlags parses command line flags // ParseFlags parses command line flags
func (c *Config) ParseFlags(args []string) { func (c *Config) ParseFlags(args []string) {
args = normalizeBoolArgs(args)
c.GoecsFlag.BoolVar(&c.Help, "h", false, "Show help information") c.GoecsFlag.BoolVar(&c.Help, "h", false, "Show help information")
c.GoecsFlag.BoolVar(&c.Help, "help", false, "Show help information") c.GoecsFlag.BoolVar(&c.Help, "help", false, "Show help information")
c.GoecsFlag.BoolVar(&c.ShowVersion, "v", false, "Display version information") c.GoecsFlag.BoolVar(&c.ShowVersion, "v", false, "Display version information")