Merge remote-tracking branch 'origin/omarchy-shell' into omarchy-4

# Conflicts:
#	bin/omarchy-debug
#	bin/omarchy-upload-log
This commit is contained in:
Ryan Hughes
2026-06-06 13:28:47 -04:00
26 changed files with 271 additions and 135 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ fi
charge_holding=false
if [[ $ac_online == "true" && -n $threshold_end ]]; then
if [[ $state == "discharging" || $state == "pending-charge" ]]; then
if [[ $state == "pending-charge" ]]; then
charge_holding=true
elif [[ $state == "fully-charged" ]] && (( percentage < 99 )); then
charge_holding=true
+1 -1
View File
@@ -75,7 +75,7 @@ ACTION=$(gum choose "${OPTIONS[@]}")
case "$ACTION" in
"Upload log")
echo "Uploading debug log to logs.omarchy.org..."
URL=$(curl -sf -F "file=@$LOG_FILE" https://logs.omarchy.org/)
URL=$(curl -sf -F "file=@$LOG_FILE" -Fexpires=24 https://logs.omarchy.org/)
if (( $? == 0 )) && [[ -n $URL ]]; then
echo "✓ Log uploaded successfully!"
echo "Share this URL:"
+23 -1
View File
@@ -94,6 +94,21 @@ require_command() {
command -v "$command_name" >/dev/null 2>&1 || fail "$command_name is required"
}
plugin_discovered() {
local id="$1" plugins
plugins=$(omarchy-shell shell listPlugins 2>/dev/null) || return 1
jq -e --arg id "$id" 'any(.[]; .id == $id)' <<<"$plugins" >/dev/null 2>&1
}
wait_for_plugin_discovery() {
local id="$1" attempt
for (( attempt = 0; attempt < 40; attempt++ )); do
plugin_discovered "$id" && return 0
sleep 0.05
done
return 1
}
section_valid() {
[[ $1 =~ ^(left|center|right)$ ]]
}
@@ -181,7 +196,14 @@ plugin_enabled() {
id=$(canonical_widget_id "$id")
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
omarchy-shell shell setPluginEnabled "$id" "$enabled" || fail "could not update plugin; is omarchy-shell running?"
if [[ $enabled == "true" ]]; then
require_command jq
wait_for_plugin_discovery "$id" || fail "plugin '$id' was not discovered; is omarchy-shell running?"
fi
local result
result=$(omarchy-shell shell setPluginEnabled "$id" "$enabled") || fail "could not update plugin; is omarchy-shell running?"
[[ $result == "ok" ]] || fail "plugin '$id' is not known; run: omarchy plugin rescan"
if [[ $enabled == "true" && $# -gt 0 ]]; then
mutate_bar move --id "$id" "$@"
+18 -1
View File
@@ -29,6 +29,21 @@ interactive() {
[[ -t 0 && -t 1 ]]
}
plugin_discovered() {
local id="$1" plugins
plugins=$(omarchy-shell shell listPlugins 2>/dev/null) || return 1
jq -e --arg id "$id" 'any(.[]; .id == $id)' <<<"$plugins" >/dev/null 2>&1
}
wait_for_plugin_discovery() {
local id="$1" attempt
for (( attempt = 0; attempt < 40; attempt++ )); do
plugin_discovered "$id" && return 0
sleep 0.05
done
return 1
}
PLUGIN_ID=""
FROM_SOURCE=""
ENABLE_AFTER="" # "", "true", or "false"
@@ -280,7 +295,9 @@ if [[ -z $ENABLE_AFTER ]]; then
fi
if [[ $ENABLE_AFTER == true ]]; then
if omarchy-shell shell setPluginEnabled "$m_id" true >/dev/null 2>&1; then
if ! wait_for_plugin_discovery "$m_id"; then
echo "Could not enable $m_id because omarchy-shell did not discover it yet. Enable later with: omarchy plugin enable $m_id" >&2
elif result=$(omarchy-shell shell setPluginEnabled "$m_id" true) && [[ $result == "ok" ]]; then
echo "Enabled $m_id."
if [[ $m_kinds == *bar-widget* ]]; then
echo "It was added to the bar; arrange it with: omarchy plugin bar move $m_id --section <left|center|right>"
+1 -2
View File
@@ -102,6 +102,5 @@ fi
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
if [[ $was_enabled == "true" ]]; then
echo "Plugin was enabled; restart the shell to fully unload it:"
echo " omarchy restart shell"
echo "Plugin was enabled and was unloaded from omarchy-shell."
fi
+1 -2
View File
@@ -208,8 +208,7 @@ done
if ((any_enabled)); then
echo
echo "Updated plugins that were enabled may need a shell restart to fully reload:"
echo " omarchy restart shell"
echo "Updated enabled plugins were hot-reloaded by omarchy-shell."
fi
exit $rc
+1 -1
View File
@@ -152,7 +152,7 @@ esac
echo ""
URL=$(curl -sf -F "file=@$TEMP_LOG" https://logs.omarchy.org/)
URL=$(curl -sf -F "file=@$TEMP_LOG" -Fexpires=24 https://logs.omarchy.org/)
if (( $? == 0 )) && [[ -n $URL ]]; then
echo "✓ Log uploaded successfully!"
+1 -1
View File
@@ -200,7 +200,7 @@ cp ~/.config/hypr/bindings.conf ~/.config/hypr/bindings.conf.bak.$(date +%s)
# 4. Apply changes
# - Hyprland: auto-reloads on save, but MUST validate with `hyprctl reload` and `hyprctl configerrors`
# - Omarchy shell: shell.json hot-reloads; use `omarchy-restart-shell` for plugin/widget code changes
# - Omarchy shell: shell.json hot-reloads; use `omarchy plugin rescan` for plugin/widget code changes
# - Launcher: restart with `omarchy restart shell`
# - Terminals: MUST restart with `omarchy restart terminal`
```
+2 -2
View File
@@ -78,9 +78,9 @@ individual plugins (`bar`, `image-selector`, …).
| `hide <id>` | close a previously-summoned |
| `toggle <id> <payloadJson>` | summon if closed, hide if open |
| `call <id> <method> <arg>` | call an already-loaded plugin |
| `rescanPlugins` | re-walk plugin dirs |
| `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code |
| `reloadConfig` | reload shell.json |
| `setPluginEnabled <id> <"true"\|…>` | flip enabled bit |
| `setPluginEnabled <id> <"true"\|…>` | flip enabled bit (`ok` / `unknown`) |
| `listPlugins` | JSON of every discovered plugin |
`setPluginEnabled` takes a string; only literal `"true"` enables.
+2 -2
View File
@@ -172,9 +172,9 @@ running a separate Quickshell instance.
| `hide <id>` | — | close a previously-summoned plugin |
| `toggle <id> <payloadJson>` | — | summon if closed, hide if open |
| `call <id> <method> <arg>` | string | call a method on an already-loaded plugin |
| `rescanPlugins` | — | re-walk plugin dirs and pick up new/changed manifests |
| `rescanPlugins` | — | re-walk plugin dirs and hot-reload plugin code |
| `reloadConfig` | `ok` | reload `~/.config/omarchy/shell.json` |
| `setPluginEnabled <id> <enabled>` | | flip the persisted enabled bit (see note) |
| `setPluginEnabled <id> <enabled>` | `ok` / `unknown` | flip the persisted enabled bit (see note) |
| `listPlugins` | JSON | every discovered plugin (id, name, kinds, enabled) |
Direct invocation:
+12 -2
View File
@@ -1257,6 +1257,12 @@ Item {
readonly property bool hovered: moduleHover.hovered
readonly property bool dragSource: root.barDragSource === slot
readonly property bool panelOpen: root.activePopout === slot.activeItem
readonly property real openIndicatorInlineOffset: {
var item = slot.activeItem
if (!item || !("openIndicatorInlineOffset" in item)) return 0
var offset = Number(item.openIndicatorInlineOffset)
return isFinite(offset) ? offset : 0
}
implicitWidth: activeItem && activeItem.visible ? (root.vertical ? root.barSize : activeItem.implicitWidth) : 0
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
@@ -1329,8 +1335,12 @@ Item {
radius: Math.min(width, height) / 2
width: root.vertical ? Style.space(2) : Math.max(Style.space(10), Math.round(parent.width * 0.55))
height: root.vertical ? Math.max(Style.space(10), Math.round(parent.height * 0.55)) : Style.space(2)
x: root.vertical ? (root.position === "left" ? parent.width - width - inset : inset) : Math.round((parent.width - width) / 2)
y: root.vertical ? Math.round((parent.height - height) / 2) : (root.position === "top" ? parent.height - height - inset : inset)
x: root.vertical
? (root.position === "left" ? parent.width - width - inset : inset)
: (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.width - width) / 2) : (parent.width - width) / 2 + slot.openIndicatorInlineOffset)
y: root.vertical
? (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.height - height) / 2) : (parent.height - height) / 2 + slot.openIndicatorInlineOffset)
: (root.position === "top" ? parent.height - height - inset : inset)
z: 50
Behavior on opacity {
+6 -3
View File
@@ -35,14 +35,17 @@ BarWidget {
root.bar.run("hyprctl dispatch " + Util.shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })"))
}
implicitWidth: grid.implicitWidth
readonly property real trailingGap: root.vertical ? 0 : Style.spaceReal(1.5)
implicitWidth: grid.implicitWidth + trailingGap
implicitHeight: grid.implicitHeight
GridLayout {
id: grid
anchors.fill: parent
anchors.rightMargin: root.trailingGap
columns: root.vertical ? 1 : root.workspaceIds().length
columnSpacing: root.vertical ? 0 : Style.space(2)
columnSpacing: root.vertical ? 0 : Style.space(1)
rowSpacing: root.vertical ? Style.space(2) : 0
Repeater {
@@ -60,7 +63,7 @@ BarWidget {
opacity: occupied || focused ? 1 : 0.5
horizontalMargin: 6
verticalPadding: 6
fixedWidth: root.vertical ? root.barSize : Style.space(22)
fixedWidth: root.vertical ? root.barSize : Style.space(20)
fixedHeight: root.barSize
onPressed: function() { root.focusWorkspace(modelData) }
}
+2
View File
@@ -528,6 +528,8 @@ Panel {
bar: root.bar
text: root.outputIcon()
fontSize: Style.font.body
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
onPressed: function(b) {
if (b === Qt.RightButton) root.toggleOutputMute()
else root.toggle()
+2
View File
@@ -406,6 +406,8 @@ Panel {
anchors.fill: parent
bar: root.bar
text: root.icon
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
onPressed: function(b) {
if (b === Qt.RightButton) root.toggleBluetooth()
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth")
+1 -1
View File
@@ -141,7 +141,7 @@ Panel {
Item {
id: button
anchors.fill: parent
implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(32)
implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(27)
implicitHeight: root.bar && root.bar.vertical ? Style.space(26) : (root.bar ? root.bar.barSize : Style.space(26))
property var registeredBar: null
+3
View File
@@ -343,6 +343,9 @@ Panel {
bar: root.bar
text: root.displays.length > 1 ? "󰍺" : "󰍹"
fontSize: Style.font.subtitle
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
rightExtraMargin: 4
onPressed: function(b) { root.toggle() }
onWheelMoved: function(delta) {
if (root.brightnessAvailable) root.setBrightness(root.brightnessPercent + (delta > 0 ? 5 : -5))
+49 -63
View File
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Networking
@@ -584,8 +585,9 @@ Panel {
anchors.fill: parent
bar: root.bar
text: root.icon
horizontalMargin: 8.5
rightExtraMargin: 2
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
rightExtraMargin: 5.5
onPressed: function(b) {
if (root.opened) root.close()
@@ -794,48 +796,41 @@ Panel {
width: parent.width
spacing: Style.spacing.labelGap
Row {
visible: root.info.rx_bytes !== undefined
GridLayout {
width: parent.width
spacing: Style.space(20)
columns: 4
columnSpacing: Style.space(20)
rowSpacing: Style.spacing.labelGap
Column {
width: (parent.width - parent.spacing) / 2
spacing: Style.spacing.labelGap
InfoPair { label: "Receiving"; value: root.formatRate(root.downloadRate) }
InfoPair { label: "Downloaded"; value: root.formatBytes(parseFloat(root.info.rx_bytes || "0")) }
}
InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Receiving" }
DetailValue { visible: root.info.rx_bytes !== undefined; text: root.formatRate(root.downloadRate) }
InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Sending" }
DetailValue { visible: root.info.rx_bytes !== undefined; text: root.formatRate(root.uploadRate) }
Column {
width: (parent.width - parent.spacing) / 2
spacing: Style.spacing.labelGap
InfoPair { label: "Sending"; value: root.formatRate(root.uploadRate) }
InfoPair { label: "Uploaded"; value: root.formatBytes(parseFloat(root.info.tx_bytes || "0")) }
}
}
InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Downloaded" }
DetailValue { visible: root.info.rx_bytes !== undefined; text: root.formatBytes(parseFloat(root.info.rx_bytes || "0")) }
InfoLabel { visible: root.info.rx_bytes !== undefined; text: "Uploaded" }
DetailValue { visible: root.info.rx_bytes !== undefined; text: root.formatBytes(parseFloat(root.info.tx_bytes || "0")) }
Row {
width: parent.width
spacing: Style.space(20)
Column {
width: (parent.width - parent.spacing) / 2
InfoPair {
visible: !!root.info.ip
label: "IP Address"
value: root.info.ip || ""
copyable: true
tooltipText: "Copy IP"
}
InfoLabel {
visible: !!root.info.ip || !!root.info.gateway
text: root.info.ip ? "IP Address" : ""
}
Column {
width: (parent.width - parent.spacing) / 2
InfoPair {
visible: !!root.info.gateway
label: "Gateway"
value: root.info.gateway || ""
copyable: true
tooltipText: "Copy gateway"
}
DetailValue {
visible: !!root.info.ip || !!root.info.gateway
text: root.info.ip || ""
copyable: !!root.info.ip
tooltipText: "Copy IP"
}
InfoLabel {
visible: !!root.info.ip || !!root.info.gateway
text: root.info.gateway ? "Gateway" : ""
}
DetailValue {
visible: !!root.info.ip || !!root.info.gateway
text: root.info.gateway || ""
copyable: !!root.info.gateway
tooltipText: "Copy gateway"
}
}
}
@@ -1313,35 +1308,26 @@ Panel {
onTriggered: if (!networkProc.running) networkProc.running = true
}
component InfoPair: Row {
property string label: ""
property string value: ""
component DetailValue: InfoValue {
property bool copyable: false
property string tooltipText: "Copy to clipboard"
width: parent.width
spacing: Style.space(8)
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
InfoLabel { text: label }
Item { width: Math.max(0, parent.width - parent.children[0].implicitWidth - valueText.implicitWidth - parent.spacing * 2); height: 1 }
InfoValue {
id: valueText
text: value
MouseArea {
id: valueMouse
anchors.fill: parent
enabled: copyable && parent.text !== ""
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.copyToClipboard(parent.text)
}
MouseArea {
id: valueMouse
anchors.fill: parent
enabled: copyable && valueText.text !== ""
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.copyToClipboard(valueText.text)
}
PanelToolTip {
visible: valueMouse.enabled && valueMouse.containsMouse
text: tooltipText
fontFamily: root.bar.fontFamily
}
PanelToolTip {
visible: valueMouse.enabled && valueMouse.containsMouse
text: tooltipText
fontFamily: root.bar.fontFamily
}
}
+4 -2
View File
@@ -55,7 +55,8 @@ function chargeThresholdActive(device, onBattery, states) {
if (!(d && d.isPresent && !onBattery)) return false
var fraction = batteryFraction(d)
if (d.state === s.Discharging || d.state === s.PendingCharge) return true
if (d.state === s.Discharging) return false
if (d.state === s.PendingCharge) return true
if (d.state === s.FullyCharged && fraction < 0.99) return true
if (d.state !== s.Charging || fraction >= 0.99) return false
@@ -74,6 +75,7 @@ function batteryIcon(device, onBattery, states) {
if (threshold) return defaultIcons[index]
if (d.state === states.FullyCharged) return "󰂅"
if (d.state === states.Charging) return chargingIcons[index]
if (d.state === states.Discharging) return defaultIcons[index]
if (!onBattery) return ""
return defaultIcons[index]
}
@@ -85,7 +87,7 @@ function modeLabel(device, onBattery, states) {
var percentage = d.isPresent ? d.percentage : 0
if (chargeThresholdActive(d, onBattery, states)) return "Threshold"
if (!onBattery && percentage >= 1) return "Fully charged"
if (onBattery) return "On battery"
if (onBattery || d.state === states.Discharging) return "On battery"
return "Charging"
}
+19 -14
View File
@@ -41,12 +41,12 @@ Panel {
function batteryIcon() {
var device = UPower.displayDevice
return Model.batteryIcon(device, UPower.onBattery, upowerStates())
return Model.batteryIcon(device, root.discharging, upowerStates())
}
function modeLabel() {
var device = UPower.displayDevice
return Model.modeLabel(device, UPower.onBattery, upowerStates())
return Model.modeLabel(device, root.discharging, upowerStates())
}
function profileIcon(name) {
@@ -57,11 +57,15 @@ Panel {
var device = UPower.displayDevice
return device && device.isPresent && device.state === UPowerDeviceState.FullyCharged && !root.chargeThresholdActive
}
readonly property bool discharging: {
var device = UPower.displayDevice
return !!(device && device.isPresent && (UPower.onBattery || device.state === UPowerDeviceState.Discharging))
}
readonly property bool chargeThresholdActive: {
var device = UPower.displayDevice
return Model.chargeThresholdActive(device, UPower.onBattery, upowerStates())
return Model.chargeThresholdActive(device, root.discharging, upowerStates())
}
readonly property bool batteryFull: fullyCharged || (!UPower.onBattery && batteryFraction >= 1)
readonly property bool batteryFull: fullyCharged || (!root.discharging && batteryFraction >= 1)
readonly property bool batteryFlowIdle: batteryFull || chargeThresholdActive
// 0..1 charge level, used by the visual progress bar.
@@ -70,14 +74,12 @@ Panel {
return Model.batteryFraction(d)
}
readonly property bool batteryLow: UPower.onBattery && batteryFraction > 0 && batteryFraction <= 0.2
readonly property bool charging: {
var d = UPower.displayDevice
return d && d.isPresent && d.state === UPowerDeviceState.Charging && !root.chargeThresholdActive
}
readonly property color batteryFillColor: {
if (batteryLow) return Color.urgent
return root.bar ? root.bar.foreground : Color.foreground
}
@@ -111,7 +113,7 @@ Panel {
readonly property var activePhrases: {
if (fullyCharged) return []
if (charging) return chargingPhrases
if (UPower.onBattery || chargeThresholdActive) return onBatteryPhrases
if (discharging) return onBatteryPhrases
return []
}
readonly property bool rotatingPhrases: activePhrases.length > 0
@@ -252,9 +254,8 @@ Panel {
anchors.fill: parent
bar: root.bar
text: root.batteryIcon()
horizontalMargin: 8.5
rightExtraMargin: 2
active: root.batteryPresent && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
tooltipText: ""
onPressed: function(b) { if (root.batteryPresent) root.toggle() }
}
@@ -296,7 +297,7 @@ Panel {
Text {
id: heroIcon
text: root.batteryIcon()
color: root.batteryLow ? Color.urgent : root.bar.foreground
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.display
anchors.left: parent.left
@@ -340,7 +341,7 @@ Panel {
Text {
id: heroPercent
text: root.batteryInfo.percentage || "—"
color: root.batteryLow ? Color.urgent : root.bar.foreground
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.displayLarge
font.bold: true
@@ -408,17 +409,21 @@ Panel {
width: (parent.width - parent.spacing) / 2
spacing: Style.spacing.labelGap
InfoPair {
label: root.chargeThresholdActive ? "Charge limit" : (UPower.onBattery ? "Time left" : "Time to full")
label: root.chargeThresholdActive ? "Charge limit" : (root.discharging ? "Time left" : "Time to full")
value: root.chargeThresholdActive ? (root.batteryInfo.threshold || "-") : (root.batteryFlowIdle ? "-" : (root.batteryInfo.time || "—"))
}
InfoPair {
label: root.chargeThresholdActive ? "Battery state" : (UPower.onBattery ? "Discharging" : "Charging")
label: root.chargeThresholdActive ? "Battery state" : (root.discharging ? "Discharging" : "Charging")
value: root.chargeThresholdActive ? "Holding" : (root.batteryFull ? "-" : (root.batteryInfo.rate || ""))
}
}
}
// ---------- Power profile picker ----------
PanelSeparator {
foreground: root.bar.foreground
}
Column {
width: parent.width
spacing: Style.space(10)
+37 -21
View File
@@ -12,6 +12,7 @@ Panel {
ipcTarget: "omarchy.tailscale"
manageIpc: false
readonly property real openIndicatorInlineOffset: bar && bar.vertical ? 0 : Style.spaceReal(1.5)
property string focusSection: "header"
property int headerIndex: 0
property int accountIndex: 0
@@ -238,7 +239,7 @@ Panel {
if (focusSection === "header") {
tailscale.toggleTailscale()
} else if (focusSection === "auth") {
tailscale.authorizeProfileSwitching()
tailscale.authorizeTailscaleOperator()
} else if (focusSection === "accounts") {
var account = selectedAccount()
if (account) tailscale.switchAccount(account.id)
@@ -362,7 +363,7 @@ Panel {
Item {
id: button
anchors.fill: parent
implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(32)
implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(27)
implicitHeight: root.bar && root.bar.vertical ? Style.space(26) : (root.bar ? root.bar.barSize : Style.space(26))
property var registeredBar: null
@@ -389,6 +390,7 @@ Panel {
TailscaleIcon {
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.openIndicatorInlineOffset
anchors.verticalCenterOffset: -Style.space(1)
iconSize: Style.space(12) * 0.85
color: root.barIconColor
@@ -748,7 +750,7 @@ Panel {
cursorShape: tailscale.busy ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !tailscale.busy
onEntered: root.setAuthCursor()
onClicked: tailscale.authorizeProfileSwitching()
onClicked: tailscale.authorizeTailscaleOperator()
}
RowLayout {
@@ -774,7 +776,7 @@ Panel {
Text {
Layout.fillWidth: true
text: "Authorize switching"
text: "Authorize Tailscale operator"
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.body
@@ -783,7 +785,7 @@ Panel {
Text {
Layout.fillWidth: true
text: "Allow this user to see and switch Tailscale connections"
text: "Allow this user to operate this Tailscale profile"
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.caption
@@ -791,14 +793,6 @@ Panel {
}
}
PanelActionButton {
iconText: "󰄬"
foreground: root.foreground
fontFamily: root.fontFamily
enabled: !tailscale.busy
Layout.alignment: Qt.AlignVCenter
onClicked: tailscale.authorizeProfileSwitching()
}
}
}
@@ -994,14 +988,7 @@ Panel {
modal: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpenedChanged: {
root.copyMenuOpen = opened
if (opened) {
peerRow.clampCopyIndex()
forceActiveFocus()
}
}
Keys.onPressed: function(event) {
function handleKey(event) {
if (event.key === Qt.Key_Escape) {
close()
event.accepted = true
@@ -1022,6 +1009,15 @@ Panel {
event.accepted = true
}
}
onOpenedChanged: {
root.copyMenuOpen = opened
if (opened) {
peerRow.clampCopyIndex()
Qt.callLater(function() { copyPopupContent.forceActiveFocus() })
} else if (root.opened) {
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
}
}
background: BorderSurface {
color: Color.background
borderSpec: Border.flat(root.dim, 1)
@@ -1029,7 +1025,11 @@ Panel {
}
contentItem: Column {
id: copyPopupContent
width: parent.width
focus: true
Keys.priority: Keys.BeforeItem
Keys.onPressed: function(event) { copyPopup.handleKey(event) }
Repeater {
model: peerRow.copyOptions
@@ -1102,6 +1102,7 @@ Panel {
readonly property bool activeExitNode: peer && peer.ExitNode === true
readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "")
readonly property string peerName: peer ? String(peer.DisplayName || peer.HostName || "Unknown") : "Unknown"
readonly property string actionTooltip: addMullvad ? "" : (activeExitNode ? "Disconnect" : "Connect")
hasCursor: root.cursorActive && root.focusSection === "exitNodes" && root.exitNodeIndex === rowIndex
current: activeExitNode || settingExitNode || (addMullvad && root.mullvadPickerOpen)
@@ -1154,12 +1155,19 @@ Panel {
}
MouseArea {
id: exitNodeMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.setExitNodeCursor(exitNodeRow.rowIndex)
onClicked: root.chooseExitNode(exitNodeRow.peer)
}
PanelToolTip {
visible: exitNodeRow.actionTooltip !== "" && exitNodeMouse.containsMouse
text: exitNodeRow.actionTooltip
fontFamily: root.fontFamily
}
}
component MullvadRegionRow: CursorSurface {
@@ -1172,6 +1180,7 @@ Panel {
readonly property bool activeExitNode: peer && peer.ExitNode === true
readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "")
readonly property bool selectedRegion: root.mullvadPickerOpen && root.mullvadRegionIndex === rowIndex
readonly property string actionTooltip: activeExitNode ? "Disconnect" : "Connect"
foreground: root.foreground
fill: root.hoverFill
@@ -1226,11 +1235,18 @@ Panel {
}
MouseArea {
id: regionMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.mullvadRegionIndex = regionRow.rowIndex
onClicked: root.chooseExitNode(regionRow.peer)
}
PanelToolTip {
visible: regionMouse.containsMouse
text: regionRow.actionTooltip
fontFamily: root.fontFamily
}
}
}
+1 -1
View File
@@ -35,4 +35,4 @@ Renders the Tailscale mark natively as a theme-colored 3×3 dot grid, matching t
## Add to the bar
This widget ships as first-party plugin `omarchy.tailscale`. Add it with `omarchy plugin bar add omarchy.tailscale`, or add an entry such as `{ "id": "omarchy.tailscale" }` to one of the `bar.layout` sections in `~/.config/omarchy/shell.json` and restart the shell.
This widget ships as first-party plugin `omarchy.tailscale`. Add it with `omarchy plugin bar add omarchy.tailscale`, or add an entry such as `{ "id": "omarchy.tailscale" }` to one of the `bar.layout` sections in `~/.config/omarchy/shell.json`; the shell reloads `shell.json` automatically.
+4 -4
View File
@@ -291,11 +291,11 @@ Item {
exitNodeProcess.running = true
}
function authorizeProfileSwitching() {
function authorizeTailscaleOperator() {
if (!installed || operatorProcess.running || userName === "") return
_operatorOutput = ""
_operatorError = ""
actionStatus = "Authorizing Tailscale profiles…"
actionStatus = "Authorizing Tailscale operator..."
operatorProcess.command = ["pkexec", "tailscale", "set", "--operator=" + userName]
operatorProcess.running = true
}
@@ -414,7 +414,7 @@ Item {
root.parseAccounts("")
if (/profiles access denied/i.test(stderr) || /profiles access denied/i.test(stdout)) {
root.accountsAccessDenied = true
root.lastError = "Authorize Tailscale profiles to show connections"
root.lastError = "Authorize Tailscale operator to show connections"
} else {
root.lastError = elideStatus(stderr || stdout || "Could not list Tailscale connections")
}
@@ -534,7 +534,7 @@ Item {
} else {
root.accountsAccessDenied = false
root.lastError = ""
root.actionStatus = "Tailscale profiles authorized"
root.actionStatus = "Tailscale operator authorized"
actionStatusTimer.restart()
root._lastAccountsRefreshMs = 0
}
+1 -1
View File
@@ -49,7 +49,7 @@ BarWidget {
bar: root.bar
text: panelLoader.item ? panelLoader.item.label : ""
active: panelLoader.item && panelLoader.item.klass === "active"
horizontalMargin: 5
horizontalMargin: 2.5
// Tooltip suppressed because the panel is the detail view.
tooltipText: ""
+8 -1
View File
@@ -26,6 +26,7 @@ QtObject {
property bool scanning: false
signal pluginsChanged()
signal scanFinished()
signal pluginLoadFailed(string id, string error)
// ---------------------------------------------------------------- helpers
@@ -157,9 +158,13 @@ QtObject {
var key = Util.canonicalWidgetId(String(id))
if (!shellConfigMutator) {
console.warn("PluginRegistry.setEnabled called before shellConfigMutator wired")
return
return false
}
var manifest = installedPlugins[key]
if (value && !manifest) {
console.warn("PluginRegistry.setEnabled: unknown plugin " + key)
return false
}
var isBarOption = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1
var isBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1
shellConfigMutator(function(config) {
@@ -196,6 +201,7 @@ QtObject {
})
registryRevision++
pluginsChanged()
return true
}
// ---------------------------------------------------------------- scanning
@@ -269,6 +275,7 @@ QtObject {
registryRevision++
scanning = false
pluginsChanged()
scanFinished()
}
property Process scanProcess: Process {
+69 -8
View File
@@ -54,6 +54,8 @@ ShellRoot {
property var defaultsConfig: builtinShellConfig
property var shellConfig: builtinShellConfig
property bool suppressUserReload: false
property bool pluginReloading: false
property bool pluginReloadPending: false
onShellConfigChanged: {
if (failedBarId !== "") failedBarId = ""
@@ -245,11 +247,11 @@ ShellRoot {
Loader {
id: pluginBarLoader
active: shell.activeBarId !== shell.defaultBarId && shell.activeBarSourceUrl !== ""
active: !shell.pluginReloading && shell.activeBarId !== shell.defaultBarId && shell.activeBarSourceUrl !== ""
source: shell.activeBarId !== shell.defaultBarId ? shell.activeBarSourceUrl : ""
asynchronous: true
onLoaded: shell.configureBar(item, shell.activeBarManifest)
onActiveChanged: if (!active && shell.activeBarId === shell.defaultBarId) shell.bar = null
onActiveChanged: if (!active) shell.bar = null
onStatusChanged: {
if (status === Loader.Error) {
var detail = errorString && errorString() ? errorString() : ""
@@ -344,9 +346,17 @@ ShellRoot {
}
}
function unloadPluginServices() {
for (var existingId in _services) {
var inst = _services[existingId]
if (inst && typeof inst.destroy === "function") inst.destroy()
}
_services = ({})
}
Connections {
target: shell.pluginRegistry
function onPluginsChanged() { shell._syncServices() }
function onPluginsChanged() { if (!shell.pluginReloading) shell._syncServices() }
}
// Writes inline settings to a bar layout entry or top-level plugin entry in
@@ -484,6 +494,14 @@ ShellRoot {
panelLoaders = next
}
function unloadPanels() {
for (var id in panelLoaders) hide(id)
panelEntries = []
panelLoaders = ({})
pendingPayloads = ({})
openPanelIds = ({})
}
function deliverIfLoaded(pluginId) {
var loader = panelLoaders[pluginId]
if (!loader || !loader.item) return
@@ -549,7 +567,7 @@ ShellRoot {
Connections {
target: shell.pluginRegistry
function onPluginsChanged() { shell.panelEntries = shell.computePanelEntries() }
function onPluginsChanged() { if (!shell.pluginReloading) shell.panelEntries = shell.computePanelEntries() }
}
Instantiator {
@@ -606,7 +624,7 @@ ShellRoot {
// widgets use the same first-party manifest contract as third-party widgets.
Connections {
target: shell.pluginRegistry
function onPluginsChanged() { shell.syncPluginWidgets() }
function onPluginsChanged() { if (!shell.pluginReloading) shell.syncPluginWidgets() }
}
property var pluginWidgetComponents: ({})
@@ -670,6 +688,49 @@ ShellRoot {
}
}
function unloadPluginWidgets() {
for (var id in pluginWidgetComponents) shell.barWidgetRegistry.unregister(id)
pluginWidgetComponents = ({})
}
function reloadPlugins() {
if (shell.pluginReloading || shell.pluginRegistry.scanning) {
shell.pluginReloadPending = true
return
}
shell.pluginReloading = true
shell.unloadPanels()
shell.unloadPluginServices()
shell.unloadPluginWidgets()
Qt.callLater(shell.finishPluginReload)
}
function finishPluginReload() {
if (!shell.pluginReloading) return
if (shell.pluginRegistry.scanning) {
shell.pluginReloadPending = true
return
}
if (typeof Qt.clearComponentCache === "function") Qt.clearComponentCache()
shell.pluginRegistry.rescan()
}
Connections {
target: shell.pluginRegistry
function onScanFinished() {
if (shell.pluginReloadPending) {
shell.pluginReloadPending = false
shell.pluginReloading = false
Qt.callLater(shell.reloadPlugins)
return
}
shell.pluginReloading = false
shell._syncServices()
shell.panelEntries = shell.computePanelEntries()
shell.syncPluginWidgets()
}
}
function loadPluginWidget(registryKey, url, meta) {
var comp = Qt.createComponent(url, Component.Asynchronous)
function finalize() {
@@ -712,7 +773,7 @@ ShellRoot {
}
function rescanPlugins(): void {
shell.pluginRegistry.rescan()
shell.reloadPlugins()
}
function reloadConfig(): string {
@@ -720,8 +781,8 @@ ShellRoot {
return "ok"
}
function setPluginEnabled(id: string, enabled: string): void {
shell.pluginRegistry.setEnabled(id, enabled === "true")
function setPluginEnabled(id: string, enabled: string): string {
return shell.pluginRegistry.setEnabled(id, enabled === "true") ? "ok" : "unknown"
}
function listPlugins(): string {
+2
View File
@@ -24,7 +24,9 @@ assertEqual(power.batteryFraction({ isPresent: true, percentage: 1.5 }), 1, 'pow
assert(power.chargeThresholdActive({ isPresent: true, percentage: 0.8, state: states.PendingCharge }, false, states), 'power detects threshold by pending charge state')
assert(power.chargeThresholdActive({ isPresent: true, percentage: 0.8, state: states.Charging, changeRate: 0.1, timeToFull: 120 }, false, states), 'power detects threshold by stalled charging')
assert(!power.chargeThresholdActive({ isPresent: true, percentage: 0.8, state: states.Charging, changeRate: 1.0, timeToFull: 120 }, false, states), 'power does not flag active charging as threshold')
assert(!power.chargeThresholdActive({ isPresent: true, percentage: 0.5, state: states.Discharging }, false, states), 'power does not flag discharging as threshold')
assertEqual(power.modeLabel({ isPresent: true, percentage: 1, state: states.FullyCharged }, false, states), 'Fully charged', 'power labels full battery')
assertEqual(power.modeLabel({ isPresent: true, percentage: 0.5, state: states.Discharging }, true, states), 'On battery', 'power labels battery mode')
assertEqual(power.modeLabel({ isPresent: true, percentage: 0.5, state: states.Discharging }, false, states), 'On battery', 'power labels discharging battery mode')
assert(power.batteryIcon({ isPresent: true, percentage: 0.4, state: states.Charging }, false, states).length > 0, 'power maps battery icons')
JS