From 93dc28b3dac365168be2abba3c98cab83bc4fd2c Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 24 Sep 2024 10:39:35 -0400 Subject: [PATCH 01/13] internal/ui: use a `ListModel` for addresses on the `SelfPage` --- internal/ui/peerpage.go | 16 ++++++++++ internal/ui/selfpage.go | 66 ++++++++++++++++----------------------- internal/ui/selfpage.ui | 6 ++++ internal/ui/trayscale.cmb | 3 ++ internal/ui/ui.go | 43 +++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 39 deletions(-) diff --git a/internal/ui/peerpage.go b/internal/ui/peerpage.go index 3cbacde..d8227a7 100644 --- a/internal/ui/peerpage.go +++ b/internal/ui/peerpage.go @@ -277,3 +277,19 @@ func (page *PeerPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St page.LastHandshake.SetText(formatTime(peer.LastHandshake)) page.Online.SetFromIconName(boolIcon(peer.Online)) } + +type addrRow struct { + ip netip.Addr + + w *adw.ActionRow + c *gtk.Button +} + +func (row *addrRow) Update(ip netip.Addr) { + row.ip = ip + row.w.SetTitle(ip.String()) +} + +func (row *addrRow) Widget() gtk.Widgetter { + return row.w +} diff --git a/internal/ui/selfpage.go b/internal/ui/selfpage.go index 3701b39..6a42136 100644 --- a/internal/ui/selfpage.go +++ b/internal/ui/selfpage.go @@ -12,6 +12,7 @@ import ( "deedles.dev/trayscale/internal/tsutil" "deedles.dev/xiter" "github.com/diamondburned/gotk4-adwaita/pkg/adw" + "github.com/diamondburned/gotk4/pkg/core/gioutil" "github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" @@ -20,13 +21,18 @@ import ( "tailscale.com/ipn/ipnstate" ) +var ( + addrModel = gioutil.NewListModelType[netip.Addr]() + addrSorter = gtk.NewCustomSorter(NewObjectComparer(netip.Addr.Compare)) +) + //go:embed selfpage.ui var selfPageXML string type SelfPage struct { *adw.StatusPage `gtk:"Page"` - IPGroup *adw.PreferencesGroup + IPList *gtk.ListBox OptionsGroup *adw.PreferencesGroup AdvertiseExitNodeRow *adw.SwitchRow AllowLANAccessRow *adw.SwitchRow @@ -63,7 +69,8 @@ type SelfPage struct { routes []netip.Prefix - addrRows rowManager[netip.Addr] + addrModel *gioutil.ListModel[netip.Addr] + routeRows rowManager[enum[netip.Prefix]] fileRows rowManager[apitype.WaitingFile] } @@ -93,31 +100,29 @@ func (page *SelfPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat actions := gio.NewSimpleActionGroup() page.InsertActionGroup("peer", actions) - page.addrRows.Parent = page.IPGroup - page.addrRows.New = func(ip netip.Addr) row[netip.Addr] { - row := addrRow{ - ip: ip, + page.addrModel = addrModel.New() + page.IPList.BindModel(gtk.NewSortListModel(page.addrModel, &addrSorter.Sorter), func(obj *glib.Object) gtk.Widgetter { + addr := addrModel.ObjectValue(obj) - w: adw.NewActionRow(), - c: gtk.NewButtonFromIconName("edit-copy-symbolic"), - } + copyButton := gtk.NewButtonFromIconName("edit-copy-symbolic") - row.c.SetMarginTop(12) // Why is this necessary? - row.c.SetMarginBottom(12) - row.c.SetHasFrame(false) - row.c.SetTooltipText("Copy to Clipboard") - row.c.ConnectClicked(func() { - a.clip(glib.NewValue(row.ip.String())) + copyButton.SetMarginTop(12) // Why is this necessary? + copyButton.SetMarginBottom(12) + copyButton.SetHasFrame(false) + copyButton.SetTooltipText("Copy to Clipboard") + copyButton.ConnectClicked(func() { + a.clip(glib.NewValue(addr.String())) a.toast("Copied to clipboard") }) - row.w.SetObjectProperty("title-selectable", true) - row.w.AddSuffix(row.c) - row.w.SetActivatableWidget(row.c) - row.w.SetTitle(ip.String()) + row := adw.NewActionRow() + row.SetObjectProperty("title-selectable", true) + row.AddSuffix(copyButton) + row.SetActivatableWidget(copyButton) + row.SetTitle(addr.String()) - return &row - } + return row + }) page.routeRows.Parent = page.AdvertisedRoutesGroup page.routeRows.New = func(route enum[netip.Prefix]) row[enum[netip.Prefix]] { @@ -373,8 +378,7 @@ func (page *SelfPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St page.SetTitle(peer.HostName) page.SetDescription(peer.DNSName) - slices.SortFunc(peer.TailscaleIPs, netip.Addr.Compare) - page.addrRows.Update(peer.TailscaleIPs) + updateListModel(page.addrModel, peer.TailscaleIPs) page.AdvertiseExitNodeRow.ActivatableWidget().(*gtk.Switch).SetState(status.Prefs.AdvertisesExitNode()) page.AdvertiseExitNodeRow.ActivatableWidget().(*gtk.Switch).SetActive(status.Prefs.AdvertisesExitNode()) @@ -421,22 +425,6 @@ func (page *SelfPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St page.routeRows.UpdateFromSeq(eroutes, len(page.routes)) } -type addrRow struct { - ip netip.Addr - - w *adw.ActionRow - c *gtk.Button -} - -func (row *addrRow) Update(ip netip.Addr) { - row.ip = ip - row.w.SetTitle(ip.String()) -} - -func (row *addrRow) Widget() gtk.Widgetter { - return row.w -} - type routeRow struct { route enum[netip.Prefix] diff --git a/internal/ui/selfpage.ui b/internal/ui/selfpage.ui index f8c1252..001aa29 100644 --- a/internal/ui/selfpage.ui +++ b/internal/ui/selfpage.ui @@ -13,6 +13,12 @@ Tailscale IPs + + + boxed-list + none + + diff --git a/internal/ui/trayscale.cmb b/internal/ui/trayscale.cmb index 4b61724..51cf817 100644 --- a/internal/ui/trayscale.cmb +++ b/internal/ui/trayscale.cmb @@ -89,6 +89,7 @@ (4,43,"AdwSwitchRow","AcceptRoutesRow",5,None,None,None,2,None,None), (4,44,"AdwActionRow","CaptivePortalRow",18,None,None,None,10,None,None), (4,45,"GtkImage","CaptivePortal",44,None,None,None,0,None,None), + (4,46,"GtkListBox","IPList",4,None,None,None,0,None,None), (5,1,"AdwStatusPage","Page",None,None,None,None,0,None,None), (5,2,"AdwClamp",None,1,None,None,None,2,None,None), (5,3,"GtkBox",None,2,None,None,None,2,None,None), @@ -193,6 +194,8 @@ (4,44,"AdwActionRow","activatable-widget","45",None,None,None,None,None,None,None,None,None), (4,44,"AdwPreferencesRow","title","Captive portal detected",None,None,None,None,None,None,None,None,None), (4,44,"GtkWidget","visible","False",None,None,None,None,None,None,None,None,None), + (4,46,"GtkListBox","selection-mode","none",None,None,None,None,None,None,None,None,None), + (4,46,"GtkWidget","css-classes","boxed-list",None,None,None,None,None,None,None,None,None), (5,1,"AdwStatusPage","title","Mullvad Exit Nodes",None,None,None,None,None,None,None,None,None), (5,3,"GtkBox","spacing","12",None,None,None,None,None,None,None,None,None), (5,3,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 441ceb9..e82bb7e 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -5,12 +5,14 @@ import ( "io" "iter" "reflect" + "slices" "strings" "time" "deedles.dev/trayscale" "deedles.dev/trayscale/internal/tsutil" "github.com/diamondburned/gotk4/pkg/core/gerror" + "github.com/diamondburned/gotk4/pkg/core/gioutil" "github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" @@ -165,3 +167,44 @@ func errHasCode(err error, code int) bool { } return gerr.ErrorCode() == code } + +func listModelBackward[T any](m *gioutil.ListModel[T]) iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + for i := int(m.NItems()) - 1; i >= 0; i-- { + if !yield(i, m.At(i)) { + return + } + } + } +} + +func listModelContains[T comparable](m *gioutil.ListModel[T], val T) bool { + for v := range m.All() { + if v == val { + return true + } + } + return false +} + +func updateListModel[T comparable](m *gioutil.ListModel[T], s []T) { + for i, v := range listModelBackward(m) { + if !slices.Contains(s, v) { + m.Remove(i) + } + } + + for _, v := range s { + if !listModelContains(m, v) { + m.Append(v) + } + } +} + +func NewObjectComparer[T any](f func(T, T) int) glib.CompareDataFunc { + return glib.NewObjectComparer(func(o1, o2 *glib.Object) int { + v1 := gioutil.ObjectValue[T](o1) + v2 := gioutil.ObjectValue[T](o2) + return f(v1, v2) + }) +} From 2cdda426179de379b408f4880af5fbd0906743ad Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 24 Sep 2024 11:04:04 -0400 Subject: [PATCH 02/13] internal/ui: use `ListModel` for advertised routes on `SelfPage` --- internal/ui/selfpage.go | 211 ++++++++++++++++++-------------------- internal/ui/selfpage.ui | 6 ++ internal/ui/trayscale.cmb | 3 + internal/ui/ui.go | 8 +- internal/xnetip/xnetip.go | 16 +++ 5 files changed, 129 insertions(+), 115 deletions(-) create mode 100644 internal/xnetip/xnetip.go diff --git a/internal/ui/selfpage.go b/internal/ui/selfpage.go index 6a42136..d9405a2 100644 --- a/internal/ui/selfpage.go +++ b/internal/ui/selfpage.go @@ -10,6 +10,7 @@ import ( "time" "deedles.dev/trayscale/internal/tsutil" + "deedles.dev/trayscale/internal/xnetip" "deedles.dev/xiter" "github.com/diamondburned/gotk4-adwaita/pkg/adw" "github.com/diamondburned/gotk4/pkg/core/gioutil" @@ -24,6 +25,9 @@ import ( var ( addrModel = gioutil.NewListModelType[netip.Addr]() addrSorter = gtk.NewCustomSorter(NewObjectComparer(netip.Addr.Compare)) + + prefixModel = gioutil.NewListModelType[netip.Prefix]() + prefixSorter = gtk.NewCustomSorter(NewObjectComparer(xnetip.ComparePrefixes)) ) //go:embed selfpage.ui @@ -32,47 +36,45 @@ var selfPageXML string type SelfPage struct { *adw.StatusPage `gtk:"Page"` - IPList *gtk.ListBox - OptionsGroup *adw.PreferencesGroup - AdvertiseExitNodeRow *adw.SwitchRow - AllowLANAccessRow *adw.SwitchRow - AcceptRoutesRow *adw.SwitchRow - AdvertisedRoutesGroup *adw.PreferencesGroup - AdvertiseRouteButton *gtk.Button - NetCheckGroup *adw.PreferencesGroup - NetCheckButton *gtk.Button - LastNetCheckRow *adw.ActionRow - LastNetCheck *gtk.Label - UDPRow *adw.ActionRow - UDP *gtk.Image - IPv4Row *adw.ActionRow - IPv4Icon *gtk.Image - IPv4Addr *gtk.Label - IPv6Row *adw.ActionRow - IPv6Icon *gtk.Image - IPv6Addr *gtk.Label - UPnPRow *adw.ActionRow - UPnP *gtk.Image - PMPRow *adw.ActionRow - PMP *gtk.Image - PCPRow *adw.ActionRow - PCP *gtk.Image - CaptivePortalRow *adw.ActionRow - CaptivePortal *gtk.Image - PreferredDERPRow *adw.ActionRow - PreferredDERP *gtk.Label - DERPLatencies *adw.ExpanderRow - FilesGroup *adw.PreferencesGroup + IPList *gtk.ListBox + OptionsGroup *adw.PreferencesGroup + AdvertiseExitNodeRow *adw.SwitchRow + AllowLANAccessRow *adw.SwitchRow + AcceptRoutesRow *adw.SwitchRow + AdvertisedRoutesList *gtk.ListBox + AdvertiseRouteButton *gtk.Button + NetCheckGroup *adw.PreferencesGroup + NetCheckButton *gtk.Button + LastNetCheckRow *adw.ActionRow + LastNetCheck *gtk.Label + UDPRow *adw.ActionRow + UDP *gtk.Image + IPv4Row *adw.ActionRow + IPv4Icon *gtk.Image + IPv4Addr *gtk.Label + IPv6Row *adw.ActionRow + IPv6Icon *gtk.Image + IPv6Addr *gtk.Label + UPnPRow *adw.ActionRow + UPnP *gtk.Image + PMPRow *adw.ActionRow + PMP *gtk.Image + PCPRow *adw.ActionRow + PCP *gtk.Image + CaptivePortalRow *adw.ActionRow + CaptivePortal *gtk.Image + PreferredDERPRow *adw.ActionRow + PreferredDERP *gtk.Label + DERPLatencies *adw.ExpanderRow + FilesGroup *adw.PreferencesGroup peer *ipnstate.PeerStatus name string - routes []netip.Prefix - - addrModel *gioutil.ListModel[netip.Addr] + addrModel *gioutil.ListModel[netip.Addr] + routeModel *gioutil.ListModel[netip.Prefix] - routeRows rowManager[enum[netip.Prefix]] - fileRows rowManager[apitype.WaitingFile] + fileRows rowManager[apitype.WaitingFile] } func NewSelfPage(a *App, peer *ipnstate.PeerStatus, status tsutil.Status) *SelfPage { @@ -101,58 +103,68 @@ func (page *SelfPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat page.InsertActionGroup("peer", actions) page.addrModel = addrModel.New() - page.IPList.BindModel(gtk.NewSortListModel(page.addrModel, &addrSorter.Sorter), func(obj *glib.Object) gtk.Widgetter { - addr := addrModel.ObjectValue(obj) - - copyButton := gtk.NewButtonFromIconName("edit-copy-symbolic") - - copyButton.SetMarginTop(12) // Why is this necessary? - copyButton.SetMarginBottom(12) - copyButton.SetHasFrame(false) - copyButton.SetTooltipText("Copy to Clipboard") - copyButton.ConnectClicked(func() { - a.clip(glib.NewValue(addr.String())) - a.toast("Copied to clipboard") - }) + page.IPList.BindModel( + gtk.NewSortListModel(page.addrModel, &addrSorter.Sorter), + func(obj *glib.Object) gtk.Widgetter { + addr := addrModel.ObjectValue(obj) + + copyButton := gtk.NewButtonFromIconName("edit-copy-symbolic") + + copyButton.SetMarginTop(12) // Why is this necessary? + copyButton.SetMarginBottom(12) + copyButton.SetHasFrame(false) + copyButton.SetTooltipText("Copy to Clipboard") + copyButton.ConnectClicked(func() { + a.clip(glib.NewValue(addr.String())) + a.toast("Copied to clipboard") + }) - row := adw.NewActionRow() - row.SetObjectProperty("title-selectable", true) - row.AddSuffix(copyButton) - row.SetActivatableWidget(copyButton) - row.SetTitle(addr.String()) + row := adw.NewActionRow() + row.SetObjectProperty("title-selectable", true) + row.AddSuffix(copyButton) + row.SetActivatableWidget(copyButton) + row.SetTitle(addr.String()) - return row - }) + return row + }, + ) - page.routeRows.Parent = page.AdvertisedRoutesGroup - page.routeRows.New = func(route enum[netip.Prefix]) row[enum[netip.Prefix]] { - row := routeRow{ - route: route, + page.routeModel = prefixModel.New() + page.AdvertisedRoutesList.BindModel( + gtk.NewSortListModel(page.routeModel, &prefixSorter.Sorter), + func(obj *glib.Object) gtk.Widgetter { + route := prefixModel.ObjectValue(obj) + + removeButton := gtk.NewButtonFromIconName("list-remove-symbolic") + + removeButton.SetMarginTop(12) + removeButton.SetMarginBottom(12) + removeButton.SetHasFrame(false) + removeButton.SetTooltipText("Remove") + removeButton.ConnectClicked(func() { + routes := slices.Collect(xiter.Filter(page.routeModel.All(), func(p netip.Prefix) bool { + return xnetip.ComparePrefixes(p, route) != 0 + })) + err := tsutil.AdvertiseRoutes(context.TODO(), routes) + if err != nil { + slog.Error("advertise routes", "err", err) + return + } + a.poller.Poll() <- struct{}{} + }) - w: adw.NewActionRow(), - r: gtk.NewButtonFromIconName("list-remove-symbolic"), - } + row := adw.NewActionRow() + row.SetObjectProperty("title-selectable", true) + row.AddSuffix(removeButton) + row.SetTitle(route.String()) - row.w.SetObjectProperty("title-selectable", true) - row.w.AddSuffix(row.r) - row.w.SetTitle(route.Val.String()) - - row.r.SetMarginTop(12) - row.r.SetMarginBottom(12) - row.r.SetHasFrame(false) - row.r.SetTooltipText("Remove") - row.r.ConnectClicked(func() { - routes := slices.Delete(page.routes, row.route.Index, row.route.Index+1) - err := tsutil.AdvertiseRoutes(context.TODO(), routes) - if err != nil { - slog.Error("advertise routes", "err", err) - return - } - a.poller.Poll() <- struct{}{} - }) + return row + }, + ) - return &row - } + advertisedRoutesListPlaceholder := adw.NewActionRow() + advertisedRoutesListPlaceholder.SetTitle("No advertised routes.") + page.AdvertisedRoutesList.SetPlaceholder(advertisedRoutesListPlaceholder) page.fileRows.Parent = page.FilesGroup page.fileRows.New = func(file apitype.WaitingFile) row[apitype.WaitingFile] { @@ -378,7 +390,7 @@ func (page *SelfPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St page.SetTitle(peer.HostName) page.SetDescription(peer.DNSName) - updateListModel(page.addrModel, peer.TailscaleIPs) + updateListModel(page.addrModel, slices.Values(peer.TailscaleIPs)) page.AdvertiseExitNodeRow.ActivatableWidget().(*gtk.Switch).SetState(status.Prefs.AdvertisesExitNode()) page.AdvertiseExitNodeRow.ActivatableWidget().(*gtk.Switch).SetActive(status.Prefs.AdvertisesExitNode()) @@ -392,37 +404,14 @@ func (page *SelfPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St routes := func(yield func(netip.Prefix) bool) { for _, r := range status.Prefs.AdvertiseRoutes { - if r.Bits() == 0 { - continue - } - if !yield(r) { - return - } - } - } - routes = xiter.Or( - routes, - xiter.Of(netip.Prefix{}), - ) - - clear(page.routes) - page.routes = page.routes[:0] - page.routes = slices.AppendSeq(page.routes, routes) - slices.SortFunc(page.routes, func(p1, p2 netip.Prefix) int { - return cmp.Or( - p1.Addr().Compare(p2.Addr()), - cmp.Compare(p1.Bits(), p2.Bits()), - ) - }) - - eroutes := func(yield func(enum[netip.Prefix]) bool) { - for i, r := range page.routes { - if !yield(enumerate(i, r)) { - return + if r.Bits() != 0 { + if !yield(r) { + return + } } } } - page.routeRows.UpdateFromSeq(eroutes, len(page.routes)) + updateListModel(page.routeModel, routes) } type routeRow struct { diff --git a/internal/ui/selfpage.ui b/internal/ui/selfpage.ui index 001aa29..71390eb 100644 --- a/internal/ui/selfpage.ui +++ b/internal/ui/selfpage.ui @@ -55,6 +55,12 @@ Advertised Routes + + + boxed-list + none + + diff --git a/internal/ui/trayscale.cmb b/internal/ui/trayscale.cmb index 51cf817..31fcbdb 100644 --- a/internal/ui/trayscale.cmb +++ b/internal/ui/trayscale.cmb @@ -90,6 +90,7 @@ (4,44,"AdwActionRow","CaptivePortalRow",18,None,None,None,10,None,None), (4,45,"GtkImage","CaptivePortal",44,None,None,None,0,None,None), (4,46,"GtkListBox","IPList",4,None,None,None,0,None,None), + (4,47,"GtkListBox","AdvertisedRoutesList",16,None,None,None,1,None,None), (5,1,"AdwStatusPage","Page",None,None,None,None,0,None,None), (5,2,"AdwClamp",None,1,None,None,None,2,None,None), (5,3,"GtkBox",None,2,None,None,None,2,None,None), @@ -196,6 +197,8 @@ (4,44,"GtkWidget","visible","False",None,None,None,None,None,None,None,None,None), (4,46,"GtkListBox","selection-mode","none",None,None,None,None,None,None,None,None,None), (4,46,"GtkWidget","css-classes","boxed-list",None,None,None,None,None,None,None,None,None), + (4,47,"GtkListBox","selection-mode","none",None,None,None,None,None,None,None,None,None), + (4,47,"GtkWidget","css-classes","boxed-list",None,None,None,None,None,None,None,None,None), (5,1,"AdwStatusPage","title","Mullvad Exit Nodes",None,None,None,None,None,None,None,None,None), (5,3,"GtkBox","spacing","12",None,None,None,None,None,None,None,None,None), (5,3,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index e82bb7e..b18ed0e 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -5,12 +5,12 @@ import ( "io" "iter" "reflect" - "slices" "strings" "time" "deedles.dev/trayscale" "deedles.dev/trayscale/internal/tsutil" + "deedles.dev/xiter" "github.com/diamondburned/gotk4/pkg/core/gerror" "github.com/diamondburned/gotk4/pkg/core/gioutil" "github.com/diamondburned/gotk4/pkg/gio/v2" @@ -187,14 +187,14 @@ func listModelContains[T comparable](m *gioutil.ListModel[T], val T) bool { return false } -func updateListModel[T comparable](m *gioutil.ListModel[T], s []T) { +func updateListModel[T comparable](m *gioutil.ListModel[T], s iter.Seq[T]) { for i, v := range listModelBackward(m) { - if !slices.Contains(s, v) { + if !xiter.Contains(s, v) { m.Remove(i) } } - for _, v := range s { + for v := range s { if !listModelContains(m, v) { m.Append(v) } diff --git a/internal/xnetip/xnetip.go b/internal/xnetip/xnetip.go new file mode 100644 index 0000000..366b148 --- /dev/null +++ b/internal/xnetip/xnetip.go @@ -0,0 +1,16 @@ +package xnetip + +import ( + "cmp" + "net/netip" +) + +func ComparePrefixes(p, p2 netip.Prefix) int { + if c := cmp.Compare(p.Addr().BitLen(), p2.Addr().BitLen()); c != 0 { + return c + } + if c := cmp.Compare(p.Bits(), p2.Bits()); c != 0 { + return c + } + return p.Addr().Compare(p2.Addr()) +} From d69a66c3c244024df81a005f2b33bd6c805b029b Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 24 Sep 2024 12:54:14 -0400 Subject: [PATCH 03/13] internal/ui: switch everything to `ListModel`s on `SelfPage` --- internal/ui/selfpage.go | 157 ++++++++++++++++++++------------------ internal/ui/selfpage.ui | 6 ++ internal/ui/trayscale.cmb | 3 + internal/ui/ui.go | 6 ++ 4 files changed, 96 insertions(+), 76 deletions(-) diff --git a/internal/ui/selfpage.go b/internal/ui/selfpage.go index d9405a2..07f138d 100644 --- a/internal/ui/selfpage.go +++ b/internal/ui/selfpage.go @@ -23,11 +23,14 @@ import ( ) var ( - addrModel = gioutil.NewListModelType[netip.Addr]() - addrSorter = gtk.NewCustomSorter(NewObjectComparer(netip.Addr.Compare)) - - prefixModel = gioutil.NewListModelType[netip.Prefix]() - prefixSorter = gtk.NewCustomSorter(NewObjectComparer(xnetip.ComparePrefixes)) + addrSorter = gtk.NewCustomSorter(NewObjectComparer(netip.Addr.Compare)) + prefixSorter = gtk.NewCustomSorter(NewObjectComparer(xnetip.ComparePrefixes)) + waitingFileSorter = gtk.NewCustomSorter(NewObjectComparer(func(f1, f2 apitype.WaitingFile) int { + return cmp.Or( + cmp.Compare(f1.Name, f2.Name), + cmp.Compare(f1.Size, f2.Size), + ) + })) ) //go:embed selfpage.ui @@ -66,15 +69,14 @@ type SelfPage struct { PreferredDERPRow *adw.ActionRow PreferredDERP *gtk.Label DERPLatencies *adw.ExpanderRow - FilesGroup *adw.PreferencesGroup + FilesList *gtk.ListBox peer *ipnstate.PeerStatus name string addrModel *gioutil.ListModel[netip.Addr] routeModel *gioutil.ListModel[netip.Prefix] - - fileRows rowManager[apitype.WaitingFile] + fileModel *gioutil.ListModel[apitype.WaitingFile] } func NewSelfPage(a *App, peer *ipnstate.PeerStatus, status tsutil.Status) *SelfPage { @@ -102,12 +104,11 @@ func (page *SelfPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat actions := gio.NewSimpleActionGroup() page.InsertActionGroup("peer", actions) - page.addrModel = addrModel.New() - page.IPList.BindModel( + page.addrModel = gioutil.NewListModel[netip.Addr]() + BindModel( + page.IPList, gtk.NewSortListModel(page.addrModel, &addrSorter.Sorter), - func(obj *glib.Object) gtk.Widgetter { - addr := addrModel.ObjectValue(obj) - + func(addr netip.Addr) gtk.Widgetter { copyButton := gtk.NewButtonFromIconName("edit-copy-symbolic") copyButton.SetMarginTop(12) // Why is this necessary? @@ -129,12 +130,15 @@ func (page *SelfPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat }, ) - page.routeModel = prefixModel.New() - page.AdvertisedRoutesList.BindModel( - gtk.NewSortListModel(page.routeModel, &prefixSorter.Sorter), - func(obj *glib.Object) gtk.Widgetter { - route := prefixModel.ObjectValue(obj) + ipListPlaceholder := adw.NewActionRow() + ipListPlaceholder.SetTitle("No addresses.") + page.IPList.SetPlaceholder(ipListPlaceholder) + page.routeModel = gioutil.NewListModel[netip.Prefix]() + BindModel( + page.AdvertisedRoutesList, + gtk.NewSortListModel(page.routeModel, &prefixSorter.Sorter), + func(route netip.Prefix) gtk.Widgetter { removeButton := gtk.NewButtonFromIconName("list-remove-symbolic") removeButton.SetMarginTop(12) @@ -166,66 +170,69 @@ func (page *SelfPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat advertisedRoutesListPlaceholder.SetTitle("No advertised routes.") page.AdvertisedRoutesList.SetPlaceholder(advertisedRoutesListPlaceholder) - page.fileRows.Parent = page.FilesGroup - page.fileRows.New = func(file apitype.WaitingFile) row[apitype.WaitingFile] { - row := fileRow{ - file: file, - - w: adw.NewActionRow(), - s: gtk.NewButtonFromIconName("document-save-symbolic"), - d: gtk.NewButtonFromIconName("edit-delete-symbolic"), - } - - row.w.AddSuffix(row.s) - row.w.AddSuffix(row.d) - row.w.SetTitle(file.Name) - row.w.SetSubtitle(bytesize.ByteSize(file.Size).String()) - - row.s.SetMarginTop(12) - row.s.SetMarginBottom(12) - row.s.SetHasFrame(false) - row.s.SetTooltipText("Save") - row.s.ConnectClicked(func() { - dialog := gtk.NewFileDialog() - dialog.SetModal(true) - dialog.SetInitialName(row.file.Name) - dialog.Save(context.TODO(), &a.win.Window, func(res gio.AsyncResulter) { - file, err := dialog.SaveFinish(res) - if err != nil { - if !errHasCode(err, int(gtk.DialogErrorDismissed)) { - slog.Error("save file", "err", err) + page.fileModel = gioutil.NewListModel[apitype.WaitingFile]() + BindModel( + page.FilesList, + gtk.NewSortListModel(page.fileModel, &waitingFileSorter.Sorter), + func(file apitype.WaitingFile) gtk.Widgetter { + saveButton := gtk.NewButtonFromIconName("document-save-symbolic") + saveButton.SetMarginTop(12) + saveButton.SetMarginBottom(12) + saveButton.SetHasFrame(false) + saveButton.SetTooltipText("Save") + saveButton.ConnectClicked(func() { + dialog := gtk.NewFileDialog() + dialog.SetModal(true) + dialog.SetInitialName(file.Name) + dialog.Save(context.TODO(), &a.win.Window, func(res gio.AsyncResulter) { + f, err := dialog.SaveFinish(res) + if err != nil { + if !errHasCode(err, int(gtk.DialogErrorDismissed)) { + slog.Error("save file", "err", err) + } + return } - return - } - go a.saveFile(context.TODO(), row.file.Name, file) + go a.saveFile(context.TODO(), file.Name, f) + }) }) - }) - row.d.SetMarginTop(12) - row.d.SetMarginBottom(12) - row.d.SetHasFrame(false) - row.d.SetTooltipText("Delete") - row.d.ConnectClicked(func() { - Confirmation{ - Heading: "Delete file?", - Body: "If you delete this file, you will no longer be able to save it to your local machine.", - Accept: "_Delete", - Reject: "_Cancel", - }.Show(a, func(accept bool) { - if accept { - err := tsutil.DeleteWaitingFile(context.TODO(), row.file.Name) - if err != nil { - slog.Error("delete file", "err", err) - return + deleteButton := gtk.NewButtonFromIconName("edit-delete-symbolic") + deleteButton.SetMarginTop(12) + deleteButton.SetMarginBottom(12) + deleteButton.SetHasFrame(false) + deleteButton.SetTooltipText("Delete") + deleteButton.ConnectClicked(func() { + Confirmation{ + Heading: "Delete file?", + Body: "If you delete this file, you will no longer be able to save it to your local machine.", + Accept: "_Delete", + Reject: "_Cancel", + }.Show(a, func(accept bool) { + if accept { + err := tsutil.DeleteWaitingFile(context.TODO(), file.Name) + if err != nil { + slog.Error("delete file", "err", err) + return + } + a.poller.Poll() <- struct{}{} } - a.poller.Poll() <- struct{}{} - } + }) }) - }) - return &row - } + row := adw.NewActionRow() + row.AddSuffix(saveButton) + row.AddSuffix(deleteButton) + row.SetTitle(file.Name) + row.SetSubtitle(bytesize.ByteSize(file.Size).String()) + + return row + }, + ) + + filesListPlaceholder := adw.NewActionRow() + filesListPlaceholder.SetTitle("No incoming files.") + page.FilesList.SetPlaceholder(filesListPlaceholder) page.AdvertiseExitNodeRow.ActivatableWidget().(*gtk.Switch).ConnectStateSet(func(s bool) bool { if s == page.AdvertiseExitNodeRow.ActivatableWidget().(*gtk.Switch).State() { @@ -390,8 +397,6 @@ func (page *SelfPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St page.SetTitle(peer.HostName) page.SetDescription(peer.DNSName) - updateListModel(page.addrModel, slices.Values(peer.TailscaleIPs)) - page.AdvertiseExitNodeRow.ActivatableWidget().(*gtk.Switch).SetState(status.Prefs.AdvertisesExitNode()) page.AdvertiseExitNodeRow.ActivatableWidget().(*gtk.Switch).SetActive(status.Prefs.AdvertisesExitNode()) page.AllowLANAccessRow.ActivatableWidget().(*gtk.Switch).SetState(status.Prefs.ExitNodeAllowLANAccess) @@ -399,9 +404,6 @@ func (page *SelfPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St page.AcceptRoutesRow.ActivatableWidget().(*gtk.Switch).SetState(status.Prefs.RouteAll) page.AcceptRoutesRow.ActivatableWidget().(*gtk.Switch).SetActive(status.Prefs.RouteAll) - page.fileRows.Update(status.Files) - page.FilesGroup.SetVisible(len(status.Files) > 0) - routes := func(yield func(netip.Prefix) bool) { for _, r := range status.Prefs.AdvertiseRoutes { if r.Bits() != 0 { @@ -411,6 +413,9 @@ func (page *SelfPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St } } } + + updateListModel(page.addrModel, slices.Values(peer.TailscaleIPs)) + updateListModel(page.fileModel, slices.Values(status.Files)) updateListModel(page.routeModel, routes) } diff --git a/internal/ui/selfpage.ui b/internal/ui/selfpage.ui index 71390eb..b580d1e 100644 --- a/internal/ui/selfpage.ui +++ b/internal/ui/selfpage.ui @@ -44,6 +44,12 @@ Files + + + boxed-list + none + + diff --git a/internal/ui/trayscale.cmb b/internal/ui/trayscale.cmb index 31fcbdb..02cf135 100644 --- a/internal/ui/trayscale.cmb +++ b/internal/ui/trayscale.cmb @@ -91,6 +91,7 @@ (4,45,"GtkImage","CaptivePortal",44,None,None,None,0,None,None), (4,46,"GtkListBox","IPList",4,None,None,None,0,None,None), (4,47,"GtkListBox","AdvertisedRoutesList",16,None,None,None,1,None,None), + (4,48,"GtkListBox","FilesList",12,None,None,None,0,None,None), (5,1,"AdwStatusPage","Page",None,None,None,None,0,None,None), (5,2,"AdwClamp",None,1,None,None,None,2,None,None), (5,3,"GtkBox",None,2,None,None,None,2,None,None), @@ -199,6 +200,8 @@ (4,46,"GtkWidget","css-classes","boxed-list",None,None,None,None,None,None,None,None,None), (4,47,"GtkListBox","selection-mode","none",None,None,None,None,None,None,None,None,None), (4,47,"GtkWidget","css-classes","boxed-list",None,None,None,None,None,None,None,None,None), + (4,48,"GtkListBox","selection-mode","none",None,None,None,None,None,None,None,None,None), + (4,48,"GtkWidget","css-classes","boxed-list",None,None,None,None,None,None,None,None,None), (5,1,"AdwStatusPage","title","Mullvad Exit Nodes",None,None,None,None,None,None,None,None,None), (5,3,"GtkBox","spacing","12",None,None,None,None,None,None,None,None,None), (5,3,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index b18ed0e..1651799 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -208,3 +208,9 @@ func NewObjectComparer[T any](f func(T, T) int) glib.CompareDataFunc { return f(v1, v2) }) } + +func BindModel[T any](lb *gtk.ListBox, m gio.ListModeller, f func(T) gtk.Widgetter) { + lb.BindModel(m, func(obj *glib.Object) gtk.Widgetter { + return f(gioutil.ObjectValue[T](obj)) + }) +} From d7d51485036723bc2594b2eb445d431d8981f058 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 24 Sep 2024 12:55:03 -0400 Subject: [PATCH 04/13] internal/ui: remove now unused `fileRow` --- internal/ui/peerpage.go | 24 +++++++++++++++++++++++ internal/ui/selfpage.go | 42 ----------------------------------------- 2 files changed, 24 insertions(+), 42 deletions(-) diff --git a/internal/ui/peerpage.go b/internal/ui/peerpage.go index d8227a7..0d645c6 100644 --- a/internal/ui/peerpage.go +++ b/internal/ui/peerpage.go @@ -293,3 +293,27 @@ func (row *addrRow) Update(ip netip.Addr) { func (row *addrRow) Widget() gtk.Widgetter { return row.w } + +type routeRow struct { + route enum[netip.Prefix] + + w *adw.ActionRow + r *gtk.Button +} + +func (row *routeRow) Update(route enum[netip.Prefix]) { + row.route = route + + if !route.Val.IsValid() { + row.r.SetVisible(false) + row.w.SetTitle("No advertised routes.") + return + } + + row.r.SetVisible(route.Index >= 0) + row.w.SetTitle(route.Val.String()) +} + +func (row *routeRow) Widget() gtk.Widgetter { + return row.w +} diff --git a/internal/ui/selfpage.go b/internal/ui/selfpage.go index 07f138d..3ff966e 100644 --- a/internal/ui/selfpage.go +++ b/internal/ui/selfpage.go @@ -418,45 +418,3 @@ func (page *SelfPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St updateListModel(page.fileModel, slices.Values(status.Files)) updateListModel(page.routeModel, routes) } - -type routeRow struct { - route enum[netip.Prefix] - - w *adw.ActionRow - r *gtk.Button -} - -func (row *routeRow) Update(route enum[netip.Prefix]) { - row.route = route - - if !route.Val.IsValid() { - row.r.SetVisible(false) - row.w.SetTitle("No advertised routes.") - return - } - - row.r.SetVisible(route.Index >= 0) - row.w.SetTitle(route.Val.String()) -} - -func (row *routeRow) Widget() gtk.Widgetter { - return row.w -} - -type fileRow struct { - file apitype.WaitingFile - - w *adw.ActionRow - s *gtk.Button - d *gtk.Button -} - -func (row *fileRow) Update(file apitype.WaitingFile) { - row.file = file - row.w.SetTitle(file.Name) - row.w.SetSubtitle(bytesize.ByteSize(file.Size).String()) -} - -func (row *fileRow) Widget() gtk.Widgetter { - return row.w -} From 64c45c0dfb738bbe61fbbeb1f887b42589897934 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 24 Sep 2024 13:03:24 -0400 Subject: [PATCH 05/13] internal/ui: use `ListModel`s on the `PeerPage`, too --- internal/ui/peerpage.go | 216 ++++++++++++++------------------------ internal/ui/peerpage.ui | 12 +++ internal/ui/selfpage.go | 11 -- internal/ui/trayscale.cmb | 6 ++ internal/ui/ui.go | 22 ++-- 5 files changed, 112 insertions(+), 155 deletions(-) diff --git a/internal/ui/peerpage.go b/internal/ui/peerpage.go index 0d645c6..e4b686d 100644 --- a/internal/ui/peerpage.go +++ b/internal/ui/peerpage.go @@ -1,7 +1,6 @@ package ui import ( - "cmp" "context" _ "embed" "log/slog" @@ -10,8 +9,10 @@ import ( "strconv" "deedles.dev/trayscale/internal/tsutil" + "deedles.dev/trayscale/internal/xnetip" "deedles.dev/xiter" "github.com/diamondburned/gotk4-adwaita/pkg/adw" + "github.com/diamondburned/gotk4/pkg/core/gioutil" "github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" @@ -24,8 +25,9 @@ var peerPageXML string type PeerPage struct { *adw.StatusPage `gtk:"Page"` - IPGroup *adw.PreferencesGroup + IPList *gtk.ListBox AdvertisedRoutesGroup *adw.PreferencesGroup + AdvertisedRoutesList *gtk.ListBox UDPRow *adw.ActionRow UDP *gtk.Image IPv4Row *adw.ActionRow @@ -68,10 +70,8 @@ type PeerPage struct { peer *ipnstate.PeerStatus name string - routes []netip.Prefix - - addrRows rowManager[netip.Addr] - routeRows rowManager[enum[netip.Prefix]] + addrModel *gioutil.ListModel[netip.Addr] + routeModel *gioutil.ListModel[netip.Prefix] } func NewPeerPage(a *App, peer *ipnstate.PeerStatus, status tsutil.Status) *PeerPage { @@ -130,61 +130,71 @@ func (page *PeerPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat return true }) - page.addrRows.Parent = page.IPGroup - page.addrRows.New = func(ip netip.Addr) row[netip.Addr] { - row := addrRow{ - ip: ip, - - w: adw.NewActionRow(), - c: gtk.NewButtonFromIconName("edit-copy-symbolic"), - } - - row.c.SetMarginTop(12) // Why is this necessary? - row.c.SetMarginBottom(12) - row.c.SetHasFrame(false) - row.c.SetTooltipText("Copy to Clipboard") - row.c.ConnectClicked(func() { - a.clip(glib.NewValue(row.ip.String())) - a.toast("Copied to clipboard") - }) - - row.w.SetObjectProperty("title-selectable", true) - row.w.AddSuffix(row.c) - row.w.SetActivatableWidget(row.c) - row.w.SetTitle(ip.String()) - - return &row - } + page.addrModel = gioutil.NewListModel[netip.Addr]() + BindModel( + page.IPList, + gtk.NewSortListModel(page.addrModel, &addrSorter.Sorter), + func(addr netip.Addr) gtk.Widgetter { + copyButton := gtk.NewButtonFromIconName("edit-copy-symbolic") + + copyButton.SetMarginTop(12) // Why is this necessary? + copyButton.SetMarginBottom(12) + copyButton.SetHasFrame(false) + copyButton.SetTooltipText("Copy to Clipboard") + copyButton.ConnectClicked(func() { + a.clip(glib.NewValue(addr.String())) + a.toast("Copied to clipboard") + }) + + row := adw.NewActionRow() + row.SetObjectProperty("title-selectable", true) + row.AddSuffix(copyButton) + row.SetActivatableWidget(copyButton) + row.SetTitle(addr.String()) + + return row + }, + ) - page.routeRows.Parent = page.AdvertisedRoutesGroup - page.routeRows.New = func(route enum[netip.Prefix]) row[enum[netip.Prefix]] { - row := routeRow{ - route: route, + ipListPlaceholder := adw.NewActionRow() + ipListPlaceholder.SetTitle("No addresses.") + page.IPList.SetPlaceholder(ipListPlaceholder) + + page.routeModel = gioutil.NewListModel[netip.Prefix]() + BindModel( + page.AdvertisedRoutesList, + gtk.NewSortListModel(page.routeModel, &prefixSorter.Sorter), + func(route netip.Prefix) gtk.Widgetter { + removeButton := gtk.NewButtonFromIconName("list-remove-symbolic") + + removeButton.SetMarginTop(12) + removeButton.SetMarginBottom(12) + removeButton.SetHasFrame(false) + removeButton.SetTooltipText("Remove") + removeButton.ConnectClicked(func() { + routes := slices.Collect(xiter.Filter(page.routeModel.All(), func(p netip.Prefix) bool { + return xnetip.ComparePrefixes(p, route) != 0 + })) + err := tsutil.AdvertiseRoutes(context.TODO(), routes) + if err != nil { + slog.Error("advertise routes", "err", err) + return + } + a.poller.Poll() <- struct{}{} + }) - w: adw.NewActionRow(), - r: gtk.NewButtonFromIconName("list-remove-symbolic"), - } + row := adw.NewActionRow() + row.SetObjectProperty("title-selectable", true) + row.AddSuffix(removeButton) + row.SetTitle(route.String()) - row.w.SetObjectProperty("title-selectable", true) - row.w.AddSuffix(row.r) - row.w.SetTitle(route.Val.String()) - - row.r.SetMarginTop(12) - row.r.SetMarginBottom(12) - row.r.SetHasFrame(false) - row.r.SetTooltipText("Remove") - row.r.ConnectClicked(func() { - routes := slices.Delete(page.routes, row.route.Index, row.route.Index+1) - err := tsutil.AdvertiseRoutes(context.TODO(), routes) - if err != nil { - slog.Error("advertise routes", "err", err) - return - } - a.poller.Poll() <- struct{}{} - }) + return row + }, + ) - return &row - } + advertisedRoutesListPlaceholder := adw.NewActionRow() + advertisedRoutesListPlaceholder.SetTitle("No advertised routes.") + page.AdvertisedRoutesList.SetPlaceholder(advertisedRoutesListPlaceholder) page.ExitNodeRow.ActivatableWidget().(*gtk.Switch).ConnectStateSet(func(s bool) bool { if s == page.ExitNodeRow.ActivatableWidget().(*gtk.Switch).State() { @@ -221,8 +231,17 @@ func (page *PeerPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St page.SetTitle(peer.HostName) page.SetDescription(peer.DNSName) - slices.SortFunc(peer.TailscaleIPs, netip.Addr.Compare) - page.addrRows.Update(peer.TailscaleIPs) + page.ExitNodeRow.SetVisible(peer.ExitNodeOption) + page.ExitNodeRow.ActivatableWidget().(*gtk.Switch).SetState(peer.ExitNode) + page.ExitNodeRow.ActivatableWidget().(*gtk.Switch).SetActive(peer.ExitNode) + page.RxBytes.SetText(strconv.FormatInt(peer.RxBytes, 10)) + page.TxBytes.SetText(strconv.FormatInt(peer.TxBytes, 10)) + page.Created.SetText(formatTime(peer.Created)) + page.LastSeen.SetText(formatTime(peer.LastSeen)) + page.LastSeenRow.SetVisible(!peer.Online) + page.LastWrite.SetText(formatTime(peer.LastWrite)) + page.LastHandshake.SetText(formatTime(peer.LastHandshake)) + page.Online.SetFromIconName(boolIcon(peer.Online)) routes := func(yield func(netip.Prefix) bool) { if peer.PrimaryRoutes == nil { @@ -238,82 +257,7 @@ func (page *PeerPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.St } } } - routes = xiter.Or( - routes, - xiter.Of(netip.Prefix{}), - ) - - clear(page.routes) - page.routes = page.routes[:0] - page.routes = slices.AppendSeq(page.routes, routes) - slices.SortFunc( - page.routes, - func(p1, p2 netip.Prefix) int { - return cmp.Or( - p1.Addr().Compare(p2.Addr()), - cmp.Compare(p1.Bits(), p2.Bits()), - ) - }, - ) - - eroutes := func(yield func(enum[netip.Prefix]) bool) { - for _, r := range page.routes { - if !yield(enumerate(-1, r)) { - return - } - } - } - page.routeRows.UpdateFromSeq(eroutes, len(page.routes)) - - page.ExitNodeRow.SetVisible(peer.ExitNodeOption) - page.ExitNodeRow.ActivatableWidget().(*gtk.Switch).SetState(peer.ExitNode) - page.ExitNodeRow.ActivatableWidget().(*gtk.Switch).SetActive(peer.ExitNode) - page.RxBytes.SetText(strconv.FormatInt(peer.RxBytes, 10)) - page.TxBytes.SetText(strconv.FormatInt(peer.TxBytes, 10)) - page.Created.SetText(formatTime(peer.Created)) - page.LastSeen.SetText(formatTime(peer.LastSeen)) - page.LastSeenRow.SetVisible(!peer.Online) - page.LastWrite.SetText(formatTime(peer.LastWrite)) - page.LastHandshake.SetText(formatTime(peer.LastHandshake)) - page.Online.SetFromIconName(boolIcon(peer.Online)) -} - -type addrRow struct { - ip netip.Addr - - w *adw.ActionRow - c *gtk.Button -} - -func (row *addrRow) Update(ip netip.Addr) { - row.ip = ip - row.w.SetTitle(ip.String()) -} - -func (row *addrRow) Widget() gtk.Widgetter { - return row.w -} - -type routeRow struct { - route enum[netip.Prefix] - - w *adw.ActionRow - r *gtk.Button -} - -func (row *routeRow) Update(route enum[netip.Prefix]) { - row.route = route - - if !route.Val.IsValid() { - row.r.SetVisible(false) - row.w.SetTitle("No advertised routes.") - return - } - - row.r.SetVisible(route.Index >= 0) - row.w.SetTitle(route.Val.String()) -} -func (row *routeRow) Widget() gtk.Widgetter { - return row.w + updateListModel(page.addrModel, slices.Values(peer.TailscaleIPs)) + updateListModel(page.routeModel, routes) } diff --git a/internal/ui/peerpage.ui b/internal/ui/peerpage.ui index e7fdd29..98836ce 100644 --- a/internal/ui/peerpage.ui +++ b/internal/ui/peerpage.ui @@ -13,6 +13,12 @@ Tailscale IPs + + + boxed-list + none + + @@ -105,6 +111,12 @@ Advertised Routes + + + boxed-list + none + + diff --git a/internal/ui/selfpage.go b/internal/ui/selfpage.go index 3ff966e..77c5f8c 100644 --- a/internal/ui/selfpage.go +++ b/internal/ui/selfpage.go @@ -22,17 +22,6 @@ import ( "tailscale.com/ipn/ipnstate" ) -var ( - addrSorter = gtk.NewCustomSorter(NewObjectComparer(netip.Addr.Compare)) - prefixSorter = gtk.NewCustomSorter(NewObjectComparer(xnetip.ComparePrefixes)) - waitingFileSorter = gtk.NewCustomSorter(NewObjectComparer(func(f1, f2 apitype.WaitingFile) int { - return cmp.Or( - cmp.Compare(f1.Name, f2.Name), - cmp.Compare(f1.Size, f2.Size), - ) - })) -) - //go:embed selfpage.ui var selfPageXML string diff --git a/internal/ui/trayscale.cmb b/internal/ui/trayscale.cmb index 02cf135..d326d56 100644 --- a/internal/ui/trayscale.cmb +++ b/internal/ui/trayscale.cmb @@ -49,6 +49,8 @@ (2,150,"GtkLabel","RxBytes",149,None,None,None,0,None,None), (2,151,"AdwActionRow","TxBytesRow",110,None,None,None,7,None,None), (2,152,"GtkLabel","TxBytes",151,None,None,None,0,None,None), + (2,153,"GtkListBox","IPList",75,None,None,None,0,None,None), + (2,154,"GtkListBox","AdvertisedRoutesList",137,None,None,None,0,None,None), (3,1,"AdwPreferencesWindow","PreferencesWindow",None,None,None,None,0,None,None), (3,2,"AdwPreferencesPage",None,1,None,None,None,0,None,None), (3,3,"AdwPreferencesGroup",None,2,None,None,None,0,None,None), @@ -148,6 +150,10 @@ (2,147,"AdwPreferencesRow","title","Last handshake",None,None,None,None,None,None,None,None,None), (2,149,"AdwPreferencesRow","title","Bytes received",None,None,None,None,None,None,None,None,None), (2,151,"AdwPreferencesRow","title","Bytes sent",None,None,None,None,None,None,None,None,None), + (2,153,"GtkListBox","selection-mode","none",None,None,None,None,None,None,None,None,None), + (2,153,"GtkWidget","css-classes","boxed-list",None,None,None,None,None,None,None,None,None), + (2,154,"GtkListBox","selection-mode","none",None,None,None,None,None,None,None,None,None), + (2,154,"GtkWidget","css-classes","boxed-list",None,None,None,None,None,None,None,None,None), (3,3,"AdwPreferencesGroup","title","General",None,None,None,None,None,None,None,None,None), (3,14,"AdwActionRow","subtitle","If enabled, an icon will be added to the system tray",None,None,None,None,None,None,None,None,None), (3,14,"AdwPreferencesRow","title","Use Tray Icon",None,None,None,None,None,None,None,None,None), diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 1651799..2b68082 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -1,33 +1,39 @@ package ui import ( + "cmp" "errors" "io" "iter" + "net/netip" "reflect" "strings" "time" "deedles.dev/trayscale" "deedles.dev/trayscale/internal/tsutil" + "deedles.dev/trayscale/internal/xnetip" "deedles.dev/xiter" "github.com/diamondburned/gotk4/pkg/core/gerror" "github.com/diamondburned/gotk4/pkg/core/gioutil" "github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" + "tailscale.com/client/tailscale/apitype" "tailscale.com/ipn/ipnstate" "tailscale.com/types/opt" ) -type enum[T any] struct { - Index int - Val T -} - -func enumerate[T any](i int, v T) enum[T] { - return enum[T]{i, v} -} +var ( + addrSorter = gtk.NewCustomSorter(NewObjectComparer(netip.Addr.Compare)) + prefixSorter = gtk.NewCustomSorter(NewObjectComparer(xnetip.ComparePrefixes)) + waitingFileSorter = gtk.NewCustomSorter(NewObjectComparer(func(f1, f2 apitype.WaitingFile) int { + return cmp.Or( + cmp.Compare(f1.Name, f2.Name), + cmp.Compare(f1.Size, f2.Size), + ) + })) +) func formatTime(t time.Time) string { if t.IsZero() { From 9b1f3e5ee2ff7acb60d5e2e1e2f66b37ae2b1e30 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 24 Sep 2024 14:04:57 -0400 Subject: [PATCH 06/13] internal/ui: add `BindModel()` --- internal/ui/peerpage.go | 4 ++-- internal/ui/selfpage.go | 6 +++--- internal/ui/ui.go | 23 ++++++++++++++++++++++- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/internal/ui/peerpage.go b/internal/ui/peerpage.go index e4b686d..e3ca6e1 100644 --- a/internal/ui/peerpage.go +++ b/internal/ui/peerpage.go @@ -131,7 +131,7 @@ func (page *PeerPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat }) page.addrModel = gioutil.NewListModel[netip.Addr]() - BindModel( + BindListBoxModel( page.IPList, gtk.NewSortListModel(page.addrModel, &addrSorter.Sorter), func(addr netip.Addr) gtk.Widgetter { @@ -161,7 +161,7 @@ func (page *PeerPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat page.IPList.SetPlaceholder(ipListPlaceholder) page.routeModel = gioutil.NewListModel[netip.Prefix]() - BindModel( + BindListBoxModel( page.AdvertisedRoutesList, gtk.NewSortListModel(page.routeModel, &prefixSorter.Sorter), func(route netip.Prefix) gtk.Widgetter { diff --git a/internal/ui/selfpage.go b/internal/ui/selfpage.go index 77c5f8c..a16bdba 100644 --- a/internal/ui/selfpage.go +++ b/internal/ui/selfpage.go @@ -94,7 +94,7 @@ func (page *SelfPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat page.InsertActionGroup("peer", actions) page.addrModel = gioutil.NewListModel[netip.Addr]() - BindModel( + BindListBoxModel( page.IPList, gtk.NewSortListModel(page.addrModel, &addrSorter.Sorter), func(addr netip.Addr) gtk.Widgetter { @@ -124,7 +124,7 @@ func (page *SelfPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat page.IPList.SetPlaceholder(ipListPlaceholder) page.routeModel = gioutil.NewListModel[netip.Prefix]() - BindModel( + BindListBoxModel( page.AdvertisedRoutesList, gtk.NewSortListModel(page.routeModel, &prefixSorter.Sorter), func(route netip.Prefix) gtk.Widgetter { @@ -160,7 +160,7 @@ func (page *SelfPage) init(a *App, peer *ipnstate.PeerStatus, status tsutil.Stat page.AdvertisedRoutesList.SetPlaceholder(advertisedRoutesListPlaceholder) page.fileModel = gioutil.NewListModel[apitype.WaitingFile]() - BindModel( + BindListBoxModel( page.FilesList, gtk.NewSortListModel(page.fileModel, &waitingFileSorter.Sorter), func(file apitype.WaitingFile) gtk.Widgetter { diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 2b68082..b2e6bef 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -7,6 +7,7 @@ import ( "iter" "net/netip" "reflect" + "slices" "strings" "time" @@ -215,8 +216,28 @@ func NewObjectComparer[T any](f func(T, T) int) glib.CompareDataFunc { }) } -func BindModel[T any](lb *gtk.ListBox, m gio.ListModeller, f func(T) gtk.Widgetter) { +func BindListBoxModel[T any](lb *gtk.ListBox, m gio.ListModeller, f func(T) gtk.Widgetter) { lb.BindModel(m, func(obj *glib.Object) gtk.Widgetter { return f(gioutil.ObjectValue[T](obj)) }) } + +func BindModel[T any](add func(int, gtk.Widgetter), remove func(int, gtk.Widgetter), m gio.ListModeller, f func(T) gtk.Widgetter) { + widgets := make([]gtk.Widgetter, 0, m.NItems()) + m.ConnectItemsChanged(func(index, removed, added uint) { + for i, w := range widgets[index : index+removed] { + remove(int(index)+i, w) + } + + new := make([]gtk.Widgetter, 0, added) + for i := index; i < added; i++ { + item := m.Item(i) + new = append(new, f(gioutil.ObjectValue[T](item))) + } + widgets = slices.Replace(widgets, int(index), int(removed), new...) + + for i, w := range new { + add(int(index)+i, w) + } + }) +} From 4de304a709e79e79e0506ca326d8ca2802cdd704 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 24 Sep 2024 14:08:54 -0400 Subject: [PATCH 07/13] internal/ui: add way to unbind a model --- internal/ui/ui.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index b2e6bef..86a4e72 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -222,9 +222,14 @@ func BindListBoxModel[T any](lb *gtk.ListBox, m gio.ListModeller, f func(T) gtk. }) } -func BindModel[T any](add func(int, gtk.Widgetter), remove func(int, gtk.Widgetter), m gio.ListModeller, f func(T) gtk.Widgetter) { +func BindModel[T any]( + add func(int, gtk.Widgetter), + remove func(int, gtk.Widgetter), + m gio.ListModeller, + f func(T) gtk.Widgetter, +) func() { widgets := make([]gtk.Widgetter, 0, m.NItems()) - m.ConnectItemsChanged(func(index, removed, added uint) { + h := m.ConnectItemsChanged(func(index, removed, added uint) { for i, w := range widgets[index : index+removed] { remove(int(index)+i, w) } @@ -240,4 +245,8 @@ func BindModel[T any](add func(int, gtk.Widgetter), remove func(int, gtk.Widgett add(int(index)+i, w) } }) + + return func() { + m.HandlerDisconnect(h) + } } From 06071cb324c98d8a577a87ebfcabd3d7a148e457 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Tue, 24 Sep 2024 14:41:24 -0400 Subject: [PATCH 08/13] internal/ui: add `updateListModelFunc()` --- internal/ui/ui.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 86a4e72..434df5a 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -34,6 +34,7 @@ var ( cmp.Compare(f1.Size, f2.Size), ) })) + peerSorter = gtk.NewCustomSorter(NewObjectComparer(tsutil.ComparePeers)) ) func formatTime(t time.Time) string { @@ -185,24 +186,29 @@ func listModelBackward[T any](m *gioutil.ListModel[T]) iter.Seq2[int, T] { } } -func listModelContains[T comparable](m *gioutil.ListModel[T], val T) bool { - for v := range m.All() { - if v == val { - return true +func updateListModel[T comparable](m *gioutil.ListModel[T], s iter.Seq[T]) { + for i, v := range listModelBackward(m) { + if !xiter.Contains(s, v) { + m.Remove(i) + } + } + + for v := range s { + if !xiter.Contains(m.All(), v) { + m.Append(v) } } - return false } -func updateListModel[T comparable](m *gioutil.ListModel[T], s iter.Seq[T]) { +func updateListModelFunc[T any](m *gioutil.ListModel[T], s iter.Seq[T], f func(T, T) bool) { for i, v := range listModelBackward(m) { - if !xiter.Contains(s, v) { + if !xiter.Any(s, func(sv T) bool { return f(v, sv) }) { m.Remove(i) } } for v := range s { - if !listModelContains(m, v) { + if !xiter.Any(m.All(), func(mv T) bool { return f(v, mv) }) { m.Append(v) } } From f6f318e0645d494f7c18105b69fe2a1b996421a7 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Sun, 29 Sep 2024 19:46:25 -0400 Subject: [PATCH 09/13] update some dependencies --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 84f7c73..58e9273 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23.1 require ( deedles.dev/mk v0.1.0 - deedles.dev/xiter v0.0.0-20240903181553-ec85411a9550 + deedles.dev/xiter v0.1.0 fyne.io/systray v1.11.0 github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20240712143708-824c3ce8a5f4 github.com/diamondburned/gotk4/pkg v0.3.1 diff --git a/go.sum b/go.sum index 984dd1f..bfd87ba 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ deedles.dev/mk v0.1.0 h1:xrvuJA3+R/j6/6AZPc+o31I1rotdKLrAYJxhZJwdOuc= deedles.dev/mk v0.1.0/go.mod h1:TSFsz0T+BvhNqJae0yrj+KadkN4elx248PCpq2Ol4ME= -deedles.dev/xiter v0.0.0-20240903181553-ec85411a9550 h1:P+XGF7JfFPWvD68mirocDKwECEvY76B9IMi3fTaH7GQ= -deedles.dev/xiter v0.0.0-20240903181553-ec85411a9550/go.mod h1:59997UHUsKAy/8bHUClTfeXdyuLZ6z/+yF++vIpxfx8= +deedles.dev/xiter v0.1.0 h1:9U6YMXLmACPavm6Ik5VkibMp6o/ln1k+bE2wMJQP7mM= +deedles.dev/xiter v0.1.0/go.mod h1:59997UHUsKAy/8bHUClTfeXdyuLZ6z/+yF++vIpxfx8= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= From 5e714f80bbe06c345ca4632c64050bd0fdfb4cb2 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Fri, 11 Oct 2024 18:22:03 -0400 Subject: [PATCH 10/13] meta: update some dependencies --- go.mod | 18 +++++++++--------- go.sum | 40 ++++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 58e9273..5711f79 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,9 @@ require ( github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20240712143708-824c3ce8a5f4 github.com/diamondburned/gotk4/pkg v0.3.1 github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf - golang.org/x/net v0.29.0 + golang.org/x/net v0.30.0 honnef.co/go/tools v0.5.1 - tailscale.com v1.74.1 + tailscale.com v1.76.0 ) require ( @@ -55,16 +55,16 @@ require ( go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/exp/typeparams v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect - golang.org/x/time v0.6.0 // indirect - golang.org/x/tools v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.26.0 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect k8s.io/client-go v0.31.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index bfd87ba..b2b9bd8 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,8 @@ github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkM github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= 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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= @@ -127,16 +127,16 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4= go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 h1:bVwtbF629Xlyxk6xLQq2TDYmqP0uiWaet5LwRebuY0k= -golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/exp/typeparams v0.0.0-20241009180824-f66d83c29e7c h1:F/15/6p7LyGUSoP0GE5CB/U9+TNEER1foNOP5sWLLnI= +golang.org/x/exp/typeparams v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -148,14 +148,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 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= @@ -174,5 +174,5 @@ sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.74.1 h1:qhhkN+0gFZasczi+0n0eBxwfP/ZaUr+05cWdsOQ3GT0= -tailscale.com v1.74.1/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8= +tailscale.com v1.76.0 h1:6fS66odV7LySVzS2ZmJebWETeS26grV8iaKZfWgXaPA= +tailscale.com v1.76.0/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= From 5312e7e94e363f2a239fdcd899ebbeb1e1c6163c Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Mon, 11 Nov 2024 22:46:17 -0500 Subject: [PATCH 11/13] internal/tsutil: add profile information to `Status` --- internal/tsutil/client.go | 8 ++++++++ internal/tsutil/poller.go | 27 +++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/internal/tsutil/client.go b/internal/tsutil/client.go index b20c9c5..6ec0f55 100644 --- a/internal/tsutil/client.go +++ b/internal/tsutil/client.go @@ -232,3 +232,11 @@ func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) { // TODO: https://github.com/tailscale/tailscale/issues/8911 return localClient.AwaitWaitingFiles(ctx, time.Second) } + +func ProfileStatus(ctx context.Context) (ipn.LoginProfile, []ipn.LoginProfile, error) { + return localClient.ProfileStatus(ctx) +} + +func SwitchProfile(ctx context.Context, id ipn.ProfileID) error { + return localClient.SwitchProfile(ctx, id) +} diff --git a/internal/tsutil/poller.go b/internal/tsutil/poller.go index a25dda3..8ca3751 100644 --- a/internal/tsutil/poller.go +++ b/internal/tsutil/poller.go @@ -101,6 +101,23 @@ func (p *Poller) Run(ctx context.Context) { } } + profile, profiles, err := ProfileStatus(ctx) + if err != nil { + if ctx.Err() != nil { + return + } + slog.Error("get profile status", "err", err) + select { + case <-ctx.Done(): + return + case <-time.After(retry): + if retry < 30*time.Second { + retry *= 2 + } + continue + } + } + retry = interval var files []apitype.WaitingFile @@ -114,7 +131,7 @@ func (p *Poller) Run(ctx context.Context) { } } - s := Status{Status: status, Prefs: prefs, Files: files} + s := Status{Status: status, Prefs: prefs, Files: files, Profile: profile, Profiles: profiles} if p.New != nil { // TODO: Only call this if the status changed from the previous // poll? Is that remotely feasible? @@ -172,9 +189,11 @@ func (p *Poller) SetInterval() chan<- time.Duration { // Status is a type that wraps various status-related types that // Tailscale provides. type Status struct { - Status *ipnstate.Status - Prefs *ipn.Prefs - Files []apitype.WaitingFile + Status *ipnstate.Status + Prefs *ipn.Prefs + Files []apitype.WaitingFile + Profile ipn.LoginProfile + Profiles []ipn.LoginProfile } // Online returns true if s indicates that the local node is online From 09479c42cd42c0c41d489cc6c0c3bea88fa1f225 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Mon, 11 Nov 2024 22:46:47 -0500 Subject: [PATCH 12/13] internal/ui: add profile switcher --- internal/ui/app.go | 45 +++++++++++++++++++++++++++++++++++++ internal/ui/mainwindow.go | 22 +++++++++++++----- internal/ui/mainwindow.ui | 3 +++ internal/ui/trayscale.cmb | 1 + internal/ui/ui.go | 47 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 6 deletions(-) diff --git a/internal/ui/app.go b/internal/ui/app.go index 8a85e62..2230829 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -15,6 +15,7 @@ import ( "github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" + "tailscale.com/ipn" "tailscale.com/types/key" ) @@ -40,6 +41,7 @@ type App struct { peerPages map[key.NodePublic]*stackPage spinnum int operatorCheck bool + profiles []ipn.LoginProfile } func (a *App) clip(v *glib.Value) { @@ -160,6 +162,23 @@ func (a *App) updatePeers(status tsutil.Status) { } } +func (a *App) updateProfiles(s tsutil.Status) { + updateStringList(a.win.ProfileModel, func(yield func(string) bool) { + for _, profile := range s.Profiles { + if !yield(profile.Name) { + return + } + } + }) + + profileIndex, ok := listModelIndex(a.win.ProfileSortModel, func(obj *glib.Object) bool { + return obj.Cast().(*gtk.StringObject).String() == s.Profile.Name + }) + if ok { + a.win.ProfileDropDown.SetSelected(uint(profileIndex)) + } +} + func (a *App) update(s tsutil.Status) { online := s.Online() a.tray.Update(s) @@ -176,9 +195,12 @@ func (a *App) update(s tsutil.Status) { return } + a.profiles = s.Profiles + a.win.StatusSwitch.SetState(online) a.win.StatusSwitch.SetActive(online) a.updatePeers(s) + a.updateProfiles(s) if a.online && !a.operatorCheck { a.operatorCheck = true @@ -307,6 +329,29 @@ func (a *App) onAppActivate(ctx context.Context) { return true }) + a.win.ProfileDropDown.NotifyProperty("selected-item", func() { + item := a.win.ProfileDropDown.SelectedItem().Cast().(*gtk.StringObject).String() + index := slices.IndexFunc(a.profiles, func(p ipn.LoginProfile) bool { + // TODO: Find a reasonable way to do this by profile ID instead. + return p.Name == item + }) + if index < 0 { + slog.Error("selected unknown profile", "name", item) + return + } + profile := a.profiles[index] + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + slog.Info("selected item changed", "selected item", item) + err := tsutil.SwitchProfile(ctx, profile.ID) + if err != nil { + slog.Error("failed to switch profiles", "err", err, "id", profile.ID, "name", profile.Name) + return + } + }) + contentVariant := glib.NewVariantString("content") a.win.PeersStack.NotifyProperty("visible-child", func() { a.win.SplitView.ActivateAction("navigation.push", contentVariant) diff --git a/internal/ui/mainwindow.go b/internal/ui/mainwindow.go index a6e4165..e9b5634 100644 --- a/internal/ui/mainwindow.go +++ b/internal/ui/mainwindow.go @@ -18,17 +18,27 @@ var ( type MainWindow struct { *adw.ApplicationWindow `gtk:"MainWindow"` - ToastOverlay *adw.ToastOverlay - SplitView *adw.NavigationSplitView - StatusSwitch *gtk.Switch - MainMenuButton *gtk.MenuButton - PeersStack *gtk.Stack - WorkSpinner *gtk.Spinner + ToastOverlay *adw.ToastOverlay + SplitView *adw.NavigationSplitView + StatusSwitch *gtk.Switch + MainMenuButton *gtk.MenuButton + PeersStack *gtk.Stack + WorkSpinner *gtk.Spinner + ProfileDropDown *gtk.DropDown + + ProfileModel *gtk.StringList + ProfileSortModel *gtk.SortListModel } func NewMainWindow(app *gtk.Application) *MainWindow { var win MainWindow fillFromBuilder(&win, menuXML, mainWindowXML) + win.SetApplication(app) + + win.ProfileModel = gtk.NewStringList(nil) + win.ProfileSortModel = gtk.NewSortListModel(win.ProfileModel, &stringListSorter.Sorter) + win.ProfileDropDown.SetModel(win.ProfileSortModel) + return &win } diff --git a/internal/ui/mainwindow.ui b/internal/ui/mainwindow.ui index 68c959b..e125e4f 100644 --- a/internal/ui/mainwindow.ui +++ b/internal/ui/mainwindow.ui @@ -64,6 +64,9 @@ MainMenu + + + diff --git a/internal/ui/trayscale.cmb b/internal/ui/trayscale.cmb index d326d56..e961e45 100644 --- a/internal/ui/trayscale.cmb +++ b/internal/ui/trayscale.cmb @@ -24,6 +24,7 @@ (1,69,"GtkMenuButton","MainMenuButton",67,None,"end",None,1,"<property name=\"menu-model\">MainMenu</property>",None), (1,71,"GtkStackSidebar",None,61,None,None,None,1,None,None), (1,72,"AdwBreakpoint",None,1,None,None,None,4,"<condition>max-width: 400sp</condition><setter object=\"SplitView\" property=\"collapsed\">True</setter>",None), + (1,73,"GtkDropDown","ProfileDropDown",67,None,"title",None,2,None,None), (2,3,"AdwStatusPage","Page",None,None,None,None,0,None,None), (2,73,"AdwClamp",None,3,None,None,None,8,None,None), (2,74,"GtkBox",None,73,None,None,None,0,None,None), diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 434df5a..cdc56b7 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -35,6 +35,10 @@ var ( ) })) peerSorter = gtk.NewCustomSorter(NewObjectComparer(tsutil.ComparePeers)) + + stringListSorter = gtk.NewCustomSorter(glib.NewObjectComparer(func(s1, s2 *gtk.StringObject) int { + return cmp.Compare(s1.String(), s2.String()) + })) ) func formatTime(t time.Time) string { @@ -186,7 +190,47 @@ func listModelBackward[T any](m *gioutil.ListModel[T]) iter.Seq2[int, T] { } } +func stringListBackward(m *gtk.StringList) iter.Seq2[uint, string] { + return func(yield func(uint, string) bool) { + for i := m.NItems(); i > 0; i-- { + if !yield(i-1, m.String(i-1)) { + return + } + } + } +} + +func listModelIndex(m gio.ListModeller, f func(obj *glib.Object) bool) (uint, bool) { + length := m.NItems() + for i := uint(0); i < length; i++ { + if f(m.Item(i)) { + return i, true + } + } + return 0, false +} + +func updateStringList(m *gtk.StringList, s iter.Seq[string]) { + m.FreezeNotify() + defer m.ThawNotify() + + for i, v := range stringListBackward(m) { + if !xiter.Contains(s, v) { + m.Remove(i) + } + } + + for v := range s { + if !xiter.Contains(xiter.V2(stringListBackward(m)), v) { + m.Append(v) + } + } +} + func updateListModel[T comparable](m *gioutil.ListModel[T], s iter.Seq[T]) { + m.FreezeNotify() + defer m.ThawNotify() + for i, v := range listModelBackward(m) { if !xiter.Contains(s, v) { m.Remove(i) @@ -201,6 +245,9 @@ func updateListModel[T comparable](m *gioutil.ListModel[T], s iter.Seq[T]) { } func updateListModelFunc[T any](m *gioutil.ListModel[T], s iter.Seq[T], f func(T, T) bool) { + m.FreezeNotify() + defer m.ThawNotify() + for i, v := range listModelBackward(m) { if !xiter.Any(s, func(sv T) bool { return f(v, sv) }) { m.Remove(i) From e4567642521e6ccf1824a4413b23e50fbef64c2a Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Mon, 11 Nov 2024 22:49:46 -0500 Subject: [PATCH 13/13] meta: update some dependencies --- go.mod | 28 ++++++++++++++-------------- go.sum | 56 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index 5711f79..7eb09c2 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,9 @@ require ( github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20240712143708-824c3ce8a5f4 github.com/diamondburned/gotk4/pkg v0.3.1 github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf - golang.org/x/net v0.30.0 + golang.org/x/net v0.31.0 honnef.co/go/tools v0.5.1 - tailscale.com v1.76.0 + tailscale.com v1.76.6 ) require ( @@ -50,23 +50,23 @@ require ( github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/toqueteos/webbrowser v1.2.0 // indirect - github.com/vishvananda/netns v0.0.4 // indirect + github.com/vishvananda/netns v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/exp/typeparams v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.26.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.8.0 // indirect + golang.org/x/tools v0.27.0 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect - k8s.io/client-go v0.31.1 // indirect + k8s.io/client-go v0.31.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect software.sslmate.com/src/go-pkcs12 v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index b2b9bd8..6ec6603 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJ 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/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= -github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= @@ -127,35 +127,35 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4= go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= -golang.org/x/exp/typeparams v0.0.0-20241009180824-f66d83c29e7c h1:F/15/6p7LyGUSoP0GE5CB/U9+TNEER1foNOP5sWLLnI= -golang.org/x/exp/typeparams v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f h1:WTyX8eCCyfdqiPYkRGm0MqElSfYFH3yR1+rl/mct9sA= +golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= 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= @@ -168,11 +168,11 @@ honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= -k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= +k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.76.0 h1:6fS66odV7LySVzS2ZmJebWETeS26grV8iaKZfWgXaPA= -tailscale.com v1.76.0/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= +tailscale.com v1.76.6 h1:qxRVe/ljIVWixIiCLOHrakbsoXcw/dKaKCZt25tJ7gc= +tailscale.com v1.76.6/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk=