Merge quattro into clock-calendar
Both sides tightened the same center-layout assertion. Taking quattro's: it asserts the weather/update adjacency the test name is about instead of pinning the whole row, which is what kept breaking it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,10 +73,35 @@ function loginPlan(needsLogin, authUrl) {
|
||||
return { authUrl: "", command: ["tailscale", "up"] }
|
||||
}
|
||||
|
||||
// Taildrop is a tailnet feature the admin can turn off, so the button for it
|
||||
// only makes sense when this profile actually carries the capability.
|
||||
function hasFileSharing(self) {
|
||||
var capability = "https://tailscale.com/cap/file-sharing"
|
||||
var capMap = (self && self.CapMap) || null
|
||||
if (capMap && capMap[capability] !== undefined) return true
|
||||
var capabilities = (self && self.Capabilities) || []
|
||||
for (var i = 0; i < capabilities.length; i++) {
|
||||
if (String(capabilities[i]) === capability) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Tailscale grades every peer itself — offline, wrong owner, an OS without
|
||||
// Taildrop, no peer API — so take its word when the status carries one, and
|
||||
// fall back to same-owner for daemons too old to say.
|
||||
function isTaildropTarget(peer, selfUserId) {
|
||||
var target = peer && peer.TaildropTarget
|
||||
if (typeof target === "number" && target !== 0) return target === 1
|
||||
var owner = String((peer && peer.UserID) || "")
|
||||
return owner !== "" && owner === String(selfUserId || "")
|
||||
}
|
||||
|
||||
function peerFromStatus(id, peer) {
|
||||
return {
|
||||
id: id,
|
||||
HostName: displayHostName(peer.HostName, peer.DNSName),
|
||||
UserID: String(peer.UserID || ""),
|
||||
TaildropTarget: typeof peer.TaildropTarget === "number" ? peer.TaildropTarget : 0,
|
||||
DNSName: cleanDnsName(peer.DNSName),
|
||||
DisplayName: displayHostName(peer.HostName, peer.DNSName),
|
||||
TailscaleIPs: filterIPv4(peer.TailscaleIPs || []),
|
||||
@@ -235,6 +260,8 @@ function parseStatus(raw) {
|
||||
selfName: displayHostName(self.HostName, self.DNSName),
|
||||
selfDnsName: cleanDnsName(self.DNSName),
|
||||
selfIp: selfIps.length > 0 ? selfIps[0] : "",
|
||||
selfUserId: String(self.UserID || ""),
|
||||
fileSharing: hasFileSharing(self),
|
||||
peers: peers,
|
||||
exitNodes: exitNodes
|
||||
}
|
||||
@@ -285,6 +312,8 @@ if (typeof module !== "undefined") {
|
||||
osIcon: osIcon,
|
||||
accountLabel: accountLabel,
|
||||
loginPlan: loginPlan,
|
||||
hasFileSharing: hasFileSharing,
|
||||
isTaildropTarget: isTaildropTarget,
|
||||
isMullvadPeer: isMullvadPeer,
|
||||
peerFromStatus: peerFromStatus,
|
||||
parseExitNodeList: parseExitNodeList,
|
||||
|
||||
@@ -295,6 +295,13 @@ Panel {
|
||||
scrollCursorIntoView()
|
||||
}
|
||||
|
||||
// The file picker takes over from here, so get the panel out of the way.
|
||||
function sendPeerFile(peer) {
|
||||
if (!tailscale.canSendFiles(peer)) return
|
||||
tailscale.sendFile(peer)
|
||||
close()
|
||||
}
|
||||
|
||||
function openSelectedPeerCopyMenu() {
|
||||
if (!peerColumn || peerIndex < 0 || peerIndex >= peerColumn.children.length) return
|
||||
var item = peerColumn.children[peerIndex]
|
||||
@@ -416,6 +423,7 @@ Panel {
|
||||
else if (t === "c" || t === "C") tailscale.copyPeerIp(root.selectedPeer())
|
||||
else if (t === "n" || t === "N") tailscale.copyPeerName(root.selectedPeer())
|
||||
else if (t === "d" || t === "D") tailscale.copyPeerDnsName(root.selectedPeer())
|
||||
else if (t === "s" || t === "S") root.sendPeerFile(root.selectedPeer())
|
||||
}
|
||||
|
||||
Flickable {
|
||||
@@ -973,6 +981,17 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
PanelActionButton {
|
||||
id: sendButton
|
||||
visible: tailscale.canSendFiles(peerRow.peer)
|
||||
iconText: ""
|
||||
tooltipText: "Send files"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: root.sendPeerFile(peerRow.peer)
|
||||
}
|
||||
|
||||
PanelActionButton {
|
||||
id: copyButton
|
||||
iconText: ""
|
||||
|
||||
@@ -10,6 +10,7 @@ Native Omarchy bar widget for Tailscale.
|
||||
- Switch between available Tailscale connections when multiple are available
|
||||
- Browse machines from `tailscale status --json`
|
||||
- Copy a machine's Tailscale IP, host name, or DNS name
|
||||
- Send files to a machine with Taildrop, when the tailnet allows file sharing
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
@@ -20,6 +21,7 @@ Inside the panel:
|
||||
- `c`: copy selected peer IP
|
||||
- `n`: copy selected peer name
|
||||
- `d`: copy selected peer DNS name
|
||||
- `s`: send files to selected peer
|
||||
- `t`: toggle Tailscale
|
||||
- `r`: refresh status
|
||||
- `esc`: close
|
||||
@@ -28,6 +30,15 @@ Inside the panel:
|
||||
|
||||
- `tailscale` CLI on `PATH`
|
||||
- `wl-copy` for clipboard copy actions
|
||||
- Taildrop enabled for the tailnet, to send files
|
||||
|
||||
## Receiving files
|
||||
|
||||
Incoming Taildrop files are saved to `~/Downloads` by the
|
||||
`omarchy-tailscale-receive` service, which announces each one with a
|
||||
notification (an image preview when the file is an image, and a click to open
|
||||
it). The Tailscale service install enables it; `omarchy tailscale receive`
|
||||
runs the same loop by hand.
|
||||
|
||||
## Icon
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ Item {
|
||||
property string selfName: ""
|
||||
property string selfDnsName: ""
|
||||
property string selfIp: ""
|
||||
property string selfUserId: ""
|
||||
property bool fileSharing: false
|
||||
property string authUrl: ""
|
||||
property var peers: []
|
||||
property var exitNodes: []
|
||||
@@ -123,6 +125,26 @@ Item {
|
||||
copyToClipboard(cleanDnsName(peer.DNSName), displayHostName(peer.HostName, peer.DNSName) + " DNS name")
|
||||
}
|
||||
|
||||
function peerAddress(peer) {
|
||||
if (!peer) return ""
|
||||
if (peer.DNSName) return cleanDnsName(peer.DNSName)
|
||||
if (peer.HostName) return String(peer.HostName)
|
||||
var ips = filterIPv4(peer.TailscaleIPs || [])
|
||||
return ips.length > 0 ? ips[0] : ""
|
||||
}
|
||||
|
||||
function canSendFiles(peer) {
|
||||
if (!fileSharing || !running || !peer) return false
|
||||
return Model.isTaildropTarget(peer, selfUserId)
|
||||
}
|
||||
|
||||
function sendFile(peer) {
|
||||
if (!canSendFiles(peer)) return
|
||||
var target = peerAddress(peer)
|
||||
if (target === "") return
|
||||
Quickshell.execDetached(["omarchy-tailscale-send", target])
|
||||
}
|
||||
|
||||
function refresh(forceAccounts) {
|
||||
if (installed) {
|
||||
refreshStatusAndAccounts(forceAccounts === true)
|
||||
@@ -137,18 +159,21 @@ Item {
|
||||
|
||||
function refreshStatusAndAccounts(forceAccounts) {
|
||||
if (!installed) return
|
||||
var launched = false
|
||||
if (!statusProcess.running) {
|
||||
_statusOutput = ""
|
||||
_statusError = ""
|
||||
refreshing = true
|
||||
statusProcess.command = ["tailscale", "status", "--json"]
|
||||
statusProcess.running = true
|
||||
launched = true
|
||||
}
|
||||
if (!mullvadExitNodesProcess.running) {
|
||||
_mullvadExitNodesOutput = ""
|
||||
_mullvadExitNodesError = ""
|
||||
mullvadExitNodesProcess.command = ["tailscale", "exit-node", "list"]
|
||||
mullvadExitNodesProcess.running = true
|
||||
launched = true
|
||||
}
|
||||
var now = Date.now()
|
||||
var shouldRefreshAccounts = forceAccounts === true || accounts.length === 0 || now - _lastAccountsRefreshMs > 60000
|
||||
@@ -158,7 +183,13 @@ Item {
|
||||
_lastAccountsRefreshMs = now
|
||||
accountsProcess.command = ["tailscale", "switch", "--list", "--json"]
|
||||
accountsProcess.running = true
|
||||
launched = true
|
||||
}
|
||||
// Arm on the launch that needs watching and leave it alone after that.
|
||||
// Restarting it every refresh pushes the deadline out ahead of a hung
|
||||
// process forever once the refresh interval is shorter than the timeout,
|
||||
// and refreshIntervalSec goes down to five seconds.
|
||||
if (launched && !pollWatchdog.running) pollWatchdog.start()
|
||||
}
|
||||
|
||||
function elideStatus(text) {
|
||||
@@ -175,6 +206,8 @@ Item {
|
||||
selfName = ""
|
||||
selfDnsName = ""
|
||||
selfIp = ""
|
||||
selfUserId = ""
|
||||
fileSharing = false
|
||||
authUrl = ""
|
||||
peers = []
|
||||
exitNodes = []
|
||||
@@ -212,6 +245,8 @@ Item {
|
||||
selfName = parsed.selfName
|
||||
selfDnsName = parsed.selfDnsName
|
||||
selfIp = parsed.selfIp
|
||||
selfUserId = parsed.selfUserId
|
||||
fileSharing = parsed.fileSharing
|
||||
peers = parsed.running ? parsed.peers : []
|
||||
tailnetExitNodes = parsed.running ? parsed.exitNodes : []
|
||||
exitNodes = parsed.running ? tailnetExitNodes.concat(mullvadRegions) : []
|
||||
@@ -295,10 +330,7 @@ Item {
|
||||
var mullvadIps = filterIPv4(peer.TailscaleIPs || [])
|
||||
if (mullvadIps.length > 0) return mullvadIps[0]
|
||||
}
|
||||
if (peer.DNSName) return cleanDnsName(peer.DNSName)
|
||||
if (peer.HostName) return String(peer.HostName)
|
||||
var ips = filterIPv4(peer.TailscaleIPs || [])
|
||||
return ips.length > 0 ? ips[0] : ""
|
||||
return peerAddress(peer)
|
||||
}
|
||||
|
||||
function setExitNode(peer) {
|
||||
@@ -386,6 +418,22 @@ Item {
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Timer {
|
||||
// Every poll is skipped while its own process is still running, so one that
|
||||
// never exits — tailscale can hang on a network that is coming and going —
|
||||
// silently stops the panel refreshing at all, and it stays stopped. Reap
|
||||
// anything still running well inside the refresh interval so the next tick
|
||||
// starts clean.
|
||||
id: pollWatchdog
|
||||
interval: 15000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (statusProcess.running) statusProcess.running = false
|
||||
if (mullvadExitNodesProcess.running) mullvadExitNodesProcess.running = false
|
||||
if (accountsProcess.running) accountsProcess.running = false
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: actionStatusTimer
|
||||
interval: 2200
|
||||
|
||||
Reference in New Issue
Block a user