Skip to content

Commit

Permalink
internal/ui: move Mullvad nodes to their own list on their own page (#…
Browse files Browse the repository at this point in the history
…111)

* internal/ui: move self check into `peerName()`

* internal/ui: begin implementing `MullvadPage`

* internal/ui: refactor page name calculation into the pages themselves

* internal/ui: remove Mullvad nodes from main peer list

* internal/ui: more simplification of updates

* internal/ui: fix `nil` check for `a.statusPage`

* internal/ui: separate self-page from peer pages

* internal/ui: add Mullvad page to list of pages

* internal/ui: implement Mullvad exit node list

* internal/ui: indicate that a Mullvad node is active

* internal/xcmp: remove

* internal/ui: add flags for Mullvad nodes

* internal/ui: don't bother uppercasing an already uppercase string

* cmd/trayscale: update PGO file

* meta: add v0.12.0 to metainfo
  • Loading branch information
DeedleFake authored May 3, 2024
1 parent a41f4c6 commit 30dbd08
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 81 deletions.
Binary file modified cmd/trayscale/default.pgo
Binary file not shown.
6 changes: 6 additions & 0 deletions dev.deedles.Trayscale.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
<content_rating type="oars-1.1" />

<releases>
<release version="v0.12.0" date="2024-05-02">
<description>
<ul>Move all Mullvad nodes into their own page.</ul>
<ul>Cleanup a large amount of code for handling pages in the UI.</ul>
</description>
</release>
<release version="v0.11.2" date="2024-04-07">
<description>
<ul>Fix warning about a missing title at startup.</ul>
Expand Down
6 changes: 6 additions & 0 deletions internal/tsutil/tsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ func IsMullvad(peer *ipnstate.PeerStatus) bool {
return tag == "tag:mullvad-exit-node"
})
}

// CanMullvad returns true if peer is allowed to access Mullvad exit
// nodes.
func CanMullvad(peer *ipnstate.PeerStatus) bool {
return peer.CapMap.Contains("mullvad")
}
96 changes: 58 additions & 38 deletions internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ import (
"deedles.dev/trayscale/internal/tray"
"deedles.dev/trayscale/internal/tsutil"
"deedles.dev/trayscale/internal/version"
"deedles.dev/trayscale/internal/xslices"
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"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/ipn/ipnstate"
"tailscale.com/types/key"
)

Expand All @@ -43,7 +41,9 @@ type App struct {
tray *tray.Tray

statusPage *adw.StatusPage
peerPages map[key.NodePublic]Page
selfPage *stackPage
mullvadPage *stackPage
peerPages map[key.NodePublic]*stackPage
spinnum int
operatorCheck bool
}
Expand Down Expand Up @@ -105,57 +105,77 @@ func (a *App) toast(msg string) *adw.Toast {
return toast
}

