From d8ce8492018d1ddfab38062f9f1e59e6e56e8291 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Thu, 20 Jun 2024 17:36:31 +0800 Subject: [PATCH 01/11] chore: upgrade dependencies --- go.mod | 24 ++++++++++++++---------- go.sum | 52 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index fac16b0..cb4b047 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,18 @@ go 1.21 require ( github.com/atotto/clipboard v0.1.4 - github.com/charmbracelet/bubbles v0.17.1 - github.com/charmbracelet/bubbletea v0.25.0 - github.com/charmbracelet/lipgloss v0.9.1 + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.26.4 + github.com/charmbracelet/lipgloss v0.11.0 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/charmbracelet/x/ansi v0.1.2 // indirect + github.com/charmbracelet/x/input v0.1.2 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.2 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -20,10 +24,10 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index ddd6d3f..c8149ca 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,22 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4= -github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= -github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= -github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= -github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= -github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40= +github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0= +github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= +github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= +github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= +github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/input v0.1.2 h1:QJAZr33eOhDowkkEQ24rsJy4Llxlm+fRDf/cQrmqJa0= +github.com/charmbracelet/x/input v0.1.2/go.mod h1:LGBim0maUY4Pitjn/4fHnuXb4KirU3DODsyuHuXdOyA= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= +github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -31,17 +39,19 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= -github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +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= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= From 22f00213b714b35e947185f2a84338f7042ad3e9 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Thu, 22 Aug 2024 22:23:47 +0800 Subject: [PATCH 02/11] feat: upgrade dependencies & re-added tailscale package --- go.mod | 48 +++++++--- go.sum | 93 ++++++++++++------ internal/tui/node_details/node_details.go | 18 ++-- internal/tui/node_list/node_list.go | 111 +++++++++++++++------- internal/tui/tui.go | 95 +++++++++--------- main.go | 15 ++- 6 files changed, 244 insertions(+), 136 deletions(-) diff --git a/go.mod b/go.mod index cb4b047..20884d0 100644 --- a/go.mod +++ b/go.mod @@ -1,33 +1,53 @@ module github.com/bilguun0203/tailscale-tui -go 1.21 +go 1.22.0 + +toolchain go1.23.0 require ( github.com/atotto/clipboard v0.1.4 - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.26.4 - github.com/charmbracelet/lipgloss v0.11.0 + github.com/charmbracelet/bubbles v0.19.0 + github.com/charmbracelet/bubbletea v0.27.0 + github.com/charmbracelet/lipgloss v0.13.0 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/akutz/memconn v0.1.0 // indirect + github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/x/ansi v0.1.2 // indirect - github.com/charmbracelet/x/input v0.1.2 // indirect - github.com/charmbracelet/x/term v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.1.2 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect + github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.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.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mdlayher/netlink v1.7.2 // 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/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect + github.com/x448/float16 v0.8.4 // indirect + go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.zx2c4.com/wireguard/windows v0.5.3 // indirect + tailscale.com v1.72.0 // indirect ) diff --git a/go.sum b/go.sum index c8149ca..45ff168 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,39 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40= -github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0= -github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= -github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= -github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= -github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/input v0.1.2 h1:QJAZr33eOhDowkkEQ24rsJy4Llxlm+fRDf/cQrmqJa0= -github.com/charmbracelet/x/input v0.1.2/go.mod h1:LGBim0maUY4Pitjn/4fHnuXb4KirU3DODsyuHuXdOyA= -github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= -github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= -github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= -github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0= +github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA= +github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU= +github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= 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/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= +github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= +github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= +github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -26,32 +42,49 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= +github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= +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/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -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= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= +go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +tailscale.com v1.72.0 h1:emsPxupFM72zJLt2wvSzpa1vymqPYbL0WVVO+170/s0= +tailscale.com v1.72.0/go.mod h1:v7OHtg0KLAnhOVf81Z8WrjNefj238QbFhgkWJQoKxbs= diff --git a/internal/tui/node_details/node_details.go b/internal/tui/node_details/node_details.go index 470a6da..bff8d72 100644 --- a/internal/tui/node_details/node_details.go +++ b/internal/tui/node_details/node_details.go @@ -2,22 +2,22 @@ package nodedetails import ( "fmt" - "strconv" "strings" "time" - "github.com/bilguun0203/tailscale-tui/internal/tailscale" "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "tailscale.com/ipn/ipnstate" + tsKey "tailscale.com/types/key" ) type Model struct { - tailStatus *tailscale.Status - nodeID string + tailStatus *ipnstate.Status + nodeID tsKey.NodePublic keyMap keymap.KeyMap w, h int help help.Model @@ -75,7 +75,7 @@ func (m Model) detailView() string { ok = true } if ok { - if user, ok := m.tailStatus.User[strconv.FormatInt(node.UserID, 10)]; ok { + if user, ok := m.tailStatus.User[node.UserID]; ok { userInfo = fmt.Sprintf("%s <%s>", user.DisplayName, user.LoginName) } else { userInfo = fmt.Sprintf("??? <%d>", node.UserID) @@ -83,7 +83,10 @@ func (m Model) detailView() string { if node.ExitNodeOption { offersExitNode = constants.WarningTextStyle.Render("yes") } - ipList := node.TailscaleIPs + var ipList []string + for _, ip := range node.TailscaleIPs { + ipList = append(ipList, ip.String()) + } if node.Online { status += constants.SuccessTextStyle.Render("Online") } else { @@ -118,7 +121,6 @@ func (m Model) Init() tea.Cmd { } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { - // var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: @@ -142,7 +144,7 @@ func (m Model) View() string { } -func New(status *tailscale.Status, nodeID string, w, h int) Model { +func New(status *ipnstate.Status, nodeID tsKey.NodePublic, w, h int) Model { m := Model{ keyMap: keymap.NewKeyMap(), tailStatus: status, diff --git a/internal/tui/node_list/node_list.go b/internal/tui/node_list/node_list.go index 21f4955..6b1baf1 100644 --- a/internal/tui/node_list/node_list.go +++ b/internal/tui/node_list/node_list.go @@ -1,33 +1,36 @@ package nodelist import ( + "context" "fmt" "sort" - "strconv" "strings" "github.com/atotto/clipboard" - "github.com/bilguun0203/tailscale-tui/internal/tailscale" "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "tailscale.com/client/tailscale" + "tailscale.com/ipn" + "tailscale.com/ipn/ipnstate" + tsKey "tailscale.com/types/key" ) type listItem struct { title, desc string - status tailscale.PeerStatus + status *ipnstate.PeerStatus } func (i listItem) Title() string { return i.title } func (i listItem) Description() string { return i.desc } -func (i listItem) Status() tailscale.PeerStatus { return i.status } +func (i listItem) Status() *ipnstate.PeerStatus { return i.status } func (i listItem) FilterValue() string { return i.title + " " + i.desc } type Model struct { - tailStatus *tailscale.Status + tailStatus *ipnstate.Status exitNode string list list.Model keyMap keymap.KeyMap @@ -43,7 +46,7 @@ func (m *Model) SetSize(w int, h int) { m.list.SetSize(w, h-headerHeight) } -type NodeSelectedMsg string +type NodeSelectedMsg tsKey.NodePublic type RefreshMsg bool func (m *Model) updateKeybindings() { @@ -73,9 +76,9 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { copyStr := "" ipCount := len(m.list.SelectedItem().(listItem).status.TailscaleIPs) if ipCount > 0 && key.Matches(msg, m.keyMap.CopyIpv4) { - copyStr = m.list.SelectedItem().(listItem).status.TailscaleIPs[0] + copyStr = m.list.SelectedItem().(listItem).status.TailscaleIPs[0].String() } else if ipCount > 1 && key.Matches(msg, m.keyMap.CopyIpv6) { - copyStr = m.list.SelectedItem().(listItem).status.TailscaleIPs[1] + copyStr = m.list.SelectedItem().(listItem).status.TailscaleIPs[1].String() } else if key.Matches(msg, m.keyMap.CopyDNSName) { copyStr = m.list.SelectedItem().(listItem).status.DNSName } @@ -83,7 +86,7 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { m.list.NewStatusMessage("Sorry, nothing to copy.") } else { clipboard.WriteAll(copyStr) - m.list.NewStatusMessage(fmt.Sprintf("Copied \"%s\"!", constants.AccentTextStyle.Copy().Underline(true).Render(copyStr))) + m.list.NewStatusMessage(fmt.Sprintf("Copied \"%s\"!", constants.AccentTextStyle.Underline(true).Render(copyStr))) } } if key.Matches(msg, m.keyMap.Refresh) { @@ -94,6 +97,28 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { cmd = func() tea.Msg { return NodeSelectedMsg(m.list.SelectedItem().(listItem).status.PublicKey) } cmds = append(cmds, cmd) } + if key.Matches(msg, m.keyMap.TSUp) { + var lc tailscale.LocalClient + lc.EditPrefs(context.Background(), &ipn.MaskedPrefs{ + Prefs: ipn.Prefs{ + WantRunning: true, + }, + WantRunningSet: true, + }) + cmd = func() tea.Msg { return RefreshMsg(true) } + cmds = append(cmds, cmd) + } + if key.Matches(msg, m.keyMap.TSDown) { + var lc tailscale.LocalClient + lc.EditPrefs(context.Background(), &ipn.MaskedPrefs{ + Prefs: ipn.Prefs{ + WantRunning: false, + }, + WantRunningSet: true, + }) + cmd = func() tea.Msg { return RefreshMsg(true) } + cmds = append(cmds, cmd) + } return m, cmds } @@ -104,20 +129,30 @@ func (m Model) headerView() string { ips := "" offersExitNode := "no" usingExitNode := "-" - if user, ok := m.tailStatus.User[strconv.FormatInt(m.tailStatus.Self.UserID, 10)]; ok { - userInfo = fmt.Sprintf("%s <%s>", user.DisplayName, user.LoginName) - } - if e, ok := m.tailStatus.Peer[string(m.exitNode)]; ok { - usingExitNode = constants.WarningTextStyle.Render(e.HostName) - } - hostname = m.tailStatus.Self.HostName - os = m.tailStatus.Self.OS - if m.tailStatus.Self.ExitNodeOption { - offersExitNode = constants.WarningTextStyle.Render("yes") + if m.tailStatus != nil { + if user, ok := m.tailStatus.User[m.tailStatus.Self.UserID]; ok { + userInfo = fmt.Sprintf("%s <%s>", user.DisplayName, user.LoginName) + } + if m.tailStatus.ExitNodeStatus != nil { + for _, peer := range m.tailStatus.Peer { + if peer.ID == m.tailStatus.ExitNodeStatus.ID { + usingExitNode = constants.WarningTextStyle.Render(peer.HostName) + break + } + } + } + hostname = m.tailStatus.Self.HostName + os = m.tailStatus.Self.OS + if m.tailStatus.Self.ExitNodeOption { + offersExitNode = constants.WarningTextStyle.Render("yes") + } + var ipList []string + for _, ip := range m.tailStatus.TailscaleIPs { + ipList = append(ipList, ip.String()) + } + ipList = append(ipList, m.tailStatus.Self.DNSName) + ips = strings.Join(ipList, " | ") } - ipList := m.tailStatus.TailscaleIPs - ipList = append(ipList, m.tailStatus.Self.DNSName) - ips = strings.Join(ipList, " | ") title := constants.TitleStyle.Render(" This node ") hostname = constants.AccentTextStyle.Render("Host: ") + hostname + " (" + os + ")" ips = constants.AccentTextStyle.Render("IPs: ") + ips @@ -128,7 +163,12 @@ func (m Model) headerView() string { func (m *Model) getItems() []list.Item { items := []list.Item{} - peers := []tailscale.PeerStatus{} + peers := []*ipnstate.PeerStatus{} + + if m.tailStatus == nil { + return items + } + for _, v := range m.tailStatus.Peer { peers = append(peers, v) } @@ -140,7 +180,7 @@ func (m *Model) getItems() []list.Item { return first < second }) - peers = append([]tailscale.PeerStatus{m.tailStatus.Self}, peers...) + peers = append([]*ipnstate.PeerStatus{m.tailStatus.Self}, peers...) m.exitNode = "" for _, v := range peers { @@ -155,21 +195,24 @@ func (m *Model) getItems() []list.Item { owner = "this device" } if v.UserID != m.tailStatus.Self.UserID { - owner = "from:" + m.tailStatus.User[strconv.FormatInt(v.UserID, 10)].LoginName + owner = "from:" + m.tailStatus.User[v.UserID].LoginName } owner = constants.MutedTextStyle.Render("[" + owner + "]") exitNode := "" if v.ExitNodeOption { - exitNode = constants.MutedTextStyle.Copy().Bold(true).Render("[→]") + exitNode = constants.MutedTextStyle.Bold(true).Render("[→]") } if v.ExitNode { - m.exitNode = v.PublicKey - exitNode = constants.SuccessTextStyle.Copy().Bold(true).Render("[→]") + m.exitNode = v.PublicKey.String() + exitNode = constants.SuccessTextStyle.Bold(true).Render("[→]") } os := v.OS title := fmt.Sprintf("%s %s %s %s %s", hostName, state, owner, os, exitNode) desc := "- " - ips := v.TailscaleIPs + var ips []string + for _, ip := range v.TailscaleIPs { + ips = append(ips, ip.String()) + } ips = append(ips, v.DNSName) desc += strings.Join(ips, " | ") items = append(items, listItem{title: title, desc: desc, status: v}) @@ -202,23 +245,23 @@ func (m Model) View() string { return fmt.Sprintf("%s\n%s", m.headerView(), m.list.View()) } -func New(status *tailscale.Status, w, h int) Model { +func New(status *ipnstate.Status, w, h int) Model { d := list.NewDefaultDelegate() d.Styles.NormalTitle = lipgloss.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}). Padding(0, 0, 0, 2) - d.Styles.NormalDesc = d.Styles.NormalTitle.Copy(). + d.Styles.NormalDesc = d.Styles.NormalTitle. Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}) d.Styles.SelectedTitle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(lipgloss.AdaptiveColor{Light: "#00C86E", Dark: "#20F394"}). Foreground(lipgloss.AdaptiveColor{Light: "#00C86E", Dark: "#20F394"}). Padding(0, 0, 0, 1) - d.Styles.SelectedDesc = d.Styles.SelectedTitle.Copy() + d.Styles.SelectedDesc = d.Styles.SelectedTitle d.Styles.DimmedTitle = lipgloss.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}). Padding(0, 0, 0, 2) - d.Styles.DimmedDesc = d.Styles.DimmedTitle.Copy(). + d.Styles.DimmedDesc = d.Styles.DimmedTitle. Foreground(lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"}) d.SetHeight(2) d.SetSpacing(1) @@ -233,7 +276,7 @@ func New(status *tailscale.Status, w, h int) Model { m.list.SetItems(m.getItems()) m.list.Title = "Nodes" - m.list.Styles.Title = constants.TitleStyle.Copy().Padding(0, 1) + m.list.Styles.Title = constants.TitleStyle.Padding(0, 1) m.list.FilterInput.PromptStyle = lipgloss.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#00C86E", Dark: "#20F394"}) m.list.FilterInput.Cursor.Style = lipgloss.NewStyle(). diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 2b095a9..6d9a8c7 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -1,14 +1,17 @@ package tui import ( + "context" "fmt" - "github.com/bilguun0203/tailscale-tui/internal/tailscale" "github.com/bilguun0203/tailscale-tui/internal/tui/constants" nodedetails "github.com/bilguun0203/tailscale-tui/internal/tui/node_details" nodelist "github.com/bilguun0203/tailscale-tui/internal/tui/node_list" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "tailscale.com/client/tailscale" + "tailscale.com/ipn/ipnstate" + tsKey "tailscale.com/types/key" ) type viewState int @@ -27,95 +30,88 @@ func (f viewState) String() string { type Model struct { viewState viewState - tsStatus tailscale.Status - selectedNodeID string + tsLocalClient *tailscale.LocalClient + tsStatus *ipnstate.Status + selectedNodeID tsKey.NodePublic isLoading bool - err error + Err error nodelist nodelist.Model nodedetails nodedetails.Model spinner spinner.Model w, h int } -type statusRequest struct { - status tailscale.Status - err error -} - -type statusLoaded tailscale.Status +type statusLoaded *ipnstate.Status type statusError error -func getTsStatusAsync(c chan statusRequest) { - ts, err := tailscale.New() - if err != nil { - c <- statusRequest{status: tailscale.Status{}, err: err} - return - } - s, e := ts.Status() - c <- statusRequest{status: s, err: e} -} - -func getTsStatus() tea.Cmd { +func (m Model) getTsStatus() tea.Cmd { return func() tea.Msg { - c := make(chan statusRequest) - go getTsStatusAsync(c) - statusReq := <-c - if statusReq.err != nil { - return statusError(statusReq.err) + status, err := m.tsLocalClient.Status(context.Background()) + if err != nil { + return statusError(err) } - return statusLoaded(statusReq.status) + return statusLoaded(status) } } func (m Model) Init() tea.Cmd { cmds := []tea.Cmd{ - getTsStatus(), m.spinner.Tick, - m.nodelist.Init(), + m.getTsStatus(), } return tea.Batch(cmds...) } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd + var tmpCmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { case statusLoaded: - m.tsStatus = tailscale.Status(msg) - m.nodelist = nodelist.New(&m.tsStatus, m.w, m.h) m.isLoading = false + m.Err = nil + m.tsStatus = msg + m.viewState = viewStateList + m.nodelist = nodelist.New(m.tsStatus, m.w, m.h) case statusError: m.isLoading = false - m.err = msg + m.Err = msg return m, tea.Quit case nodedetails.BackMsg: m.viewState = viewStateList case nodelist.RefreshMsg: - cmd = getTsStatus() - cmds = append(cmds, cmd) + m.isLoading = true + cmds = append(cmds, m.getTsStatus()) + cmds = append(cmds, m.spinner.Tick) case nodelist.NodeSelectedMsg: - m.selectedNodeID = string(msg) - m.nodedetails = nodedetails.New(&m.tsStatus, m.selectedNodeID, m.w, m.h) + m.selectedNodeID = tsKey.NodePublic(msg) + m.nodedetails = nodedetails.New(m.tsStatus, m.selectedNodeID, m.w, m.h) m.viewState = viewStateDetails case tea.WindowSizeMsg: m.w, m.h = msg.Width, msg.Height if !m.isLoading { m.nodelist.SetSize(msg.Width, msg.Height) } + case spinner.TickMsg: + if m.isLoading { + m.spinner, tmpCmd = m.spinner.Update(msg) + cmds = append(cmds, tmpCmd) + } } switch m.viewState { case viewStateDetails: - m.nodedetails, cmd = m.nodedetails.Update(msg) + m.nodedetails, tmpCmd = m.nodedetails.Update(msg) + cmds = append(cmds, tmpCmd) case viewStateList: if m.isLoading { - m.spinner, cmd = m.spinner.Update(msg) + m.spinner, tmpCmd = m.spinner.Update(msg) + cmds = append(cmds, tmpCmd) } else { - m.nodelist, cmd = m.nodelist.Update(msg) + m.nodelist, tmpCmd = m.nodelist.Update(msg) + cmds = append(cmds, tmpCmd) } } - cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } @@ -123,21 +119,26 @@ func (m Model) View() string { switch m.viewState { case viewStateDetails: return m.nodedetails.View() - default: + case viewStateList: if m.isLoading { - return fmt.Sprintf("\n\n %s Loading ...\n\n", m.spinner.View()) + return fmt.Sprintf("\n\n %s Loading...\n\n", m.spinner.View()) } return m.nodelist.View() + default: + return "*_*" } } -func New() Model { +func New(lc *tailscale.LocalClient) Model { m := Model{ - viewState: viewStateList, - isLoading: true, - spinner: spinner.New(), + tsLocalClient: lc, + viewState: viewStateList, + isLoading: false, + spinner: spinner.New(), } m.spinner.Spinner = spinner.Dot m.spinner.Style = constants.SpinnerStyle + + m.nodelist = nodelist.New(nil, m.w, m.h) return m } diff --git a/main.go b/main.go index 25e5cf0..b0e6cd1 100644 --- a/main.go +++ b/main.go @@ -6,15 +6,24 @@ import ( "github.com/bilguun0203/tailscale-tui/internal/tui" tea "github.com/charmbracelet/bubbletea" + "tailscale.com/client/tailscale" ) -func main() { - m := tui.New() +var lc tailscale.LocalClient +func main() { + m := tui.New(&lc) p := tea.NewProgram(m, tea.WithAltScreen()) - if _, err := p.Run(); err != nil { + fm, err := p.Run() + + if err != nil { fmt.Println("Error running program:", err) os.Exit(1) } + + if fm.(tui.Model).Err != nil { + fmt.Println("Error running program:", fm.(tui.Model).Err) + os.Exit(1) + } } From 055690ec3b1ff3f72b75329fa44659c6e910cde2 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Thu, 22 Aug 2024 23:01:30 +0800 Subject: [PATCH 03/11] feat: move ts actions to separate package --- internal/tailscale/tailscale.go | 39 ---------------- internal/tailscale/types.go | 72 ----------------------------- internal/ts/ts.go | 26 +++++++++++ internal/tui/keymap/keymap.go | 10 ++++ internal/tui/node_list/node_list.go | 20 ++------ internal/tui/tui.go | 15 +++--- main.go | 5 +- 7 files changed, 46 insertions(+), 141 deletions(-) delete mode 100644 internal/tailscale/tailscale.go delete mode 100644 internal/tailscale/types.go create mode 100644 internal/ts/ts.go diff --git a/internal/tailscale/tailscale.go b/internal/tailscale/tailscale.go deleted file mode 100644 index 1219cec..0000000 --- a/internal/tailscale/tailscale.go +++ /dev/null @@ -1,39 +0,0 @@ -package tailscale - -import ( - "encoding/json" - "os/exec" - "runtime" -) - -type Tailscale struct { - cliPath string -} - -func (ts Tailscale) runCommand(arg ...string) ([]byte, error) { - cmd := exec.Command(ts.cliPath, arg...) - output, err := cmd.Output() - return output, err -} - -func (ts Tailscale) Status() (Status, error) { - output, err := ts.runCommand("status", "--json") - status := Status{} - if err != nil { - return status, err - } - err = json.Unmarshal(output, &status) - if err != nil { - return status, err - } - return status, nil -} - -func New() (Tailscale, error) { - cliPath := "tailscale" - if runtime.GOOS == "darwin" { - cliPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale" - } - _, err := exec.LookPath(cliPath) - return Tailscale{cliPath: cliPath}, err -} diff --git a/internal/tailscale/types.go b/internal/tailscale/types.go deleted file mode 100644 index c98cbb8..0000000 --- a/internal/tailscale/types.go +++ /dev/null @@ -1,72 +0,0 @@ -package tailscale - -import ( - "time" -) - -type Status struct { - Version string `json:"Version"` - Tun bool `json:"TUN"` - BackendState string `json:"BackendState"` - AuthURL string `json:"AuthURL"` - TailscaleIPs []string `json:"TailscaleIPs"` - Self PeerStatus `json:"Self"` - Health []string `json:"Health"` - MagicDNSSuffix string `json:"MagicDNSSuffix"` - CurrentTailnet TailnetStatus `json:"CurrentTailnet"` - Peer map[string]PeerStatus `json:"Peer"` - User map[string]User `json:"User"` - ClientVersion ClientVersion `json:"ClientVersion"` -} - -type ClientVersion struct { - RunningLatest bool `json:"RunningLatest,omitempty"` - LatestVersion string `json:"LatestVersion,omitempty"` - UrgentSecurityUpdate bool `json:"UrgentSecurityUpdate,omitempty"` - Notify bool `json:"Notify,omitempty"` - NotifyURL string `json:"NotifyURL,omitempty"` - NotifyText string `json:"NotifyText,omitempty"` -} - -type TailnetStatus struct { - Name string `json:"Name"` - MagicDNSSuffix string `json:"MagicDNSSuffix"` - MagicDNSEnabled bool `json:"MagicDNSEnabled"` -} - -type PeerStatus struct { - ID string `json:"ID"` - PublicKey string `json:"PublicKey"` - HostName string `json:"HostName"` - DNSName string `json:"DNSName"` - OS string `json:"OS"` - UserID int64 `json:"UserID"` - TailscaleIPs []string `json:"TailscaleIPs"` - AllowedIPs []string `json:"AllowedIPs"` - Addrs []string `json:"Addrs"` - CurAddr string `json:"CurAddr"` - Relay string `json:"Relay"` - RxBytes int `json:"RxBytes"` - TxBytes int `json:"TxBytes"` - Created time.Time `json:"Created"` - LastWrite time.Time `json:"LastWrite"` - LastSeen time.Time `json:"LastSeen"` - LastHandshake time.Time `json:"LastHandshake"` - Online bool `json:"Online"` - ExitNode bool `json:"ExitNode"` - ExitNodeOption bool `json:"ExitNodeOption"` - Active bool `json:"Active"` - PeerAPIURL []string `json:"PeerAPIURL"` - InNetworkMap bool `json:"InNetworkMap"` - InMagicSock bool `json:"InMagicSock"` - InEngine bool `json:"InEngine"` - Expired bool `json:"Expired,omitempty"` - KeyExpiry *time.Time `json:"KeyExpiry,omitempty"` -} - -type User struct { - ID int64 `json:"ID"` - LoginName string `json:"LoginName"` - DisplayName string `json:"DisplayName"` - ProfilePicURL string `json:"ProfilePicURL"` -} diff --git a/internal/ts/ts.go b/internal/ts/ts.go new file mode 100644 index 0000000..9df5f6c --- /dev/null +++ b/internal/ts/ts.go @@ -0,0 +1,26 @@ +package ts + +import ( + "context" + + "tailscale.com/client/tailscale" + "tailscale.com/ipn" + "tailscale.com/ipn/ipnstate" +) + +var lc tailscale.LocalClient + +func GetStatus() (*ipnstate.Status, error) { + return lc.Status(context.Background()) +} + +// Connect/Disconnect Tailscale network. +// Equivalent to `tailscale up` (status=true) `tailscale down` (status=false) commands +func SetTSStatus(status bool) { + lc.EditPrefs(context.Background(), &ipn.MaskedPrefs{ + Prefs: ipn.Prefs{ + WantRunning: status, + }, + WantRunningSet: true, + }) +} diff --git a/internal/tui/keymap/keymap.go b/internal/tui/keymap/keymap.go index 4e3a7cd..d338f6d 100644 --- a/internal/tui/keymap/keymap.go +++ b/internal/tui/keymap/keymap.go @@ -15,6 +15,8 @@ type KeyMap struct { ForceQuit key.Binding ShowFullHelp key.Binding CloseFullHelp key.Binding + TSUp key.Binding + TSDown key.Binding } func (k KeyMap) ShortHelp() []key.Binding { @@ -66,6 +68,14 @@ func NewKeyMap() KeyMap { key.WithKeys("q", "esc"), key.WithHelp("q", "quit"), ), + TSUp: key.NewBinding( + key.WithKeys("["), + key.WithHelp("[", "up"), + ), + TSDown: key.NewBinding( + key.WithKeys("]"), + key.WithHelp("]", "down"), + ), ForceQuit: key.NewBinding(key.WithKeys("ctrl+c")), } } diff --git a/internal/tui/node_list/node_list.go b/internal/tui/node_list/node_list.go index 6b1baf1..f8247b1 100644 --- a/internal/tui/node_list/node_list.go +++ b/internal/tui/node_list/node_list.go @@ -1,20 +1,18 @@ package nodelist import ( - "context" "fmt" "sort" "strings" "github.com/atotto/clipboard" + "github.com/bilguun0203/tailscale-tui/internal/ts" "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "tailscale.com/client/tailscale" - "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" tsKey "tailscale.com/types/key" ) @@ -98,24 +96,12 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { cmds = append(cmds, cmd) } if key.Matches(msg, m.keyMap.TSUp) { - var lc tailscale.LocalClient - lc.EditPrefs(context.Background(), &ipn.MaskedPrefs{ - Prefs: ipn.Prefs{ - WantRunning: true, - }, - WantRunningSet: true, - }) + ts.SetTSStatus(true); cmd = func() tea.Msg { return RefreshMsg(true) } cmds = append(cmds, cmd) } if key.Matches(msg, m.keyMap.TSDown) { - var lc tailscale.LocalClient - lc.EditPrefs(context.Background(), &ipn.MaskedPrefs{ - Prefs: ipn.Prefs{ - WantRunning: false, - }, - WantRunningSet: true, - }) + ts.SetTSStatus(false); cmd = func() tea.Msg { return RefreshMsg(true) } cmds = append(cmds, cmd) } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 6d9a8c7..9e21d74 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -1,15 +1,14 @@ package tui import ( - "context" "fmt" + "github.com/bilguun0203/tailscale-tui/internal/ts" "github.com/bilguun0203/tailscale-tui/internal/tui/constants" nodedetails "github.com/bilguun0203/tailscale-tui/internal/tui/node_details" nodelist "github.com/bilguun0203/tailscale-tui/internal/tui/node_list" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" - "tailscale.com/client/tailscale" "tailscale.com/ipn/ipnstate" tsKey "tailscale.com/types/key" ) @@ -30,7 +29,6 @@ func (f viewState) String() string { type Model struct { viewState viewState - tsLocalClient *tailscale.LocalClient tsStatus *ipnstate.Status selectedNodeID tsKey.NodePublic isLoading bool @@ -46,7 +44,7 @@ type statusError error func (m Model) getTsStatus() tea.Cmd { return func() tea.Msg { - status, err := m.tsLocalClient.Status(context.Background()) + status, err := ts.GetStatus() if err != nil { return statusError(err) } @@ -129,12 +127,11 @@ func (m Model) View() string { } } -func New(lc *tailscale.LocalClient) Model { +func New() Model { m := Model{ - tsLocalClient: lc, - viewState: viewStateList, - isLoading: false, - spinner: spinner.New(), + viewState: viewStateList, + isLoading: false, + spinner: spinner.New(), } m.spinner.Spinner = spinner.Dot m.spinner.Style = constants.SpinnerStyle diff --git a/main.go b/main.go index b0e6cd1..7894b4a 100644 --- a/main.go +++ b/main.go @@ -6,13 +6,10 @@ import ( "github.com/bilguun0203/tailscale-tui/internal/tui" tea "github.com/charmbracelet/bubbletea" - "tailscale.com/client/tailscale" ) -var lc tailscale.LocalClient - func main() { - m := tui.New(&lc) + m := tui.New() p := tea.NewProgram(m, tea.WithAltScreen()) fm, err := p.Run() From d64ad0cef47ab94bde490882548bca5579fffb7f Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Fri, 23 Aug 2024 12:51:45 +0800 Subject: [PATCH 04/11] chore: go mod --- go.mod | 24 +++++++-------- go.sum | 96 ++++++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 86 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index 20884d0..7751f0e 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,15 @@ module github.com/bilguun0203/tailscale-tui -go 1.22.0 +go 1.23 toolchain go1.23.0 require ( github.com/atotto/clipboard v0.1.4 github.com/charmbracelet/bubbles v0.19.0 - github.com/charmbracelet/bubbletea v0.27.0 + github.com/charmbracelet/bubbletea v0.27.1 github.com/charmbracelet/lipgloss v0.13.0 + tailscale.com v1.72.1 ) require ( @@ -18,20 +19,20 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/x/ansi v0.2.3 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect - github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect + github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/fxamacker/cbor/v2 v2.6.0 // indirect - github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect - github.com/jsimonetti/rtnetlink v1.4.0 // indirect + github.com/jsimonetti/rtnetlink v1.4.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.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.16 // indirect github.com/mdlayher/netlink v1.7.2 // indirect - github.com/mdlayher/socket v0.5.0 // indirect + github.com/mdlayher/socket v0.5.1 // 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 @@ -40,14 +41,13 @@ require ( github.com/sahilm/fuzzy v0.1.1 // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/x448/float16 v0.8.4 // indirect - go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect + go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/net v0.27.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect - tailscale.com v1.72.0 // indirect ) diff --git a/go.sum b/go.sum index 45ff168..09f67d5 100644 --- a/go.sum +++ b/go.sum @@ -8,32 +8,60 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0= github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA= -github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU= -github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y= +github.com/charmbracelet/bubbletea v0.27.1 h1:/yhaJKX52pxG4jZVKCNWj/oq0QouPdXycriDRA6m6r8= +github.com/charmbracelet/bubbletea v0.27.1/go.mod h1:xc4gm5yv+7tbniEvQ0naiG9P3fzYhk16cTgDZQQW6YE= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= -github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= -github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e h1:L+XrFvD0vBIBm+Wf9sFN6aU395t7JROoai0qXZraA4U= +github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU= 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/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= -github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= -github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= +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.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= +github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= +github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 h1:xcuWappghOVI8iNWoF2OKahVejd1LSVi/v4JED44Amo= +github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= +github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= -github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= -github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= +github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90= +github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -44,10 +72,12 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J 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/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= -github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= -github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= 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= @@ -56,25 +86,41 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= +github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw= +github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/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= -go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= -go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +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= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -84,7 +130,13 @@ golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= -tailscale.com v1.72.0 h1:emsPxupFM72zJLt2wvSzpa1vymqPYbL0WVVO+170/s0= -tailscale.com v1.72.0/go.mod h1:v7OHtg0KLAnhOVf81Z8WrjNefj238QbFhgkWJQoKxbs= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= +tailscale.com v1.72.1 h1:hk82jek36ph2S3Tfsh57NVWKEm/pZ9nfUonvlowpfaA= +tailscale.com v1.72.1/go.mod h1:v7OHtg0KLAnhOVf81Z8WrjNefj238QbFhgkWJQoKxbs= From d1f44e08dda6048fa27573fe10e7a8100c22b70a Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Fri, 23 Aug 2024 16:36:56 +0800 Subject: [PATCH 05/11] refactor: styling --- README.md | 2 +- internal/ts/msg.go | 7 ++ internal/tui/constants/constants.go | 30 +++++--- internal/tui/keymap/keymap.go | 20 ++--- internal/tui/node_details/node_details.go | 66 +---------------- internal/tui/node_details/render.go | 87 ++++++++++++++++++++++ internal/tui/node_list/node_list.go | 89 ++++++++--------------- internal/tui/tui.go | 18 ++--- 8 files changed, 164 insertions(+), 155 deletions(-) create mode 100644 internal/ts/msg.go create mode 100644 internal/tui/node_details/render.go diff --git a/README.md b/README.md index b19f324..a48df63 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ tailscale-tui ### Shortcuts - `↑/k` `↓/j` - up/down -- `→/l/pgdn` `←/h/pgup` - next/prev page +- `enter/→/l` `esc/←/h` - enter/back navigation - `g/home` `G/end` - go to start/end - `q` `Ctrl+c` - quit - `/` - filter diff --git a/internal/ts/msg.go b/internal/ts/msg.go new file mode 100644 index 0000000..c5e35c3 --- /dev/null +++ b/internal/ts/msg.go @@ -0,0 +1,7 @@ +package ts + +import "tailscale.com/ipn/ipnstate" + +type StatusDataMsg *ipnstate.Status + +type StatusErrorMsg error diff --git a/internal/tui/constants/constants.go b/internal/tui/constants/constants.go index c179d5b..572f3c8 100644 --- a/internal/tui/constants/constants.go +++ b/internal/tui/constants/constants.go @@ -2,15 +2,25 @@ package constants import "github.com/charmbracelet/lipgloss" -var TitleStyle = lipgloss.NewStyle().Background(lipgloss.Color("#20F394")).Foreground(lipgloss.Color("#000000")) -var AltTitleStyle = lipgloss.NewStyle().Background(lipgloss.Color("#05EAFF")).Foreground(lipgloss.Color("#000000")) -var NormalTextStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#1A1A1A", Dark: "#DDDDDD"}) -var DangerTextStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "197", Dark: "197"}) -var SuccessTextStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "034", Dark: "049"}) -var WarningTextStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "214", Dark: "214"}) -var MutedTextStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"}) -var AccentTextStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#00C86E", Dark: "#20F394"}) -var AltAccentTextStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#05EAFF", Dark: "#00E5FA"}) +var ColorNormal = lipgloss.AdaptiveColor{Light: "#1A1A1A", Dark: "#DDDDDD"} +var ColorDanger = lipgloss.AdaptiveColor{Light: "197", Dark: "197"} +var ColorSuccess = lipgloss.AdaptiveColor{Light: "034", Dark: "049"} +var ColorWarning = lipgloss.AdaptiveColor{Light: "214", Dark: "214"} +var ColorDimmed = lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"} +var ColorMuted = lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"} +var ColorPrimary = lipgloss.AdaptiveColor{Light: "#00C86E", Dark: "#20F394"} +var ColorSecondary = lipgloss.AdaptiveColor{Light: "#05EAFF", Dark: "#00E5FA"} + +var PrimaryTitleStyle = lipgloss.NewStyle().Padding(0, 1).Background(lipgloss.Color(ColorPrimary.Dark)).Foreground(lipgloss.Color("#000000")) +var SecondaryTitleStyle = lipgloss.NewStyle().Padding(0, 1).Background(lipgloss.Color(ColorSecondary.Light)).Foreground(lipgloss.Color("#000000")) +var NormalTextStyle = lipgloss.NewStyle().Foreground(ColorNormal) +var DangerTextStyle = lipgloss.NewStyle().Foreground(ColorDanger) +var SuccessTextStyle = lipgloss.NewStyle().Foreground(ColorSuccess) +var WarningTextStyle = lipgloss.NewStyle().Foreground(ColorWarning) +var DimmedTextStyle = lipgloss.NewStyle().Foreground(ColorDimmed) +var MutedTextStyle = lipgloss.NewStyle().Foreground(ColorMuted) +var PrimaryTextStyle = lipgloss.NewStyle().Foreground(ColorPrimary) +var SecondaryTextStyle = lipgloss.NewStyle().Foreground(ColorSecondary) var LayoutStyle = lipgloss.NewStyle() -var SpinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) +var SpinnerStyle = lipgloss.NewStyle().Foreground(ColorPrimary) var HeaderStyle = lipgloss.NewStyle().Margin(1, 2) diff --git a/internal/tui/keymap/keymap.go b/internal/tui/keymap/keymap.go index d338f6d..a26b0e9 100644 --- a/internal/tui/keymap/keymap.go +++ b/internal/tui/keymap/keymap.go @@ -20,18 +20,26 @@ type KeyMap struct { } func (k KeyMap) ShortHelp() []key.Binding { - return []key.Binding{k.CopyIpv4, k.Back, k.Quit, k.ShowFullHelp} + return []key.Binding{k.CopyIpv4, k.Enter, k.Back, k.Quit, k.ShowFullHelp} } func (k KeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ {k.CopyIpv4, k.CopyIpv6, k.CopyDNSName}, - {k.Back, k.Quit, k.CloseFullHelp}, + {k.Enter, k.Back, k.Quit, k.CloseFullHelp}, } } func NewKeyMap() KeyMap { return KeyMap{ + Enter: key.NewBinding( + key.WithKeys("enter", "right", "l"), + key.WithHelp("enter/→/l", "details"), + ), + Back: key.NewBinding( + key.WithKeys("esc", "left", "h"), + key.WithHelp("esc/←/h", "back"), + ), CopyIpv4: key.NewBinding( key.WithKeys("y"), key.WithHelp("y", "copy ipv4"), @@ -56,14 +64,6 @@ func NewKeyMap() KeyMap { key.WithKeys("?"), key.WithHelp("?", "close help"), ), - Enter: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "details"), - ), - Back: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "back"), - ), Quit: key.NewBinding( key.WithKeys("q", "esc"), key.WithHelp("q", "quit"), diff --git a/internal/tui/node_details/node_details.go b/internal/tui/node_details/node_details.go index bff8d72..60c7764 100644 --- a/internal/tui/node_details/node_details.go +++ b/internal/tui/node_details/node_details.go @@ -1,11 +1,6 @@ package nodedetails import ( - "fmt" - "strings" - "time" - - "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -57,65 +52,6 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { return m, cmds } -func (m Model) detailView() string { - title := constants.AltTitleStyle.Render(" Node info ") - status := constants.AltAccentTextStyle.Render("Status: ") - hostname := constants.AltAccentTextStyle.Render("Host: ") - userInfo := "??? " - ips := constants.AltAccentTextStyle.Render("IPs: ") - relay := constants.AltAccentTextStyle.Render("Relay: ") - offersExitNode := "no" - exitNode := constants.AltAccentTextStyle.Render("Exit node: ") - asExitNode := "" - keyExpiry := constants.AltAccentTextStyle.Render("Key expiry: ") - if m.tailStatus != nil { - node, ok := m.tailStatus.Peer[m.nodeID] - if !ok && m.tailStatus.Self.PublicKey == m.nodeID { - node = m.tailStatus.Self - ok = true - } - if ok { - if user, ok := m.tailStatus.User[node.UserID]; ok { - userInfo = fmt.Sprintf("%s <%s>", user.DisplayName, user.LoginName) - } else { - userInfo = fmt.Sprintf("??? <%d>", node.UserID) - } - if node.ExitNodeOption { - offersExitNode = constants.WarningTextStyle.Render("yes") - } - var ipList []string - for _, ip := range node.TailscaleIPs { - ipList = append(ipList, ip.String()) - } - if node.Online { - status += constants.SuccessTextStyle.Render("Online") - } else { - status += constants.DangerTextStyle.Render("Offline") - } - if node.KeyExpiry == nil { - keyExpiry += "Disabled" - } else { - if node.Expired { - keyExpiry += constants.DangerTextStyle.Render("Expired ") - } else { - keyExpiry += "Active " - } - keyExpiry += constants.MutedTextStyle.Render("(" + node.KeyExpiry.Local().Format(time.RFC3339) + ")") - } - ipList = append(ipList, node.DNSName) - ips += strings.Join(ipList, " | ") - hostname += node.HostName + " (" + node.OS + ")" - relay += node.Relay - exitNode += constants.MutedTextStyle.Render("offers:") + offersExitNode - if node.ExitNode { - asExitNode = constants.WarningTextStyle.Render("~ This node is currently being used as an exit node.") - } - } - } - body := lipgloss.JoinVertical(lipgloss.Left, userInfo+"\n", hostname, status, ips, relay, keyExpiry, exitNode, asExitNode) - return constants.HeaderStyle.Render(fmt.Sprintf("%s\n\n%s", title, body)) -} - func (m Model) Init() tea.Cmd { return nil } @@ -138,7 +74,7 @@ func (m Model) View() string { helpHeight := lipgloss.Height(m.help.View(m.keyMap)) detailHeight := m.h - helpHeight return lipgloss.JoinVertical( - lipgloss.Left, lipgloss.NewStyle().Height(detailHeight).Render(m.detailView()), + lipgloss.Left, lipgloss.NewStyle().Height(detailHeight).Render(NodeDetailRender(m.tailStatus, m.nodeID, "")), lipgloss.NewStyle().Margin(0, 2).Render(m.help.View(m.keyMap)), ) diff --git a/internal/tui/node_details/render.go b/internal/tui/node_details/render.go new file mode 100644 index 0000000..524a3f4 --- /dev/null +++ b/internal/tui/node_details/render.go @@ -0,0 +1,87 @@ +package nodedetails + +import ( + "fmt" + "strings" + "time" + + "github.com/bilguun0203/tailscale-tui/internal/tui/constants" + "github.com/charmbracelet/lipgloss" + "tailscale.com/ipn/ipnstate" + tsKey "tailscale.com/types/key" +) + +func NodeDetailRender(tsStatus *ipnstate.Status, nodeID tsKey.NodePublic, customTitle string) string { + title := constants.SecondaryTitleStyle.Render("Node info") + if customTitle != "" { + title = customTitle + } + status := constants.SecondaryTextStyle.Render("Status: ") + hostname := constants.SecondaryTextStyle.Render("Host: ") + userInfo := "??? " + ips := constants.SecondaryTextStyle.Render("IPs: ") + relay := constants.SecondaryTextStyle.Render("Relay: ") + offersExitNode := "no" + exitNode := constants.SecondaryTextStyle.Render("Exit node: ") + asExitNode := "" + keyExpiry := constants.SecondaryTextStyle.Render("Key expiry: ") + currentDevice := false + if tsStatus != nil { + node, ok := tsStatus.Peer[nodeID] + if !ok && tsStatus.Self.PublicKey == nodeID { + node = tsStatus.Self + currentDevice = true + ok = true + } + if ok { + if user, ok := tsStatus.User[node.UserID]; ok { + userInfo = fmt.Sprintf("%s <%s>", user.DisplayName, user.LoginName) + } else { + userInfo = fmt.Sprintf("??? <%d>", node.UserID) + } + if node.ExitNodeOption { + offersExitNode = constants.WarningTextStyle.Render("yes") + } + var ipList []string + for _, ip := range node.TailscaleIPs { + ipList = append(ipList, ip.String()) + } + if node.Online { + status += constants.SuccessTextStyle.Render("Online") + } else { + status += constants.DangerTextStyle.Render("Offline") + } + if node.KeyExpiry == nil { + keyExpiry += "Disabled" + } else { + if node.Expired { + keyExpiry += constants.DangerTextStyle.Render("Expired ") + } else { + keyExpiry += "Active " + } + keyExpiry += constants.DimmedTextStyle.Render("(" + node.KeyExpiry.Local().Format(time.RFC3339) + ")") + } + ipList = append(ipList, node.DNSName) + ips += strings.Join(ipList, " | ") + hostname += node.HostName + " (" + node.OS + ")" + if currentDevice { + hostname += " " + constants.DimmedTextStyle.Render("*This device*") + } + relay += node.Relay + exitNode += constants.DimmedTextStyle.Render("offers: ") + offersExitNode + if node.ExitNode { + asExitNode = constants.WarningTextStyle.Render("~ This node is currently being used as an exit node.") + } + if currentDevice && tsStatus.ExitNodeStatus != nil { + for _, peer := range tsStatus.Peer { + if peer.ID == tsStatus.ExitNodeStatus.ID { + exitNode += constants.DimmedTextStyle.Render(" / using: ") + constants.WarningTextStyle.Render(peer.HostName) + break + } + } + } + } + } + body := lipgloss.JoinVertical(lipgloss.Left, userInfo+"\n", hostname, status, ips, relay, keyExpiry, exitNode, asExitNode) + return constants.HeaderStyle.Render(fmt.Sprintf("%s\n\n%s", title, body)) +} diff --git a/internal/tui/node_list/node_list.go b/internal/tui/node_list/node_list.go index f8247b1..8e5423f 100644 --- a/internal/tui/node_list/node_list.go +++ b/internal/tui/node_list/node_list.go @@ -9,8 +9,10 @@ import ( "github.com/bilguun0203/tailscale-tui/internal/ts" "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" + nodedetails "github.com/bilguun0203/tailscale-tui/internal/tui/node_details" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "tailscale.com/ipn/ipnstate" @@ -32,7 +34,6 @@ type Model struct { exitNode string list list.Model keyMap keymap.KeyMap - msg string w int h int } @@ -60,11 +61,12 @@ func (m *Model) updateKeybindings() { m.keyMap.Enter.SetEnabled(false) } m.keyMap.Back.SetEnabled(false) + m.keyMap.Quit.SetEnabled(false) m.keyMap.ShowFullHelp.SetEnabled(false) m.keyMap.CloseFullHelp.SetEnabled(false) - m.keyMap.Back.SetEnabled(false) - m.keyMap.Quit.SetEnabled(false) m.keyMap.ForceQuit.SetEnabled(false) + m.list.KeyMap.NextPage.SetEnabled(false) + m.list.KeyMap.PrevPage.SetEnabled(false) } func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { @@ -84,7 +86,7 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { m.list.NewStatusMessage("Sorry, nothing to copy.") } else { clipboard.WriteAll(copyStr) - m.list.NewStatusMessage(fmt.Sprintf("Copied \"%s\"!", constants.AccentTextStyle.Underline(true).Render(copyStr))) + m.list.NewStatusMessage(fmt.Sprintf("Copied \"%s\"!", constants.PrimaryTextStyle.Underline(true).Render(copyStr))) } } if key.Matches(msg, m.keyMap.Refresh) { @@ -96,12 +98,12 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { cmds = append(cmds, cmd) } if key.Matches(msg, m.keyMap.TSUp) { - ts.SetTSStatus(true); + ts.SetTSStatus(true) cmd = func() tea.Msg { return RefreshMsg(true) } cmds = append(cmds, cmd) } if key.Matches(msg, m.keyMap.TSDown) { - ts.SetTSStatus(false); + ts.SetTSStatus(false) cmd = func() tea.Msg { return RefreshMsg(true) } cmds = append(cmds, cmd) } @@ -109,42 +111,10 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { } func (m Model) headerView() string { - hostname := "" - userInfo := "" - os := "" - ips := "" - offersExitNode := "no" - usingExitNode := "-" - if m.tailStatus != nil { - if user, ok := m.tailStatus.User[m.tailStatus.Self.UserID]; ok { - userInfo = fmt.Sprintf("%s <%s>", user.DisplayName, user.LoginName) - } - if m.tailStatus.ExitNodeStatus != nil { - for _, peer := range m.tailStatus.Peer { - if peer.ID == m.tailStatus.ExitNodeStatus.ID { - usingExitNode = constants.WarningTextStyle.Render(peer.HostName) - break - } - } - } - hostname = m.tailStatus.Self.HostName - os = m.tailStatus.Self.OS - if m.tailStatus.Self.ExitNodeOption { - offersExitNode = constants.WarningTextStyle.Render("yes") - } - var ipList []string - for _, ip := range m.tailStatus.TailscaleIPs { - ipList = append(ipList, ip.String()) - } - ipList = append(ipList, m.tailStatus.Self.DNSName) - ips = strings.Join(ipList, " | ") + if m.tailStatus == nil { + return nodedetails.NodeDetailRender(nil, tsKey.NodePublic{}, constants.PrimaryTitleStyle.Render("Current Node")) } - title := constants.TitleStyle.Render(" This node ") - hostname = constants.AccentTextStyle.Render("Host: ") + hostname + " (" + os + ")" - ips = constants.AccentTextStyle.Render("IPs: ") + ips - exitNode := constants.AccentTextStyle.Render("Exit node: ") + constants.MutedTextStyle.Render("offers:") + (offersExitNode) + constants.MutedTextStyle.Render(" / using:") + usingExitNode - body := constants.NormalTextStyle.Render(fmt.Sprintf("%s\n\n%s\n%s\n%s %s", userInfo, hostname, ips, exitNode, m.msg)) - return constants.HeaderStyle.Render(fmt.Sprintf("%s\n\n%s", title, body)) + return nodedetails.NodeDetailRender(m.tailStatus, m.tailStatus.Self.PublicKey, constants.PrimaryTitleStyle.Render("Current Node")) } func (m *Model) getItems() []list.Item { @@ -183,16 +153,16 @@ func (m *Model) getItems() []list.Item { if v.UserID != m.tailStatus.Self.UserID { owner = "from:" + m.tailStatus.User[v.UserID].LoginName } - owner = constants.MutedTextStyle.Render("[" + owner + "]") + owner = constants.DimmedTextStyle.Render("[" + owner + "]") exitNode := "" if v.ExitNodeOption { - exitNode = constants.MutedTextStyle.Bold(true).Render("[→]") + exitNode = constants.DimmedTextStyle.Bold(true).Render("[→]") } if v.ExitNode { m.exitNode = v.PublicKey.String() exitNode = constants.SuccessTextStyle.Bold(true).Render("[→]") } - os := v.OS + os := constants.NormalTextStyle.Render(v.OS) title := fmt.Sprintf("%s %s %s %s %s", hostName, state, owner, os, exitNode) desc := "- " var ips []string @@ -214,6 +184,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { + case ts.StatusDataMsg: + m.tailStatus = msg + if m.tailStatus != nil { + cmds = append(cmds, m.list.SetItems(m.getItems())) + } + m.list.StopSpinner() case tea.KeyMsg: var kcmds []tea.Cmd m, kcmds = m.keyBindingsHandler(msg) @@ -240,33 +216,32 @@ func New(status *ipnstate.Status, w, h int) Model { Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}) d.Styles.SelectedTitle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(lipgloss.AdaptiveColor{Light: "#00C86E", Dark: "#20F394"}). - Foreground(lipgloss.AdaptiveColor{Light: "#00C86E", Dark: "#20F394"}). + BorderForeground(constants.PrimaryTextStyle.GetForeground()). + Foreground(constants.PrimaryTextStyle.GetForeground()). Padding(0, 0, 0, 1) d.Styles.SelectedDesc = d.Styles.SelectedTitle - d.Styles.DimmedTitle = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}). - Padding(0, 0, 0, 2) - d.Styles.DimmedDesc = d.Styles.DimmedTitle. - Foreground(lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"}) + d.Styles.DimmedTitle = constants.DimmedTextStyle.Padding(0, 0, 0, 2) + d.Styles.DimmedDesc = d.Styles.DimmedTitle.Foreground(constants.MutedTextStyle.GetForeground()) d.SetHeight(2) d.SetSpacing(1) m := Model{ list: list.New([]list.Item{}, d, w, h), keyMap: keymap.NewKeyMap(), tailStatus: status, + w: w, + h: h, } + m.list.SetSpinner(spinner.Dot) + m.list.StartSpinner() headerHeight := lipgloss.Height(m.headerView()) - m.list.SetHeight(h - headerHeight) + m.list.SetHeight(h - headerHeight - 4) m.list.SetItems(m.getItems()) m.list.Title = "Nodes" - m.list.Styles.Title = constants.TitleStyle.Padding(0, 1) - m.list.FilterInput.PromptStyle = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#00C86E", Dark: "#20F394"}) - m.list.FilterInput.Cursor.Style = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#00C86E", Dark: "#20F394"}) + m.list.Styles.Title = constants.PrimaryTitleStyle + m.list.FilterInput.PromptStyle = constants.PrimaryTextStyle + m.list.FilterInput.Cursor.Style = constants.PrimaryTextStyle m.list.AdditionalShortHelpKeys = func() []key.Binding { return []key.Binding{ m.keyMap.CopyIpv4, diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 9e21d74..0c06a8f 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -39,16 +39,13 @@ type Model struct { w, h int } -type statusLoaded *ipnstate.Status -type statusError error - func (m Model) getTsStatus() tea.Cmd { return func() tea.Msg { status, err := ts.GetStatus() if err != nil { - return statusError(err) + return ts.StatusErrorMsg(err) } - return statusLoaded(status) + return ts.StatusDataMsg(status) } } @@ -65,13 +62,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { - case statusLoaded: + case ts.StatusDataMsg: m.isLoading = false m.Err = nil m.tsStatus = msg m.viewState = viewStateList - m.nodelist = nodelist.New(m.tsStatus, m.w, m.h) - case statusError: + case ts.StatusErrorMsg: m.isLoading = false m.Err = msg return m, tea.Quit @@ -87,9 +83,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewState = viewStateDetails case tea.WindowSizeMsg: m.w, m.h = msg.Width, msg.Height - if !m.isLoading { - m.nodelist.SetSize(msg.Width, msg.Height) - } + m.nodelist.SetSize(msg.Width, msg.Height) case spinner.TickMsg: if m.isLoading { m.spinner, tmpCmd = m.spinner.Update(msg) @@ -130,7 +124,7 @@ func (m Model) View() string { func New() Model { m := Model{ viewState: viewStateList, - isLoading: false, + isLoading: true, spinner: spinner.New(), } m.spinner.Spinner = spinner.Dot From dcfd6a662b6a29d319d13d3da815bec7e5ac6979 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Fri, 23 Aug 2024 16:41:16 +0800 Subject: [PATCH 06/11] fix: copy in details view --- internal/tui/node_details/node_details.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/tui/node_details/node_details.go b/internal/tui/node_details/node_details.go index 60c7764..e839882 100644 --- a/internal/tui/node_details/node_details.go +++ b/internal/tui/node_details/node_details.go @@ -1,6 +1,7 @@ package nodedetails import ( + "github.com/atotto/clipboard" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -35,6 +36,27 @@ func (m *Model) updateKeybindings() { func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd + node, ok := m.tailStatus.Peer[m.nodeID] + if !ok && m.tailStatus.Self.PublicKey == m.nodeID { + node = m.tailStatus.Self + ok = true + } + if ok { + if key.Matches(msg, m.keyMap.CopyIpv4) || key.Matches(msg, m.keyMap.CopyIpv6) || key.Matches(msg, m.keyMap.CopyDNSName) { + copyStr := "" + ipCount := len(node.TailscaleIPs) + if ipCount > 0 && key.Matches(msg, m.keyMap.CopyIpv4) { + copyStr = node.TailscaleIPs[0].String() + } else if ipCount > 1 && key.Matches(msg, m.keyMap.CopyIpv6) { + copyStr = node.TailscaleIPs[1].String() + } else if key.Matches(msg, m.keyMap.CopyDNSName) { + copyStr = node.DNSName + } + if copyStr != "" { + clipboard.WriteAll(copyStr) + } + } + } switch { case key.Matches(msg, m.keyMap.Back): cmd = func() tea.Msg { From 21acf150abe8d61733c5e1403554921eda6ff6b7 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Fri, 23 Aug 2024 22:03:55 +0800 Subject: [PATCH 07/11] feat: status bar --- internal/tui/constants/constants.go | 3 + internal/tui/node_details/node_details.go | 7 +++ internal/tui/node_list/node_list.go | 20 ++++-- internal/tui/status_bar/status_bar.go | 76 +++++++++++++++++++++++ internal/tui/tui.go | 35 ++++++++--- internal/tui/types/types.go | 4 ++ 6 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 internal/tui/status_bar/status_bar.go create mode 100644 internal/tui/types/types.go diff --git a/internal/tui/constants/constants.go b/internal/tui/constants/constants.go index 572f3c8..4a7ca82 100644 --- a/internal/tui/constants/constants.go +++ b/internal/tui/constants/constants.go @@ -2,7 +2,9 @@ package constants import "github.com/charmbracelet/lipgloss" +var ColorBW = lipgloss.AdaptiveColor{Light: "#000", Dark: "#FFF"} var ColorNormal = lipgloss.AdaptiveColor{Light: "#1A1A1A", Dark: "#DDDDDD"} +var ColorNormalInv = lipgloss.AdaptiveColor{Light: "#DDDDDD", Dark: "#1A1A1A"} var ColorDanger = lipgloss.AdaptiveColor{Light: "197", Dark: "197"} var ColorSuccess = lipgloss.AdaptiveColor{Light: "034", Dark: "049"} var ColorWarning = lipgloss.AdaptiveColor{Light: "214", Dark: "214"} @@ -13,6 +15,7 @@ var ColorSecondary = lipgloss.AdaptiveColor{Light: "#05EAFF", Dark: "#00E5FA"} var PrimaryTitleStyle = lipgloss.NewStyle().Padding(0, 1).Background(lipgloss.Color(ColorPrimary.Dark)).Foreground(lipgloss.Color("#000000")) var SecondaryTitleStyle = lipgloss.NewStyle().Padding(0, 1).Background(lipgloss.Color(ColorSecondary.Light)).Foreground(lipgloss.Color("#000000")) +var WarningTitleStyle = lipgloss.NewStyle().Padding(0, 1).Background(lipgloss.Color(ColorWarning.Light)).Foreground(lipgloss.Color("#000000")) var NormalTextStyle = lipgloss.NewStyle().Foreground(ColorNormal) var DangerTextStyle = lipgloss.NewStyle().Foreground(ColorDanger) var SuccessTextStyle = lipgloss.NewStyle().Foreground(ColorSuccess) diff --git a/internal/tui/node_details/node_details.go b/internal/tui/node_details/node_details.go index e839882..b1f757b 100644 --- a/internal/tui/node_details/node_details.go +++ b/internal/tui/node_details/node_details.go @@ -1,8 +1,12 @@ package nodedetails import ( + "fmt" + "github.com/atotto/clipboard" + "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" + "github.com/bilguun0203/tailscale-tui/internal/tui/types" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" @@ -54,6 +58,9 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { } if copyStr != "" { clipboard.WriteAll(copyStr) + status := fmt.Sprintf("Copied \"%s\"!", constants.PrimaryTextStyle.Underline(true).Render(copyStr)) + cmd = func() tea.Msg { return types.StatusMsg(status) } + cmds = append(cmds, cmd) } } } diff --git a/internal/tui/node_list/node_list.go b/internal/tui/node_list/node_list.go index 8e5423f..52abd4c 100644 --- a/internal/tui/node_list/node_list.go +++ b/internal/tui/node_list/node_list.go @@ -10,6 +10,7 @@ import ( "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" nodedetails "github.com/bilguun0203/tailscale-tui/internal/tui/node_details" + "github.com/bilguun0203/tailscale-tui/internal/tui/types" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/spinner" @@ -46,7 +47,6 @@ func (m *Model) SetSize(w int, h int) { } type NodeSelectedMsg tsKey.NodePublic -type RefreshMsg bool func (m *Model) updateKeybindings() { if m.list.SelectedItem() != nil { @@ -84,13 +84,21 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { } if copyStr == "" { m.list.NewStatusMessage("Sorry, nothing to copy.") + cmd = func() tea.Msg { return types.StatusMsg("Sorry, nothing to copy.") } + cmds = append(cmds, cmd) } else { - clipboard.WriteAll(copyStr) - m.list.NewStatusMessage(fmt.Sprintf("Copied \"%s\"!", constants.PrimaryTextStyle.Underline(true).Render(copyStr))) + err := clipboard.WriteAll(copyStr) + status := fmt.Sprintf("Copied \"%s\"!", constants.PrimaryTextStyle.Underline(true).Render(copyStr)) + if err != nil { + status = fmt.Sprintf("Sorry, error occured: %s", err) + } + m.list.NewStatusMessage(status) + cmd = func() tea.Msg { return types.StatusMsg(status) } + cmds = append(cmds, cmd) } } if key.Matches(msg, m.keyMap.Refresh) { - cmd = func() tea.Msg { return RefreshMsg(true) } + cmd = func() tea.Msg { return types.RefreshMsg(true) } cmds = append(cmds, cmd) } if key.Matches(msg, m.keyMap.Enter) { @@ -99,12 +107,12 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { } if key.Matches(msg, m.keyMap.TSUp) { ts.SetTSStatus(true) - cmd = func() tea.Msg { return RefreshMsg(true) } + cmd = func() tea.Msg { return types.RefreshMsg(true) } cmds = append(cmds, cmd) } if key.Matches(msg, m.keyMap.TSDown) { ts.SetTSStatus(false) - cmd = func() tea.Msg { return RefreshMsg(true) } + cmd = func() tea.Msg { return types.RefreshMsg(true) } cmds = append(cmds, cmd) } return m, cmds diff --git a/internal/tui/status_bar/status_bar.go b/internal/tui/status_bar/status_bar.go new file mode 100644 index 0000000..ced3aac --- /dev/null +++ b/internal/tui/status_bar/status_bar.go @@ -0,0 +1,76 @@ +package statusbar + +import ( + "github.com/bilguun0203/tailscale-tui/internal/tui/constants" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type Model struct { + barStyle lipgloss.Style + prefix string + prefixStyle lipgloss.Style + msg string + msgStyle lipgloss.Style + suffix string + suffixStyle lipgloss.Style + w int + h int +} + +func (m *Model) UpdatePrefix(v string) { + m.prefix = v +} + +func (m *Model) UpdateMessage(v string) { + m.msg = v +} + +func (m *Model) UpdateSuffix(v string) { + m.suffix = v +} + +func (m *Model) UpdatePrefixStyle(style lipgloss.Style) { + m.prefixStyle = style +} + +func (m *Model) UpdateMessageStyle(style lipgloss.Style) { + m.msgStyle = style +} + +func (m *Model) UpdateSuffixStyle(style lipgloss.Style) { + m.suffixStyle = style +} + +func (m Model) Init() tea.Cmd { + return nil +} +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.w, m.h = msg.Width, msg.Height + m.barStyle = m.barStyle.Width(m.w) + } + return m, nil +} + +func (m Model) View() string { + prefixView := m.prefixStyle.Render(m.prefix) + suffixView := m.suffixStyle.Render(m.suffix) + msgW := m.w - lipgloss.Width(prefixView) - lipgloss.Width(suffixView) + msgView := m.msgStyle.Padding(0, 1).Width(msgW).Render(m.msg) + return m.barStyle.Render(prefixView + msgView + suffixView) +} + +func New() Model { + barStyle := lipgloss.NewStyle().Background(constants.ColorNormalInv).Margin(1, 0).Height(1) + return Model{ + barStyle: barStyle, + prefix: "TAILSCALE-TUI", + prefixStyle: constants.PrimaryTitleStyle, + msg: "", + msgStyle: lipgloss.NewStyle().Background(barStyle.GetBackground()).Foreground(constants.ColorBW), + suffix: "", + suffixStyle: constants.SecondaryTitleStyle, + } +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 0c06a8f..06d23de 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -7,8 +7,11 @@ import ( "github.com/bilguun0203/tailscale-tui/internal/tui/constants" nodedetails "github.com/bilguun0203/tailscale-tui/internal/tui/node_details" nodelist "github.com/bilguun0203/tailscale-tui/internal/tui/node_list" + statusbar "github.com/bilguun0203/tailscale-tui/internal/tui/status_bar" + "github.com/bilguun0203/tailscale-tui/internal/tui/types" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "tailscale.com/ipn/ipnstate" tsKey "tailscale.com/types/key" ) @@ -35,6 +38,7 @@ type Model struct { Err error nodelist nodelist.Model nodedetails nodedetails.Model + statusbar statusbar.Model spinner spinner.Model w, h int } @@ -67,23 +71,29 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Err = nil m.tsStatus = msg m.viewState = viewStateList + m.statusbar.UpdateMessage("Showing all network devices") case ts.StatusErrorMsg: m.isLoading = false m.Err = msg + m.statusbar.UpdateMessage(fmt.Sprintf("Error: %s", msg)) return m, tea.Quit case nodedetails.BackMsg: m.viewState = viewStateList - case nodelist.RefreshMsg: + m.statusbar.UpdateMessage("Showing all network devices") + case types.RefreshMsg: m.isLoading = true cmds = append(cmds, m.getTsStatus()) cmds = append(cmds, m.spinner.Tick) + case types.StatusMsg: + m.statusbar.UpdateMessage(string(msg)) case nodelist.NodeSelectedMsg: m.selectedNodeID = tsKey.NodePublic(msg) m.nodedetails = nodedetails.New(m.tsStatus, m.selectedNodeID, m.w, m.h) m.viewState = viewStateDetails + m.statusbar.UpdateMessage("Showing device details") case tea.WindowSizeMsg: - m.w, m.h = msg.Width, msg.Height - m.nodelist.SetSize(msg.Width, msg.Height) + m.w, m.h = msg.Width, msg.Height-3 + m.nodelist.SetSize(m.w, m.h) case spinner.TickMsg: if m.isLoading { m.spinner, tmpCmd = m.spinner.Update(msg) @@ -91,6 +101,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + if m.isLoading { + m.statusbar.UpdatePrefixStyle(constants.WarningTitleStyle) + } else { + m.statusbar.UpdatePrefixStyle(constants.PrimaryTitleStyle) + } + switch m.viewState { case viewStateDetails: m.nodedetails, tmpCmd = m.nodedetails.Update(msg) @@ -104,18 +120,20 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, tmpCmd) } } + m.statusbar, tmpCmd = m.statusbar.Update(msg) + cmds = append(cmds, tmpCmd) return m, tea.Batch(cmds...) } func (m Model) View() string { switch m.viewState { case viewStateDetails: - return m.nodedetails.View() + return lipgloss.JoinVertical(lipgloss.Left, m.nodedetails.View(), m.statusbar.View()) case viewStateList: if m.isLoading { - return fmt.Sprintf("\n\n %s Loading...\n\n", m.spinner.View()) + m.statusbar.UpdateMessage(fmt.Sprintf("%s Loading...", m.spinner.View())) } - return m.nodelist.View() + return lipgloss.JoinVertical(lipgloss.Left, m.nodelist.View(), m.statusbar.View()) default: return "*_*" } @@ -126,10 +144,11 @@ func New() Model { viewState: viewStateList, isLoading: true, spinner: spinner.New(), + statusbar: statusbar.New(), } - m.spinner.Spinner = spinner.Dot + m.spinner.Spinner = spinner.Line m.spinner.Style = constants.SpinnerStyle - m.nodelist = nodelist.New(nil, m.w, m.h) + m.nodelist = nodelist.New(nil, m.w, m.h-lipgloss.Height(m.statusbar.View())) return m } diff --git a/internal/tui/types/types.go b/internal/tui/types/types.go new file mode 100644 index 0000000..744bc0d --- /dev/null +++ b/internal/tui/types/types.go @@ -0,0 +1,4 @@ +package types + +type RefreshMsg bool +type StatusMsg string From f6ce1a9e5cd2a726ccc791a4a6dec02cb64e07e9 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Fri, 23 Aug 2024 22:41:04 +0800 Subject: [PATCH 08/11] refactor: move header from list to tui --- internal/tui/node_list/node_list.go | 15 +++------------ internal/tui/tui.go | 26 +++++++++++++++++++++----- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/internal/tui/node_list/node_list.go b/internal/tui/node_list/node_list.go index 52abd4c..1fa41bd 100644 --- a/internal/tui/node_list/node_list.go +++ b/internal/tui/node_list/node_list.go @@ -9,7 +9,6 @@ import ( "github.com/bilguun0203/tailscale-tui/internal/ts" "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" - nodedetails "github.com/bilguun0203/tailscale-tui/internal/tui/node_details" "github.com/bilguun0203/tailscale-tui/internal/tui/types" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" @@ -42,8 +41,7 @@ type Model struct { func (m *Model) SetSize(w int, h int) { m.w = w m.h = h - headerHeight := lipgloss.Height(m.headerView()) - m.list.SetSize(w, h-headerHeight) + m.list.SetSize(w, h) } type NodeSelectedMsg tsKey.NodePublic @@ -118,12 +116,6 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { return m, cmds } -func (m Model) headerView() string { - if m.tailStatus == nil { - return nodedetails.NodeDetailRender(nil, tsKey.NodePublic{}, constants.PrimaryTitleStyle.Render("Current Node")) - } - return nodedetails.NodeDetailRender(m.tailStatus, m.tailStatus.Self.PublicKey, constants.PrimaryTitleStyle.Render("Current Node")) -} func (m *Model) getItems() []list.Item { items := []list.Item{} @@ -212,7 +204,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } func (m Model) View() string { - return fmt.Sprintf("%s\n%s", m.headerView(), m.list.View()) + return m.list.View() } func New(status *ipnstate.Status, w, h int) Model { @@ -241,8 +233,7 @@ func New(status *ipnstate.Status, w, h int) Model { } m.list.SetSpinner(spinner.Dot) m.list.StartSpinner() - headerHeight := lipgloss.Height(m.headerView()) - m.list.SetHeight(h - headerHeight - 4) + m.list.SetHeight(h) m.list.SetItems(m.getItems()) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 06d23de..c756662 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -41,6 +41,8 @@ type Model struct { statusbar statusbar.Model spinner spinner.Model w, h int + statusH int + headerH int } func (m Model) getTsStatus() tea.Cmd { @@ -53,6 +55,13 @@ func (m Model) getTsStatus() tea.Cmd { } } +func (m Model) headerView() string { + if m.tsStatus == nil { + return nodedetails.NodeDetailRender(nil, tsKey.NodePublic{}, constants.PrimaryTitleStyle.Render("Current Node")) + } + return nodedetails.NodeDetailRender(m.tsStatus, m.tsStatus.Self.PublicKey, constants.PrimaryTitleStyle.Render("Current Node")) +} + func (m Model) Init() tea.Cmd { cmds := []tea.Cmd{ m.spinner.Tick, @@ -80,6 +89,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case nodedetails.BackMsg: m.viewState = viewStateList m.statusbar.UpdateMessage("Showing all network devices") + cmds = append(cmds, tea.ClearScreen) case types.RefreshMsg: m.isLoading = true cmds = append(cmds, m.getTsStatus()) @@ -88,12 +98,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.statusbar.UpdateMessage(string(msg)) case nodelist.NodeSelectedMsg: m.selectedNodeID = tsKey.NodePublic(msg) - m.nodedetails = nodedetails.New(m.tsStatus, m.selectedNodeID, m.w, m.h) + m.nodedetails = nodedetails.New(m.tsStatus, m.selectedNodeID, m.w, m.h-m.statusH) m.viewState = viewStateDetails m.statusbar.UpdateMessage("Showing device details") + cmds = append(cmds, tea.ClearScreen) case tea.WindowSizeMsg: - m.w, m.h = msg.Width, msg.Height-3 - m.nodelist.SetSize(m.w, m.h) + m.w, m.h = msg.Width, msg.Height + m.headerH = lipgloss.Height(m.headerView()) + m.statusH = lipgloss.Height(m.statusbar.View()) + listH := m.h - m.headerH - m.statusH + m.nodelist.SetSize(m.w, listH) case spinner.TickMsg: if m.isLoading { m.spinner, tmpCmd = m.spinner.Update(msg) @@ -133,7 +147,7 @@ func (m Model) View() string { if m.isLoading { m.statusbar.UpdateMessage(fmt.Sprintf("%s Loading...", m.spinner.View())) } - return lipgloss.JoinVertical(lipgloss.Left, m.nodelist.View(), m.statusbar.View()) + return lipgloss.JoinVertical(lipgloss.Left, m.headerView(), m.nodelist.View(), m.statusbar.View()) default: return "*_*" } @@ -149,6 +163,8 @@ func New() Model { m.spinner.Spinner = spinner.Line m.spinner.Style = constants.SpinnerStyle - m.nodelist = nodelist.New(nil, m.w, m.h-lipgloss.Height(m.statusbar.View())) + m.headerH = lipgloss.Height(m.headerView()) + m.statusH = lipgloss.Height(m.statusbar.View()) + m.nodelist = nodelist.New(nil, m.w, m.h-m.headerH-m.statusH) return m } From 85ad7338c144f6fa56cd0af3d445a31415ef84cd Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Mon, 26 Aug 2024 15:51:05 +0800 Subject: [PATCH 09/11] feat: basic node actions (connect/disconnect, ping) --- internal/ts/msg.go | 7 -- internal/ts/ts.go | 40 ++++++ internal/ts/types.go | 28 +++++ internal/tui/action_list/action_list.go | 94 ++++++++++++++ internal/tui/keymap/keymap.go | 10 -- internal/tui/node_details/node_details.go | 146 +++++++++++++++++++--- internal/tui/node_details/render.go | 2 +- internal/tui/node_list/node_list.go | 28 ++--- internal/tui/status_bar/status_bar.go | 4 + internal/tui/tui.go | 39 ++++-- internal/tui/types/types.go | 7 ++ main.go | 4 + 12 files changed, 346 insertions(+), 63 deletions(-) delete mode 100644 internal/ts/msg.go create mode 100644 internal/ts/types.go create mode 100644 internal/tui/action_list/action_list.go diff --git a/internal/ts/msg.go b/internal/ts/msg.go deleted file mode 100644 index c5e35c3..0000000 --- a/internal/ts/msg.go +++ /dev/null @@ -1,7 +0,0 @@ -package ts - -import "tailscale.com/ipn/ipnstate" - -type StatusDataMsg *ipnstate.Status - -type StatusErrorMsg error diff --git a/internal/ts/ts.go b/internal/ts/ts.go index 9df5f6c..a783394 100644 --- a/internal/ts/ts.go +++ b/internal/ts/ts.go @@ -2,10 +2,15 @@ package ts import ( "context" + "errors" + "fmt" + "net/netip" + "time" "tailscale.com/client/tailscale" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" + "tailscale.com/tailcfg" ) var lc tailscale.LocalClient @@ -24,3 +29,38 @@ func SetTSStatus(status bool) { WantRunningSet: true, }) } + +func Ping(ip netip.Addr) (*ipnstate.PingResult, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + pr, err := lc.Ping(ctx, ip, tailcfg.PingDisco) + cancel() + return pr, err +} + +func PingResultString(pr *ipnstate.PingResult) (string, error) { + if pr == nil { + return "", nil + } + var message string + if pr.Err != "" { + if pr.IsLocalIP { + message = "local ip" + return message, nil + } + return message, errors.New(pr.Err) + } + latency := time.Duration(pr.LatencySeconds * float64(time.Second)).Round(time.Millisecond) + via := pr.Endpoint + if pr.DERPRegionID != 0 { + via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode) + } + if via == "" { + via = string(tailcfg.PingDisco) + } + extra := "" + if pr.PeerAPIPort != 0 { + extra = fmt.Sprintf(", %d", pr.PeerAPIPort) + } + message = fmt.Sprintf("pong from %s (%s%s) via %v in %v", pr.NodeName, pr.NodeIP, extra, via, latency) + return message, nil +} diff --git a/internal/ts/types.go b/internal/ts/types.go new file mode 100644 index 0000000..9cb262a --- /dev/null +++ b/internal/ts/types.go @@ -0,0 +1,28 @@ +package ts + +import ( + "net/netip" + + "tailscale.com/ipn/ipnstate" +) + +type StatusDataMsg *ipnstate.Status +type StatusErrorMsg error +type ConnectMsg bool +type ToggleConnectionMsg bool +type PingMsg netip.Addr + +type ActionType int + +const ( + ConnectAction ActionType = iota + OfferExitNode + PingAction +) + +func (f ActionType) String() string { + return [...]string{ + "TSConnect", + "TSPing", + }[f] +} diff --git a/internal/tui/action_list/action_list.go b/internal/tui/action_list/action_list.go new file mode 100644 index 0000000..0334a2c --- /dev/null +++ b/internal/tui/action_list/action_list.go @@ -0,0 +1,94 @@ +package actionlist + +import ( + "github.com/bilguun0203/tailscale-tui/internal/ts" + "github.com/bilguun0203/tailscale-tui/internal/tui/constants" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type ActionListItem struct { + title, desc string + value ts.ActionType +} + +func NewActionListItem(title string, desc string, value ts.ActionType) ActionListItem { + return ActionListItem{title: title, desc: desc, value: value} +} +func (i ActionListItem) Title() string { return i.title } +func (i ActionListItem) Description() string { return i.desc } +func (i ActionListItem) Value() ts.ActionType { return i.value } +func (i ActionListItem) FilterValue() string { return i.title + " " + i.desc } + +type Model struct { + list list.Model +} + +func (m *Model) updateKeybindings() { + m.list.KeyMap.NextPage.SetEnabled(false) + m.list.KeyMap.PrevPage.SetEnabled(false) + m.list.KeyMap.ShowFullHelp.SetEnabled(false) + m.list.KeyMap.CloseFullHelp.SetEnabled(false) + m.list.KeyMap.GoToStart.SetEnabled(false) + m.list.KeyMap.GoToEnd.SetEnabled(false) + m.list.KeyMap.Filter.SetEnabled(false) + m.list.KeyMap.ClearFilter.SetEnabled(false) + m.list.KeyMap.CancelWhileFiltering.SetEnabled(false) + m.list.KeyMap.AcceptWhileFiltering.SetEnabled(false) + m.list.KeyMap.Quit.SetEnabled(false) + m.list.KeyMap.ForceQuit.SetEnabled(false) +} + +func (m *Model) SetSize(w int, h int) { + m.list.SetSize(w, h) +} + +func (m Model) SelectedItem() ActionListItem { + return m.list.SelectedItem().(ActionListItem) +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + m.list, cmd = m.list.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) +} + +func (m Model) View() string { + return lipgloss.NewStyle().Width(m.list.Width()).Height(m.list.Height()).Render(m.list.View()) +} + +func New(items []ActionListItem, w int, h int) Model { + d := list.NewDefaultDelegate() + descStyle := lipgloss.NewStyle().Margin(0, 0, 0, 4) + d.Styles.NormalTitle = lipgloss.NewStyle().Foreground(constants.ColorNormal).Margin(0, 0, 0, 2) + d.Styles.NormalDesc = descStyle.Foreground(constants.ColorDimmed) + d.Styles.SelectedTitle = lipgloss.NewStyle().Foreground(constants.ColorPrimary).Margin(0, 0, 0, 3) + d.Styles.SelectedDesc = descStyle.Foreground(constants.ColorWarning).Margin(0, 0, 0, 5) + d.Styles.DimmedTitle = constants.DimmedTextStyle.Margin(0, 0, 0, 2) + d.Styles.DimmedDesc = descStyle.Foreground(constants.ColorMuted) + + lis := []list.Item{} + for _, item := range items { + lis = append(lis, item) + } + + m := Model{ + list: list.New(lis, d, w, h), + } + m.list.Title = "Actions" + m.list.Styles.Title = constants.PrimaryTitleStyle + m.list.SetFilteringEnabled(false) + m.list.SetShowFilter(false) + m.list.SetShowHelp(false) + m.list.SetShowPagination(false) + m.list.SetShowStatusBar(false) + m.updateKeybindings() + return m +} diff --git a/internal/tui/keymap/keymap.go b/internal/tui/keymap/keymap.go index a26b0e9..ab038b3 100644 --- a/internal/tui/keymap/keymap.go +++ b/internal/tui/keymap/keymap.go @@ -15,8 +15,6 @@ type KeyMap struct { ForceQuit key.Binding ShowFullHelp key.Binding CloseFullHelp key.Binding - TSUp key.Binding - TSDown key.Binding } func (k KeyMap) ShortHelp() []key.Binding { @@ -68,14 +66,6 @@ func NewKeyMap() KeyMap { key.WithKeys("q", "esc"), key.WithHelp("q", "quit"), ), - TSUp: key.NewBinding( - key.WithKeys("["), - key.WithHelp("[", "up"), - ), - TSDown: key.NewBinding( - key.WithKeys("]"), - key.WithHelp("]", "down"), - ), ForceQuit: key.NewBinding(key.WithKeys("ctrl+c")), } } diff --git a/internal/tui/node_details/node_details.go b/internal/tui/node_details/node_details.go index b1f757b..4317a3c 100644 --- a/internal/tui/node_details/node_details.go +++ b/internal/tui/node_details/node_details.go @@ -1,9 +1,15 @@ package nodedetails import ( + "context" + "errors" "fmt" + "net/netip" + "strings" "github.com/atotto/clipboard" + "github.com/bilguun0203/tailscale-tui/internal/ts" + actionlist "github.com/bilguun0203/tailscale-tui/internal/tui/action_list" "github.com/bilguun0203/tailscale-tui/internal/tui/constants" "github.com/bilguun0203/tailscale-tui/internal/tui/keymap" "github.com/bilguun0203/tailscale-tui/internal/tui/types" @@ -16,17 +22,34 @@ import ( ) type Model struct { - tailStatus *ipnstate.Status - nodeID tsKey.NodePublic - keyMap keymap.KeyMap - w, h int - help help.Model + tailStatus *ipnstate.Status + nodeID tsKey.NodePublic + keyMap keymap.KeyMap + w, h int + help help.Model + actionsList actionlist.Model + helpH int + detailH int + contentH int + messages []string + pingCount int } type BackMsg bool +func (m Model) getCurrentNode() *ipnstate.PeerStatus { + node, ok := m.tailStatus.Peer[m.nodeID] + if !ok && m.tailStatus.Self.PublicKey == m.nodeID { + node = m.tailStatus.Self + ok = true + } + if ok { + return node + } + return nil +} + func (m *Model) updateKeybindings() { - m.keyMap.Enter.SetEnabled(false) m.keyMap.Refresh.SetEnabled(false) if m.help.ShowAll { m.keyMap.ShowFullHelp.SetEnabled(false) @@ -40,12 +63,8 @@ func (m *Model) updateKeybindings() { func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd - node, ok := m.tailStatus.Peer[m.nodeID] - if !ok && m.tailStatus.Self.PublicKey == m.nodeID { - node = m.tailStatus.Self - ok = true - } - if ok { + node := m.getCurrentNode() + if node != nil { if key.Matches(msg, m.keyMap.CopyIpv4) || key.Matches(msg, m.keyMap.CopyIpv6) || key.Matches(msg, m.keyMap.CopyDNSName) { copyStr := "" ipCount := len(node.TailscaleIPs) @@ -59,12 +78,25 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { if copyStr != "" { clipboard.WriteAll(copyStr) status := fmt.Sprintf("Copied \"%s\"!", constants.PrimaryTextStyle.Underline(true).Render(copyStr)) - cmd = func() tea.Msg { return types.StatusMsg(status) } + cmd = types.NewStatusMsg(status) cmds = append(cmds, cmd) } } } switch { + case key.Matches(msg, m.keyMap.Enter): + if m.actionsList.SelectedItem().Value() == ts.ConnectAction { + cmd = func() tea.Msg { + return ts.ToggleConnectionMsg(true) + } + } else if m.actionsList.SelectedItem().Value() == ts.PingAction { + node := m.getCurrentNode() + if node != nil { + cmd = func() tea.Msg { + return ts.PingMsg(node.TailscaleIPs[0]) + } + } + } case key.Matches(msg, m.keyMap.Back): cmd = func() tea.Msg { return BackMsg(true) @@ -81,13 +113,67 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { return m, cmds } +func (m Model) messagesView() string { + v := lipgloss.JoinVertical( + lipgloss.Left, + constants.PrimaryTitleStyle.Render("Messages"), + constants.NormalTextStyle.Margin(1).Render(strings.Join(m.messages, "\n"))) + return lipgloss.NewStyle().Width(m.w / 2).Height(m.contentH).Render(v) +} + +func (m *Model) SetSize(w int, h int) { + m.w = w + m.h = h + m.helpH = lipgloss.Height(m.help.View(m.keyMap)) + m.detailH = lipgloss.Height(NodeDetailRender(m.tailStatus, m.nodeID, "")) + m.contentH = m.h - m.helpH - m.detailH + m.actionsList.SetSize(m.w/2, m.contentH) +} + func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { + case ts.PingMsg: + if m.pingCount <= 0 { + m.pingCount = 10 + m.messages = []string{} + m.messages = append(m.messages, fmt.Sprintf("> Pinging %s. (max: 10 or until direct)", netip.Addr(msg))) + cmds = append(cmds, types.NewStatusMsg("Pinging...")) + } + m.pingCount -= 1 + pr, err := ts.Ping(netip.Addr(msg)) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + m.messages = append(m.messages, fmt.Sprintf("ping %q timed out", netip.Addr(msg))) + } else { + m.messages = append(m.messages, fmt.Sprintf("error: %s", err)) + } + } + if pr != nil { + prmsg, err := ts.PingResultString(pr) + if err != nil { + m.messages = append(m.messages, err.Error()) + } else { + m.messages = append(m.messages, constants.DimmedTextStyle.Render(prmsg)) + } + if pr.Endpoint != "" { + m.pingCount = -1 + } + if m.pingCount > 0 { + cmds = append(cmds, func() tea.Msg { return msg }) + } else if m.pingCount == -1 { + m.messages = append(m.messages, "\nDone!") + cmds = append(cmds, types.NewStatusMsg("Pinging finished.")) + } else { + m.messages = append(m.messages, "\nDone, direct connection not established!") + cmds = append(cmds, types.NewStatusMsg("Pinging finished, direct connectsion not established.")) + } + } case tea.KeyMsg: var kcmds []tea.Cmd m, kcmds = m.keyBindingsHandler(msg) @@ -96,14 +182,23 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { default: } + maxMessageCount := m.contentH - 3 + messageCount := len(m.messages) + if messageCount > maxMessageCount { + beg := messageCount - maxMessageCount + m.messages = m.messages[beg:] + } + m.actionsList, cmd = m.actionsList.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) } func (m Model) View() string { - helpHeight := lipgloss.Height(m.help.View(m.keyMap)) - detailHeight := m.h - helpHeight return lipgloss.JoinVertical( - lipgloss.Left, lipgloss.NewStyle().Height(detailHeight).Render(NodeDetailRender(m.tailStatus, m.nodeID, "")), + lipgloss.Left, + NodeDetailRender(m.tailStatus, m.nodeID, ""), + lipgloss.JoinHorizontal(lipgloss.Top, m.actionsList.View(), m.messagesView()), lipgloss.NewStyle().Margin(0, 2).Render(m.help.View(m.keyMap)), ) @@ -118,6 +213,25 @@ func New(status *ipnstate.Status, nodeID tsKey.NodePublic, w, h int) Model { h: h, help: help.New(), } + + var actionItems []actionlist.ActionListItem + if m.tailStatus != nil { + if m.tailStatus.Self.PublicKey == m.nodeID { + connection := m.tailStatus.Self.Online + // offerExitNode := m.tailStatus.Self.ExitNodeOption + actionItems = []actionlist.ActionListItem{ + actionlist.NewActionListItem("> Tailscale", fmt.Sprintf("Connection: %t", connection), ts.ConnectAction), + // actionlist.NewActionListItem("> Offer Exit Node", fmt.Sprintf("%t", offerExitNode), ts.OfferExitNode), + } + } else { + actionItems = []actionlist.ActionListItem{ + actionlist.NewActionListItem("> Ping", "run tailscale ping", ts.PingAction), + } + } + } + m.updateKeybindings() + m.actionsList = actionlist.New(actionItems, m.w/2, m.h) + m.SetSize(m.w, m.h) return m } diff --git a/internal/tui/node_details/render.go b/internal/tui/node_details/render.go index 524a3f4..57957c9 100644 --- a/internal/tui/node_details/render.go +++ b/internal/tui/node_details/render.go @@ -12,7 +12,7 @@ import ( ) func NodeDetailRender(tsStatus *ipnstate.Status, nodeID tsKey.NodePublic, customTitle string) string { - title := constants.SecondaryTitleStyle.Render("Node info") + title := constants.PrimaryTitleStyle.Render("Node info") if customTitle != "" { title = customTitle } diff --git a/internal/tui/node_list/node_list.go b/internal/tui/node_list/node_list.go index 1fa41bd..ef73f63 100644 --- a/internal/tui/node_list/node_list.go +++ b/internal/tui/node_list/node_list.go @@ -82,7 +82,7 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { } if copyStr == "" { m.list.NewStatusMessage("Sorry, nothing to copy.") - cmd = func() tea.Msg { return types.StatusMsg("Sorry, nothing to copy.") } + cmd = types.NewStatusMsg("Sorry, nothing to copy.") cmds = append(cmds, cmd) } else { err := clipboard.WriteAll(copyStr) @@ -91,7 +91,7 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { status = fmt.Sprintf("Sorry, error occured: %s", err) } m.list.NewStatusMessage(status) - cmd = func() tea.Msg { return types.StatusMsg(status) } + cmd = types.NewStatusMsg(status) cmds = append(cmds, cmd) } } @@ -103,20 +103,9 @@ func (m Model) keyBindingsHandler(msg tea.KeyMsg) (Model, []tea.Cmd) { cmd = func() tea.Msg { return NodeSelectedMsg(m.list.SelectedItem().(listItem).status.PublicKey) } cmds = append(cmds, cmd) } - if key.Matches(msg, m.keyMap.TSUp) { - ts.SetTSStatus(true) - cmd = func() tea.Msg { return types.RefreshMsg(true) } - cmds = append(cmds, cmd) - } - if key.Matches(msg, m.keyMap.TSDown) { - ts.SetTSStatus(false) - cmd = func() tea.Msg { return types.RefreshMsg(true) } - cmds = append(cmds, cmd) - } return m, cmds } - func (m *Model) getItems() []list.Item { items := []list.Item{} peers := []*ipnstate.PeerStatus{} @@ -209,19 +198,16 @@ func (m Model) View() string { func New(status *ipnstate.Status, w, h int) Model { d := list.NewDefaultDelegate() - d.Styles.NormalTitle = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}). - Padding(0, 0, 0, 2) - d.Styles.NormalDesc = d.Styles.NormalTitle. - Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}) + d.Styles.NormalTitle = lipgloss.NewStyle().Foreground(constants.ColorNormal).Padding(0, 0, 0, 2) + d.Styles.NormalDesc = d.Styles.NormalTitle.Foreground(constants.ColorDimmed) d.Styles.SelectedTitle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(constants.PrimaryTextStyle.GetForeground()). - Foreground(constants.PrimaryTextStyle.GetForeground()). + BorderForeground(constants.ColorPrimary). + Foreground(constants.ColorPrimary). Padding(0, 0, 0, 1) d.Styles.SelectedDesc = d.Styles.SelectedTitle d.Styles.DimmedTitle = constants.DimmedTextStyle.Padding(0, 0, 0, 2) - d.Styles.DimmedDesc = d.Styles.DimmedTitle.Foreground(constants.MutedTextStyle.GetForeground()) + d.Styles.DimmedDesc = d.Styles.DimmedTitle.Foreground(constants.ColorMuted) d.SetHeight(2) d.SetSpacing(1) m := Model{ diff --git a/internal/tui/status_bar/status_bar.go b/internal/tui/status_bar/status_bar.go index ced3aac..e3ec7f6 100644 --- a/internal/tui/status_bar/status_bar.go +++ b/internal/tui/status_bar/status_bar.go @@ -18,6 +18,10 @@ type Model struct { h int } +func (m Model) Message() string { + return m.msg +} + func (m *Model) UpdatePrefix(v string) { m.prefix = v } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index c756662..f07de52 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -2,6 +2,7 @@ package tui import ( "fmt" + "time" "github.com/bilguun0203/tailscale-tui/internal/ts" "github.com/bilguun0203/tailscale-tui/internal/tui/constants" @@ -36,6 +37,7 @@ type Model struct { selectedNodeID tsKey.NodePublic isLoading bool Err error + ExitMessage string nodelist nodelist.Model nodedetails nodedetails.Model statusbar statusbar.Model @@ -80,15 +82,26 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Err = nil m.tsStatus = msg m.viewState = viewStateList - m.statusbar.UpdateMessage("Showing all network devices") + cmds = append(cmds, types.NewStatusMsg("Showing all network devices")) case ts.StatusErrorMsg: m.isLoading = false m.Err = msg - m.statusbar.UpdateMessage(fmt.Sprintf("Error: %s", msg)) return m, tea.Quit + case ts.ToggleConnectionMsg: + if m.tsStatus != nil { + newStatus := !m.tsStatus.Self.Online + m.isLoading = true + if newStatus { + cmds = append(cmds, types.NewStatusMsg("Connecting...")) + } else { + cmds = append(cmds, types.NewStatusMsg("Disconnecting...")) + } + ts.SetTSStatus(!m.tsStatus.Self.Online) + cmds = append(cmds, func() tea.Msg { time.Sleep(2 * time.Second); return types.RefreshMsg(true) }) + } case nodedetails.BackMsg: m.viewState = viewStateList - m.statusbar.UpdateMessage("Showing all network devices") + cmds = append(cmds, types.NewStatusMsg("Showing all network devices")) cmds = append(cmds, tea.ClearScreen) case types.RefreshMsg: m.isLoading = true @@ -96,18 +109,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, m.spinner.Tick) case types.StatusMsg: m.statusbar.UpdateMessage(string(msg)) + case types.ExitMsg: + m.ExitMessage = string(msg) + return m, tea.Quit case nodelist.NodeSelectedMsg: m.selectedNodeID = tsKey.NodePublic(msg) - m.nodedetails = nodedetails.New(m.tsStatus, m.selectedNodeID, m.w, m.h-m.statusH) + contentH := m.h - m.statusH + m.nodedetails = nodedetails.New(m.tsStatus, m.selectedNodeID, m.w, contentH) m.viewState = viewStateDetails - m.statusbar.UpdateMessage("Showing device details") + cmds = append(cmds, types.NewStatusMsg("Showing device details")) cmds = append(cmds, tea.ClearScreen) case tea.WindowSizeMsg: m.w, m.h = msg.Width, msg.Height m.headerH = lipgloss.Height(m.headerView()) m.statusH = lipgloss.Height(m.statusbar.View()) - listH := m.h - m.headerH - m.statusH - m.nodelist.SetSize(m.w, listH) + contentH := m.h - m.headerH - m.statusH + m.nodelist.SetSize(m.w, contentH) + m.nodedetails.SetSize(m.w, contentH) case spinner.TickMsg: if m.isLoading { m.spinner, tmpCmd = m.spinner.Update(msg) @@ -142,6 +160,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) View() string { switch m.viewState { case viewStateDetails: + if m.isLoading { + m.statusbar.UpdateMessage(fmt.Sprintf("%s %s", m.spinner.View(), m.statusbar.Message())) + } return lipgloss.JoinVertical(lipgloss.Left, m.nodedetails.View(), m.statusbar.View()) case viewStateList: if m.isLoading { @@ -165,6 +186,8 @@ func New() Model { m.headerH = lipgloss.Height(m.headerView()) m.statusH = lipgloss.Height(m.statusbar.View()) - m.nodelist = nodelist.New(nil, m.w, m.h-m.headerH-m.statusH) + contentH := m.h - m.headerH - m.statusH + m.nodelist = nodelist.New(nil, m.w, contentH) + m.nodedetails = nodedetails.New(m.tsStatus, tsKey.NodePublic{}, m.w, contentH) return m } diff --git a/internal/tui/types/types.go b/internal/tui/types/types.go index 744bc0d..ac5a54e 100644 --- a/internal/tui/types/types.go +++ b/internal/tui/types/types.go @@ -1,4 +1,11 @@ package types +import tea "github.com/charmbracelet/bubbletea" + type RefreshMsg bool type StatusMsg string +type ExitMsg string + +func NewStatusMsg(msg string) func() tea.Msg { + return func() tea.Msg { return StatusMsg(msg) } +} diff --git a/main.go b/main.go index 7894b4a..1d3ae43 100644 --- a/main.go +++ b/main.go @@ -23,4 +23,8 @@ func main() { fmt.Println("Error running program:", fm.(tui.Model).Err) os.Exit(1) } + + if fm.(tui.Model).ExitMessage != "" { + fmt.Println(fm.(tui.Model).ExitMessage) + } } From b9872247e83a6b70dab07af2754784ba559d4be3 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Mon, 26 Aug 2024 16:01:05 +0800 Subject: [PATCH 10/11] ci: add goreleaser --- .github/workflows/go.yml | 23 +++++++---------------- .github/workflows/release.yml | 31 +++++++++++++++++++++++++++++++ .gitignore | 3 ++- .goreleaser.yaml | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yaml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c42b047..eef53a6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,26 +2,17 @@ name: Go on: push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] jobs: - build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.21' + - uses: actions/checkout@v3 - - name: Build - run: make build + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.23" - - uses: actions/upload-artifact@v4 - with: - name: tailscale-tui - path: tailscale-tui + - name: Build + run: make build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4812dfc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: goreleaser + +on: + pull_request: + push: + tags: + - "*" + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT }} diff --git a/.gitignore b/.gitignore index 7bb8488..88ab2b7 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ Thumbs.db go.work # binary -tailscale-tui \ No newline at end of file +tailscale-tui +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..374de69 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,33 @@ +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + +archives: + - format: tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + format_overrides: + - goos: windows + format: zip + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" From d58054a6a8c316ac20320ce59d2c226cb5c9cac6 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Mon, 26 Aug 2024 16:10:02 +0800 Subject: [PATCH 11/11] ci: add snapshot flag on pr --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4812dfc..6567d26 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,6 @@ jobs: with: distribution: goreleaser version: "~> v2" - args: release --clean + args: ${{ github.event_name == 'pull_request' && '--snapshot --clean' || 'release --clean' }} env: GITHUB_TOKEN: ${{ secrets.GH_PAT }}