mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-01 20:28:16 +02:00
Merge remote-tracking branch 'origin/omarchy-shell' into omarchy-4
# Conflicts: # bin/omarchy-debug # bin/omarchy-upload-log
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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>"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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`
|
||||
```
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user