mirror of
http://bgp.hk.skcks.cn:10088/github.com/oneclickvirt/ecs
synced 2026-04-20 21:01:12 +08:00
feat: 增加类GUI显示,增强参数解析
This commit is contained in:
14
go.mod
14
go.mod
@@ -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
31
go.sum
@@ -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=
|
||||||
|
|||||||
2
goecs.go
2
goecs.go
@@ -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) // 用于跟踪哪些参数是用户显式设置的
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
762
internal/menu/tui.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user