This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"tsrun/internal/prompt"
|
||||
"tsrun/internal/runtime/full"
|
||||
"tsrun/internal/runtime/proxy"
|
||||
"tsrun/internal/ui"
|
||||
"tsrun/internal/vault"
|
||||
)
|
||||
|
||||
@@ -28,9 +29,19 @@ func main() {
|
||||
}
|
||||
|
||||
useProxy := flag.Bool("proxy", false, "run proxy mode instead of full client mode")
|
||||
debugMode := flag.Bool("debug", false, "use raw prompt and log mode instead of TUI")
|
||||
userFlag := flag.String("u", "", "username or username:password")
|
||||
flag.Parse()
|
||||
|
||||
if !*debugMode {
|
||||
prefill, _, _ := strings.Cut(*userFlag, ":")
|
||||
if err := ui.Run(*useProxy, prefill); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
fmt.Println("shutting down")
|
||||
return
|
||||
}
|
||||
|
||||
username, password, err := resolveCredentials(*userFlag)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
|
||||
@@ -3,6 +3,10 @@ module tsrun
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.45.0
|
||||
@@ -14,6 +18,7 @@ require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect
|
||||
@@ -28,11 +33,19 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
|
||||
github.com/creachadair/msync v0.7.1 // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gaissmai/bart v0.26.1 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
||||
@@ -52,13 +65,21 @@ require (
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // 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.19 // indirect
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mdlayher/sdnotify v1.0.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // 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/pierrec/lz4/v4 v4.1.25 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
@@ -70,6 +91,7 @@ require (
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
|
||||
@@ -12,6 +12,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
|
||||
@@ -42,8 +44,30 @@ github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
||||
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
||||
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
||||
@@ -67,6 +91,8 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
@@ -125,6 +151,14 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
@@ -137,6 +171,12 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
@@ -154,6 +194,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||
@@ -194,6 +236,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
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=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
@@ -217,6 +261,7 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -30,14 +31,21 @@ import (
|
||||
"tailscale.com/wgengine/router"
|
||||
|
||||
"tsrun/internal/oauth"
|
||||
base "tsrun/internal/runtime"
|
||||
"tsrun/internal/vault"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, payload vault.Payload) error {
|
||||
return RunWithReporter(ctx, payload, nil)
|
||||
}
|
||||
|
||||
func RunWithReporter(ctx context.Context, payload vault.Payload, report base.Reporter) error {
|
||||
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "starting full mode"})
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "creating ephemeral auth key"})
|
||||
authKey, err := oauth.CreateEphemeralAuthKey(ctx, payload, "tsrun temporary access")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -49,7 +57,11 @@ func Run(ctx context.Context, payload vault.Payload) error {
|
||||
}
|
||||
defer os.RemoveAll(varRoot)
|
||||
|
||||
logf := log.New(os.Stderr, "", log.LstdFlags).Printf
|
||||
logWriter := io.Writer(os.Stderr)
|
||||
if report != nil {
|
||||
logWriter = io.Discard
|
||||
}
|
||||
logf := log.New(logWriter, "", log.LstdFlags).Printf
|
||||
sys := tsd.NewSystem()
|
||||
|
||||
netMon, err := netmon.New(sys.Bus.Get(), logf)
|
||||
@@ -139,6 +151,7 @@ func Run(ctx context.Context, payload vault.Payload) error {
|
||||
if err := lb.Start(ipn.Options{AuthKey: authKey, UpdatePrefs: prefs}); err != nil {
|
||||
return err
|
||||
}
|
||||
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "waiting for tailscale network"})
|
||||
if st := lb.State(); st == ipn.NeedsLogin || st == ipn.NoState {
|
||||
if err := lb.StartLoginInteractive(ctx); err != nil {
|
||||
return err
|
||||
@@ -150,7 +163,18 @@ func Run(ctx context.Context, payload vault.Payload) error {
|
||||
return err
|
||||
}
|
||||
|
||||
printStatus(lb)
|
||||
if report == nil {
|
||||
printStatus(lb)
|
||||
}
|
||||
ipv4, ipv6 := tailscaleIPs(lb)
|
||||
report.Send(base.Event{
|
||||
Kind: base.EventRunning,
|
||||
Mode: "full",
|
||||
Message: "connected",
|
||||
Hostname: prefs.Hostname,
|
||||
IPv4: ipv4,
|
||||
IPv6: ipv6,
|
||||
})
|
||||
<-ctx.Done()
|
||||
|
||||
logoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -199,6 +223,24 @@ func printStatus(lb *ipnlocal.LocalBackend) {
|
||||
fmt.Printf("connected: %v\n", ips)
|
||||
}
|
||||
|
||||
func tailscaleIPs(lb *ipnlocal.LocalBackend) (string, string) {
|
||||
nm := lb.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
return "", ""
|
||||
}
|
||||
var ip4, ip6 string
|
||||
for _, p := range nm.GetAddresses().All() {
|
||||
addr := p.Addr()
|
||||
if addr.Is4() && ip4 == "" {
|
||||
ip4 = addr.String()
|
||||
}
|
||||
if addr.Is6() && ip6 == "" {
|
||||
ip6 = addr.String()
|
||||
}
|
||||
}
|
||||
return ip4, ip6
|
||||
}
|
||||
|
||||
func requireRoot() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("full client mode requires root on linux; use sudo or run with --proxy for low-privilege mode")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -32,17 +33,23 @@ import (
|
||||
"tailscale.com/wgengine/router"
|
||||
|
||||
"tsrun/internal/oauth"
|
||||
base "tsrun/internal/runtime"
|
||||
"tsrun/internal/vault"
|
||||
)
|
||||
|
||||
var startCOMOnce sync.Once
|
||||
|
||||
func Run(ctx context.Context, payload vault.Payload) error {
|
||||
return RunWithReporter(ctx, payload, nil)
|
||||
}
|
||||
|
||||
func RunWithReporter(ctx context.Context, payload vault.Payload, report base.Reporter) error {
|
||||
startCOMOnce.Do(func() {
|
||||
if err := com.StartRuntime(com.ConsoleApp); err != nil {
|
||||
log.Printf("wingoes.com.StartRuntime failed: %v", err)
|
||||
}
|
||||
})
|
||||
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "starting full mode"})
|
||||
|
||||
if err := requireElevation(); err != nil {
|
||||
return err
|
||||
@@ -51,6 +58,7 @@ func Run(ctx context.Context, payload vault.Payload) error {
|
||||
return err
|
||||
}
|
||||
|
||||
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "creating ephemeral auth key"})
|
||||
authKey, err := oauth.CreateEphemeralAuthKey(ctx, payload, "tsrun temporary access")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -62,7 +70,11 @@ func Run(ctx context.Context, payload vault.Payload) error {
|
||||
}
|
||||
defer os.RemoveAll(varRoot)
|
||||
|
||||
logf := log.New(os.Stderr, "", log.LstdFlags).Printf
|
||||
logWriter := io.Writer(os.Stderr)
|
||||
if report != nil {
|
||||
logWriter = io.Discard
|
||||
}
|
||||
logf := log.New(logWriter, "", log.LstdFlags).Printf
|
||||
sys := tsd.NewSystem()
|
||||
|
||||
netMon, err := netmon.New(sys.Bus.Get(), logf)
|
||||
@@ -149,6 +161,7 @@ func Run(ctx context.Context, payload vault.Payload) error {
|
||||
if err := lb.Start(ipn.Options{AuthKey: authKey, UpdatePrefs: prefs}); err != nil {
|
||||
return err
|
||||
}
|
||||
report.Send(base.Event{Kind: base.EventStage, Mode: "full", Message: "waiting for tailscale network"})
|
||||
if st := lb.State(); st == ipn.NeedsLogin || st == ipn.NoState {
|
||||
if err := lb.StartLoginInteractive(ctx); err != nil {
|
||||
return err
|
||||
@@ -160,7 +173,18 @@ func Run(ctx context.Context, payload vault.Payload) error {
|
||||
return err
|
||||
}
|
||||
|
||||
printStatus(lb)
|
||||
if report == nil {
|
||||
printStatus(lb)
|
||||
}
|
||||
ipv4, ipv6 := tailscaleIPs(lb)
|
||||
report.Send(base.Event{
|
||||
Kind: base.EventRunning,
|
||||
Mode: "full",
|
||||
Message: "connected",
|
||||
Hostname: prefs.Hostname,
|
||||
IPv4: ipv4,
|
||||
IPv6: ipv6,
|
||||
})
|
||||
<-ctx.Done()
|
||||
|
||||
logoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -209,6 +233,24 @@ func printStatus(lb *ipnlocal.LocalBackend) {
|
||||
fmt.Printf("connected: %v\n", ips)
|
||||
}
|
||||
|
||||
func tailscaleIPs(lb *ipnlocal.LocalBackend) (string, string) {
|
||||
nm := lb.NetMapNoPeers()
|
||||
if nm == nil {
|
||||
return "", ""
|
||||
}
|
||||
var ip4, ip6 string
|
||||
for _, p := range nm.GetAddresses().All() {
|
||||
addr := p.Addr()
|
||||
if addr.Is4() && ip4 == "" {
|
||||
ip4 = addr.String()
|
||||
}
|
||||
if addr.Is6() && ip6 == "" {
|
||||
ip6 = addr.String()
|
||||
}
|
||||
}
|
||||
return ip4, ip6
|
||||
}
|
||||
|
||||
func requireElevation() error {
|
||||
adminSID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,10 +11,16 @@ import (
|
||||
_ "tailscale.com/feature/condregister/oauthkey"
|
||||
"tailscale.com/tsnet"
|
||||
|
||||
base "tsrun/internal/runtime"
|
||||
"tsrun/internal/vault"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, payload vault.Payload) error {
|
||||
return RunWithReporter(ctx, payload, nil)
|
||||
}
|
||||
|
||||
func RunWithReporter(ctx context.Context, payload vault.Payload, report base.Reporter) error {
|
||||
report.Send(base.Event{Kind: base.EventStage, Mode: "proxy", Message: "starting proxy mode"})
|
||||
baseDir, err := os.MkdirTemp("", "tsrun-proxy-*")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -34,6 +40,7 @@ func Run(ctx context.Context, payload vault.Payload) error {
|
||||
if _, err := srv.Up(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
report.Send(base.Event{Kind: base.EventStage, Mode: "proxy", Message: "tailscale connected"})
|
||||
|
||||
proxyListener, err := startProxyListener(ctx, srv)
|
||||
if err != nil {
|
||||
@@ -41,11 +48,22 @@ func Run(ctx context.Context, payload vault.Payload) error {
|
||||
}
|
||||
defer proxyListener.closeFn()
|
||||
|
||||
fmt.Printf("proxy mode active\n")
|
||||
fmt.Printf("%s\n", directRequestHint())
|
||||
fmt.Printf("Proxy: %s\n", formatProxyURL(proxyListener.addr))
|
||||
fmt.Printf("Username: tsnet\n")
|
||||
fmt.Printf("Password: %s\n", proxyListener.proxyCred)
|
||||
if report == nil {
|
||||
fmt.Printf("proxy mode active\n")
|
||||
fmt.Printf("%s\n", directRequestHint())
|
||||
fmt.Printf("Proxy: %s\n", formatProxyURL(proxyListener.addr))
|
||||
fmt.Printf("Username: tsnet\n")
|
||||
fmt.Printf("Password: %s\n", proxyListener.proxyCred)
|
||||
}
|
||||
report.Send(base.Event{
|
||||
Kind: base.EventRunning,
|
||||
Mode: "proxy",
|
||||
Message: "proxy ready",
|
||||
Hostname: hostname,
|
||||
ProxyAddr: proxyListener.addr,
|
||||
ProxyUser: "tsnet",
|
||||
ProxyPass: proxyListener.proxyCred,
|
||||
})
|
||||
|
||||
waitCtx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package runtime
|
||||
|
||||
type EventKind string
|
||||
|
||||
const (
|
||||
EventStage EventKind = "stage"
|
||||
EventRunning EventKind = "running"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Kind EventKind
|
||||
Message string
|
||||
Mode string
|
||||
Hostname string
|
||||
IPv4 string
|
||||
IPv6 string
|
||||
ProxyAddr string
|
||||
ProxyUser string
|
||||
ProxyPass string
|
||||
}
|
||||
|
||||
type Reporter func(Event)
|
||||
|
||||
func (r Reporter) Send(event Event) {
|
||||
if r != nil {
|
||||
r(event)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"tsrun/internal/embedded"
|
||||
base "tsrun/internal/runtime"
|
||||
"tsrun/internal/runtime/full"
|
||||
"tsrun/internal/runtime/proxy"
|
||||
"tsrun/internal/vault"
|
||||
)
|
||||
|
||||
type screen int
|
||||
|
||||
const (
|
||||
screenLogin screen = iota
|
||||
screenConnecting
|
||||
screenDashboard
|
||||
screenError
|
||||
)
|
||||
|
||||
type runtimeEventMsg struct{ event base.Event }
|
||||
type runtimeDoneMsg struct{ err error }
|
||||
|
||||
type App struct {
|
||||
useProxy bool
|
||||
prefill string
|
||||
|
||||
screen screen
|
||||
spinner spinner.Model
|
||||
username textinput.Model
|
||||
password textinput.Model
|
||||
focus int
|
||||
|
||||
status string
|
||||
errText string
|
||||
event base.Event
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
eventCh chan base.Event
|
||||
doneCh chan error
|
||||
started bool
|
||||
connected bool
|
||||
}
|
||||
|
||||
func Run(useProxy bool, prefill string) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sp := spinner.New()
|
||||
sp.Spinner = spinner.Line
|
||||
|
||||
username := textinput.New()
|
||||
username.Placeholder = "username"
|
||||
username.Prompt = "Username: "
|
||||
username.CharLimit = 64
|
||||
username.SetValue(prefill)
|
||||
|
||||
password := textinput.New()
|
||||
password.Placeholder = "password"
|
||||
password.Prompt = "Password: "
|
||||
password.CharLimit = 128
|
||||
password.EchoMode = textinput.EchoPassword
|
||||
password.EchoCharacter = '•'
|
||||
|
||||
if prefill == "" {
|
||||
username.Focus()
|
||||
} else {
|
||||
password.Focus()
|
||||
}
|
||||
|
||||
app := &App{
|
||||
useProxy: useProxy,
|
||||
prefill: prefill,
|
||||
screen: screenLogin,
|
||||
spinner: sp,
|
||||
username: username,
|
||||
password: password,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
_, err := tea.NewProgram(app, tea.WithAltScreen()).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if app.errText != "" {
|
||||
return errors.New(app.errText)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Init() tea.Cmd { return nil }
|
||||
|
||||
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch a.screen {
|
||||
case screenLogin:
|
||||
return a.updateLogin(msg)
|
||||
case screenConnecting, screenDashboard:
|
||||
if msg.String() == "q" || msg.String() == "ctrl+c" {
|
||||
a.cancel()
|
||||
return a, tea.Quit
|
||||
}
|
||||
case screenError:
|
||||
if msg.String() == "enter" || msg.String() == "q" || msg.String() == "esc" || msg.String() == "ctrl+c" {
|
||||
return a, tea.Quit
|
||||
}
|
||||
}
|
||||
case spinner.TickMsg:
|
||||
if a.screen == screenConnecting {
|
||||
var cmd tea.Cmd
|
||||
a.spinner, cmd = a.spinner.Update(msg)
|
||||
return a, cmd
|
||||
}
|
||||
case runtimeEventMsg:
|
||||
a.status = msg.event.Message
|
||||
a.event = msg.event
|
||||
if msg.event.Kind == base.EventRunning {
|
||||
a.connected = true
|
||||
a.screen = screenDashboard
|
||||
}
|
||||
return a, a.waitForEventCmd()
|
||||
case runtimeDoneMsg:
|
||||
if msg.err != nil && !errors.Is(msg.err, context.Canceled) {
|
||||
a.errText = msg.err.Error()
|
||||
a.screen = screenError
|
||||
return a, nil
|
||||
}
|
||||
if a.connected {
|
||||
return a, tea.Quit
|
||||
}
|
||||
if msg.err != nil {
|
||||
a.errText = msg.err.Error()
|
||||
a.screen = screenError
|
||||
return a, nil
|
||||
}
|
||||
return a, tea.Quit
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *App) updateLogin(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
return a, tea.Quit
|
||||
case "tab", "shift+tab", "up", "down":
|
||||
if a.focus == 0 {
|
||||
a.focus = 1
|
||||
a.username.Blur()
|
||||
a.password.Focus()
|
||||
} else {
|
||||
a.focus = 0
|
||||
a.password.Blur()
|
||||
a.username.Focus()
|
||||
}
|
||||
return a, nil
|
||||
case "enter":
|
||||
if a.focus == 0 {
|
||||
a.focus = 1
|
||||
a.username.Blur()
|
||||
a.password.Focus()
|
||||
return a, nil
|
||||
}
|
||||
return a, a.startRuntime()
|
||||
}
|
||||
|
||||
if a.focus == 0 {
|
||||
var cmd tea.Cmd
|
||||
a.username, cmd = a.username.Update(msg)
|
||||
return a, cmd
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
a.password, cmd = a.password.Update(msg)
|
||||
return a, cmd
|
||||
}
|
||||
|
||||
func (a *App) startRuntime() tea.Cmd {
|
||||
username := strings.TrimSpace(a.username.Value())
|
||||
passwordBytes := []byte(a.password.Value())
|
||||
a.screen = screenConnecting
|
||||
a.status = "unlocking credentials"
|
||||
|
||||
payload, err := vault.Unlock(embedded.Blob, username, passwordBytes)
|
||||
vault.Zero(passwordBytes)
|
||||
if err != nil {
|
||||
if errors.Is(err, vault.ErrAuthFailed) {
|
||||
a.errText = "authentication failed"
|
||||
} else {
|
||||
a.errText = err.Error()
|
||||
}
|
||||
a.screen = screenError
|
||||
return nil
|
||||
}
|
||||
|
||||
a.eventCh = make(chan base.Event, 16)
|
||||
a.doneCh = make(chan error, 1)
|
||||
a.started = true
|
||||
|
||||
go func() {
|
||||
report := base.Reporter(func(event base.Event) {
|
||||
a.eventCh <- event
|
||||
})
|
||||
var runErr error
|
||||
if a.useProxy {
|
||||
runErr = proxy.RunWithReporter(a.ctx, payload, report)
|
||||
} else {
|
||||
runErr = full.RunWithReporter(a.ctx, payload, report)
|
||||
}
|
||||
a.doneCh <- runErr
|
||||
close(a.eventCh)
|
||||
close(a.doneCh)
|
||||
}()
|
||||
|
||||
return tea.Batch(a.spinner.Tick, a.waitForEventCmd(), a.waitForDoneCmd())
|
||||
}
|
||||
|
||||
func (a *App) waitForEventCmd() tea.Cmd {
|
||||
if !a.started || a.eventCh == nil {
|
||||
return nil
|
||||
}
|
||||
return func() tea.Msg {
|
||||
event, ok := <-a.eventCh
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return runtimeEventMsg{event: event}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) waitForDoneCmd() tea.Cmd {
|
||||
if !a.started || a.doneCh == nil {
|
||||
return nil
|
||||
}
|
||||
return func() tea.Msg {
|
||||
err, ok := <-a.doneCh
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return runtimeDoneMsg{err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) View() string {
|
||||
switch a.screen {
|
||||
case screenLogin:
|
||||
return pageStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
||||
titleStyle.Render("tsrun"),
|
||||
subStyle.Render("Temporary Tailscale access"),
|
||||
"",
|
||||
a.username.View(),
|
||||
a.password.View(),
|
||||
"",
|
||||
helpStyle.Render("Tab to switch, Enter to continue, Esc to quit"),
|
||||
))
|
||||
case screenConnecting:
|
||||
return pageStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
||||
titleStyle.Render("tsrun"),
|
||||
"",
|
||||
fmt.Sprintf("%s %s", a.spinner.View(), a.status),
|
||||
"",
|
||||
helpStyle.Render("q to cancel"),
|
||||
))
|
||||
case screenDashboard:
|
||||
lines := []string{
|
||||
fmt.Sprintf("Mode: %s", a.event.Mode),
|
||||
fmt.Sprintf("Status: %s", a.event.Message),
|
||||
}
|
||||
if a.event.Hostname != "" {
|
||||
lines = append(lines, fmt.Sprintf("Hostname: %s", a.event.Hostname))
|
||||
}
|
||||
if validAddr(a.event.IPv4) {
|
||||
lines = append(lines, fmt.Sprintf("IPv4: %s", a.event.IPv4))
|
||||
}
|
||||
if validAddr(a.event.IPv6) {
|
||||
lines = append(lines, fmt.Sprintf("IPv6: %s", a.event.IPv6))
|
||||
}
|
||||
if a.event.ProxyAddr != "" {
|
||||
lines = append(lines,
|
||||
fmt.Sprintf("Proxy: %s", a.event.ProxyAddr),
|
||||
fmt.Sprintf("Proxy user: %s", a.event.ProxyUser),
|
||||
fmt.Sprintf("Proxy password: %s", a.event.ProxyPass),
|
||||
)
|
||||
}
|
||||
return pageStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
||||
titleStyle.Render("tsrun"),
|
||||
"",
|
||||
strings.Join(lines, "\n"),
|
||||
"",
|
||||
helpStyle.Render("q to disconnect"),
|
||||
))
|
||||
case screenError:
|
||||
return pageStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
|
||||
titleStyle.Render("tsrun"),
|
||||
"",
|
||||
errorStyle.Render(a.errText),
|
||||
"",
|
||||
helpStyle.Render("Enter or q to exit"),
|
||||
))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func validAddr(v string) bool {
|
||||
if v == "" || v == "<nil>" {
|
||||
return false
|
||||
}
|
||||
_, err := netip.ParseAddr(v)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
var (
|
||||
pageStyle = lipgloss.NewStyle().Padding(2, 4)
|
||||
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
|
||||
subStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Bold(true)
|
||||
)
|
||||
Reference in New Issue
Block a user