Make built-in widgets plugins

This commit is contained in:
Ryan Hughes
2026-05-23 04:32:20 -04:00
parent fc180af54e
commit 4f0bdb790b
51 changed files with 565 additions and 230 deletions
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "omarchy.active-window",
"name": "Active window",
"version": "1.0.0",
"author": "Omarchy",
"description": "Title of the focused window",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "ActiveWindow.qml"
},
"barWidget": {
"displayName": "Active window",
"description": "Title of the focused window",
"category": "Compositor",
"allowMultiple": false
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Ui
BarWidget {
id: root
moduleName: "ActiveWindow"
moduleName: "omarchy.active-window"
readonly property var toplevel: ToplevelManager.activeToplevel
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "omarchy.clock",
"name": "Clock",
"version": "1.0.0",
"author": "Omarchy",
"description": "Day/time label; click to toggle alternate format",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "Clock.qml"
},
"barWidget": {
"displayName": "Clock",
"description": "Day/time label; click to toggle alternate format",
"category": "Time",
"allowMultiple": false
}
}
+2 -2
View File
@@ -6,7 +6,7 @@ import qs.Ui
BarWidget {
id: root
moduleName: "Clock"
moduleName: "omarchy.clock"
property bool alt: false
property date displayDate: clock.date
@@ -46,7 +46,7 @@ BarWidget {
}
IpcHandler {
target: "Clock"
target: "omarchy.clock"
function refresh(): void { root.refresh() }
}
@@ -0,0 +1,65 @@
{
"schemaVersion": 1,
"id": "omarchy.indicators",
"name": "Indicators",
"version": "1.0.0",
"author": "Omarchy",
"description": "Manual state indicators",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "Indicators.qml"
},
"barWidget": {
"displayName": "Indicators",
"description": "Manual state indicators",
"category": "Status",
"allowMultiple": true,
"schema": [
{
"key": "items",
"type": "multiselect",
"label": "Indicators",
"description": "Choose which indicators this widget instance should show. Leave empty to show all indicators.",
"noSelectionText": "All indicators",
"placeholderText": "Search indicators...",
"emptyText": "No indicators",
"options": [
{
"value": "Dnd",
"label": "Do not disturb",
"description": "Notification silencing"
},
{
"value": "NightLight",
"label": "Night light",
"description": "Blue-light filter"
},
{
"value": "StayAwake",
"label": "Stay awake",
"description": "Idle lock and screensaver override"
},
{
"value": "ScreenRecording",
"label": "Screen recording",
"description": "GPU screen recorder status"
},
{
"value": "Dictation",
"label": "Dictation",
"description": "Voice typing status"
}
]
},
{
"key": "alwaysShow",
"type": "boolean",
"label": "Always Show",
"description": "Show inactive indicators without waiting for hover.",
"defaultValue": false
}
]
}
}
+2 -2
View File
@@ -6,7 +6,7 @@ import qs.Ui
BarWidget {
id: root
moduleName: "Indicators"
moduleName: "omarchy.indicators"
readonly property int indicatorSlotExtent: Style.space(22)
readonly property var defaultIndicatorEntries: [ "Dnd", "NightLight", "StayAwake", "ScreenRecording", "Dictation" ]
@@ -160,7 +160,7 @@ BarWidget {
implicitHeight: root.vertical ? verticalIndicators.implicitHeight : horizontalIndicators.implicitHeight
IpcHandler {
target: "Indicators"
target: "omarchy.indicators"
function refresh(): void {
root.refreshRequested()
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "omarchy.keyboard-layout",
"name": "Keyboard layout",
"version": "1.0.0",
"author": "Omarchy",
"description": "Current xkb layout, click cycles",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "KeyboardLayout.qml"
},
"barWidget": {
"displayName": "Keyboard layout",
"description": "Current xkb layout, click cycles",
"category": "Compositor",
"allowMultiple": false
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ import qs.Commons
BarWidget {
id: root
moduleName: "KeyboardLayout"
moduleName: "omarchy.keyboard-layout"
property string layoutLabel: ""
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "omarchy.lock-keys",
"name": "Lock keys",
"version": "1.0.0",
"author": "Omarchy",
"description": "Caps / Num / Scroll lock indicators",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "LockKeys.qml"
},
"barWidget": {
"displayName": "Lock keys",
"description": "Caps / Num / Scroll lock indicators",
"category": "System",
"allowMultiple": false
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Ui
BarWidget {
id: root
moduleName: "LockKeys"
moduleName: "omarchy.lock-keys"
property bool capsOn: false
-304
View File
@@ -1,304 +0,0 @@
import QtQuick
import Quickshell
import qs.Ui
import qs.Commons
BarWidget {
id: root
moduleName: "Media"
readonly property var mediaService: bar && bar.shell ? bar.shell.firstPartyServiceFor("omarchy.media") : null
readonly property var activePlayer: mediaService ? mediaService.activePlayer : null
readonly property var sourcePlayers: mediaService ? mediaService.sourcePlayers : []
readonly property bool hasMedia: activePlayer !== null && (activePlayer.trackTitle || activePlayer.trackArtist)
readonly property string playIcon: activePlayer && activePlayer.isPlaying ? "󰏤" : "󰐊"
readonly property string title: activePlayer ? (activePlayer.trackTitle || "") : ""
readonly property string artist: activePlayer ? (activePlayer.trackArtist || "") : ""
property bool popupOpen: false
function close() { popupOpen = false }
property real maxLabelWidth: 180
visible: hasMedia
implicitWidth: hasMedia ? row.implicitWidth + Style.space(14) : 0
implicitHeight: barSize
Row {
id: row
anchors.centerIn: parent
spacing: Style.space(6)
Text {
id: glyph
anchors.verticalCenter: parent.verticalCenter
text: root.playIcon
color: activePlayer && activePlayer.isPlaying ? root.bar.foreground : Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.body
Behavior on color { ColorAnimation { duration: 160 } }
}
Item {
id: scrollClip
width: Math.min(root.maxLabelWidth, labelText.implicitWidth)
height: glyph.height
clip: true
anchors.verticalCenter: parent.verticalCenter
visible: !root.bar.vertical && root.title !== ""
Text {
id: labelText
text: root.title + (root.artist ? " · " + root.artist : "")
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.body
anchors.verticalCenter: parent.verticalCenter
property bool needsScroll: implicitWidth > scrollClip.width
NumberAnimation on x {
id: scrollAnim
running: labelText.needsScroll && !root.popupOpen && !root.bar.vertical
loops: Animation.Infinite
duration: Math.max(6000, labelText.implicitWidth * 25)
from: scrollClip.width
to: -labelText.implicitWidth
easing.type: Easing.Linear
}
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: root.activePlayer ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: function(mouse) {
if (!root.activePlayer) return
if (mouse.button === Qt.MiddleButton) {
if (root.mediaService) root.mediaService.runAction("next", false)
} else if (mouse.button === Qt.RightButton) {
root.popupOpen = !root.popupOpen
} else {
if (root.mediaService) root.mediaService.runAction("playPause", false)
}
}
onWheel: function(wheel) {
if (!root.activePlayer) return
if (wheel.angleDelta.y > 0 && root.mediaService) root.mediaService.runAction("previous", false)
else if (wheel.angleDelta.y < 0 && root.mediaService) root.mediaService.runAction("next", false)
}
onEntered: if (root.bar) root.bar.showTooltip(root, root.hasMedia ? (root.title + (root.artist ? " — " + root.artist : "")) : "")
onExited: if (root.bar) root.bar.hideTooltip(root)
}
PopupCard {
id: popup
anchorItem: root
bar: root.bar
owner: root
open: root.popupOpen
contentWidth: popup.fittedContentWidth(Style.space(320))
contentHeight: popup.fittedContentHeight(column.implicitHeight)
Column {
id: column
anchors.fill: parent
spacing: Style.space(10)
Row {
spacing: Style.space(10)
width: parent.width
Rectangle {
width: Style.space(64)
height: Style.space(64)
radius: Style.spacing.labelGap
color: Style.normalFillFor(root.bar.foreground, Color.accent)
border.color: Style.normalBorderFor(root.bar.foreground, Color.accent)
border.width: Style.normalBorderWidth
Image {
anchors.fill: parent
anchors.margins: Style.space(2)
fillMode: Image.PreserveAspectCrop
asynchronous: true
source: root.activePlayer && root.activePlayer.trackArtUrl ? root.activePlayer.trackArtUrl : ""
visible: source !== ""
}
Text {
anchors.centerIn: parent
visible: !root.activePlayer || !root.activePlayer.trackArtUrl
text: "󰝚"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.displayLarge
}
}
Column {
spacing: Style.space(4)
width: parent.width - Style.space(74)
Text {
text: root.title || "Nothing playing"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.subtitle
font.bold: true
elide: Text.ElideRight
width: parent.width
}
Text {
text: root.artist
color: Qt.darker(root.bar.foreground, 1.3)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall
elide: Text.ElideRight
width: parent.width
visible: text !== ""
}
Text {
text: root.activePlayer && root.activePlayer.trackAlbum ? root.activePlayer.trackAlbum : ""
color: Qt.darker(root.bar.foreground, 1.6)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.caption
elide: Text.ElideRight
width: parent.width
visible: text !== ""
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.space(6)
Button {
iconText: "󰒮"
foreground: root.bar.foreground
horizontalPadding: Style.spacing.controlPaddingX
verticalPadding: Style.spacing.controlPaddingY
enabled: root.activePlayer && root.activePlayer.canGoPrevious
opacity: enabled ? 1.0 : 0.4
onClicked: if (root.mediaService) root.mediaService.runAction("previous", false, root.mediaService.playerKey(root.activePlayer))
}
Button {
iconText: root.activePlayer && root.activePlayer.isPlaying ? "󰏤" : "󰐊"
foreground: root.bar.foreground
horizontalPadding: Style.spacing.panelGap
verticalPadding: Style.spacing.controlPaddingY
iconSize: Style.font.iconLarge
enabled: root.activePlayer && (root.activePlayer.canTogglePlaying || root.activePlayer.canPlay || root.activePlayer.canPause)
opacity: enabled ? 1.0 : 0.4
onClicked: if (root.mediaService) root.mediaService.runAction("playPause", false, root.mediaService.playerKey(root.activePlayer))
}
Button {
iconText: "󰒭"
foreground: root.bar.foreground
horizontalPadding: Style.spacing.controlPaddingX
verticalPadding: Style.spacing.controlPaddingY
enabled: root.activePlayer && root.activePlayer.canGoNext
opacity: enabled ? 1.0 : 0.4
onClicked: if (root.mediaService) root.mediaService.runAction("next", false, root.mediaService.playerKey(root.activePlayer))
}
}
PanelSeparator {
visible: root.sourcePlayers.length > 1
foreground: root.bar.foreground
}
Column {
id: sourceList
visible: root.sourcePlayers.length > 1
width: parent.width
spacing: Style.space(4)
Repeater {
model: root.sourcePlayers
Rectangle {
id: sourceRow
required property var modelData
readonly property var player: modelData
readonly property bool selected: root.activePlayer && player
&& root.mediaService.playerKey(root.activePlayer) === root.mediaService.playerKey(player)
readonly property string sourceTitle: player ? (player.trackTitle || player.identity || player.desktopEntry || "Media source") : "Media source"
readonly property string sourceDetail: player && player.trackArtist ? player.trackArtist : (player && player.identity ? player.identity : "")
width: sourceList.width
height: sourceInner.implicitHeight + Style.space(10)
radius: Style.spacing.labelGap
color: selected ? Style.selectedFillFor(root.bar.foreground, Color.accent) : "transparent"
border.color: selected ? Style.selectedBorderFor(root.bar.foreground, Color.accent) : "transparent"
border.width: selected ? Style.normalBorderWidth : 0
Row {
id: sourceInner
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.space(8)
anchors.rightMargin: Style.space(8)
spacing: Style.space(8)
Text {
text: sourceRow.player && sourceRow.player.isPlaying ? "󰏤" : "󰐊"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.body
width: Style.space(18)
horizontalAlignment: Text.AlignHCenter
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Style.space(26)
spacing: Style.space(1)
anchors.verticalCenter: parent.verticalCenter
Text {
text: sourceRow.sourceTitle
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall
font.bold: sourceRow.selected
elide: Text.ElideRight
width: parent.width
}
Text {
text: sourceRow.sourceDetail
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.caption
elide: Text.ElideRight
width: parent.width
visible: text !== ""
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: if (root.mediaService) root.mediaService.selectPlayer(root.mediaService.playerKey(sourceRow.player))
}
}
}
}
}
}
}
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "omarchy.microphone",
"name": "Microphone",
"version": "1.0.0",
"author": "Omarchy",
"description": "Mic input state and mute toggle",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "Microphone.qml"
},
"barWidget": {
"displayName": "Microphone",
"description": "Mic input state and mute toggle",
"category": "Audio",
"allowMultiple": false
}
}
+2 -2
View File
@@ -5,7 +5,7 @@ import qs.Ui
BarWidget {
id: root
moduleName: "Microphone"
moduleName: "omarchy.microphone"
readonly property var source: Pipewire.defaultAudioSource
@@ -42,7 +42,7 @@ BarWidget {
active: root.inUse
tooltipText: root.muted ? "Microphone muted" : (root.inUse ? "Microphone in use" : "Microphone live")
onPressed: function(b) {
if (b === Qt.MiddleButton) root.bar.run("omarchy-shell panels.audio toggle")
if (b === Qt.MiddleButton) root.bar.run("omarchy-shell omarchy.audio toggle")
else root.toggleMute()
}
onWheelMoved: function(delta) {
@@ -1,435 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Ui
BarWidget {
id: root
moduleName: "NotificationCenter"
property bool popupOpen: false
function close() { popupOpen = false }
// Always default to the pending tab when there's anything unseen, no
// matter how the popup was opened (click, keybind/IPC, or the close
// path). Keeps the spec from drifting based on the user's last manual
// tab selection.
onPopupOpenChanged: {
if (popupOpen) {
activeTab = pendingCount > 0 ? "pending" : "past"
}
}
// Look up the long-running notifications service through the shell host.
readonly property var hostShell: bar && bar.shell ? bar.shell : null
readonly property var notificationService: hostShell && typeof hostShell.firstPartyServiceFor === "function"
? hostShell.firstPartyServiceFor("omarchy.notifications")
: null
function isChromiumDerived(app, appIcon) {
var source = (String(app || "") + "\n" + String(appIcon || "")).toLowerCase()
return source.indexOf("chrom") >= 0 || source.indexOf("brave") >= 0 ||
source.indexOf("vivaldi") >= 0 || source.indexOf("microsoft-edge") >= 0 ||
source.indexOf("opera") >= 0
}
function sanitizeBody(s, app, appIcon) {
var text = String(s || "").replace(/<img[^>]*>/gi, "")
if (!isChromiumDerived(app, appIcon)) return text
return text
.replace(/^\s*<a\b[^>]*>\s*(?:https?:\/\/|www\.)?(?:[a-z0-9-]+\.)+[a-z]{2,}(?::\d+)?(?:\/[^<\s]*)?\s*<\/a>\s*/i, "")
.replace(/^\s*(?:https?:\/\/|www\.)?(?:[a-z0-9-]+\.)+[a-z]{2,}(?::\d+)?(?:\/\S*)?\s+/i, "")
}
function notificationIconSource(icon) {
var value = String(icon || "")
if (value.length === 0) return ""
if (value.indexOf("file://") === 0 || value.indexOf("image://") === 0) return value
if (value.charAt(0) === "/") return Util.fileUrl(value)
return Quickshell.iconPath(value, true)
}
readonly property int pendingCount: notificationService ? notificationService.pendingModel.count : 0
readonly property int pastCount: notificationService ? notificationService.pastModel.count : 0
readonly property bool dnd: notificationService ? notificationService.doNotDisturb : false
// Which tab is active in the popup. Auto-selects pending when there's
// something unseen; otherwise opens past.
property string activeTab: "pending"
readonly property string icon: {
if (dnd) return "󰂛"
if (pendingCount > 0) return "󱅫"
return "󰂚"
}
// Theme palette (mirrors HistoryPanel's tokens so the popup matches the
// rest of the notification stack).
readonly property color colForeground: Color.foreground
readonly property color colDim: Qt.darker(Color.foreground, 1.4)
readonly property color colBorder: Style.normalBorderFor(Color.foreground, Color.accent)
readonly property color colSurface: Style.normalFillFor(Color.foreground, Color.accent)
readonly property color colAccent: Color.accent
readonly property int cardRadius: notificationService ? notificationService.cornerRadius : 0
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.icon
active: root.pendingCount > 0 && !root.dnd
tooltipText: root.dnd ? "Do Not Disturb"
: (root.pendingCount > 0 ? root.pendingCount + " pending" : "No notifications")
onPressed: function(b) {
if (b === Qt.RightButton) {
if (root.notificationService) {
root.notificationService.setDoNotDisturb(!root.notificationService.doNotDisturb)
}
} else {
root.popupOpen = !root.popupOpen
}
}
}
// Service-side IPC (omarchy-shell notifications showHistory) flips
// historyOpenRequested; we toggle our local popup state from here so the
// keybind path lands in the same PopupCard the click path uses.
Connections {
target: root.notificationService
ignoreUnknownSignals: true
function onHistoryOpenRequested() {
root.popupOpen = true
}
}
PopupCard {
id: popup
anchorItem: button
bar: root.bar
owner: root
open: root.popupOpen
contentWidth: popup.fittedContentWidth(Style.space(440))
contentHeight: popup.cappedContentHeight(Style.space(540))
ColumnLayout {
anchors.fill: parent
spacing: Style.space(10)
// ----------------------------------------- header
RowLayout {
Layout.fillWidth: true
spacing: Style.space(8)
Text {
text: "Notifications"
font.family: root.bar ? root.bar.fontFamily : ""
color: root.colForeground
font.pixelSize: Style.font.title
font.bold: true
}
Item { Layout.fillWidth: true }
Rectangle {
id: dndPill
Layout.preferredHeight: Math.max(Style.space(24), Style.font.bodySmall + Style.spacing.controlPaddingY * 2)
Layout.preferredWidth: dndLabel.implicitWidth + dndGlyph.implicitWidth + Style.space(18)
radius: Math.min(Style.space(12), root.cardRadius + Style.space(6))
color: dndOn ? root.colAccent : root.colSurface
border.color: dndOn ? root.colAccent : root.colBorder
border.width: Style.normalBorderWidth
readonly property bool dndOn: !!root.notificationService && root.notificationService.doNotDisturb
Row {
anchors.centerIn: parent
spacing: Style.space(4)
Text {
id: dndGlyph
text: dndPill.dndOn ? "󰂛" : "󰂚"
font.family: root.bar ? root.bar.fontFamily : ""
color: dndPill.dndOn ? Color.background : root.colDim
font.pixelSize: Style.font.body
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: dndLabel
text: dndPill.dndOn ? "DND on" : "DND off"
font.family: root.bar ? root.bar.fontFamily : ""
color: dndPill.dndOn ? Color.background : root.colDim
font.pixelSize: Style.font.caption
font.bold: true
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: if (root.notificationService) root.notificationService.setDoNotDisturb(!dndPill.dndOn)
}
}
}
// ----------------------------------------- tabs
RowLayout {
Layout.fillWidth: true
spacing: 0
Repeater {
model: [
{ key: "pending", label: "Pending", count: root.pendingCount },
{ key: "past", label: "Recently", count: root.pastCount }
]
delegate: Rectangle {
required property var modelData
readonly property bool isActive: root.activeTab === modelData.key
Layout.fillWidth: true
Layout.preferredHeight: Math.max(Style.space(30), Style.font.body + Style.spacing.controlPaddingY * 2)
color: "transparent"
Text {
anchors.centerIn: parent
text: modelData.label + (modelData.count > 0 ? " " + modelData.count : "")
font.family: root.bar ? root.bar.fontFamily : ""
color: parent.isActive ? root.colForeground : root.colDim
font.pixelSize: Style.font.body
font.bold: parent.isActive
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: Math.max(1, Style.space(2))
color: parent.isActive ? root.colAccent : root.colBorder
opacity: parent.isActive ? 1 : 0.4
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.activeTab = modelData.key
}
}
}
}
// ----------------------------------------- action row
RowLayout {
Layout.fillWidth: true
visible: (root.activeTab === "pending" && root.pendingCount > 0)
|| (root.activeTab === "past" && root.pastCount > 0)
spacing: Style.space(8)
Item { Layout.fillWidth: true }
Rectangle {
Layout.preferredWidth: actionLabel.implicitWidth + Style.space(16)
Layout.preferredHeight: Math.max(Style.space(22), Style.font.bodySmall + Style.spacing.controlPaddingY * 2)
radius: Math.min(Style.space(6), root.cardRadius)
color: actionArea.containsMouse ? root.colBorder : "transparent"
border.color: root.colBorder
border.width: Style.normalBorderWidth
Text {
id: actionLabel
anchors.centerIn: parent
text: root.activeTab === "pending" ? "Mark all as seen" : "Clear recent"
font.family: root.bar ? root.bar.fontFamily : ""
color: root.colForeground
font.pixelSize: Style.font.caption
}
MouseArea {
id: actionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.notificationService) return
if (root.activeTab === "pending") root.notificationService.markAllSeen()
else root.notificationService.clearPast()
}
}
}
}
// ----------------------------------------- list
ListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: Style.space(8)
readonly property bool onPending: root.activeTab === "pending"
model: !root.notificationService ? null
: (onPending ? root.notificationService.pendingModel : root.notificationService.pastModel)
visible: count > 0
delegate: Rectangle {
id: rowCard
required property int index
required property string app
required property string appIcon
required property string summary
required property string body
required property string image
required property int urgency
required property double timestamp
readonly property bool hasMedia: image.length > 0 && (
image.indexOf("image://icon//") === 0 || image.indexOf("file://") === 0)
readonly property string smallIconSource: image.length > 0 ? image : root.notificationIconSource(appIcon)
readonly property bool hasIcon: !hasMedia && smallIconSource.length > 0
readonly property string sanitizedBody: root.sanitizeBody(body, app, appIcon)
width: listView.width
implicitHeight: rowContent.implicitHeight + Style.spacing.panelGap
radius: root.cardRadius
color: "transparent"
border.color: root.colBorder
border.width: Style.normalBorderWidth
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: { /* no-op */ }
}
RowLayout {
id: rowContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.space(12)
anchors.rightMargin: Style.space(12)
spacing: Style.space(10)
Item {
id: imageSlot
Layout.preferredWidth: Style.space(32)
Layout.preferredHeight: Style.space(32)
Layout.alignment: Qt.AlignVCenter
// Hide on icon load failure so unresolved themed-icon names
// don't render Qt's broken-image placeholder.
visible: (rowCard.hasIcon || rowCard.hasMedia) && rowIconImage.status !== Image.Error
Image {
id: rowIconImage
anchors.fill: parent
source: rowCard.hasMedia ? rowCard.image : rowCard.smallIconSource
fillMode: rowCard.hasMedia ? Image.PreserveAspectCrop : Image.PreserveAspectFit
sourceSize.width: imageSlot.width * Screen.devicePixelRatio
sourceSize.height: imageSlot.height * Screen.devicePixelRatio
asynchronous: true
smooth: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.space(2)
Text {
Layout.fillWidth: true
visible: rowCard.summary.length > 0
text: rowCard.summary
font.family: root.bar ? root.bar.fontFamily : ""
color: root.colForeground
font.pixelSize: Style.font.subtitle
font.bold: true
wrapMode: Text.WordWrap
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
Layout.fillWidth: true
visible: rowCard.sanitizedBody.length > 0
text: rowCard.sanitizedBody
font.family: root.bar ? root.bar.fontFamily : ""
textFormat: Text.PlainText
color: root.colDim
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WordWrap
elide: Text.ElideRight
maximumLineCount: 2
}
}
Rectangle {
Layout.preferredWidth: Style.space(18)
Layout.preferredHeight: Style.space(18)
Layout.alignment: Qt.AlignVCenter
radius: Math.min(4, root.cardRadius)
color: rowCloseArea.containsMouse ? root.colBorder : "transparent"
Text {
anchors.centerIn: parent
text: "✕"
font.family: root.bar ? root.bar.fontFamily : ""
color: root.colDim
font.pixelSize: Style.font.bodySmall
}
MouseArea {
id: rowCloseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.notificationService) return
if (listView.onPending) root.notificationService.dismissPending(rowCard.index)
else root.notificationService.dismissPast(rowCard.index)
}
}
}
}
}
}
// ----------------------------------------- empty state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: listView.count === 0
ColumnLayout {
anchors.centerIn: parent
spacing: Style.space(6)
Text {
Layout.alignment: Qt.AlignHCenter
text: "󰂚"
font.family: root.bar ? root.bar.fontFamily : ""
color: root.colBorder
font.pixelSize: Style.font.displayLarge
}
Text {
Layout.alignment: Qt.AlignHCenter
text: root.activeTab === "pending"
? "Nothing waiting for you"
: "Nothing recent"
font.family: root.bar ? root.bar.fontFamily : ""
color: root.colDim
font.pixelSize: Style.font.body
}
}
}
}
}
}
-24
View File
@@ -1,24 +0,0 @@
import QtQuick
import qs.Ui
BarWidget {
id: root
moduleName: "Omarchy"
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: "\ue900"
fontFamily: "omarchy"
horizontalMargin: 7.5
onPressed: function(button) {
if (!root.bar) return
if (button === Qt.RightButton) root.bar.run("xdg-terminal-exec")
else root.bar.run("omarchy-shell menu toggle root")
}
}
}
@@ -0,0 +1,21 @@
{
"schemaVersion": 1,
"id": "omarchy.spacer",
"name": "Spacer",
"version": "1.0.0",
"author": "Omarchy",
"description": "Configurable blank space",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "Spacer.qml"
},
"barWidget": {
"displayName": "Spacer",
"description": "Configurable blank space",
"category": "Layout",
"allowMultiple": true,
"settingsForm": "spacerSettings"
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import qs.Ui
BarWidget {
id: root
moduleName: "Spacer"
moduleName: "omarchy.spacer"
readonly property int span: settings && settings.size !== undefined ? Number(settings.size) : 12
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "omarchy.system-stats",
"name": "System stats",
"version": "1.0.0",
"author": "Omarchy",
"description": "CPU icon \u2014 hover for graphs, click to open btop",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "SystemStats.qml"
},
"barWidget": {
"displayName": "System stats",
"description": "CPU icon \u2014 hover for graphs, click to open btop",
"category": "System",
"allowMultiple": false
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Commons
BarWidget {
id: root
moduleName: "SystemStats"
moduleName: "omarchy.system-stats"
property real cpuPercent: 0
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "omarchy.system-update",
"name": "System update",
"version": "1.0.0",
"author": "Omarchy",
"description": "Indicates available system updates",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "SystemUpdate.qml"
},
"barWidget": {
"displayName": "System update",
"description": "Indicates available system updates",
"category": "System",
"allowMultiple": false
}
}
+2 -2
View File
@@ -6,7 +6,7 @@ import qs.Ui
BarWidget {
id: root
moduleName: "SystemUpdate"
moduleName: "omarchy.system-update"
property bool updateAvailable: false
@@ -18,7 +18,7 @@ BarWidget {
implicitHeight: button.implicitHeight
IpcHandler {
target: "SystemUpdate"
target: "omarchy.system-update"
function refresh(): void {
root.refresh()
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "omarchy.tray",
"name": "System tray",
"version": "1.0.0",
"author": "Omarchy",
"description": "Status notifier items",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "Tray.qml"
},
"barWidget": {
"displayName": "System tray",
"description": "Status notifier items",
"category": "Status",
"allowMultiple": false
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Ui
BarWidget {
id: root
moduleName: "Tray"
moduleName: "omarchy.tray"
property bool expanded: false
property bool managePopupOpen: false
-63
View File
@@ -1,63 +0,0 @@
import QtQuick
import qs.Commons
import qs.Ui
BarWidget {
id: root
moduleName: "Weather"
function injectPanel() {
var target = panelLoader.item
if (!target) return
if ("bar" in target) target.bar = root.bar
if ("settings" in target) target.settings = root.settings
if ("anchorItem" in target) target.anchorItem = button
}
function refresh() {
if (panelLoader.item && panelLoader.item.refresh) panelLoader.item.refresh()
}
function togglePanel() {
if (panelLoader.item && panelLoader.item.toggle) panelLoader.item.toggle()
}
visible: panelLoader.item && panelLoader.item.label !== ""
implicitWidth: bar && bar.vertical ? button.implicitWidth : button.implicitWidth + Style.spacing.controlGap
implicitHeight: button.implicitHeight
onBarChanged: injectPanel()
onSettingsChanged: injectPanel()
Loader {
id: panelLoader
active: true
source: Qt.resolvedUrl("../../panels/Weather.qml")
visible: false
onLoaded: {
root.injectPanel()
Qt.callLater(root.injectPanel)
}
}
WidgetButton {
id: button
anchors.verticalCenter: parent.verticalCenter
x: bar && bar.vertical ? Math.round((parent.width - width) / 2) : 0
width: implicitWidth
height: implicitHeight
bar: root.bar
text: panelLoader.item ? panelLoader.item.label : ""
active: panelLoader.item && panelLoader.item.klass === "active"
horizontalMargin: 1
// Tooltip suppressed because the panel is the detail view.
tooltipText: ""
onPressed: function(b) {
if (!root.bar) return
if (b === Qt.RightButton) root.bar.run("omarchy-notification-send \"$(omarchy-weather-status)\"")
else if (b === Qt.MiddleButton) root.refresh()
else root.togglePanel()
}
}
}
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "omarchy.workspaces",
"name": "Workspaces",
"version": "1.0.0",
"author": "Omarchy",
"description": "Workspace number indicators",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "Workspaces.qml"
},
"barWidget": {
"displayName": "Workspaces",
"description": "Workspace number indicators",
"category": "Compositor",
"allowMultiple": false
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Ui
BarWidget {
id: root
moduleName: "Workspaces"
moduleName: "omarchy.workspaces"
function workspaceById(id) {
var values = Hyprland.workspaces.values