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.
258 lines
8.9 KiB
QML
258 lines
8.9 KiB
QML
// Notification card. Pure presentational — no service, Notification, or
|
|
// ListModel references. The popup container drives lifetime; the history
|
|
// panel drives static rendering. Both use the same component.
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import qs.Commons
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
property string app: ""
|
|
property string appIcon: ""
|
|
property string summary: ""
|
|
property string body: ""
|
|
property string image: ""
|
|
// Nerd Font glyph rendered in the icon slot when no real icon is set.
|
|
// Used by omarchy-notification-send so user-action toasts (`Silenced
|
|
// notifications` etc.) show their bell/lock/etc. glyph without leaking
|
|
// into the summary text.
|
|
property string glyph: ""
|
|
// NotificationUrgency: Low=0, Normal=1, Critical=2 (upstream).
|
|
property int urgency: 1
|
|
property double timestamp: 0
|
|
property int cornerRadius: 10
|
|
|
|
property real progress: 1.0
|
|
property bool showProgress: false
|
|
|
|
// Container can override the theme defaults per-card. Defaults bind to
|
|
// the live Color.* tokens, so when the container leaves them alone the
|
|
// card still tracks the theme.
|
|
property color borderColorOverride: Color.border
|
|
property color backgroundColorOverride: Color.background
|
|
property color textColorOverride: Color.foreground
|
|
property color countdownColorOverride: Color.accent
|
|
// System font from shell.json bar.fontFamily, injected by the container.
|
|
property string fontFamily: ""
|
|
|
|
readonly property bool hovered: hoverTracker.hovered
|
|
|
|
signal closeRequested()
|
|
signal cardClicked()
|
|
signal imageClicked()
|
|
|
|
// Media mode = the notification carries a real screenshot or screen
|
|
// recording preview. Quickshell normalizes file paths from `-i` and the
|
|
// `image-path` hint into `image://icon//<absolute path>` (double slash
|
|
// marks an absolute filesystem path vs a themed icon name like
|
|
// `image://icon/firefox`).
|
|
function _imageFilePath(s) {
|
|
if (!s) return ""
|
|
if (s.indexOf("image://icon//") === 0) return s.substring("image://icon/".length)
|
|
if (s.indexOf("file://") === 0) return decodeURIComponent(s.substring(7))
|
|
return ""
|
|
}
|
|
function _isMediaFile(path) {
|
|
if (!path) return false
|
|
var lower = path.toLowerCase()
|
|
return lower.endsWith(".png") || lower.endsWith(".jpg") ||
|
|
lower.endsWith(".jpeg") || lower.endsWith(".webp") ||
|
|
lower.endsWith(".gif")
|
|
}
|
|
readonly property string mediaImageSource: {
|
|
if (_isMediaFile(_imageFilePath(image))) return image
|
|
if (_isMediaFile(_imageFilePath(appIcon))) return appIcon
|
|
return ""
|
|
}
|
|
readonly property bool mediaMode: mediaImageSource.length > 0
|
|
// Use only what the notification explicitly carries — no themed-icon
|
|
// theme-lookup fallback because Quickshell's icon image provider returns
|
|
// a placeholder for missing names (rather than erroring), which means
|
|
// we'd render Qt's pink "broken image" pattern for any unknown app.
|
|
// Apps that send their own icon via `image` (image-data hint) or
|
|
// `appIcon` (-i flag) still get one.
|
|
readonly property string smallIconSource: image.length > 0 ? image : appIcon
|
|
readonly property bool hasGlyph: glyph.length > 0
|
|
readonly property bool hasSmallIcon: !mediaMode && (smallIconSource.length > 0 || hasGlyph)
|
|
|
|
readonly property color dimColor: Qt.darker(textColorOverride, 1.4)
|
|
readonly property color bodyColor: Qt.darker(textColorOverride, 1.15)
|
|
readonly property color hoverColor: Qt.rgba(textColorOverride.r, textColorOverride.g, textColorOverride.b, 0.14)
|
|
readonly property color accentColor: urgency === 2 ? Color.urgent : (urgency === 0 ? dimColor : countdownColorOverride)
|
|
|
|
function sanitizeBody(s) {
|
|
return String(s).replace(/<img[^>]*>/gi, "")
|
|
}
|
|
|
|
implicitWidth: 380
|
|
// Add 2 * border.width so mainColumn (inset by border.width on top/left/right)
|
|
// doesn't push content under the bottom edge. The bottom edge is also inset
|
|
// for symmetry except when the progress bar replaces it.
|
|
implicitHeight: mainColumn.implicitHeight + border.width * 2 + (showProgress ? 3 : 0)
|
|
radius: cornerRadius
|
|
color: backgroundColorOverride
|
|
border.color: urgency === 2 ? Color.urgent : borderColorOverride
|
|
border.width: 2
|
|
clip: true
|
|
|
|
HoverHandler { id: hoverTracker }
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.cardClicked()
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: mainColumn
|
|
// Inset by the card border so the hero image (and the text row) don't
|
|
// paint over the card's outer border. Without this the left/right/top
|
|
// border is invisible under the image.
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.topMargin: root.border.width
|
|
anchors.leftMargin: root.border.width
|
|
anchors.rightMargin: root.border.width
|
|
spacing: 0
|
|
|
|
// Hero image strip (media notifications only). PreserveAspectCrop so
|
|
// the preview looks like a clean banner without dark letterboxing.
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 140
|
|
visible: root.mediaMode
|
|
clip: true
|
|
|
|
Image {
|
|
anchors.fill: parent
|
|
source: root.mediaImageSource
|
|
fillMode: Image.PreserveAspectCrop
|
|
sourceSize.width: width > 0 ? width * Screen.devicePixelRatio : 0
|
|
sourceSize.height: height > 0 ? height * Screen.devicePixelRatio : 0
|
|
asynchronous: true
|
|
smooth: true
|
|
cache: false
|
|
}
|
|
|
|
// Bottom divider matching the card border so the screenshot is
|
|
// visually framed on every side (card border wraps top/left/right;
|
|
// this line completes the bottom).
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
height: root.border.width
|
|
color: root.urgency === 2 ? Color.urgent : root.borderColorOverride
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.imageClicked()
|
|
}
|
|
|
|
}
|
|
|
|
// Text content. Always rendered — for media notifications this carries
|
|
// the summary/body ("Screenshot saved" etc) under the hero image.
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 12
|
|
Layout.rightMargin: 12
|
|
Layout.topMargin: 10
|
|
Layout.bottomMargin: 10
|
|
spacing: 10
|
|
|
|
Item {
|
|
id: smallIconSlot
|
|
Layout.preferredWidth: 32
|
|
Layout.preferredHeight: 32
|
|
Layout.alignment: Qt.AlignVCenter
|
|
// Hide the slot when the icon failed to resolve (themed-icon name
|
|
// not in the user's icon theme) AND we don't have a glyph fallback
|
|
// — prevents rendering Qt's pink broken-image placeholder.
|
|
visible: root.hasSmallIcon && (root.hasGlyph || smallIconImage.status !== Image.Error)
|
|
|
|
Image {
|
|
id: smallIconImage
|
|
anchors.fill: parent
|
|
source: root.smallIconSource
|
|
sourceSize.width: 32 * Screen.devicePixelRatio
|
|
sourceSize.height: 32 * Screen.devicePixelRatio
|
|
fillMode: Image.PreserveAspectFit
|
|
asynchronous: true
|
|
smooth: true
|
|
visible: !root.hasGlyph || smallIconImage.status === Image.Ready
|
|
}
|
|
|
|
// Glyph fallback (Nerd Font character) when no image icon is
|
|
// available. Used by omarchy-notification-send's `-g` flag.
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: root.hasGlyph && smallIconImage.status !== Image.Ready
|
|
text: root.glyph
|
|
color: root.textColorOverride
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 18
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignVCenter
|
|
spacing: 2
|
|
|
|
Text {
|
|
Layout.fillWidth: true
|
|
visible: root.summary.length > 0
|
|
text: root.summary
|
|
font.family: root.fontFamily
|
|
color: root.textColorOverride
|
|
font.pixelSize: 13
|
|
font.bold: true
|
|
wrapMode: Text.WordWrap
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 2
|
|
}
|
|
|
|
Text {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: 2
|
|
visible: root.body.length > 0
|
|
text: root.sanitizeBody(root.body)
|
|
textFormat: Text.StyledText
|
|
font.family: root.fontFamily
|
|
color: root.bodyColor
|
|
font.pixelSize: 12
|
|
wrapMode: Text.WordWrap
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 3
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Progress bar at the bottom edge. Stays visible while the container has
|
|
// a finite lifetime; freezes (doesn't decrement) when hover pauses the
|
|
// tick from the container side.
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
height: 3
|
|
color: root.borderColorOverride
|
|
visible: root.showProgress
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
width: parent.width * Math.max(0, Math.min(1, root.progress))
|
|
color: root.accentColor
|
|
}
|
|
}
|
|
}
|