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:
David Heinemeier Hansson
2026-07-26 22:07:08 -07:00
co-authored by Claude Opus 5
54 changed files with 1510 additions and 148 deletions
+3
View File
@@ -309,6 +309,9 @@ function searchScore(items, entry, query) {
else if (descriptionTextMatches(needle, descriptionText)) score = 60
if (entry.kind === "menu" || entry.kind === "link") score -= 2
// App rows sort after all menu items, so they lose the tiebreak below to an
// equal match. Outrank those, but stay inside the tier so better ones win.
if (entry.kind === "app") score -= 5
return score * 1000 + depthFor(items, entry.id) * 25 + entry.order
}
+29
View File
@@ -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,
+19
View File
@@ -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: "󰆏"
+11
View File
@@ -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
+52 -4
View File
@@ -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