Refine quickshell bar and Noctalia compatibility
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user