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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user