Refine quickshell bar and Noctalia compatibility
This commit is contained in:
@@ -2,12 +2,23 @@
|
||||
|
||||
# omarchy:summary=Clean up the voxtype --follow child when Waybar reloads
|
||||
|
||||
trap 'kill 0' EXIT
|
||||
|
||||
if omarchy-cmd-present voxtype; then
|
||||
voxtype status --follow --extended --format json | while read -r line; do
|
||||
echo "$line" | jq -c '. + {alt: .class}'
|
||||
coproc VOXTYPE_STATUS { voxtype status --follow --extended --format json; }
|
||||
voxtype_pid=$VOXTYPE_STATUS_PID
|
||||
|
||||
cleanup() {
|
||||
kill "$voxtype_pid" 2>/dev/null || true
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
while IFS= read -r line <&"${VOXTYPE_STATUS[0]}"; do
|
||||
if omarchy-cmd-present jq; then
|
||||
jq -c '. + {alt: (.class // "")}' <<<"$line" || printf '%s\n' "$line"
|
||||
else
|
||||
printf '%s\n' "$line"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo '{"alt": "", "tooltip": ""}'
|
||||
echo '{"alt": "", "class": "idle", "tooltip": ""}'
|
||||
fi
|
||||
|
||||
@@ -50,6 +50,8 @@ QtObject {
|
||||
"lock": "\udb80\udd3e", // mdi-lock
|
||||
"unlock": "\udb80\udd3f",
|
||||
"refresh": "\udb81\udc50", // mdi-refresh
|
||||
"ai": "", // mdi-robot
|
||||
"robot": "",
|
||||
"x": "\udb80\udd56", // mdi-close
|
||||
"check": "\udb80\udc12", // mdi-check
|
||||
"chevron-down": "\udb80\udd40",
|
||||
|
||||
@@ -10,6 +10,7 @@ QtObject {
|
||||
id: root
|
||||
|
||||
// Radii.
|
||||
readonly property real radiusXXS: 1
|
||||
readonly property real radiusXS: 2
|
||||
readonly property real radiusS: 3
|
||||
readonly property real radiusM: 4
|
||||
@@ -22,6 +23,7 @@ QtObject {
|
||||
|
||||
// Margins / paddings. Noctalia uses two parallel scales for margins (one
|
||||
// tighter, one looser); we map them to the same values.
|
||||
readonly property real marginXXS: 1
|
||||
readonly property real marginXS: 2
|
||||
readonly property real marginS: 4
|
||||
readonly property real marginM: 6
|
||||
|
||||
@@ -163,8 +163,7 @@ rewrites `shell.json` from the current `shell-defaults.json`.
|
||||
"center": [ { "id": "calendar", "format": "HH:mm" } ],
|
||||
"right": [
|
||||
{ "id": "audioPanel" },
|
||||
{ "id": "controlCenter" },
|
||||
{ "id": "powerMenu" }
|
||||
{ "id": "controlCenter" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,8 +18,13 @@ QtObject {
|
||||
|
||||
function hide(item) {
|
||||
if (!bar) return
|
||||
if (typeof bar.hideTooltip === "function") {
|
||||
bar.hideTooltip(item || null)
|
||||
if (item && typeof bar.hideTooltip === "function") {
|
||||
bar.hideTooltip(item)
|
||||
} else if (bar.tooltipTarget) {
|
||||
// Noctalia plugins often call TooltipService.hide() without the source
|
||||
// item on click. Native bar.hideTooltip(target) ignores null targets, so
|
||||
// clear the currently visible tooltip explicitly.
|
||||
bar.hideTooltip(bar.tooltipTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,57 @@ import qs.Commons
|
||||
|
||||
ComboBox {
|
||||
id: root
|
||||
property string label: ""
|
||||
|
||||
property string label: ""
|
||||
// Noctalia's NComboBox API commonly uses a model of { key, name } objects,
|
||||
// a currentKey property, and an onSelected(key, item) handler. Qt's ComboBox
|
||||
// only knows currentIndex/currentText, so bridge the small API surface here.
|
||||
property string currentKey: ""
|
||||
|
||||
signal selected(var key, var item)
|
||||
|
||||
textRole: "name"
|
||||
valueRole: "key"
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: Style.fontSizeS
|
||||
|
||||
function itemAt(index) {
|
||||
if (index < 0) return null
|
||||
if (Array.isArray(root.model)) return root.model[index] || null
|
||||
return null
|
||||
}
|
||||
|
||||
function keyAt(index) {
|
||||
var item = itemAt(index)
|
||||
if (!item) return ""
|
||||
if (item.key !== undefined && item.key !== null) return String(item.key)
|
||||
if (item.value !== undefined && item.value !== null) return String(item.value)
|
||||
if (item.name !== undefined && item.name !== null) return String(item.name)
|
||||
return String(item)
|
||||
}
|
||||
|
||||
function syncCurrentIndex() {
|
||||
if (!Array.isArray(root.model)) return
|
||||
for (var i = 0; i < root.model.length; i++) {
|
||||
if (keyAt(i) === String(root.currentKey || "")) {
|
||||
if (root.currentIndex !== i) root.currentIndex = i
|
||||
return
|
||||
}
|
||||
}
|
||||
if (root.model.length > 0 && root.currentIndex < 0) root.currentIndex = 0
|
||||
}
|
||||
|
||||
Component.onCompleted: syncCurrentIndex()
|
||||
onCurrentKeyChanged: syncCurrentIndex()
|
||||
onModelChanged: syncCurrentIndex()
|
||||
|
||||
onActivated: function(index) {
|
||||
var item = itemAt(index)
|
||||
var key = keyAt(index)
|
||||
root.currentKey = key
|
||||
root.selected(key, item)
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
|
||||
@@ -8,14 +8,18 @@ Menu {
|
||||
id: root
|
||||
|
||||
property var model: []
|
||||
signal triggered(string action)
|
||||
// Noctalia passes the owning ShellScreen through here. Qt Quick Controls
|
||||
// Menu doesn't need it, but accepting the property keeps plugin QML from
|
||||
// failing at load time.
|
||||
property var screen: null
|
||||
signal triggered(var action, var item)
|
||||
|
||||
Instantiator {
|
||||
model: root.model
|
||||
delegate: MenuItem {
|
||||
required property var modelData
|
||||
text: modelData ? String(modelData.label || modelData.action || "") : ""
|
||||
onTriggered: root.triggered(modelData ? String(modelData.action || "") : "")
|
||||
onTriggered: root.triggered(modelData ? String(modelData.action || "") : "", modelData)
|
||||
}
|
||||
onObjectAdded: function(index, object) { root.insertItem(index, object) }
|
||||
onObjectRemoved: function(index, object) { root.removeItem(object) }
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property int currentIndex: 0
|
||||
property int margins: Style.marginXS
|
||||
property bool distributeEvenly: false
|
||||
|
||||
implicitHeight: 32
|
||||
radius: Style.radiusS
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
default property alias content: row.children
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.margins
|
||||
spacing: Style.marginXS
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property int tabIndex: -1
|
||||
property bool checked: false
|
||||
|
||||
signal clicked()
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: label.implicitWidth + Style.marginL * 2
|
||||
implicitHeight: 26
|
||||
radius: Style.radiusXS
|
||||
color: area.containsMouse
|
||||
? Color.mHover
|
||||
: (checked ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.18) : "transparent")
|
||||
border.color: checked ? Color.mPrimary : "transparent"
|
||||
border.width: checked ? 1 : 0
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Style.animationFast } }
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
color: checked ? Color.mPrimary : Color.mOnSurface
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: Style.fontSizeS
|
||||
font.bold: checked
|
||||
elide: Text.ElideRight
|
||||
width: Math.max(0, parent.width - Style.marginM * 2)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: area
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,26 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
|
||||
Switch {
|
||||
Rectangle {
|
||||
id: root
|
||||
property string label: ""
|
||||
|
||||
indicator: Rectangle {
|
||||
implicitWidth: 32
|
||||
implicitHeight: 18
|
||||
x: root.leftPadding
|
||||
y: root.height / 2 - height / 2
|
||||
property string label: ""
|
||||
property string text: ""
|
||||
property bool checked: false
|
||||
|
||||
signal toggled(var value)
|
||||
signal clicked()
|
||||
|
||||
implicitWidth: labelItem.visible ? indicator.width + 8 + labelItem.implicitWidth : indicator.width
|
||||
implicitHeight: Math.max(indicator.height, labelItem.implicitHeight)
|
||||
color: "transparent"
|
||||
opacity: enabled ? 1 : 0.4
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
width: 32
|
||||
height: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: height / 2
|
||||
color: root.checked ? Color.mPrimary : Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
@@ -27,12 +37,28 @@ Switch {
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
leftPadding: 38
|
||||
Text {
|
||||
id: labelItem
|
||||
visible: (root.label || root.text) !== ""
|
||||
anchors.left: indicator.right
|
||||
anchors.leftMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.label || root.text
|
||||
color: Color.mOnSurface
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: Style.fontSizeS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: root.enabled
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.checked = !root.checked
|
||||
root.clicked()
|
||||
root.toggled(root.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,3 +13,5 @@ NTextInput 1.0 NTextInput.qml
|
||||
NComboBox 1.0 NComboBox.qml
|
||||
NCheckbox 1.0 NCheckbox.qml
|
||||
NDivider 1.0 NDivider.qml
|
||||
NTabBar 1.0 NTabBar.qml
|
||||
NTabButton 1.0 NTabButton.qml
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
// Wraps Noctalia panel entry points, which are plain Item content intended to
|
||||
// be hosted inside Noctalia's SmartPanel. We host them as anchored PopupWindows
|
||||
// so clicking a Noctalia bar widget behaves like native Omarchy popups instead
|
||||
// of opening a tiled app window.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string panelSource: ""
|
||||
property var pluginApi: null
|
||||
property var manifest: ({})
|
||||
property string omarchyPath: ""
|
||||
property var shell: null
|
||||
property var pluginRegistry: null
|
||||
property var barWidgetRegistry: null
|
||||
property bool popupOpen: false
|
||||
property bool closingFromShell: false
|
||||
|
||||
readonly property var anchorItem: pluginApi ? pluginApi.panelAnchorItem : null
|
||||
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
|
||||
readonly property string barPosition: {
|
||||
if (shell && shell.barConfig && shell.barConfig.position) return String(shell.barConfig.position)
|
||||
return "top"
|
||||
}
|
||||
|
||||
function open(payloadJson) {
|
||||
popupOpen = true
|
||||
}
|
||||
|
||||
function close() {
|
||||
closingFromShell = true
|
||||
popupOpen = false
|
||||
if (pluginApi) {
|
||||
pluginApi.panelOpenScreen = null
|
||||
pluginApi.panelAnchorItem = null
|
||||
}
|
||||
closingFromShell = false
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
if (!popupOpen) return
|
||||
if (pluginApi && typeof pluginApi.closePanel === "function") pluginApi.closePanel(null)
|
||||
else if (shell && manifest && manifest.id && typeof shell.hide === "function") shell.hide(manifest.id)
|
||||
else close()
|
||||
}
|
||||
|
||||
PopupWindow {
|
||||
id: popup
|
||||
visible: root.popupOpen
|
||||
color: "transparent"
|
||||
implicitWidth: Math.max(320, panelLoader.item && panelLoader.item.contentPreferredWidth
|
||||
? panelLoader.item.contentPreferredWidth : 420)
|
||||
implicitHeight: Math.max(260, panelLoader.item && panelLoader.item.contentPreferredHeight
|
||||
? panelLoader.item.contentPreferredHeight : 520)
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible && root.popupOpen && !root.closingFromShell) root.dismiss()
|
||||
}
|
||||
|
||||
// Do not use HyprlandFocusGrab here. Noctalia panels are larger,
|
||||
// interactive surfaces with tabs and nested Flickables; focus grabs made
|
||||
// internal clicks (notably tab switches) look like outside clicks and also
|
||||
// interfered with wheel scrolling. Native Omarchy micro-popups can keep
|
||||
// focus-grab dismissal, but compat panels behave more like pinned panels.
|
||||
anchor {
|
||||
id: popupAnchor
|
||||
window: root.anchorWindow
|
||||
adjustment: PopupAdjustment.Slide
|
||||
edges: Edges.Top | Edges.Left
|
||||
gravity: Edges.Bottom | Edges.Right
|
||||
rect.width: 1
|
||||
rect.height: 1
|
||||
|
||||
onAnchoring: {
|
||||
var target = root.anchorItem
|
||||
var window = root.anchorWindow
|
||||
if (!target || !window) {
|
||||
popupAnchor.rect.x = 0
|
||||
popupAnchor.rect.y = 0
|
||||
return
|
||||
}
|
||||
|
||||
var popupWidth = popup.implicitWidth
|
||||
var popupHeight = popup.implicitHeight
|
||||
var margin = 8
|
||||
var localX = target.width / 2 - popupWidth / 2
|
||||
var localY = target.height + margin
|
||||
|
||||
if (root.barPosition === "bottom") {
|
||||
localY = -popupHeight - margin
|
||||
} else if (root.barPosition === "left") {
|
||||
localX = target.width + margin
|
||||
localY = target.height / 2 - popupHeight / 2
|
||||
} else if (root.barPosition === "right") {
|
||||
localX = -popupWidth - margin
|
||||
localY = target.height / 2 - popupHeight / 2
|
||||
}
|
||||
|
||||
var point = window.contentItem.mapFromItem(target, localX, localY)
|
||||
popupAnchor.rect.x = Math.round(point.x)
|
||||
popupAnchor.rect.y = Math.round(point.y)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: 1
|
||||
radius: 0
|
||||
|
||||
Loader {
|
||||
id: panelLoader
|
||||
anchors.fill: parent
|
||||
source: root.panelSource
|
||||
onLoaded: root.injectPanelProps()
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
console.warn("noctalia panel failed for " + (root.manifest ? root.manifest.id : "") + ":", errorString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPluginApiChanged: injectPanelProps()
|
||||
onManifestChanged: injectPanelProps()
|
||||
|
||||
function injectPanelProps() {
|
||||
var item = panelLoader.item
|
||||
if (!item) return
|
||||
if ("pluginApi" in item) item.pluginApi = root.pluginApi
|
||||
if ("manifest" in item) item.manifest = root.manifest
|
||||
if ("omarchyPath" in item) item.omarchyPath = root.omarchyPath
|
||||
if ("shell" in item) item.shell = root.shell
|
||||
if ("pluginRegistry" in item) item.pluginRegistry = root.pluginRegistry
|
||||
if ("barWidgetRegistry" in item) item.barWidgetRegistry = root.barWidgetRegistry
|
||||
if ("screen" in item) {
|
||||
var screens = Quickshell.screens
|
||||
item.screen = screens && screens.length > 0 ? screens[0] : null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,10 +46,14 @@ Item {
|
||||
property var _hostBridge: ({})
|
||||
property var mainInstance: null
|
||||
|
||||
// Resolved every read so the plugin sees current shell.json state.
|
||||
readonly property var pluginSettings: _settingsProvider ? _settingsProvider() : ({})
|
||||
// Writable because Noctalia Settings.qml components commonly stage edits
|
||||
// with `pluginApi.pluginSettings = ...; pluginApi.saveSettings()`.
|
||||
// The initial binding resolves shell.json + defaults; assignment during
|
||||
// save intentionally replaces it with the staged value to persist.
|
||||
property var pluginSettings: _settingsProvider ? _settingsProvider() : ({})
|
||||
|
||||
property var panelOpenScreen: null
|
||||
property var panelAnchorItem: null
|
||||
property var ipcHandlers: ({})
|
||||
|
||||
// Noctalia i18n surface — v1 returns the key as-is.
|
||||
@@ -62,19 +66,28 @@ Item {
|
||||
function trp(key, count, interp) { return String(key === undefined ? "" : key) }
|
||||
function hasTranslation(key) { return false }
|
||||
|
||||
function saveSettings() {
|
||||
function saveSettings(settings) {
|
||||
if (settings !== undefined) pluginSettings = settings
|
||||
if (_hostBridge && typeof _hostBridge.persistSettings === "function")
|
||||
_hostBridge.persistSettings(pluginId, pluginSettings)
|
||||
// Re-read after persisting so mainInstance/bar widgets see the
|
||||
// canonical merged settings (defaults + saved overrides) immediately.
|
||||
if (_settingsProvider) pluginSettings = _settingsProvider()
|
||||
if (mainInstance && typeof mainInstance.refresh === "function") mainInstance.refresh()
|
||||
}
|
||||
|
||||
function openPanel(screen, buttonItem) {
|
||||
panelOpenScreen = screen
|
||||
panelOpenScreen = screen || true
|
||||
panelAnchorItem = buttonItem || null
|
||||
if (_hostBridge && _hostBridge.tooltipService && typeof _hostBridge.tooltipService.hide === "function")
|
||||
_hostBridge.tooltipService.hide(buttonItem)
|
||||
if (_hostBridge && typeof _hostBridge.openPanel === "function")
|
||||
_hostBridge.openPanel(pluginId, screen, buttonItem)
|
||||
}
|
||||
|
||||
function closePanel(screen) {
|
||||
panelOpenScreen = null
|
||||
panelAnchorItem = null
|
||||
if (_hostBridge && typeof _hostBridge.closePanel === "function")
|
||||
_hostBridge.closePanel(pluginId, screen)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ We ship just enough of Noctalia's QML namespace to render typical bar widgets.
|
||||
| Module | Symbols |
|
||||
|---|---|
|
||||
| `qs.Commons` | `Color`, `Style`, `Logger`, `Settings` (read-only), `I18n` (stub), `Time`, `Icons` (~50 entries), `ThemeIcons` (stub), `ShellState` (stub) |
|
||||
| `qs.Widgets` | `NText`, `NIcon`, `NIconButton`, `NBox`, `NButton`, `NPopupContextMenu`, `NScrollText`, `NToggle`, `NSpinBox`, `NSlider`, `NTextInput`, `NComboBox`, `NCheckbox`, `NDivider` |
|
||||
| `qs.Widgets` | `NText`, `NIcon`, `NIconButton`, `NBox`, `NButton`, `NPopupContextMenu`, `NScrollText`, `NToggle`, `NSpinBox`, `NSlider`, `NTextInput`, `NComboBox`, `NCheckbox`, `NDivider`, `NTabBar`, `NTabButton` |
|
||||
| `qs.Services.UI` | `BarService`, `TooltipService`, `PanelService` |
|
||||
| `qs.Services.System` | `HostService` (reads `/etc/os-release`) |
|
||||
| `qs.Services.Power` | `PowerProfileService` (returns `noctaliaPerformanceMode: false`) |
|
||||
|
||||
@@ -10,10 +10,20 @@ Item {
|
||||
id: root
|
||||
|
||||
// Plugin lifecycle hooks. omarchy-shell calls open(payloadJson) on summon
|
||||
// and close() on hide. We don't consume payloads yet, and visibility is
|
||||
// driven by the host Loader's `active`, so both are no-ops for now.
|
||||
function open(payloadJson) { /* no payload schema yet; reserved for future use */ }
|
||||
function close() { /* visibility handled by parent Loader; nothing to clean up */ }
|
||||
// and close() on hide. The Loader stays mounted while shell thinks the panel
|
||||
// is open, so reopening after a WM close must explicitly re-show the window.
|
||||
property bool closingFromHost: false
|
||||
|
||||
function open(payloadJson) {
|
||||
closingFromHost = false
|
||||
window.visible = true
|
||||
}
|
||||
|
||||
function close() {
|
||||
closingFromHost = true
|
||||
window.visible = false
|
||||
closingFromHost = false
|
||||
}
|
||||
|
||||
// Injected by the host shell when the panel is summoned. Shared instances
|
||||
// so the panel sees the same registry state the bar wrote into.
|
||||
@@ -66,8 +76,7 @@ Item {
|
||||
right: [
|
||||
{ id: "tray" }, { id: "systemStats" }, { id: "microphone" },
|
||||
{ id: "bluetoothPanel" }, { id: "networkPanel" }, { id: "audioPanel" },
|
||||
{ id: "nightLight" }, { id: "brightness" }, { id: "powerProfile" },
|
||||
{ id: "battery" }, { id: "controlCenter" }, { id: "powerMenu" }
|
||||
{ id: "battery" }, { id: "controlCenter" }
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -317,6 +326,25 @@ Item {
|
||||
if (root.barWidgetRegistry && root.barWidgetRegistry.has(key))
|
||||
return root.barWidgetRegistry.metadataFor(key) || {}
|
||||
if (legacyWidgetMeta[key]) return legacyWidgetMeta[key]
|
||||
|
||||
// If a plugin widget failed to instantiate, it may not be in
|
||||
// BarWidgetRegistry yet. Still use the manifest metadata so cards/dialogs
|
||||
// show "Model Usage" instead of the raw id "noctalia.model-usage" and the
|
||||
// settings gear can still expose the plugin's Settings.qml.
|
||||
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[key] : null
|
||||
if (manifest) {
|
||||
var meta = manifest.barWidget || {}
|
||||
return {
|
||||
displayName: meta.displayName || manifest.name || key,
|
||||
name: meta.displayName || manifest.name || key,
|
||||
description: meta.description || manifest.description || "",
|
||||
category: meta.category || (manifest.__noctaliaCompat ? "Noctalia" : "Plugin"),
|
||||
allowMultiple: meta.allowMultiple === true,
|
||||
settingsForm: meta.settingsForm || "",
|
||||
schema: Array.isArray(meta.schema) ? meta.schema : [],
|
||||
source: "plugin"
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -373,6 +401,14 @@ Item {
|
||||
var registered = root.barWidgetRegistry.availableIds()
|
||||
for (var i = 0; i < registered.length; i++) ids[registered[i]] = true
|
||||
}
|
||||
if (root.pluginRegistry && root.pluginRegistry.installedPlugins) {
|
||||
var plugins = root.pluginRegistry.installedPlugins
|
||||
for (var pid in plugins) {
|
||||
var manifest = plugins[pid]
|
||||
if (manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1)
|
||||
ids[pid] = true
|
||||
}
|
||||
}
|
||||
for (var key in legacyWidgetMeta) ids[key] = true
|
||||
return Object.keys(ids)
|
||||
}
|
||||
@@ -404,8 +440,9 @@ Item {
|
||||
for (var k = 0; k < ids.length; k++) {
|
||||
var id = ids[k]
|
||||
var meta = widgetMetadata(id)
|
||||
var isBarWidget = !!(meta && meta.source !== "plugin")
|
||||
|| (meta && meta.kinds && meta.kinds.indexOf && meta.kinds.indexOf("bar-widget") !== -1)
|
||||
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
|
||||
var manifestIsBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1
|
||||
var isBarWidget = !!(meta && meta.source !== "plugin") || manifestIsBarWidget
|
||||
if (isBarSection) {
|
||||
if (!isBarWidget && !legacyWidgetMeta[id]) continue
|
||||
var inSection = sectionArray(section)
|
||||
@@ -426,7 +463,6 @@ Item {
|
||||
// Plugins section: accept third-party plugins with any non-bar kind.
|
||||
// First-party panels/overlays (bar-settings, image-picker) are
|
||||
// shell infrastructure and don't belong in user-editable lists.
|
||||
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
|
||||
if (!manifest) continue
|
||||
if (manifest.__isFirstParty) continue
|
||||
if (existingInPlugins[id]) continue
|
||||
@@ -479,6 +515,11 @@ Item {
|
||||
implicitHeight: 720
|
||||
minimumSize: Qt.size(560, 500)
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible && !root.closingFromHost && root.shell && typeof root.shell.hide === "function")
|
||||
root.shell.hide("omarchy.bar-settings")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.background
|
||||
@@ -953,7 +994,15 @@ Item {
|
||||
}
|
||||
|
||||
function commit() {
|
||||
root.updateEntry(sectionKey, entryIndex, workingEntry)
|
||||
// Native forms update workingEntry through fieldChanged(). Noctalia
|
||||
// Settings.qml components keep their own editSettings state and expose a
|
||||
// saveSettings() method that writes via pluginApi.saveSettings(). Avoid
|
||||
// overwriting that freshly-saved entry with the stale workingEntry shell.
|
||||
if (formLoader.item && typeof formLoader.item.saveSettings === "function") {
|
||||
formLoader.item.saveSettings()
|
||||
} else {
|
||||
root.updateEntry(sectionKey, entryIndex, workingEntry)
|
||||
}
|
||||
win.visible = false
|
||||
}
|
||||
|
||||
@@ -1037,7 +1086,6 @@ Item {
|
||||
switch (meta.settingsForm) {
|
||||
case "spacerSettings": return spacerSettingsComponent
|
||||
case "calendarSettings": return calendarSettingsComponent
|
||||
case "brightnessSettings": return brightnessSettingsComponent
|
||||
}
|
||||
}
|
||||
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
|
||||
@@ -1057,11 +1105,9 @@ Item {
|
||||
}
|
||||
|
||||
// Loader stub for Noctalia plugins that bundle a Settings.qml. We load the
|
||||
// plugin's form and inject pluginApi so the plugin's own "save" button
|
||||
// routes through pluginApi.saveSettings() — which lands in shell.json via
|
||||
// shell.updateEntryInline. The plugin form doesn't emit `fieldChanged`,
|
||||
// so the dialog's Apply/Cancel buttons are mostly decorative for these
|
||||
// (writes already happened by the time you click Apply).
|
||||
// plugin's form and inject pluginApi. The outer Omarchy dialog owns the
|
||||
// Apply button, so this wrapper must forward saveSettings() to the loaded
|
||||
// Noctalia form.
|
||||
Component {
|
||||
id: noctaliaSettingsComponent
|
||||
|
||||
@@ -1072,6 +1118,14 @@ Item {
|
||||
property var manifest: pluginId && root.pluginRegistry
|
||||
? root.pluginRegistry.installedPlugins[pluginId] : null
|
||||
|
||||
function saveSettings() {
|
||||
if (settingsLoader.item && typeof settingsLoader.item.saveSettings === "function") {
|
||||
settingsLoader.item.saveSettings()
|
||||
} else {
|
||||
console.warn("Noctalia settings form has no saveSettings():", pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: settingsLoader.item ? settingsLoader.item.implicitHeight : 0
|
||||
implicitWidth: settingsLoader.item ? settingsLoader.item.implicitWidth : 0
|
||||
|
||||
@@ -1183,32 +1237,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: brightnessSettingsComponent
|
||||
|
||||
Column {
|
||||
id: brightForm
|
||||
signal fieldChanged(string key, var value)
|
||||
property var entry: ({})
|
||||
|
||||
spacing: 8
|
||||
width: parent ? parent.width : 0
|
||||
|
||||
Text {
|
||||
text: "Scroll step (% per notch)"
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
}
|
||||
SpinBox {
|
||||
from: 1
|
||||
to: 25
|
||||
value: brightForm.entry.step !== undefined ? brightForm.entry.step : 5
|
||||
onValueModified: brightForm.fieldChanged("step", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component TabButton: Rectangle {
|
||||
id: tab
|
||||
property string label: ""
|
||||
|
||||
@@ -287,15 +287,11 @@ Item {
|
||||
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false },
|
||||
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false, settingsForm: "calendarSettings" },
|
||||
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND (replaces mako)", category: "Status", allowMultiple: false },
|
||||
"brightness": { displayName: "Brightness", description: "Screen brightness slider", category: "System", allowMultiple: false, settingsForm: "brightnessSettings" },
|
||||
"powerProfile": { displayName: "Power profile", description: "power-profiles-daemon selector", category: "System", allowMultiple: false },
|
||||
"systemStats": { displayName: "System stats", description: "Inline CPU + memory sparklines", category: "System", allowMultiple: false },
|
||||
"weatherFlyout": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false },
|
||||
"powerMenu": { displayName: "Power menu", description: "Lock / suspend / reboot / shutdown", category: "System", allowMultiple: false },
|
||||
"idleInhibitor": { displayName: "Keep awake", description: "Toggle idle inhibitor", category: "System", allowMultiple: false },
|
||||
"microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false },
|
||||
"activeWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false },
|
||||
"nightLight": { displayName: "Night light", description: "hyprsunset toggle", category: "System", allowMultiple: false },
|
||||
"keyboardLayout": { displayName: "Keyboard layout", description: "Current xkb layout, click cycles", category: "Compositor", allowMultiple: false },
|
||||
"lockKeys": { displayName: "Lock keys", description: "Caps / Num / Scroll lock indicators", category: "System", allowMultiple: false },
|
||||
"spacer": { displayName: "Spacer", description: "Configurable blank space", category: "Layout", allowMultiple: true, settingsForm: "spacerSettings" },
|
||||
@@ -445,7 +441,7 @@ Item {
|
||||
|
||||
function updateVoxtype(raw) {
|
||||
var data = parseModuleJson(raw)
|
||||
var state = data.alt || data.class || "idle"
|
||||
var state = String(data.alt || data.class || "idle")
|
||||
|
||||
voxtypeClass = state
|
||||
if (state === "recording") voxtypeIcon = ""
|
||||
@@ -739,7 +735,7 @@ Item {
|
||||
|
||||
Process {
|
||||
id: voxtypeProc
|
||||
command: ["bash", "-lc", "omarchy-voxtype-status"]
|
||||
command: ["bash", "-lc", root.commandWithOmarchyPath("omarchy-voxtype-status")]
|
||||
running: true
|
||||
stdout: SplitParser {
|
||||
onRead: function(data) {
|
||||
|
||||
@@ -40,8 +40,7 @@ Example `shell.json` (bar subtree only shown):
|
||||
{ "id": "systemStats" },
|
||||
{ "id": "audioPanel" },
|
||||
{ "id": "battery" },
|
||||
{ "id": "controlCenter" },
|
||||
{ "id": "powerMenu" }
|
||||
{ "id": "controlCenter" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -62,11 +61,8 @@ Example `shell.json` (bar subtree only shown):
|
||||
| `bluetoothPanel` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI |
|
||||
| `calendar` | Clock + popup with month-grid calendar | left = popup · right = tz selector |
|
||||
| `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND |
|
||||
| `brightness` | Brightness slider + scroll | scroll = adjust · left = popup · middle = reset to 80% |
|
||||
| `powerProfile` | Current power profile + popup picker | left = popup |
|
||||
| `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal |
|
||||
| `weatherFlyout` | Weather icon + popup with forecast | left = popup · right = full notification |
|
||||
| `powerMenu` | Power icon → popup with lock/suspend/log out/reboot/shutdown | left = popup |
|
||||
| `idleInhibitor` | Coffee-cup that toggles `omarchy-toggle-idle` | left = toggle |
|
||||
| `microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio TUI · scroll = source volume |
|
||||
|
||||
|
||||
@@ -63,7 +63,10 @@ Item {
|
||||
hoverEnabled: true
|
||||
onEntered: if (root.bar) root.bar.showTooltip(root, root.tooltipText)
|
||||
onExited: if (root.bar) root.bar.hideTooltip(root)
|
||||
onClicked: function(mouse) { root.pressed(mouse.button) }
|
||||
onClicked: function(mouse) {
|
||||
if (root.bar) root.bar.hideTooltip(root)
|
||||
root.pressed(mouse.button)
|
||||
}
|
||||
onWheel: function(wheel) { root.wheelMoved(wheel.angleDelta.y) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import "../common" as Common
|
||||
@@ -21,8 +22,21 @@ Item {
|
||||
readonly property var candidateSinks: {
|
||||
var list = []
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i]
|
||||
if (node && node.isSink && !node.isStream) list.push(node)
|
||||
var n = nodes[i]
|
||||
if (n && n.isSink && !n.isStream) list.push(n)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property var candidateSources: {
|
||||
var list = []
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var n = nodes[i]
|
||||
if (n && !n.isSink && !n.isStream && n.audio) {
|
||||
var name = n.name || ""
|
||||
if (name === "quickshell") continue
|
||||
list.push(n)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -30,12 +44,23 @@ Item {
|
||||
readonly property var candidateStreams: {
|
||||
var list = []
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i]
|
||||
if (node && node.isStream && !node.isSink) list.push(node)
|
||||
var n = nodes[i]
|
||||
if (n && n.isStream && isPlaybackStream(n)) list.push(n)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Identify true playback streams without reading node.properties here:
|
||||
// PwNode.properties is invalid until the node is bound, and reading it while
|
||||
// capture streams are appearing (for example, when Voxtype starts recording)
|
||||
// can destabilize Quickshell's Pipewire service. `type` mirrors media.class
|
||||
// and is safe enough for pre-bind filtering.
|
||||
function isPlaybackStream(node) {
|
||||
if (!node) return false
|
||||
var mediaClass = String(node.type || "")
|
||||
return mediaClass.indexOf("Output") !== -1
|
||||
}
|
||||
|
||||
readonly property var audioSinks: {
|
||||
var list = []
|
||||
for (var i = 0; i < candidateSinks.length; i++)
|
||||
@@ -43,6 +68,8 @@ Item {
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property var audioSources: candidateSources
|
||||
|
||||
readonly property var audioStreams: {
|
||||
var list = []
|
||||
for (var i = 0; i < candidateStreams.length; i++)
|
||||
@@ -50,54 +77,115 @@ Item {
|
||||
return list
|
||||
}
|
||||
|
||||
readonly property real currentVolume: sink && sink.audio ? sink.audio.volume : 0
|
||||
readonly property bool muted: sink && sink.audio ? sink.audio.muted : false
|
||||
readonly property real outputVolume: sink && sink.audio ? sink.audio.volume : 0
|
||||
readonly property bool outputMuted: sink && sink.audio ? sink.audio.muted : false
|
||||
readonly property real inputVolume: source && source.audio ? source.audio.volume : 0
|
||||
readonly property bool inputMuted: source && source.audio ? source.audio.muted : false
|
||||
|
||||
readonly property string volumeIcon: {
|
||||
if (!sink || !sink.audio) return ""
|
||||
if (muted) return ""
|
||||
var v = currentVolume
|
||||
if (v >= 0.67) return ""
|
||||
if (v >= 0.34) return ""
|
||||
if (v > 0) return ""
|
||||
return ""
|
||||
function outputIcon() {
|
||||
// Match the old Waybar pulseaudio glyph set. The Material Design speaker
|
||||
// icons render visually smaller in JetBrainsMono Nerd Font.
|
||||
if (!sink || !sink.audio) return ""
|
||||
if (outputMuted) return ""
|
||||
var v = outputVolume
|
||||
if (v >= 0.67) return ""
|
||||
if (v >= 0.34) return ""
|
||||
if (v > 0) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
function setVolume(v) {
|
||||
function inputIcon() {
|
||||
if (!source || !source.audio) return ""
|
||||
return inputMuted ? "" : ""
|
||||
}
|
||||
|
||||
function setOutputVolume(v) {
|
||||
if (!sink || !sink.audio) return
|
||||
sink.audio.volume = Math.max(0, Math.min(1, v))
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
function setInputVolume(v) {
|
||||
if (!source || !source.audio) return
|
||||
source.audio.volume = Math.max(0, Math.min(1, v))
|
||||
}
|
||||
|
||||
function toggleOutputMute() {
|
||||
if (sink && sink.audio) sink.audio.muted = !sink.audio.muted
|
||||
}
|
||||
|
||||
function setDefaultSink(node) {
|
||||
Pipewire.preferredDefaultAudioSink = node
|
||||
function toggleInputMute() {
|
||||
if (source && source.audio) source.audio.muted = !source.audio.muted
|
||||
}
|
||||
|
||||
function setDefaultSink(node) { Pipewire.preferredDefaultAudioSink = node }
|
||||
function setDefaultSource(node) { Pipewire.preferredDefaultAudioSource = node }
|
||||
|
||||
function nodeLabel(node) {
|
||||
if (!node) return "Unknown"
|
||||
return node.description || node.nickname || node.name || "Unknown"
|
||||
}
|
||||
|
||||
function nodeProps(node) {
|
||||
return node && node.ready && node.properties ? node.properties : {}
|
||||
}
|
||||
|
||||
function sinkGlyph(node) {
|
||||
if (!node) return ""
|
||||
var p = nodeProps(node)
|
||||
var blob = String([
|
||||
node.name, node.description, node.nickname,
|
||||
p["device.icon-name"] || "",
|
||||
p["device.product.name"] || ""
|
||||
].join(" ")).toLowerCase()
|
||||
if (blob.indexOf("headphone") !== -1 || blob.indexOf("headset") !== -1) return ""
|
||||
if (blob.indexOf("bluetooth") !== -1) return ""
|
||||
if (blob.indexOf("hdmi") !== -1 || blob.indexOf("display") !== -1) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
function sourceGlyph(node) {
|
||||
if (!node) return ""
|
||||
var p = nodeProps(node)
|
||||
var blob = String([
|
||||
node.name, node.description, node.nickname,
|
||||
p["device.icon-name"] || ""
|
||||
].join(" ")).toLowerCase()
|
||||
if (blob.indexOf("headset") !== -1) return ""
|
||||
if (blob.indexOf("bluetooth") !== -1) return ""
|
||||
if (blob.indexOf("webcam") !== -1 || blob.indexOf("camera") !== -1) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
function streamLabel(node) {
|
||||
if (!node) return "Stream"
|
||||
var p = nodeProps(node)
|
||||
return p["application.name"] || node.description || p["media.name"] || p["node.name"] || node.name || "Stream"
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
PwObjectTracker { objects: root.candidateSinks }
|
||||
PwObjectTracker { objects: root.candidateStreams }
|
||||
PwObjectTracker { objects: root.candidateSources }
|
||||
PwObjectTracker { objects: root.audioStreams }
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.volumeIcon
|
||||
tooltipText: root.sink ? (root.sink.description || root.sink.nickname || "Audio") + " · " + Math.round(root.currentVolume * 100) + "%" : "No audio"
|
||||
text: root.outputIcon()
|
||||
fontSize: 14
|
||||
tooltipText: root.sink ? root.nodeLabel(root.sink) + " · " + Math.round(root.outputVolume * 100) + "%" : "No audio"
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.RightButton) root.toggleMute()
|
||||
if (b === Qt.RightButton) root.toggleOutputMute()
|
||||
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-audio")
|
||||
else root.popupOpen = !root.popupOpen
|
||||
}
|
||||
|
||||
onWheelMoved: function(delta) {
|
||||
var step = 0.05
|
||||
root.setVolume(root.currentVolume + (delta > 0 ? step : -step))
|
||||
root.setOutputVolume(root.outputVolume + (delta > 0 ? step : -step))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,123 +194,411 @@ Item {
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 340
|
||||
contentHeight: panelColumn.implicitHeight + 28
|
||||
contentWidth: 380
|
||||
contentHeight: Math.min(560, panelColumn.implicitHeight + 28)
|
||||
|
||||
Column {
|
||||
id: panelColumn
|
||||
ScrollView {
|
||||
id: scrollArea
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
|
||||
// Master volume
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: root.volumeIcon
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggleMute()
|
||||
}
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
bar: root.bar
|
||||
width: parent.width - 50
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
step: 0.05
|
||||
value: root.currentVolume
|
||||
opacity: root.muted ? 0.5 : 1.0
|
||||
|
||||
onMoved: function(v) { root.setVolume(v) }
|
||||
}
|
||||
}
|
||||
|
||||
// Output device picker
|
||||
Column {
|
||||
spacing: 4
|
||||
width: parent.width
|
||||
visible: root.audioSinks.length > 0
|
||||
id: panelColumn
|
||||
width: scrollArea.availableWidth
|
||||
spacing: 14
|
||||
|
||||
Text {
|
||||
text: "Output"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioSinks
|
||||
|
||||
Common.PillButton {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
text: modelData ? (modelData.description || modelData.nickname || modelData.name || "Unknown") : ""
|
||||
iconText: root.sinkGlyph(modelData)
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
active: root.sink && modelData && root.sink.id === modelData.id
|
||||
onClicked: { root.setDefaultSink(modelData); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-app streams
|
||||
Column {
|
||||
spacing: 4
|
||||
width: parent.width
|
||||
visible: root.audioStreams.length > 0
|
||||
|
||||
Text {
|
||||
text: "Playing"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioStreams
|
||||
// ---- Output ----
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Row {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: modelData && modelData.properties ? (modelData.properties["application.name"] || modelData.properties["node.name"] || "Stream") : "Stream"
|
||||
color: root.bar.foreground
|
||||
text: "Output"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
width: 110
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.sink ? "· " + root.nodeLabel(root.sink) : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.8)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 70
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
id: outputIconText
|
||||
text: root.outputIcon()
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 16
|
||||
width: 22
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: root.outputMuted ? 0.5 : 1.0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggleOutputMute()
|
||||
}
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
id: outputSlider
|
||||
bar: root.bar
|
||||
width: parent.width - 124
|
||||
width: parent.width - outputIconText.width - outputPercent.width - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: 0
|
||||
maximum: 1.5
|
||||
maximum: 1
|
||||
step: 0.05
|
||||
value: modelData && modelData.audio ? modelData.audio.volume : 0
|
||||
value: root.outputVolume
|
||||
opacity: root.outputMuted ? 0.5 : 1.0
|
||||
enabled: !!root.sink
|
||||
|
||||
onMoved: function(v) {
|
||||
if (modelData && modelData.audio) modelData.audio.volume = v
|
||||
onMoved: function(v) { root.setOutputVolume(v) }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: outputPercent
|
||||
text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
width: 36
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: root.outputMuted ? 0.5 : 1.0
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioSinks
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
|
||||
readonly property bool active: root.sink && modelData && root.sink.id === modelData.id
|
||||
|
||||
width: panelColumn.width
|
||||
height: deviceRow.implicitHeight + 10
|
||||
radius: 4
|
||||
color: deviceArea.pressed
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.22)
|
||||
: deviceArea.containsMouse
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
: (active ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.18) : "transparent")
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Row {
|
||||
id: deviceRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.sinkGlyph(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
width: 18
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.nodeLabel(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 18 - 14 - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: active ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
width: 14
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.setDefaultSink(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Input ----
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
visible: root.audioSources.length > 0 || !!root.source
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Input"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.source ? "· " + root.nodeLabel(root.source) : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.8)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 56
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
visible: !!root.source
|
||||
|
||||
Text {
|
||||
id: inputIconText
|
||||
text: root.inputIcon()
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 16
|
||||
width: 22
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: root.inputMuted ? 0.5 : 1.0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggleInputMute()
|
||||
}
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
id: inputSlider
|
||||
bar: root.bar
|
||||
width: parent.width - inputIconText.width - inputPercent.width - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
step: 0.05
|
||||
value: root.inputVolume
|
||||
opacity: root.inputMuted ? 0.5 : 1.0
|
||||
enabled: !!root.source
|
||||
|
||||
onMoved: function(v) { root.setInputVolume(v) }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: inputPercent
|
||||
text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
width: 36
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: root.inputMuted ? 0.5 : 1.0
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioSources
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
|
||||
readonly property bool active: root.source && modelData && root.source.id === modelData.id
|
||||
|
||||
width: panelColumn.width
|
||||
height: sourceRow.implicitHeight + 10
|
||||
radius: 4
|
||||
color: sourceArea.pressed
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.22)
|
||||
: sourceArea.containsMouse
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
: (active ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.18) : "transparent")
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Row {
|
||||
id: sourceRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.sourceGlyph(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
width: 18
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.nodeLabel(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 18 - 14 - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: active ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
width: 14
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sourceArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.setDefaultSource(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Per-app streams ----
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
visible: root.audioStreams.length > 0
|
||||
|
||||
Text {
|
||||
text: "Playing"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioStreams
|
||||
|
||||
Item {
|
||||
required property var modelData
|
||||
|
||||
readonly property real streamVolume: modelData && modelData.audio ? modelData.audio.volume : 0
|
||||
readonly property bool streamMuted: modelData && modelData.audio ? modelData.audio.muted : false
|
||||
|
||||
width: panelColumn.width
|
||||
height: streamColumn.implicitHeight + 4
|
||||
|
||||
Column {
|
||||
id: streamColumn
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
id: streamMuteIcon
|
||||
text: streamMuted ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
width: 14
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: streamMuted ? 0.5 : 1.0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && modelData.audio) modelData.audio.muted = !modelData.audio.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.streamLabel(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - streamMuteIcon.width - streamPct.width - 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: streamPct
|
||||
text: Math.round(streamVolume * 100) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
width: 36
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
bar: root.bar
|
||||
width: parent.width
|
||||
minimum: 0
|
||||
maximum: 1.5
|
||||
step: 0.05
|
||||
value: streamVolume
|
||||
opacity: streamMuted ? 0.5 : 1.0
|
||||
|
||||
onMoved: function(v) {
|
||||
if (modelData && modelData.audio) modelData.audio.volume = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,17 +606,4 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sinkGlyph(node) {
|
||||
if (!node) return ""
|
||||
var blob = String([
|
||||
node.name, node.description, node.nickname,
|
||||
node.properties ? node.properties["device.icon-name"] : "",
|
||||
node.properties ? node.properties["device.product.name"] : ""
|
||||
].join(" ")).toLowerCase()
|
||||
if (blob.indexOf("headphone") !== -1 || blob.indexOf("headset") !== -1) return ""
|
||||
if (blob.indexOf("bluetooth") !== -1) return ""
|
||||
if (blob.indexOf("hdmi") !== -1 || blob.indexOf("display") !== -1) return ""
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "brightness"
|
||||
property var settings: ({})
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
property int currentPercent: -1
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
readonly property string iconGlyph: {
|
||||
if (currentPercent < 0) return ""
|
||||
if (currentPercent > 66) return ""
|
||||
if (currentPercent > 33) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
visible: currentPercent >= 0
|
||||
|
||||
function refresh() {
|
||||
if (!readProc.running) readProc.running = true
|
||||
}
|
||||
|
||||
property int pendingPercent: -1
|
||||
|
||||
function setBrightness(percent) {
|
||||
var clamped = Math.max(1, Math.min(100, Math.round(percent)))
|
||||
currentPercent = clamped
|
||||
pendingPercent = clamped
|
||||
writeTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: writeTimer
|
||||
interval: 60
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (writeProc.running) {
|
||||
writeTimer.restart()
|
||||
return
|
||||
}
|
||||
if (pendingPercent < 0) return
|
||||
writeProc.command = ["bash", "-lc", "brightnessctl set " + pendingPercent + "% >/dev/null"]
|
||||
pendingPercent = -1
|
||||
writeProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Process {
|
||||
id: readProc
|
||||
command: ["bash", "-lc", "if command -v brightnessctl >/dev/null; then echo $(( 100 * $(brightnessctl get) / $(brightnessctl max) )); fi"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var n = parseInt(String(text || "").trim(), 10)
|
||||
if (!isNaN(n)) root.currentPercent = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: writeProc }
|
||||
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.iconGlyph
|
||||
horizontalMargin: 6.5
|
||||
tooltipText: root.currentPercent >= 0 ? "Brightness " + root.currentPercent + "%" : ""
|
||||
|
||||
onPressed: function(b) {
|
||||
if (b === Qt.MiddleButton) {
|
||||
root.popupOpen = false
|
||||
root.setBrightness(80)
|
||||
} else {
|
||||
root.popupOpen = !root.popupOpen
|
||||
}
|
||||
}
|
||||
|
||||
onWheelMoved: function(delta) {
|
||||
var step = Number(root.setting("step", 5))
|
||||
root.setBrightness(root.currentPercent + (delta > 0 ? step : -step))
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 280
|
||||
contentHeight: 80
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Row {
|
||||
spacing: 10
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: root.iconGlyph
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Brightness"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: 10; height: 1 }
|
||||
|
||||
Text {
|
||||
text: root.currentPercent + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
bar: root.bar
|
||||
width: parent.width
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
step: 5
|
||||
integer: true
|
||||
value: root.currentPercent
|
||||
|
||||
onMoved: function(v) { root.setBrightness(v) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,6 +322,43 @@ Item {
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
visible: root.powerProfileAvailable
|
||||
|
||||
Text {
|
||||
text: "Power profile"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{ profile: PowerProfile.PowerSaver, label: "Power Saver", glyph: "" },
|
||||
{ profile: PowerProfile.Balanced, label: "Balanced", glyph: "" },
|
||||
{ profile: PowerProfile.Performance, label: "Performance", glyph: "" }
|
||||
]
|
||||
|
||||
ProfileButton {
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
profile: modelData.profile
|
||||
label: modelData.label
|
||||
glyph: modelData.glyph
|
||||
profileEnabled: modelData.profile !== PowerProfile.Performance || PowerProfiles.hasPerformanceProfile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
@@ -331,34 +368,77 @@ Item {
|
||||
verticalPadding: 8
|
||||
onClicked: { root.run("omarchy-launch-bar-settings"); root.popupOpen = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
visible: root.powerProfileAvailable
|
||||
component ProfileButton: Rectangle {
|
||||
id: profileButton
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{ profile: PowerProfile.PowerSaver, label: "Saver", glyph: "" },
|
||||
{ profile: PowerProfile.Balanced, label: "Balanced", glyph: "" },
|
||||
{ profile: PowerProfile.Performance, label: "Performance", glyph: "" }
|
||||
]
|
||||
property int profile: PowerProfile.Balanced
|
||||
property string label: ""
|
||||
property string glyph: ""
|
||||
property bool profileEnabled: true
|
||||
readonly property bool active: root.currentProfile === profile
|
||||
|
||||
Common.PillButton {
|
||||
required property var modelData
|
||||
width: (parent.width - 12) / 3
|
||||
iconText: modelData.glyph
|
||||
text: modelData.label
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 8
|
||||
active: root.currentProfile === modelData.profile
|
||||
enabled: modelData.profile !== PowerProfile.Performance || PowerProfiles.hasPerformanceProfile
|
||||
opacity: enabled ? 1 : 0.4
|
||||
onClicked: PowerProfiles.profile = modelData.profile
|
||||
}
|
||||
}
|
||||
height: 34
|
||||
radius: 4
|
||||
color: profileArea.pressed
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.22)
|
||||
: profileArea.containsMouse
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
: (active ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.18) : "transparent")
|
||||
border.color: active ? root.bar.foreground : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
border.width: active ? 1 : 0
|
||||
opacity: profileEnabled ? 1 : 0.4
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: profileButton.glyph
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
width: 18
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: profileButton.label
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 18 - 14 - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: profileButton.active ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
width: 14
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: profileArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: profileButton.profileEnabled
|
||||
cursorShape: profileButton.profileEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: PowerProfiles.profile = profileButton.profile
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "nightLight"
|
||||
property var settings: ({})
|
||||
|
||||
property bool active: false
|
||||
property bool toolAvailable: false
|
||||
property bool toggling: false
|
||||
|
||||
readonly property int onTemp: 4000
|
||||
readonly property int offTemp: 6000
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!statusProc.running) statusProc.running = true
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (toggling) return
|
||||
toggling = true
|
||||
if (root.bar) root.bar.run("omarchy-toggle-nightlight")
|
||||
refreshTimer.restart()
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Process {
|
||||
id: statusProc
|
||||
command: ["bash", "-lc", "command -v hyprsunset >/dev/null || { echo missing; exit; }; if pgrep -x hyprsunset >/dev/null 2>&1; then hyprctl hyprsunset temperature 2>/dev/null | grep -oE '[0-9]+' | head -1; else echo idle; fi"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var state = String(text || "").trim()
|
||||
root.toggling = false
|
||||
if (state === "missing") {
|
||||
root.toolAvailable = false
|
||||
root.active = false
|
||||
return
|
||||
}
|
||||
root.toolAvailable = true
|
||||
var temp = parseInt(state, 10)
|
||||
root.active = !isNaN(temp) && temp < root.offTemp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 1500
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 10000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
visible: toolAvailable
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.active ? "" : ""
|
||||
active: root.active
|
||||
tooltipText: root.active ? "Night light on" : "Night light off"
|
||||
onPressed: function() { root.toggle() }
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "powerMenu"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
function run(command) {
|
||||
if (root.bar) root.bar.run(command)
|
||||
popupOpen = false
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: ""
|
||||
fontSize: 14
|
||||
tooltipText: "Power menu"
|
||||
onPressed: function() { root.popupOpen = !root.popupOpen }
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 220
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: "Power"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Lock"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("loginctl lock-session")
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Suspend"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("systemctl suspend")
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Log out"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("hyprctl dispatch exit")
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Reboot"
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("systemctl reboot")
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
width: parent.width
|
||||
iconText: ""
|
||||
text: "Shut down"
|
||||
foreground: root.bar.urgent
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
onClicked: root.run("systemctl poweroff")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "powerProfile"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
|
||||
readonly property var profileGlyphs: ({
|
||||
[PowerProfile.PowerSaver]: "",
|
||||
[PowerProfile.Balanced]: "",
|
||||
[PowerProfile.Performance]: ""
|
||||
})
|
||||
|
||||
readonly property var profileLabels: ({
|
||||
[PowerProfile.PowerSaver]: "Power Saver",
|
||||
[PowerProfile.Balanced]: "Balanced",
|
||||
[PowerProfile.Performance]: "Performance"
|
||||
})
|
||||
|
||||
readonly property bool available: PowerProfiles.hasPerformanceProfile || PowerProfiles.profile === PowerProfile.PowerSaver || PowerProfiles.profile === PowerProfile.Balanced
|
||||
readonly property int current: PowerProfiles.profile
|
||||
|
||||
visible: available
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
function setProfile(profile) {
|
||||
PowerProfiles.profile = profile
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.profileGlyphs[root.current] || ""
|
||||
tooltipText: "Power profile: " + (root.profileLabels[root.current] || "Unknown")
|
||||
onPressed: function() { root.popupOpen = !root.popupOpen }
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: 240
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: "Power Profile"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{ profile: PowerProfile.PowerSaver, label: "Power Saver", glyph: "" },
|
||||
{ profile: PowerProfile.Balanced, label: "Balanced", glyph: "" },
|
||||
{ profile: PowerProfile.Performance, label: "Performance", glyph: "" }
|
||||
]
|
||||
|
||||
Common.PillButton {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
iconText: modelData.glyph
|
||||
text: modelData.label
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 8
|
||||
active: root.current === modelData.profile
|
||||
enabled: modelData.profile !== PowerProfile.Performance || PowerProfiles.hasPerformanceProfile
|
||||
opacity: enabled ? 1 : 0.4
|
||||
onClicked: { root.setProfile(modelData.profile); root.popupOpen = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,13 +195,19 @@ QtObject {
|
||||
// be either a layout entry inside `bar.layout.*` (bar widgets) or a top-level
|
||||
// entry in `plugins[]` (panels, overlays, services).
|
||||
//
|
||||
// Special case: plugins with `kinds` containing "bar" are directly mounted
|
||||
// by the shell host rather than loaded through plugins[], so they're
|
||||
// implicitly always enabled.
|
||||
// Special cases (implicitly always enabled, no shell.json entry needed):
|
||||
// - plugins whose `kinds` contains "bar" are mounted directly by the host.
|
||||
// - first-party plugins are shell infrastructure (bar-settings,
|
||||
// image-picker, ...). Requiring users to add them to plugins[] just to
|
||||
// summon them was a footgun: a stock shell.json with `plugins: []` would
|
||||
// silently make `omarchy launch bar-settings` a no-op.
|
||||
function isEnabled(id) {
|
||||
var key = String(id)
|
||||
var manifest = installedPlugins[key]
|
||||
if (manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1) return true
|
||||
if (manifest) {
|
||||
if (Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1) return true
|
||||
if (manifest.__isFirstParty) return true
|
||||
}
|
||||
var config = shellConfigProvider ? shellConfigProvider() : null
|
||||
return findEntryLocation(config, key).found
|
||||
}
|
||||
|
||||
@@ -27,12 +27,8 @@
|
||||
{ "id": "bluetoothPanel" },
|
||||
{ "id": "networkPanel" },
|
||||
{ "id": "audioPanel" },
|
||||
{ "id": "nightLight" },
|
||||
{ "id": "brightness" },
|
||||
{ "id": "powerProfile" },
|
||||
{ "id": "battery" },
|
||||
{ "id": "controlCenter" },
|
||||
{ "id": "powerMenu" }
|
||||
{ "id": "controlCenter" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ ShellRoot {
|
||||
layout: {
|
||||
left: [{ id: "omarchy" }, { id: "workspaces" }],
|
||||
center: [{ id: "calendar", format: "dddd HH:mm" }],
|
||||
right: [{ id: "audioPanel" }, { id: "controlCenter" }, { id: "powerMenu" }]
|
||||
right: [{ id: "audioPanel" }, { id: "controlCenter" }]
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
@@ -196,6 +196,11 @@ ShellRoot {
|
||||
// captured in the closure.
|
||||
Compat.PluginApiFactory { id: noctaliaApiFactory }
|
||||
|
||||
Item {
|
||||
id: noctaliaServiceHost
|
||||
visible: false
|
||||
}
|
||||
|
||||
property var _noctaliaApis: ({})
|
||||
property var _noctaliaServices: ({})
|
||||
|
||||
@@ -254,6 +259,7 @@ ShellRoot {
|
||||
shell.summon(key, JSON.stringify({ source: "noctalia" }))
|
||||
},
|
||||
closePanel: function(_pluginId, _screen) { shell.hide(key) },
|
||||
tooltipService: NoctaliaUI.TooltipService,
|
||||
currentScreen: function() {
|
||||
var screens = Quickshell.screens
|
||||
return screens && screens.length > 0 ? screens[0] : null
|
||||
@@ -293,7 +299,7 @@ ShellRoot {
|
||||
console.warn("noctalia service load failed for " + key + ": " + comp.errorString())
|
||||
return
|
||||
}
|
||||
var inst = comp.createObject(shell, { pluginApi: api })
|
||||
var inst = comp.createObject(noctaliaServiceHost, { pluginApi: api })
|
||||
if (!inst) {
|
||||
console.warn("noctalia service createObject returned null for", key)
|
||||
return
|
||||
@@ -418,6 +424,13 @@ ShellRoot {
|
||||
console.warn("summon: unknown plugin", id)
|
||||
return false
|
||||
}
|
||||
// A disabled plugin has no Loader, so setting openPanelIds would only
|
||||
// produce an invisible "open" state that toggle() then has to unwind.
|
||||
// Tell the caller plainly instead of silently no-op'ing.
|
||||
if (!shell.pluginRegistry.isEnabled(id)) {
|
||||
console.warn("summon: plugin not enabled, not summoning:", id)
|
||||
return false
|
||||
}
|
||||
var next = ({})
|
||||
for (var k in openPanelIds) next[k] = openPanelIds[k]
|
||||
next[id] = true
|
||||
@@ -539,7 +552,9 @@ ShellRoot {
|
||||
readonly property string sourceUrl: shell.pluginRegistry.entryPointUrl(manifest, entryKind)
|
||||
|
||||
property Loader panelLoader: Loader {
|
||||
source: panelEntry.sourceUrl
|
||||
readonly property bool wrapNoctaliaPanel: !!(panelEntry.manifest && panelEntry.manifest.__noctaliaCompat && panelEntry.entryKind === "panel")
|
||||
source: wrapNoctaliaPanel ? "" : panelEntry.sourceUrl
|
||||
sourceComponent: wrapNoctaliaPanel ? noctaliaPanelWrapperComponent : null
|
||||
active: panelEntry.sourceUrl !== "" && (panelEntry.keepLoaded || shell.openPanelIds[panelEntry.pluginId] === true)
|
||||
asynchronous: true
|
||||
onLoaded: {
|
||||
@@ -558,13 +573,23 @@ ShellRoot {
|
||||
}
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
console.warn("panel plugin " + panelEntry.pluginId + " failed:",
|
||||
sourceComponent ? sourceComponent.errorString() : "")
|
||||
// Loader.errorString() reflects the source-load failure even when
|
||||
// sourceComponent is null. Surface both so the user sees something
|
||||
// actionable instead of a panel that silently refuses to open.
|
||||
var detail = errorString && errorString() ? errorString() : ""
|
||||
if (!detail && sourceComponent) detail = sourceComponent.errorString()
|
||||
console.warn("panel plugin " + panelEntry.pluginId + " failed to load:", detail)
|
||||
shell.hide(panelEntry.pluginId)
|
||||
}
|
||||
}
|
||||
Component.onDestruction: shell.unregisterPanelLoader(panelEntry.pluginId)
|
||||
}
|
||||
|
||||
property Component noctaliaPanelWrapperComponent: Component {
|
||||
Compat.PanelWrapper {
|
||||
panelSource: panelEntry.sourceUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
echo "Remove standalone night light, brightness, power profile, and power menu bar widgets (moved into quick settings or removed)"
|
||||
|
||||
config="$HOME/.config/omarchy/shell.json"
|
||||
[[ -f $config ]] || return 0
|
||||
|
||||
if ! grep -Eq '"id": *"(nightLight|brightness|powerProfile|powerMenu)"' "$config"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if omarchy-cmd-missing jq; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
tmp=$(mktemp)
|
||||
if jq '
|
||||
def keep_widget: select(.id != "nightLight" and .id != "brightness" and .id != "powerProfile" and .id != "powerMenu");
|
||||
.bar.layout.left |= map(keep_widget) |
|
||||
.bar.layout.center |= map(keep_widget) |
|
||||
.bar.layout.right |= map(keep_widget)
|
||||
' "$config" > "$tmp" 2>/dev/null && [[ -s $tmp ]]; then
|
||||
mv "$tmp" "$config"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
Reference in New Issue
Block a user