Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions shell/plugins/panels/tailscale/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,17 @@ function parseStatus(raw) {
var peer = rawPeers[id] || {}
var normalized = peerFromStatus(id, peer)
if (normalized.Mullvad) continue
if (normalized.Online) {
peers.push(normalized)
if (normalized.ExitNodeOption) exitNodes.push(normalized)
}
peers.push(normalized)
// Exit nodes stay online-only: routing through a machine that is down
// just breaks connectivity, so an offline one is never a valid choice.
if (normalized.Online && normalized.ExitNodeOption) exitNodes.push(normalized)
}

// Online machines first, each group alphabetical. The ones you can act on
// stay at the top of the list while offline machines remain reachable for
// copying an address.
peers.sort(function(a, b) {
if (a.Online !== b.Online) return a.Online ? -1 : 1
return String(a.HostName).localeCompare(String(b.HostName))
})
exitNodes.sort(function(a, b) {
Expand Down
6 changes: 4 additions & 2 deletions shell/plugins/panels/tailscale/Panel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ Panel {
return String(peer.TailscaleIPv6[0] || "")
}
readonly property string peerDns: peer ? String(peer.DNSName || "") : ""
readonly property bool peerOnline: peer ? peer.Online === true : false
readonly property var copyOptions: {
var options = []
if (peerName !== "") options.push({ kind: "name", label: peerName })
Expand Down Expand Up @@ -937,7 +938,7 @@ Panel {
Text {
textFormat: Text.PlainText
text: tailscale.osIcon(peer ? peer.OS : "")
color: root.foreground
color: peerRow.peerOnline ? root.foreground : root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.icon
Layout.alignment: Qt.AlignVCenter
Expand All @@ -952,7 +953,7 @@ Panel {
textFormat: Text.PlainText
Layout.fillWidth: true
text: peerRow.peerName
color: root.foreground
color: peerRow.peerOnline ? root.foreground : root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.body
elide: Text.ElideRight
Expand All @@ -963,6 +964,7 @@ Panel {
Layout.fillWidth: true
text: {
var parts = []
if (!peerRow.peerOnline) parts.push("Offline")
if (peerRow.peerIp !== "") parts.push(peerRow.peerIp)
if (peerRow.peerDns !== "") parts.push(peerRow.peerDns)
return parts.join(" · ")
Expand Down
2 changes: 1 addition & 1 deletion shell/plugins/panels/tailscale/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Native Omarchy bar widget for Tailscale.
- Left click opens a keyboard-friendly panel
- Right click toggles Tailscale on/off
- Switch between available Tailscale connections when multiple are available
- Browse machines from `tailscale status --json`
- Browse machines from `tailscale status --json`, offline ones dimmed below the online ones
- Copy a machine's Tailscale IP, host name, or DNS name
- Send files to a machine with Taildrop, when the tailnet allows file sharing

Expand Down
1 change: 1 addition & 0 deletions shell/plugins/panels/tailscale/Service.qml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Item {

function canSendFiles(peer) {
if (!fileSharing || !running || !peer) return false
if (peer.Online !== true) return false
return Model.isTaildropTarget(peer, selfUserId)
}

Expand Down
7 changes: 4 additions & 3 deletions test/shell.d/tailscale-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,21 +87,22 @@ const status = tailscale.parseStatus(JSON.stringify({

assert(status.ok && status.running, 'tailscale parses running status')
assertEqual(status.selfIp, '100.74.97.73', 'tailscale parses self IP')
assertDeepEqual(status.peers.map(peer => peer.HostName), ['alpha', 'zed'], 'tailscale filters offline and Mullvad peers and sorts online peers')
assertDeepEqual(status.peers.map(peer => peer.HostName), ['alpha', 'zed', 'mbu-ser9', 'offline'], 'tailscale lists offline peers after online ones, each group alphabetical, and skips Mullvad')
assertDeepEqual(status.peers.map(peer => peer.Online), [true, true, false, false], 'tailscale records which peers are online')
assertDeepEqual(status.peers[0].TailscaleIPv6, ['fd7a:115c:a1e0::1901:334b'], 'tailscale preserves peer IPv6 addresses for copy menu')
assert(status.peers[1].ExitNodeOption && status.peers[1].ExitNode, 'tailscale preserves exit node flags')
assertDeepEqual(status.exitNodes.map(peer => peer.HostName), ['zed'], 'tailscale lists only online tailnet exit nodes')
assert(tailscale.isMullvadPeer({ HostName: 'al-tia-wg-003', DNSName: 'al-tia-wg-003.mullvad.ts.net.' }), 'tailscale detects Mullvad status peers')

assert(status.fileSharing, 'tailscale reads Taildrop capability from the status capability map')
assertEqual(status.selfUserId, '1001', 'tailscale records the owning user of this machine')
assertDeepEqual(status.peers.map(peer => peer.UserID), ['1001', '1002'], 'tailscale records the owning user of each peer')
assertDeepEqual(status.peers.map(peer => peer.UserID), ['1001', '1002', '', ''], 'tailscale records the owning user of each peer')
assert(
tailscale.hasFileSharing({ Capabilities: ['https://tailscale.com/cap/file-sharing'] }),
'tailscale reads Taildrop capability from the legacy capability list'
)
assert(!tailscale.hasFileSharing({ CapMap: { funnel: null } }), 'tailscale reports no Taildrop without the capability')
assertDeepEqual(status.peers.map(peer => peer.TaildropTarget), [1, 5], 'tailscale records how Tailscale grades each Taildrop target')
assertDeepEqual(status.peers.map(peer => peer.TaildropTarget), [1, 5, 0, 0], 'tailscale records how Tailscale grades each Taildrop target')
assert(tailscale.isTaildropTarget({ TaildropTarget: 1, UserID: '1001' }, '2002'), 'tailscale trusts an available Taildrop target')
assert(!tailscale.isTaildropTarget({ TaildropTarget: 7, UserID: '1001' }, '1001'), 'tailscale skips peers Tailscale rules out')
assert(tailscale.isTaildropTarget({ UserID: '1001' }, '1001'), 'tailscale falls back to same-owner peers without a grade')
Expand Down