Skip to content

Commit

Permalink
Add network peers menu (#15)
Browse files Browse the repository at this point in the history
* port forward network devices code

* Misc. refactors

* Fix menu system

* Update additional label

---------

Co-authored-by: Lexi Mattick <[email protected]>
  • Loading branch information
leon332157 and kognise authored Sep 3, 2024
1 parent cc12e8b commit 7728a64
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 73 deletions.
77 changes: 51 additions & 26 deletions libts/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,17 @@ type State struct {
// True if the node is locked out by tailnet lock.
IsLockedOut bool

// List of exit node peers, alphabetically pre-sorted by the result of the PeerName function.
SortedExitNodes []*ipnstate.PeerStatus
// Exit node peers sorted by PeerName.
ExitNodes []*ipnstate.PeerStatus
// Peers owned by the user sorted by PeerName.
MyNodes []*ipnstate.PeerStatus
// Tagged peers sorted by PeerName.
TaggedNodes []*ipnstate.PeerStatus
// Alphabetically sorted keys of AccountNodes.
OwnedNodeKeys []string
// Peers owned by other accoutns, sorted by PeerName, and keyed by account name.
OwnedNodes map[string][]*ipnstate.PeerStatus

// ID of the currently selected exit node or nil if none is selected.
CurrentExitNode *tailcfg.StableNodeID
// Name of the currently selected exit node or an empty string if none is selected.
Expand All @@ -49,25 +58,11 @@ type State struct {
TxBytes int64
}

// Get a sorted list of exit node peers, alphabetically pre-sorted by the result of the PeerName function.
func getSortedExitNodes(tsStatus *ipnstate.Status) []*ipnstate.PeerStatus {
exitNodes := make([]*ipnstate.PeerStatus, 0)

if tsStatus == nil {
return exitNodes
}

for _, peer := range tsStatus.Peer {
if peer.ExitNodeOption {
exitNodes = append(exitNodes, peer)
}
}

slices.SortFunc(exitNodes, func(a, b *ipnstate.PeerStatus) int {
// Sort a list of node statuses by PeerName.
func sortNodes(nodes []*ipnstate.PeerStatus) {
slices.SortFunc(nodes, func(a, b *ipnstate.PeerStatus) int {
return strings.Compare(PeerName(a), PeerName(b))
})

return exitNodes
}

// Create an ipn.State from the string representation.
Expand Down Expand Up @@ -118,18 +113,48 @@ func GetState(ctx context.Context) (State, error) {
}

state := State{
Prefs: prefs,
AuthURL: status.AuthURL,
BackendState: backendState,
TSVersion: status.Version,
Self: status.Self,
SortedExitNodes: getSortedExitNodes(status),
Prefs: prefs,
AuthURL: status.AuthURL,
BackendState: backendState,
TSVersion: status.Version,
Self: status.Self,
OwnedNodes: make(map[string][]*ipnstate.PeerStatus),
}

for _, peer := range status.Peer {
state.TxBytes += peer.TxBytes
state.RxBytes += peer.RxBytes

if peer.ExitNodeOption {
state.ExitNodes = append(state.ExitNodes, peer)
} else if peer.UserID == status.Self.UserID {
state.MyNodes = append(state.MyNodes, peer)
} else if peer.IsTagged() {
state.TaggedNodes = append(state.TaggedNodes, peer)
} else {
var accountName string
if user, ok := status.User[peer.UserID]; ok {
accountName = user.DisplayName
if accountName == "" {
accountName = user.LoginName
}
}

if _, ok := state.OwnedNodes[accountName]; !ok {
state.OwnedNodes[accountName] = make([]*ipnstate.PeerStatus, 0)
}
state.OwnedNodes[accountName] = append(state.OwnedNodes[accountName], peer)
}
}

sortNodes(state.ExitNodes)
sortNodes(state.MyNodes)
sortNodes(state.TaggedNodes)
for key, value := range state.OwnedNodes {
sortNodes(value)
state.OwnedNodeKeys = append(state.OwnedNodeKeys, key)
}
slices.Sort(state.OwnedNodeKeys)

versionSplitIndex := strings.IndexByte(state.TSVersion, '-')
if versionSplitIndex != -1 {
Expand All @@ -152,7 +177,7 @@ func GetState(ctx context.Context) (State, error) {
if status.ExitNodeStatus != nil {
state.CurrentExitNode = &status.ExitNodeStatus.ID

for _, peer := range state.SortedExitNodes {
for _, peer := range state.ExitNodes {
if peer.ID == status.ExitNodeStatus.ID {
state.CurrentExitNodeName = PeerName(peer)
break
Expand Down
77 changes: 75 additions & 2 deletions menus.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,51 @@ import (
"github.com/neuralinkcorp/tsui/libts"
"github.com/neuralinkcorp/tsui/ui"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/types/opt"
"tailscale.com/types/preftype"
)

func buildNetworkDevicesSubmenuSection(title string, peers []*ipnstate.PeerStatus) []ui.SubmenuItem {
items := []ui.SubmenuItem{
&ui.TitleSubmenuItem{Label: title},
}

if len(peers) == 0 {
items = append(items, &ui.DividerSubmenuItem{})
} else {
for _, peer := range peers {
peerName := libts.PeerName(peer)

// Normalize the capitalization of the OS, because some are capitalized but some aren't.
osName := peer.OS
switch osName {
case "android":
osName = "Android"
case "windows":
osName = "Windows"
case "linux":
osName = "Linux"
}

items = append(items, &ui.LabeledSubmenuItem{
Label: peerName,
AdditionalLabel: osName,
OnActivate: func() tea.Msg {
err := clipboard.WriteString(peer.TailscaleIPs[0].String())
if err != nil {
return errorMsg(err)
}
return successMsg(fmt.Sprintf("Copied IP address of %s.", peerName))
},
IsDim: false,
})
}
}

return items
}

// Update all of the menu UIs from the current state.
func (m *model) updateMenus() {
if m.state.BackendState == ipn.Running {
Expand Down Expand Up @@ -124,7 +165,7 @@ func (m *model) updateMenus() {

// Update the exit node submenu.
{
exitNodeItems := make([]ui.SubmenuItem, 2+len(m.state.SortedExitNodes))
exitNodeItems := make([]ui.SubmenuItem, 2+len(m.state.ExitNodes))
exitNodeItems[0] = &ui.ToggleableSubmenuItem{
LabeledSubmenuItem: ui.LabeledSubmenuItem{
Label: "None",
Expand All @@ -139,7 +180,7 @@ func (m *model) updateMenus() {
IsActive: m.state.CurrentExitNode == nil,
}
exitNodeItems[1] = &ui.DividerSubmenuItem{}
for i, exitNode := range m.state.SortedExitNodes {
for i, exitNode := range m.state.ExitNodes {
// Offset for the "None" item and the divider.
i += 2

Expand Down Expand Up @@ -171,6 +212,37 @@ func (m *model) updateMenus() {
m.exitNodes.Submenu.SetItems(exitNodeItems)
}

// Update the network devices submenu.
{
networkNodes := make([]ui.SubmenuItem, 0)

networkNodes = append(networkNodes,
buildNetworkDevicesSubmenuSection("My Devices", m.state.MyNodes)...)
networkNodes = append(networkNodes,
&ui.SpacerSubmenuItem{})
networkNodes = append(networkNodes,
buildNetworkDevicesSubmenuSection("Tagged Devices", m.state.TaggedNodes)...)

for _, key := range m.state.OwnedNodeKeys {
if key == "" {
key = "<none>"
}

networkNodes = append(networkNodes,
&ui.SpacerSubmenuItem{})
networkNodes = append(networkNodes,
buildNetworkDevicesSubmenuSection(key, m.state.OwnedNodes[key])...)
}

lenSum := len(m.state.MyNodes) + len(m.state.TaggedNodes)
for _, value := range m.state.OwnedNodes {
lenSum += len(value)
}

m.networkDevices.AdditionalLabel = fmt.Sprintf("%d visible", lenSum)
m.networkDevices.Submenu.SetItems(networkNodes)
}

// Update the settings submenu.
{
exitNode := "No"
Expand Down Expand Up @@ -338,6 +410,7 @@ func (m *model) updateMenus() {
m.menu.SetItems([]*ui.AppmenuItem{
m.deviceInfo,
m.exitNodes,
m.networkDevices,
m.settings,
})
} else {
Expand Down
14 changes: 8 additions & 6 deletions tsui.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ type model struct {
canWrite bool

// Main menu.
menu ui.Appmenu
deviceInfo *ui.AppmenuItem
exitNodes *ui.AppmenuItem
settings *ui.AppmenuItem
menu ui.Appmenu
deviceInfo *ui.AppmenuItem
exitNodes *ui.AppmenuItem
networkDevices *ui.AppmenuItem
settings *ui.AppmenuItem

// Current width of the terminal.
terminalWidth int
Expand Down Expand Up @@ -92,7 +93,8 @@ func initialModel() (model, error) {
exitNodes: &ui.AppmenuItem{Label: "Exit Nodes",
Submenu: ui.Submenu{Exclusivity: ui.SubmenuExclusivityOne},
},
settings: &ui.AppmenuItem{Label: "Settings"},
networkDevices: &ui.AppmenuItem{Label: "Network Devices"},
settings: &ui.AppmenuItem{Label: "Settings"},
}

state, err := libts.GetState(ctx)
Expand All @@ -113,7 +115,7 @@ func (m model) Init() tea.Cmd {
// Perform our initial state fetch to populate menus
updateState,
// Run an initial batch of pings.
makeDoPings(m.state.SortedExitNodes),
makeDoPings(m.state.ExitNodes),
// Kick off our ticks.
tea.Tick(tickInterval, func(_ time.Time) tea.Msg {
return tickMsg{}
Expand Down
Loading

0 comments on commit 7728a64

Please sign in to comment.