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:
2026-08-15 17:04:41 +01:00
co-authored by Claude Opus 5
parent ba90129612
commit 0912b2b50d
9 changed files with 275 additions and 102 deletions
+90 -54
View File
@@ -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 {