mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-06 12:39:35 +02:00
Adds first-party omarchy.notifications service plugin that hosts a freedesktop notification server and renders popups + a history popup inside the shell. Uninstalls mako and retargets every helper, keybind, indicator, and migration entry to the new daemon. Plugin (default/quickshell/omarchy-shell/plugins/notifications/): - Service.qml: NotificationServer, popupModel + pendingModel + pastModel (two-tier history, see below), DND via PersistentProperties + cache-file backstop, image cache for /tmp screenshots, IpcHandler with toggleDnd/setDnd/isDnd/showHistory/clear/clearPending/markAllSeen/ dismissAll/dismissOne/invokeLast/dismiss, per-theme override file ~/.config/omarchy/current/theme/notifications.json honoring borderColor/backgroundColor/textColor/countdownColor. - components/NotificationCard.qml: theme-driven card (Color.foreground/ background/border tokens from Commons/Color.qml), 32x32 icon slot, Nerd Font glyph fallback via omarchy-glyph hint, hero image strip for screenshot/image-path notifications, hover-pause progress bar, uses bar.fontFamily so all surfaces share one font. Filtering and DND: - transient hint and CLI-style senders (app_name in notify-send / omarchy-action) bypass history but still pop. - DND only allows omarchy-action toasts and notify-send -u critical through; real-app urgency=critical (Discord, Slack, Vesktop) is silenced and lands in pending instead. - Pending vs past split surfaced via tabs in the bar widget popup; past tab is auto-pruned at the 15-minute mark. - Click-to-jump: notifications without a libnotify default action focus the matching Hyprland window via class lookup. Shell host: - shell.qml: generic first-party service loader (mirrors the existing noctalia-compat path) and an alias for the bar so plugins can read barSize / barHidden / position for anchoring. - Commons/Color.qml: parses the theme's hyprland.conf for $activeBorderColor so notifications match Hyprland window borders; picks the explicit accent= key over the color4= alias. Bar widget rebase (plugins/bar/widgets/notificationCenter.qml): - Drops the chunk-1 stub server, binds count/dnd state to the service, hosts the history popup via PopupCard so it drops down from the notification glyph the same way Quick Settings does. - Pending/Past tabs, dismiss-individual close X, mark-all-as-seen and clear-recent action buttons, theme-driven palette. Quick Settings rework (plugins/bar/widgets/controlCenter.qml): - DND tile binds directly to service.doNotDisturb for instant feedback. - Drops the volume slider (already in audioPanel) and the no-op Theme tile; adds a Bluetooth toggle bound to Quickshell.Bluetooth. - Bigger 44x44 wallet was scaled back to 32x32 for tighter rows. Notification scripts (bin/omarchy-*): - omarchy-notification-send: passes glyph as a custom hint instead of prepending to the summary; adds -a omarchy-action and -u urgency automatically; supports -e/--transient passthrough. - User-action toasts in the capture / toggle / hyprland / default-* scripts and bindings/utilities.lua now tag themselves -a omarchy-action so DND treats them as intent-based bypass. - omarchy-toggle-notification-silencing, omarchy-notification-dismiss, default/waybar/indicators/notification-silencing.sh, and the Hyprland comma-keybinds all route through omarchy-shell-ipc notifications. - omarchy-capture-screenshot / -screenrecording set the image-path hint properly so the hero-image rendering kicks in. Mako removal (migrations/1778743515.sh): - pkill -x mako, systemctl --user stop mako.service, pacman -Rns mako (uninstalling deletes /usr/lib/systemd/user/mako.service so D-Bus activation can't respawn it). Removes ~/.config/mako/ and the legacy toggle file. Restarts quickshell so it claims the bus name. - Drops mako from install/omarchy-base.packages, autostart.lua, install/config/theme.sh + toggles.sh, default/themed/mako.ini.tpl, default/mako/, the omarchy-menu Mako restart row, bin/omarchy GROUP_DESCRIPTIONS, the settings panel catalogue, and the default/omarchy-skill paths table. - Removed scripts: bin/omarchy-restart-mako, bin/omarchy-style-corners-mako. - bin/omarchy-style-corners summary updated; corner radius for the notification card reads ~/.local/state/omarchy/toggles/quickshell-menu.json alongside the rest of the shell.
475 lines
14 KiB
QML
475 lines
14 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Bluetooth
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Pipewire
|
|
import Quickshell.Services.UPower
|
|
import "../common" as Common
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property QtObject bar: null
|
|
property string moduleName: "controlCenter"
|
|
property var settings: ({})
|
|
|
|
property bool popupOpen: false
|
|
|
|
function closePopout() { popupOpen = false }
|
|
|
|
function run(command) {
|
|
if (root.bar) root.bar.run(command)
|
|
}
|
|
|
|
function setting(name, fallback) {
|
|
var value = settings ? settings[name] : undefined
|
|
return value === undefined || value === null ? fallback : value
|
|
}
|
|
|
|
// Volume controls live in the audioPanel bar widget, so the quick-settings
|
|
// popup just hosts brightness + toggles. Bluetooth adapter status is
|
|
// exposed via Quickshell.Bluetooth.
|
|
readonly property var btAdapter: Bluetooth.defaultAdapter
|
|
readonly property bool btEnabled: btAdapter ? btAdapter.enabled : false
|
|
|
|
property int currentBrightness: -1
|
|
property int pendingBrightness: -1
|
|
|
|
// DND state is bound straight to the notifications service so toggling
|
|
// from the quick-settings tile or the bar widget reflects instantly.
|
|
readonly property var hostShell: bar && bar.shell ? bar.shell : null
|
|
readonly property var notificationService: hostShell && typeof hostShell.firstPartyServiceFor === "function"
|
|
? hostShell.firstPartyServiceFor("omarchy.notifications")
|
|
: null
|
|
readonly property bool dndActive: notificationService ? notificationService.doNotDisturb : false
|
|
property bool idleInhibited: false
|
|
property bool nightLightActive: false
|
|
property bool nightLightAvailable: false
|
|
property string themeName: ""
|
|
|
|
readonly property bool powerProfileAvailable: PowerProfiles.hasPerformanceProfile || PowerProfiles.profile === PowerProfile.PowerSaver || PowerProfiles.profile === PowerProfile.Balanced
|
|
readonly property int currentProfile: PowerProfiles.profile
|
|
|
|
function setBrightness(percent) {
|
|
var clamped = Math.max(1, Math.min(100, Math.round(percent)))
|
|
currentBrightness = clamped
|
|
pendingBrightness = clamped
|
|
brightnessWriteTimer.restart()
|
|
}
|
|
|
|
function refresh() {
|
|
if (!brightnessProc.running) brightnessProc.running = true
|
|
if (!idleProc.running) idleProc.running = true
|
|
if (!nightLightProc.running) nightLightProc.running = true
|
|
if (!themeProc.running) themeProc.running = true
|
|
}
|
|
|
|
Component.onCompleted: refresh()
|
|
|
|
Process {
|
|
id: brightnessProc
|
|
command: ["bash", "-lc", "command -v brightnessctl >/dev/null || { echo missing; exit; }; cur=$(brightnessctl get); max=$(brightnessctl max); [[ $max -gt 0 ]] && echo $((cur*100/max)) || echo 0"]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: {
|
|
var v = String(text || "").trim()
|
|
root.currentBrightness = v === "missing" ? -1 : (parseInt(v, 10) || 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: brightnessWriteProc
|
|
}
|
|
|
|
Timer {
|
|
id: brightnessWriteTimer
|
|
interval: 60
|
|
repeat: false
|
|
onTriggered: {
|
|
if (brightnessWriteProc.running) { brightnessWriteTimer.restart(); return }
|
|
if (root.pendingBrightness < 0) return
|
|
brightnessWriteProc.command = ["bash", "-lc", "brightnessctl set " + root.pendingBrightness + "% >/dev/null"]
|
|
root.pendingBrightness = -1
|
|
brightnessWriteProc.running = true
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: idleProc
|
|
command: ["bash", "-lc", "if pgrep -x hypridle >/dev/null 2>&1; then echo running; else echo inhibited; fi"]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: root.idleInhibited = String(text || "").trim() === "inhibited"
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: nightLightProc
|
|
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()
|
|
if (state === "missing") {
|
|
root.nightLightAvailable = false
|
|
root.nightLightActive = false
|
|
return
|
|
}
|
|
root.nightLightAvailable = true
|
|
var temp = parseInt(state, 10)
|
|
root.nightLightActive = !isNaN(temp) && temp < 6000
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: themeProc
|
|
command: ["bash", "-lc", "readlink ~/.config/omarchy/current/theme 2>/dev/null | xargs -r basename"]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: root.themeName = String(text || "").trim()
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
interval: 3000
|
|
running: true
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: root.refresh()
|
|
}
|
|
|
|
implicitWidth: button.implicitWidth
|
|
implicitHeight: button.implicitHeight
|
|
|
|
Common.WidgetButton {
|
|
id: button
|
|
anchors.fill: parent
|
|
bar: root.bar
|
|
text: ""
|
|
fontSize: 14
|
|
tooltipText: "Quick Settings"
|
|
onPressed: function() { root.popupOpen = !root.popupOpen }
|
|
}
|
|
|
|
Common.PopupCard {
|
|
id: popup
|
|
anchorItem: button
|
|
bar: root.bar
|
|
owner: root
|
|
open: root.popupOpen
|
|
contentWidth: 320
|
|
contentHeight: layout.implicitHeight + 28
|
|
|
|
Column {
|
|
id: layout
|
|
anchors.fill: parent
|
|
spacing: 14
|
|
|
|
Text {
|
|
text: "Quick settings"
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: 12
|
|
font.bold: true
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: 10
|
|
visible: root.currentBrightness >= 0
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: 8
|
|
|
|
Common.PillButton {
|
|
iconText: ""
|
|
foreground: root.bar.foreground
|
|
horizontalPadding: 8
|
|
verticalPadding: 6
|
|
iconSize: 16
|
|
}
|
|
|
|
Common.Slider {
|
|
bar: root.bar
|
|
width: parent.width - 90
|
|
value: Math.max(0, root.currentBrightness / 100)
|
|
minimum: 0.01
|
|
maximum: 1
|
|
step: 0.05
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onMoved: function(v) { root.setBrightness(v * 100) }
|
|
onReleased: function(v) { root.setBrightness(v * 100) }
|
|
}
|
|
|
|
Text {
|
|
text: root.currentBrightness + "%"
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: 11
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 32
|
|
horizontalAlignment: Text.AlignRight
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 1
|
|
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
|
}
|
|
|
|
Grid {
|
|
id: tileGrid
|
|
width: parent.width
|
|
columns: 2
|
|
columnSpacing: 8
|
|
rowSpacing: 8
|
|
|
|
Tile {
|
|
width: (tileGrid.width - tileGrid.columnSpacing) / 2
|
|
glyph: root.dndActive ? "" : ""
|
|
title: "Do Not Disturb"
|
|
subtitle: root.dndActive ? "On" : "Off"
|
|
active: root.dndActive
|
|
onClicked: {
|
|
if (!root.notificationService) return
|
|
root.notificationService.setDoNotDisturb(!root.notificationService.doNotDisturb)
|
|
}
|
|
}
|
|
|
|
Tile {
|
|
width: (tileGrid.width - tileGrid.columnSpacing) / 2
|
|
glyph: root.nightLightActive ? "" : ""
|
|
title: "Night Light"
|
|
subtitle: !root.nightLightAvailable ? "—" : (root.nightLightActive ? "On" : "Off")
|
|
active: root.nightLightActive
|
|
tileEnabled: root.nightLightAvailable
|
|
onClicked: { root.run("omarchy-toggle-nightlight"); nightLightProc.running = true }
|
|
}
|
|
|
|
Tile {
|
|
width: (tileGrid.width - tileGrid.columnSpacing) / 2
|
|
glyph: root.idleInhibited ? "" : ""
|
|
title: "Keep Awake"
|
|
subtitle: root.idleInhibited ? "On" : "Off"
|
|
active: root.idleInhibited
|
|
onClicked: { root.run("omarchy-toggle-idle"); idleProc.running = true }
|
|
}
|
|
|
|
Tile {
|
|
width: (tileGrid.width - tileGrid.columnSpacing) / 2
|
|
glyph: root.btEnabled ? "" : ""
|
|
title: "Bluetooth"
|
|
subtitle: !root.btAdapter ? "—" : (root.btEnabled ? "On" : "Off")
|
|
active: root.btEnabled
|
|
tileEnabled: root.btAdapter !== null
|
|
onClicked: {
|
|
if (!root.btAdapter) return
|
|
root.btAdapter.enabled = !root.btAdapter.enabled
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: root.powerProfileAvailable
|
|
width: parent.width
|
|
height: 1
|
|
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: ""
|
|
text: "Settings…"
|
|
foreground: root.bar.foreground
|
|
horizontalPadding: 10
|
|
verticalPadding: 8
|
|
onClicked: { root.run("omarchy-launch-settings"); root.popupOpen = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
component ProfileButton: Rectangle {
|
|
id: profileButton
|
|
|
|
property int profile: PowerProfile.Balanced
|
|
property string label: ""
|
|
property string glyph: ""
|
|
property bool profileEnabled: true
|
|
readonly property bool active: root.currentProfile === 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
|
|
}
|
|
}
|
|
|
|
component Tile: Rectangle {
|
|
id: tile
|
|
|
|
property string glyph: ""
|
|
property string title: ""
|
|
property string subtitle: ""
|
|
property bool active: false
|
|
property bool tileEnabled: true
|
|
|
|
signal clicked()
|
|
|
|
implicitHeight: 56
|
|
radius: 6
|
|
color: tileArea.containsMouse
|
|
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.16)
|
|
: (active ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.10)
|
|
: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.04))
|
|
border.color: active ? root.bar.foreground : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
|
border.width: 1
|
|
opacity: tileEnabled ? 1 : 0.4
|
|
|
|
Behavior on color { ColorAnimation { duration: 120 } }
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.margins: 10
|
|
spacing: 8
|
|
|
|
Text {
|
|
text: tile.glyph
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: 18
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Column {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
width: parent.width - parent.children[0].implicitWidth - 8
|
|
|
|
Text {
|
|
text: tile.title
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: 11
|
|
font.bold: true
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
}
|
|
Text {
|
|
text: tile.subtitle
|
|
color: Qt.darker(root.bar.foreground, 1.4)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: 10
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: tileArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: tile.tileEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
enabled: tile.tileEnabled
|
|
onClicked: tile.clicked()
|
|
}
|
|
}
|
|
}
|