Rebrand to Offscale and rework the panel header
Forked from omarchy.tailscale at the packaged commit (omarchy-dev 4.0.0.r1744.gf002044). The `tailscale` branch holds the pristine upstream subtree history so upstream changes can still be merged in; `master` is this fork. - Rename the plugin to pastilhas.offscale: manifest id/name, moduleName and ipcTarget, and display strings. Deliberately NOT renamed: the lowercase "tailscale" CLI invocations, the TailscaleIPs/TailscaleIPv6 JSON fields from `tailscale status --json`, and authorizeTailscaleOperator. Renaming those loads fine but silently breaks peer parsing. - TailscaleIcon.qml -> OffscaleIcon.qml, now rendering offscale-logo.png instead of the 3x3 dot grid, keeping the crossed/warning states. - Add OffscaleHero.qml: a copy of qs.Ui.PanelHero with a titleComponent slot, so the wordmark (offscale-text.png) can replace the title text. Otherwise byte-identical to the original, so it stays diffable. - Drop the cycling hero phrases entirely - the array, the 2.8s timer and the metaOpacity fade. - Show the local machine as a PeerRow of its own, so it gets the same layout and copy menu as tailnet peers. Adds `navigable` to PeerRow so that row stays out of the peers keyboard cursor. - Enlarge the bar icon: BarIconButton pins its icon to opticalCanvas, so opticalSize/slotSize are overridden alongside iconSize. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -86,7 +86,7 @@ function hasFileSharing(self) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Tailscale grades every peer itself — offline, wrong owner, an OS without
|
||||
// Offscale 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) {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
|
||||
// Local copy of qs.Ui.PanelHero with one addition: `titleComponent`.
|
||||
//
|
||||
// The stock hero renders `title` as a plain Text and offers no slot to put
|
||||
// anything else there, so showing the Offscale wordmark instead of a string
|
||||
// needed either editing a shared /usr/share component (never) or owning a
|
||||
// copy here. Everything else is unchanged from the original, so diffing
|
||||
// against /usr/share/omarchy/shell/Ui/PanelHero.qml shows exactly what we did.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property Component iconComponent: null
|
||||
property string title: ""
|
||||
// When set, replaces the title Text. `title` is still used as the accessible
|
||||
// / fallback string, so callers should keep setting it.
|
||||
property Component titleComponent: null
|
||||
property string meta: ""
|
||||
property string detail: ""
|
||||
property color foreground: Color.foreground
|
||||
property string fontFamily: Style.font.family
|
||||
property real iconSize: Style.font.display
|
||||
property real iconOpacity: 1.0
|
||||
property alias metaOpacity: metaText.opacity
|
||||
|
||||
// Optional control pinned to the trailing edge of the hero — a ToggleSwitch,
|
||||
// a small button. The hero centers it against the labels and reserves the
|
||||
// space itself, so callers never do the geometry.
|
||||
property Component trailingControl: null
|
||||
|
||||
readonly property color dim: Qt.darker(foreground, 1.4)
|
||||
readonly property real trailingInset: trailingLoader.item && trailingLoader.item.visible ? trailingLoader.width + Style.space(12) : 0
|
||||
|
||||
width: parent ? parent.width : implicitWidth
|
||||
implicitHeight: Math.max(iconLoader.implicitHeight, heroLabels.implicitHeight, trailingLoader.implicitHeight)
|
||||
|
||||
Loader {
|
||||
id: iconLoader
|
||||
sourceComponent: root.iconComponent
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: root.iconOpacity
|
||||
}
|
||||
|
||||
Column {
|
||||
id: heroLabels
|
||||
anchors.left: iconLoader.right
|
||||
anchors.leftMargin: Style.space(14)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.trailingInset
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Stock PanelHero uses space(2) = 3px, which is fine for two lines of text
|
||||
// but too tight under an image wordmark. space(5) = 8px gives the machine
|
||||
// name room to read as its own line.
|
||||
spacing: Style.space(5)
|
||||
|
||||
Row {
|
||||
id: titleRow
|
||||
visible: root.title !== "" || root.titleComponent !== null || detailPill.visible
|
||||
width: parent.width
|
||||
|
||||
// Either the custom title component or the stock Text — never both.
|
||||
Loader {
|
||||
id: titleSlot
|
||||
visible: root.titleComponent !== null
|
||||
active: root.titleComponent !== null
|
||||
sourceComponent: root.titleComponent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: titleText
|
||||
visible: root.titleComponent === null && root.title !== ""
|
||||
text: root.title
|
||||
width: visible ? Math.min(implicitWidth, Math.max(0, parent.width - (detailPill.visible ? detailPill.implicitWidth + Style.space(8) : 0))) : 0
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.title
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Math.max(0, parent.width - titleSlot.width - titleText.width - detailPill.implicitWidth)
|
||||
height: 1
|
||||
}
|
||||
|
||||
BorderSurface {
|
||||
id: detailPill
|
||||
visible: root.detail !== ""
|
||||
implicitWidth: detailText.implicitWidth + Style.space(10)
|
||||
implicitHeight: detailText.implicitHeight + Style.space(4)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "transparent"
|
||||
borderSpec: Border.controlSpec("normal", root.foreground, Color.accent)
|
||||
radius: Style.cornerRadius
|
||||
|
||||
Text {
|
||||
id: detailText
|
||||
anchors.centerIn: parent
|
||||
text: root.detail
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: metaText
|
||||
width: parent.width
|
||||
text: root.meta.toUpperCase()
|
||||
visible: text !== ""
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: trailingLoader
|
||||
sourceComponent: root.trailingControl
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
@@ -16,23 +16,31 @@ Item {
|
||||
implicitWidth: iconSize
|
||||
implicitHeight: iconSize
|
||||
|
||||
readonly property real dotSize: Math.max(2, root.iconSize * 0.24)
|
||||
readonly property real mid: (root.iconSize - dotSize) / 2
|
||||
readonly property real end: root.iconSize - dotSize
|
||||
|
||||
// Native rendering of the Tailscale mark from the SVG: a 3×3 dot grid
|
||||
// with the inactive dots faded. This avoids Qt SVG/effect rendering quirks
|
||||
// in tiny bar slots while keeping the official silhouette.
|
||||
Dot { x: 0; y: 0; opacity: 0.24 }
|
||||
Dot { x: root.mid; y: 0; opacity: 0.24 }
|
||||
Dot { x: root.end; y: 0; opacity: 0.24 }
|
||||
Dot { x: 0; y: root.mid; opacity: 1.0 }
|
||||
Dot { x: root.mid; y: root.mid; opacity: 1.0 }
|
||||
Dot { x: root.end; y: root.mid; opacity: 1.0 }
|
||||
Dot { x: 0; y: root.end; opacity: 0.24 }
|
||||
Dot { x: root.mid; y: root.end; opacity: 1.0 }
|
||||
Dot { x: root.end; y: root.end; opacity: 0.24 }
|
||||
// The Offscale mark, replacing the stock 3x3 Tailscale dot grid.
|
||||
//
|
||||
// offscale-logo.png lives beside this file rather than being referenced from
|
||||
// ~/projects, so the plugin stays self-contained and keeps working if that
|
||||
// tree moves. It is the source PNG trimmed of its transparent margin, padded
|
||||
// back to square so it centres, and scaled 1254 -> 256 (1.1 MB -> 77 KB).
|
||||
//
|
||||
// sourceSize caps the decode at 256 regardless of how large iconSize gets, so
|
||||
// the bar slot never decodes more pixels than it needs; mipmap keeps the
|
||||
// downscale to ~27px clean instead of aliased.
|
||||
Image {
|
||||
id: logo
|
||||
anchors.fill: parent
|
||||
source: Qt.resolvedUrl("offscale-logo.png")
|
||||
sourceSize.width: 256
|
||||
sourceSize.height: 256
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
// Dim the mark when Tailscale is down, matching how the stock icon faded
|
||||
// its inactive dots.
|
||||
opacity: root.crossed ? 0.45 : 1.0
|
||||
}
|
||||
|
||||
// Diagonal strike when disconnected.
|
||||
Rectangle {
|
||||
visible: root.crossed
|
||||
anchors.centerIn: parent
|
||||
@@ -43,6 +51,7 @@ Item {
|
||||
rotation: -45
|
||||
}
|
||||
|
||||
// "!" badge when a login is needed.
|
||||
BorderSurface {
|
||||
visible: root.warning
|
||||
width: Math.max(7, parent.width * 0.42)
|
||||
@@ -62,11 +71,4 @@ Item {
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
component Dot: Rectangle {
|
||||
width: root.dotSize
|
||||
height: root.dotSize
|
||||
radius: width / 2
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import qs.Ui
|
||||
|
||||
Panel {
|
||||
id: root
|
||||
moduleName: "omarchy.tailscale"
|
||||
ipcTarget: "omarchy.tailscale"
|
||||
moduleName: "pastilhas.offscale"
|
||||
ipcTarget: "pastilhas.offscale"
|
||||
manageIpc: false
|
||||
|
||||
property string focusSection: "header"
|
||||
@@ -22,20 +22,6 @@ Panel {
|
||||
property bool copyMenuOpen: false
|
||||
property bool mullvadPickerOpen: false
|
||||
property string mullvadQuery: ""
|
||||
property int phraseIndex: 0
|
||||
readonly property var activePhrases: [
|
||||
"Encrypting connections",
|
||||
"Sending secrets",
|
||||
"Guarding wires",
|
||||
"Braiding packets",
|
||||
"Polishing tunnels",
|
||||
"Hiding routes",
|
||||
"Sealing ports",
|
||||
"Sorting tailnets",
|
||||
"Shuffling keys",
|
||||
"Watching machines"
|
||||
]
|
||||
readonly property string heroPhraseText: activePhrases[phraseIndex % activePhrases.length]
|
||||
|
||||
readonly property color foreground: bar ? bar.foreground : Color.foreground
|
||||
readonly property color urgent: bar ? bar.urgent : Color.urgent
|
||||
@@ -52,7 +38,7 @@ Panel {
|
||||
// "header" stays navigable, but an absent CLI leaves nothing to highlight.
|
||||
readonly property bool headerHasCursor: cursorActive && focusSection === "header" && tailscale.installed
|
||||
readonly property color iconColor: tailscale.active ? foreground : dim
|
||||
readonly property string toggleHint: tailscale.active ? "Turn Tailscale off" : (tailscale.needsLogin ? "Authorize this device" : "Turn Tailscale on")
|
||||
readonly property string toggleHint: tailscale.active ? "Turn Offscale off" : (tailscale.needsLogin ? "Authorize this device" : "Turn Offscale on")
|
||||
readonly property color barIconColor: tailscale.active ? barForeground : Qt.darker(barForeground, 1.55)
|
||||
readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
|
||||
readonly property color selectedFill: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "transparent"
|
||||
@@ -240,7 +226,7 @@ Panel {
|
||||
function activateCursor() {
|
||||
ensureCursor()
|
||||
if (focusSection === "header") {
|
||||
tailscale.toggleTailscale()
|
||||
tailscale.toggleOffscale()
|
||||
} else if (focusSection === "auth") {
|
||||
tailscale.authorizeTailscaleOperator()
|
||||
} else if (focusSection === "accounts") {
|
||||
@@ -373,7 +359,7 @@ Panel {
|
||||
function refresh(): string { tailscale.refresh(); return "ok" }
|
||||
function up(): string { tailscale.loginOrUp(); return "ok" }
|
||||
function down(): string { tailscale.down(); return "ok" }
|
||||
function toggleTailscale(): string { tailscale.toggleTailscale(); return "ok" }
|
||||
function toggleOffscale(): string { tailscale.toggleOffscale(); return "ok" }
|
||||
function status(): string { return tailscale.statusText }
|
||||
}
|
||||
|
||||
@@ -381,11 +367,20 @@ Panel {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
// BarIconButton pins its icon Loader to an `opticalCanvas` sized by
|
||||
// Style.bar.iconCanvas (16 * fontScale ~= 27px here). Growing iconSize
|
||||
// alone can't get past that box, so widen the canvas first. slotSize is
|
||||
// raised to match so the widget still reserves room for it in the bar.
|
||||
opticalSize: Style.space(21)
|
||||
slotSize: Style.space(23)
|
||||
iconComponent: Component {
|
||||
Item {
|
||||
TailscaleIcon {
|
||||
OffscaleIcon {
|
||||
anchors.centerIn: parent
|
||||
iconSize: Style.space(11)
|
||||
// Stock was space(11) = 18px at base-size 20 — noticeably smaller
|
||||
// than the neighbouring widgets. space(19) ~= 32px, inside the
|
||||
// widened canvas above. centerIn means it grows from the centre.
|
||||
iconSize: Style.space(19)
|
||||
color: root.barIconColor
|
||||
badgeColor: root.urgent
|
||||
crossed: !tailscale.active && !tailscale.needsLogin
|
||||
@@ -394,7 +389,7 @@ Panel {
|
||||
}
|
||||
}
|
||||
onPressed: function(buttonCode) {
|
||||
if (buttonCode === Qt.RightButton) tailscale.toggleTailscale()
|
||||
if (buttonCode === Qt.RightButton) tailscale.toggleOffscale()
|
||||
else if (buttonCode === Qt.MiddleButton) tailscale.refresh()
|
||||
else root.toggle()
|
||||
}
|
||||
@@ -422,7 +417,7 @@ Panel {
|
||||
onCloseRequested: root.close()
|
||||
onTabRequested: function(direction) { root.switchPanel(direction) }
|
||||
onTextKey: function(t) {
|
||||
if (t === "t" || t === "T") tailscale.toggleTailscale()
|
||||
if (t === "t" || t === "T") tailscale.toggleOffscale()
|
||||
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())
|
||||
@@ -454,17 +449,44 @@ Panel {
|
||||
readonly property bool ringVisible: root.headerHasCursor
|
||||
function focusHero() { root.setHeaderCursor() }
|
||||
|
||||
PanelHero {
|
||||
OffscaleHero {
|
||||
id: hero
|
||||
width: parent.width
|
||||
title: tailscale.installed ? (tailscale.selfName || "Tailscale") : "Tailscale"
|
||||
meta: tailscale.active ? root.heroPhraseText : "Tailscale is disconnected"
|
||||
// The wordmark replaces the title text; `title` is kept as the
|
||||
// fallback string. The machine name moves down into `meta`, where
|
||||
// the cycling phrases used to be.
|
||||
title: tailscale.installed ? (tailscale.selfName || "Offscale") : "Offscale"
|
||||
titleComponent: Component {
|
||||
Image {
|
||||
source: Qt.resolvedUrl("offscale-text.png")
|
||||
// Asset is 773x128 (the source trimmed and scaled); cap the
|
||||
// decode there and let the width follow the aspect ratio.
|
||||
sourceSize.height: 128
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
// Sized against the title font so it tracks base-size.
|
||||
// 1.25 read a touch heavy next to the icon; 1.05 sits closer
|
||||
// to the weight of the text it replaced.
|
||||
height: Style.font.title * 1.05
|
||||
width: height * (773 / 128)
|
||||
}
|
||||
}
|
||||
// No meta line: the header is the icon and the wordmark, centred
|
||||
// against each other. metaText hides itself when empty, so the
|
||||
// label Column collapses to the single title row and its
|
||||
// verticalCenter lines up with the icon's.
|
||||
//
|
||||
// Losing this line also drops the "Offscale is disconnected"
|
||||
// text, but that state is still carried by the icon (crossed and
|
||||
// faded to 45%) and by the toggle switch on the trailing edge.
|
||||
meta: ""
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
iconOpacity: tailscale.active ? 1.0 : 0.5
|
||||
// Status only — the switch owns toggling, mouse and keyboard alike.
|
||||
iconComponent: Component {
|
||||
TailscaleIcon {
|
||||
OffscaleIcon {
|
||||
iconSize: Style.font.display
|
||||
color: root.iconColor
|
||||
badgeColor: root.urgent
|
||||
@@ -485,7 +507,7 @@ Panel {
|
||||
hasCursor: header.ringVisible
|
||||
foreground: hero.foreground
|
||||
onHovered: function(on) { if (on) header.focusHero() }
|
||||
onToggled: tailscale.toggleTailscale()
|
||||
onToggled: tailscale.toggleOffscale()
|
||||
|
||||
PanelToolTip {
|
||||
visible: powerSwitch.containsMouse
|
||||
@@ -497,6 +519,36 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
// The local machine, rendered with the same PeerRow used for the
|
||||
// tailnet machines below — so it gets the identical layout, OS icon
|
||||
// and copy menu (name / DNS / IP) for free. No section header: the
|
||||
// row is the machine.
|
||||
//
|
||||
// The service exposes self as flat strings rather than a peer object,
|
||||
// so the peer shape is assembled here. IPv6 is not among them, so
|
||||
// that copy option simply doesn't appear. `navigable: false` keeps it
|
||||
// out of the peers keyboard cursor, and canSendFiles() is false for
|
||||
// self, so the Taildrop button hides itself.
|
||||
PanelSeparator {
|
||||
visible: tailscale.installed
|
||||
foreground: root.foreground
|
||||
}
|
||||
|
||||
PeerRow {
|
||||
visible: tailscale.installed
|
||||
width: parent.width
|
||||
navigable: false
|
||||
rowIndex: -1
|
||||
peer: ({
|
||||
DisplayName: tailscale.selfName,
|
||||
HostName: tailscale.selfName,
|
||||
DNSName: tailscale.selfDnsName,
|
||||
TailscaleIPs: tailscale.selfIp !== "" ? [tailscale.selfIp] : [],
|
||||
TailscaleIPv6: [],
|
||||
OS: "linux"
|
||||
})
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
||||
width: parent.width
|
||||
@@ -519,7 +571,7 @@ Panel {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Style.space(12)
|
||||
text: "Tailscale CLI is not installed or not on PATH."
|
||||
text: "Offscale CLI is not installed or not on PATH."
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
@@ -713,28 +765,7 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: phraseTimer
|
||||
interval: 2800
|
||||
running: root.opened && tailscale.active
|
||||
repeat: true
|
||||
onTriggered: phraseSwap.restart()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: phraseSwap
|
||||
PropertyAnimation {
|
||||
target: hero; property: "metaOpacity"
|
||||
to: 0.0; duration: 180; easing.type: Easing.OutQuad
|
||||
}
|
||||
ScriptAction {
|
||||
script: root.phraseIndex = (root.phraseIndex + 1) % root.activePhrases.length
|
||||
}
|
||||
PropertyAnimation {
|
||||
target: hero; property: "metaOpacity"
|
||||
to: 1.0; duration: 260; easing.type: Easing.InQuad
|
||||
}
|
||||
}
|
||||
|
||||
component AuthRow: CursorSurface {
|
||||
id: authRow
|
||||
@@ -776,7 +807,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "Authorize Tailscale operator"
|
||||
text: "Authorize Offscale operator"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
@@ -785,7 +816,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "Allow this user to operate this Tailscale profile"
|
||||
text: "Allow this user to operate this Offscale profile"
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
@@ -882,7 +913,12 @@ Panel {
|
||||
}
|
||||
property int copyIndex: 0
|
||||
|
||||
hasCursor: root.cursorActive && root.focusSection === "peers" && root.peerIndex === rowIndex
|
||||
// Peers live in the keyboard-navigable "peers" section. The local machine
|
||||
// row reuses this component purely for its copy menu, and must not claim
|
||||
// the peers cursor or renumber it on hover — hence navigable: false there.
|
||||
property bool navigable: true
|
||||
|
||||
hasCursor: navigable && root.cursorActive && root.focusSection === "peers" && root.peerIndex === rowIndex
|
||||
foreground: root.foreground
|
||||
|
||||
implicitHeight: Math.max(peerContent.implicitHeight, copyButton.implicitHeight) + Style.spacing.rowPaddingX
|
||||
@@ -921,7 +957,7 @@ Panel {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.ArrowCursor
|
||||
onContainsMouseChanged: if (containsMouse) root.setPeerCursor(peerRow.rowIndex)
|
||||
onContainsMouseChanged: if (containsMouse && peerRow.navigable) root.setPeerCursor(peerRow.rowIndex)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Tailscale Omarchy Widget
|
||||
# Offscale Omarchy Widget
|
||||
|
||||
Native Omarchy bar widget for Tailscale.
|
||||
Native Omarchy bar widget for Offscale.
|
||||
|
||||
## Features
|
||||
|
||||
- Shows Tailscale connection state in the bar
|
||||
- Shows Offscale connection state in the bar
|
||||
- Left click opens a keyboard-friendly panel
|
||||
- Right click toggles Tailscale on/off
|
||||
- Switch between available Tailscale connections when multiple are available
|
||||
- Right click toggles Offscale on/off
|
||||
- Switch between available Offscale connections when multiple are available
|
||||
- Browse machines from `tailscale status --json`
|
||||
- Copy a machine's Tailscale IP, host name, or DNS name
|
||||
- Copy a machine's Offscale IP, host name, or DNS name
|
||||
- Send files to a machine with Taildrop, when the tailnet allows file sharing
|
||||
|
||||
## Keyboard shortcuts
|
||||
@@ -22,7 +22,7 @@ Inside the panel:
|
||||
- `n`: copy selected peer name
|
||||
- `d`: copy selected peer DNS name
|
||||
- `s`: send files to selected peer
|
||||
- `t`: toggle Tailscale
|
||||
- `t`: toggle Offscale
|
||||
- `r`: refresh status
|
||||
- `esc`: close
|
||||
|
||||
@@ -37,12 +37,12 @@ Inside the panel:
|
||||
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`
|
||||
it). The Offscale service install enables it; `omarchy tailscale receive`
|
||||
runs the same loop by hand.
|
||||
|
||||
## Icon
|
||||
|
||||
Renders the Tailscale mark natively as a theme-colored 3×3 dot grid, matching the official SVG silhouette while avoiding tiny-SVG rendering quirks in the bar.
|
||||
Renders the Offscale mark natively as a theme-colored 3×3 dot grid, matching the official SVG silhouette while avoiding tiny-SVG rendering quirks in the bar.
|
||||
|
||||
## Add to the bar
|
||||
|
||||
|
||||
+9
-9
@@ -280,7 +280,7 @@ Item {
|
||||
exitNodes = running ? tailnetExitNodes.concat(mullvadRegions) : []
|
||||
}
|
||||
|
||||
function toggleTailscale() {
|
||||
function toggleOffscale() {
|
||||
if (!installed) return
|
||||
if (active) down()
|
||||
else loginOrUp()
|
||||
@@ -304,7 +304,7 @@ Item {
|
||||
}
|
||||
_loginOutput = ""
|
||||
_loginError = ""
|
||||
if (needsLogin) actionStatus = "Starting Tailscale login…"
|
||||
if (needsLogin) actionStatus = "Starting Offscale login…"
|
||||
else _desired = 1
|
||||
_loginInProgress = needsLogin
|
||||
_loginUrlOpened = false
|
||||
@@ -349,7 +349,7 @@ Item {
|
||||
if (!installed || operatorProcess.running || userName === "") return
|
||||
_operatorOutput = ""
|
||||
_operatorError = ""
|
||||
actionStatus = "Authorizing Tailscale operator..."
|
||||
actionStatus = "Authorizing Offscale operator..."
|
||||
operatorProcess.command = ["pkexec", "tailscale", "set", "--operator=" + userName]
|
||||
operatorProcess.running = true
|
||||
}
|
||||
@@ -449,7 +449,7 @@ Item {
|
||||
if (!root._loginInProgress || root._loginUrlOpened) return
|
||||
if (!root.openAuthUrlFrom(root.authUrl, true)) {
|
||||
root._loginInProgress = false
|
||||
root.actionStatus = "Tailscale login link not available yet"
|
||||
root.actionStatus = "Offscale login link not available yet"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,9 +500,9 @@ Item {
|
||||
root.parseAccounts("")
|
||||
if (/profiles access denied/i.test(stderr) || /profiles access denied/i.test(stdout)) {
|
||||
root.accountsAccessDenied = true
|
||||
root.lastError = "Authorize Tailscale operator to show connections"
|
||||
root.lastError = "Authorize Offscale operator to show connections"
|
||||
} else {
|
||||
root.lastError = elideStatus(stderr || stdout || "Could not list Tailscale connections")
|
||||
root.lastError = elideStatus(stderr || stdout || "Could not list Offscale connections")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,7 +532,7 @@ Item {
|
||||
var stderr = String(actionStderr.text || root._actionError || "")
|
||||
if (exitCode !== 0) {
|
||||
root._desired = -1
|
||||
root.lastError = elideStatus(stderr || stdout || "Tailscale command failed")
|
||||
root.lastError = elideStatus(stderr || stdout || "Offscale command failed")
|
||||
root.actionStatus = root.lastError
|
||||
actionStatusTimer.restart()
|
||||
} else {
|
||||
@@ -621,13 +621,13 @@ Item {
|
||||
var stdout = String(operatorStdout.text || root._operatorOutput || "")
|
||||
var stderr = String(operatorStderr.text || root._operatorError || "")
|
||||
if (exitCode !== 0) {
|
||||
root.lastError = elideStatus(stderr || stdout || "Tailscale authorization failed")
|
||||
root.lastError = elideStatus(stderr || stdout || "Offscale authorization failed")
|
||||
root.actionStatus = root.lastError
|
||||
actionStatusTimer.restart()
|
||||
} else {
|
||||
root.accountsAccessDenied = false
|
||||
root.lastError = ""
|
||||
root.actionStatus = "Tailscale operator authorized"
|
||||
root.actionStatus = "Offscale operator authorized"
|
||||
actionStatusTimer.restart()
|
||||
root._lastAccountsRefreshMs = 0
|
||||
}
|
||||
|
||||
+9
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "omarchy.tailscale",
|
||||
"name": "Tailscale",
|
||||
"id": "pastilhas.offscale",
|
||||
"name": "Offscale",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"author": "pastilhas",
|
||||
"license": "MIT",
|
||||
"description": "Tailscale status, connection switching, machine browsing, and quick copy actions in the Omarchy bar.",
|
||||
"description": "Offscale status, connection switching, machine browsing, and quick copy actions in the Omarchy bar.",
|
||||
"kinds": [
|
||||
"bar-widget"
|
||||
],
|
||||
@@ -13,8 +13,8 @@
|
||||
"barWidget": "Panel.qml"
|
||||
},
|
||||
"barWidget": {
|
||||
"displayName": "Tailscale",
|
||||
"description": "Toggle Tailscale, switch connections, browse machines, and copy machine IPs or names.",
|
||||
"displayName": "Offscale",
|
||||
"description": "Toggle Offscale, switch connections, browse machines, and copy machine IPs or names.",
|
||||
"category": "Network",
|
||||
"allowMultiple": false,
|
||||
"defaultSection": "right",
|
||||
@@ -32,5 +32,8 @@
|
||||
"defaultValue": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
"omarchy": {
|
||||
"clonedFrom": "omarchy.tailscale"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Reference in New Issue
Block a user