func (a *App) updatePeers(status tsutil.Status) {
const statusPageName = "status"
func (a *App) updatePeersOffline() {
stack := a.win.PeersStack

w := a.win.PeersStack
for _, page := range a.peerPages {
stack.Remove(page.page.Root())
}
clear(a.peerPages)

var peerMap map[key.NodePublic]*ipnstate.PeerStatus
var peers []key.NodePublic
if (a.selfPage != nil) && (stack.Page(a.selfPage.page.Root()).Object != nil) {
stack.Remove(a.selfPage.page.Root())
a.selfPage = nil
}

if status.Online() {
if c := w.ChildByName(statusPageName); c != nil {
w.Remove(c)
}
if stack.Page(a.statusPage).Object == nil {
stack.AddTitled(a.statusPage, "status", "Not Connected")
}
}

peerMap = status.Status.Peer
if peerMap == nil {
mk.Map(&peerMap, 1)
}
func (a *App) updatePeers(status tsutil.Status) {
if !status.Online() {
a.updatePeersOffline()
return
}

stack := a.win.PeersStack

peers = slices.Insert(status.Status.Peers(), 0, status.Status.Self.PublicKey) // Add this manually to guarantee ordering.
peerMap[status.Status.Self.PublicKey] = status.Status.Self
if a.selfPage == nil {
a.selfPage = &stackPage{page: NewSelfPage()}
a.selfPage.Init(a, status.Status.Self, status)
}
a.selfPage.Update(a, status.Status.Self, status)

oldPeers, newPeers := xslices.Partition(peers, func(peer key.NodePublic) bool {
_, ok := a.peerPages[peer]
return ok
switch {
case tsutil.CanMullvad(status.Status.Self):
if a.mullvadPage == nil {
a.mullvadPage = &stackPage{page: NewMullvadPage()}
a.mullvadPage.Init(a, nil, status)
}
a.mullvadPage.Update(a, nil, status)
case a.mullvadPage != nil:
stack.Remove(a.mullvadPage.page.Root())
a.mullvadPage = nil
}

peerMap := status.Status.Peer
peers := slices.DeleteFunc(status.Status.Peers(), func(peer key.NodePublic) bool {
return tsutil.IsMullvad(peerMap[peer])
})

for id, page := range a.peerPages {
_, ok := peerMap[id]
if !ok {
w.Remove(page.Root())
delete(a.peerPages, id)
for key, page := range a.peerPages {
if _, ok := peerMap[key]; !ok {
stack.Remove(page.page.Root())
delete(a.peerPages, key)
}
}

for _, p := range newPeers {
for _, p := range peers {
peerStatus := peerMap[p]
page := stackPage{page: NewPage(peerStatus, status)}
page.Init(a, peerStatus, status)
page.Update(a, peerStatus, status)
a.peerPages[p] = &page
}

for _, p := range oldPeers {
page := a.peerPages[p]
page.Update(a, peerMap[p], status)
page, ok := a.peerPages[p]
if !ok {
page = &stackPage{page: NewPeerPage()}
page.Init(a, peerStatus, status)
a.peerPages[p] = page
}

page.Update(a, peerStatus, status)
}

if w.Pages().NItems() == 0 {
w.AddTitled(a.statusPage, statusPageName, "Not Connected")
return
if stack.Page(a.statusPage).Object != nil {
stack.Remove(a.statusPage)
}
}

Expand Down
152 changes: 152 additions & 0 deletions internal/ui/mullvadpage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package ui

import (
"cmp"
"context"
_ "embed"
"fmt"
"log/slog"
"slices"

"deedles.dev/trayscale/internal/tsutil"
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)

//go:embed mullvadpage.ui
var mullvadPageXML string

type MullvadPage struct {
*adw.StatusPage `gtk:"Page"`

ExitNodesGroup *adw.PreferencesGroup

name string

exitNodeRows rowManager[*ipnstate.PeerStatus]
}

func NewMullvadPage() *MullvadPage {
var page MullvadPage
fillFromBuilder(&page, mullvadPageXML)
return &page
}

func (page *MullvadPage) Root() gtk.Widgetter {
return page.StatusPage
}

func (page *MullvadPage) ID() string {
return "mullvad"
}

func (page *MullvadPage) Name() string {
return page.name
}

func (page *MullvadPage) Init(a *App, peer *ipnstate.PeerStatus, status tsutil.Status) {
page.name = "Mullvad Exit Nodes"

page.exitNodeRows.Parent = page.ExitNodesGroup
page.exitNodeRows.New = func(peer *ipnstate.PeerStatus) row[*ipnstate.PeerStatus] {
row := exitNodeRow{
peer: peer,

w: adw.NewActionRow(),
r: gtk.NewSwitch(),
}

row.w.AddSuffix(row.r)
row.w.SetTitle(mullvadExitNodeName(peer))

row.r.SetMarginTop(12)
row.r.SetMarginBottom(12)
row.r.ConnectStateSet(func(s bool) bool {
if s == row.r.State() {
return false
}

if s {
err := a.TS.AdvertiseExitNode(context.TODO(), false)
if err != nil {
slog.Error("disable exit node advertisement", "err", err)
// Continue anyways.
}
}

var node *ipnstate.PeerStatus
if s {
node = row.peer
}
err := a.TS.ExitNode(context.TODO(), node)
if err != nil {
slog.Error("set exit node", "err", err)
row.r.SetActive(!s)
return true
}
a.poller.Poll() <- struct{}{}
return true
})

return &row
}
}

func (page *MullvadPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.Status) {
page.name = "Mullvad Exit Nodes"

var exitNodeID tailcfg.StableNodeID
if status.Status.ExitNodeStatus != nil {
exitNodeID = status.Status.ExitNodeStatus.ID
}

nodes := make([]*ipnstate.PeerStatus, 0, len(status.Status.Peer))
for _, peer := range status.Status.Peer {
if tsutil.IsMullvad(peer) {
nodes = append(nodes, peer)
if peer.ID == exitNodeID {
page.name = fmt.Sprintf("Mullvad Exit Nodes [%v]", mullvadExitNodeName(peer))
}
}
}
slices.SortFunc(nodes, func(p1 *ipnstate.PeerStatus, p2 *ipnstate.PeerStatus) int {
return cmp.Compare(p1.DNSName, p2.DNSName)
})

page.exitNodeRows.Update(nodes)
}

type exitNodeRow struct {
peer *ipnstate.PeerStatus

w *adw.ActionRow
r *gtk.Switch
}

func (row *exitNodeRow) Update(peer *ipnstate.PeerStatus) {
row.peer = peer

row.w.SetTitle(mullvadExitNodeName(peer))

row.r.SetState(peer.ExitNode)
row.r.SetActive(peer.ExitNode)
}

func (row *exitNodeRow) Widget() gtk.Widgetter {
return row.w
}

func mullvadExitNodeName(peer *ipnstate.PeerStatus) string {
return fmt.Sprintf("%v %v, %v", countryCodeToFlag(peer.Location.CountryCode), peer.Location.Country, peer.Location.City)
}

func countryCodeToFlag(code string) string {
var raw [2]rune
for i, c := range code {
raw[i] = 127397 + c
}

return string(raw[:])
}
22 changes: 22 additions & 0 deletions internal/ui/mullvadpage.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.2 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.0"/>
<object class="AdwStatusPage" id="Page">
<property name="title">Mullvad Exit Nodes</property>
<child>
<object class="AdwClamp">
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="AdwPreferencesGroup" id="ExitNodesGroup"/>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>
34 changes: 13 additions & 21 deletions internal/ui/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ type Page interface {
// Root returns the root widget that is can be placed into a container.
Root() gtk.Widgetter

// An identifier for the page.
ID() string

// Name returns a displayable name for the page.
Name() string

// Init performs first-time initialization of the page, i.e. setting
// values to their defaults and whatnot. It should not call Update
// unless doing so is idempotent, though even then it's better not
Expand All @@ -22,38 +28,24 @@ type Page interface {
Update(*App, *ipnstate.PeerStatus, tsutil.Status)
}

// NewPage returns an instance of page that represents the given peer.
func NewPage(peer *ipnstate.PeerStatus, status tsutil.Status) Page {
if peer.PublicKey == status.Status.Self.PublicKey {
return NewSelfPage()
}
return NewPeerPage()
}

type stackPage struct {
page Page
stackPage *gtk.StackPage
}

func (page *stackPage) Root() gtk.Widgetter {
return page.page.Root()
}

func (page *stackPage) Init(a *App, peer *ipnstate.PeerStatus, status tsutil.Status) {
page.page.Init(a, peer, status)

page.stackPage = a.win.PeersStack.AddTitled(
page.Root(),
peer.PublicKey.String(),
peerName(status, peer, peer.PublicKey == status.Status.Self.PublicKey),
page.page.Root(),
page.page.ID(),
page.page.Name(),
)

page.page.Init(a, peer, status)
}

func (page *stackPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil.Status) {
self := peer.PublicKey == status.Status.Self.PublicKey
page.page.Update(a, peer, status)

page.stackPage.SetIconName(peerIcon(peer))
page.stackPage.SetTitle(peerName(status, peer, self))

page.page.Update(a, peer, status)
page.stackPage.SetTitle(page.page.Name())
}
Loading

0 comments on commit 30dbd08

Please sign in to comment.