first commit

This commit is contained in:
SomeElse
2026-04-24 02:14:35 +00:00
commit 5f3b357344
20 changed files with 3205 additions and 0 deletions

8
README.md Normal file
View File

@@ -0,0 +1,8 @@
# Quickshell Dots
**Warning:** I used AI to create the theme styling and pass many variables to `config.qml`.
Additionally, I used AI to fix several bugs, which suggests the code may not be very readable and could be broken in some aspects.
I am currently identifying and cleaning up bugs and bad code as I find them. (im not qml expert)
Just git clone this repo into .config/quickshell and it will work.

179
bar/Bar.qml Normal file
View File

@@ -0,0 +1,179 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "../config" as Cfg
PanelWindow {
id: barWindow
required property var modelData
property bool isTop
readonly property var cfg: Cfg.Config
color: "transparent"
anchors {
top: isTop
bottom: !isTop
left: true
right: true
}
implicitHeight: cfg.barHeight
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-bar"
WlrLayershell.exclusiveZone: cfg.barHeight
Item {
id: animatedWrapper
anchors.fill: parent
opacity: 0
scale: 0.95
y: barWindow.isTop ? -20 : 20
Component.onCompleted: barEntrance.start()
ParallelAnimation {
id: barEntrance
NumberAnimation {
target: animatedWrapper
property: "opacity"
from: 0; to: 1
duration: 400
easing.type: Easing.OutCubic
}
NumberAnimation {
target: animatedWrapper
property: "scale"
from: 0.95; to: 1.0
duration: 500
easing.type: Easing.OutBack
}
NumberAnimation {
target: animatedWrapper
property: "y"
to: 0
duration: 600
easing.type: Easing.OutExpo
}
}
Item {
id: barContent
anchors.fill: parent
anchors.leftMargin: cfg.margin
anchors.rightMargin: cfg.margin
function bindModule(item, side) {
item.height = Qt.binding(() => barContent.height - 12)
const props = {
"panelRadius": cfg.radius / 2,
"borderWidth": cfg.panelBorderWidth,
"isTop": barWindow.isTop,
"targetScreen": barWindow.modelData,
"barHeight": cfg.barHeight,
"barMargin": cfg.margin,
"barSide": side,
"menuWindow": barWindow
}
for (let prop in props) {
if (prop in item) item[prop] = props[prop]
}
}
readonly property real sideMaxWidth: Math.max(0,
(width - centerRow.implicitWidth - 2 * cfg.panelSpacing) / 2)
Item {
id: leftContainer
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
height: parent.height
width: Math.max(0, Math.min(leftRow.implicitWidth, barContent.sideMaxWidth))
clip: true
Row {
id: leftRow
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: cfg.moduleSpacing
transformOrigin: Item.Left
scale: leftContainer.width / Math.max(leftContainer.width, implicitWidth)
Repeater {
model: cfg.leftModules
Loader {
required property string modelData
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
onLoaded: barContent.bindModule(item, "left")
}
}
}
}
Item {
id: rightContainer
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: parent.height
width: Math.max(0, Math.min(rightRow.implicitWidth, barContent.sideMaxWidth))
clip: true
Row {
id: rightRow
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: cfg.moduleSpacing
transformOrigin: Item.Right
scale: rightContainer.width / Math.max(rightContainer.width, implicitWidth)
Repeater {
model: cfg.rightModules
Loader {
required property string modelData
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
onLoaded: barContent.bindModule(item, "right")
}
}
}
}
Item {
id: centerContainer
anchors.verticalCenter: parent.verticalCenter
height: parent.height
width: Math.max(0, Math.min(
centerRow.implicitWidth,
barContent.width
- 2 * (Math.max(leftContainer.width, rightContainer.width)
+ cfg.panelSpacing)
))
x: (barContent.width - width) / 2
clip: true
Row {
id: centerRow
anchors.centerIn: parent
spacing: cfg.moduleSpacing
transformOrigin: Item.Center
scale: centerContainer.width / Math.max(centerContainer.width, implicitWidth)
Repeater {
model: cfg.centerModules
Loader {
required property string modelData
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
onLoaded: barContent.bindModule(item, "center")
}
}
}
}
}
}
}

91
bar/modules/ArchLogo.qml Normal file
View File

@@ -0,0 +1,91 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
property var launcher: null
readonly property bool launcherActive: !!launcher && launcher.isOpen
readonly property bool isHovered: hoverHandler.hovered
property real spinTarget: 0
onTargetScreenChanged: Qt.callLater(() => {
launcher = (targetScreen && targetScreen.launcher) ? targetScreen.launcher : null
})
onLauncherActiveChanged: {
spinTarget += launcherActive ? 360 : -360
if (launcherActive && !clickAnim.running)
clickAnim.restart()
}
width: Cfg.Config.logoIconSize * 2 + 4
height: parent.height
color: isHovered ? t.archLogoBgHover : t.archLogoBg
radius: panelRadius
border.color: launcherActive ? t.archLogoBorderActive : t.archLogoBorder
border.width: borderWidth
Behavior on color { ColorAnimation { duration: 250 } }
Behavior on border.color { ColorAnimation { duration: 250 } }
HoverHandler { id: hoverHandler }
TapHandler {
onTapped: {
clickAnim.restart()
if (root.targetScreen && root.targetScreen.launcher) {
root.targetScreen.launcher.toggle()
}
}
}
SequentialAnimation {
id: clickAnim
NumberAnimation { target: logoText; property: "scale"; from: 1.0; to: 0.7; duration: 60; easing.type: Easing.OutQuad }
NumberAnimation { target: logoText; property: "scale"; from: 0.7; to: 1.2; duration: 150; easing.type: Easing.OutBack }
NumberAnimation { target: logoText; property: "scale"; to: 1.0; duration: 100 }
}
Text {
id: logoText
anchors.centerIn: parent
text: Cfg.Config.logoIcon
color: launcherActive ? t.archLogoIconActive : t.archLogoIcon
font.pixelSize: Cfg.Config.logoIconSize
Behavior on color { ColorAnimation { duration: 250 } }
scale: clickAnim.running ? logoText.scale : 1.0
Behavior on scale {
enabled: !clickAnim.running
NumberAnimation { duration: 250; easing.type: Easing.OutBack }
}
rotation: root.spinTarget
Behavior on rotation {
NumberAnimation {
duration: 600
easing.type: Easing.InOutBack
}
}
}
}

331
bar/modules/Clock.qml Normal file
View File

@@ -0,0 +1,331 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
readonly property int tzOffsetHours: -timeManager.now.getTimezoneOffset() / 60
width: clockLayout.implicitWidth + 24
height: Cfg.Config.moduleHeight
radius: panelRadius
color: hoverHandler.hovered ? t.clockBgHover : t.clockBg
border.color: hoverHandler.hovered ? t.clockBorderHover : t.clockBorder
border.width: borderWidth
anchors.right: parent.right
Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler {
id: hoverHandler
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: calendarPopup.toggle()
}
Item {
id: timeManager
property date now: new Date()
Timer {
interval: 1000; running: true; repeat: true
onTriggered: timeManager.now = new Date()
}
}
RowLayout {
id: clockLayout
anchors.centerIn: parent
spacing: 0
Text {
text: "󱑂"
color: root.t.clockIcon
font.pixelSize: 14
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 8
}
Text {
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm")
color: root.t.clockTime
font.pixelSize: 13
font.bold: true
verticalAlignment: Text.AlignVCenter
}
Text {
id: secondsText
text: ":" + timeManager.now.toLocaleTimeString(Qt.locale(), "ss")
color: root.t.clockSeconds
font.pixelSize: 13
font.bold: true
verticalAlignment: Text.AlignVCenter
opacity: (hoverHandler.hovered || calendarPopup.popupOpen) ? 1.0 : 0.0
visible: opacity > 0
Layout.preferredWidth: (hoverHandler.hovered || calendarPopup.popupOpen) ? implicitWidth : 0
clip: true
Behavior on opacity { NumberAnimation { duration: 200 } }
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
}
}
PanelWindow {
id: calendarPopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
function open() { hideTimer.stop(); visible = true; popupOpen = true }
function close() { popupOpen = false; hideTimer.restart() }
function toggle() { popupOpen ? close() : open() }
Timer {
id: hideTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: calendarPopup.visible = false
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-calendar"
WlrLayershell.exclusiveZone: -1
mask: popupOpen ? null : _noInput
Region { id: _noInput }
TapHandler { onTapped: calendarPopup.close() }
Item {
id: calClipContainer
readonly property int cardW: 284
readonly property int screenPad: root.barMargin + 10
x: {
let centerX = root.mapToItem(null, 0, 0).x + root.width / 2
let desiredX = centerX - cardW / 2
return Math.max(screenPad, Math.min(desiredX, parent.width - cardW - screenPad))
}
width: cardW
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
height: parent.height - root.barHeight - 10
clip: true
Rectangle {
id: card
width: parent.width
height: cardCol.implicitHeight + 24
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: calendarPopup.popupOpen ? 0 : (root.isTop ? -card.height : card.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration;
easing.type: Easing.OutExpo
}
enabled: calendarPopup.visible
}
}
opacity: calendarPopup.popupOpen ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 200 } }
color: root.t.calCardBg
radius: Cfg.Config.popupRadius
border.color: root.t.calCardBorder
border.width: Cfg.Config.popupBorderWidth
layer.enabled: true
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
ColumnLayout {
id: cardCol
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.margins: 16
anchors.topMargin: 18
spacing: 14
ColumnLayout {
spacing: 2
Layout.fillWidth: true
Text {
text: timeManager.now.toLocaleDateString(Qt.locale(), "dddd")
color: root.t.clockPopupDim
font.pixelSize: 12
font.capitalization: Font.AllUppercase
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
Text {
text: timeManager.now.toLocaleDateString(Qt.locale(), "d MMMM yyyy")
color: root.t.clockPopupHeader
font.pixelSize: 22
font.bold: true
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.calSeparator }
RowLayout {
Layout.fillWidth: true
Text {
text: ""; color: root.t.clockPopupDim; font.pixelSize: 18; font.bold: true
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: {
cal.displayMonth -= 1
if (cal.displayMonth < 0) { cal.displayMonth = 11; cal.displayYear -= 1 }
}
}
}
Text {
text: new Date(cal.displayYear, cal.displayMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy")
color: root.t.clockPopupText
font.pixelSize: 13; font.bold: true
Layout.fillWidth: true; horizontalAlignment: Text.AlignHCenter
}
Text {
text: ""; color: root.t.clockPopupDim; font.pixelSize: 18; font.bold: true
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: {
cal.displayMonth += 1
if (cal.displayMonth > 11) { cal.displayMonth = 0; cal.displayYear += 1 }
}
}
}
}
Item {
id: cal
Layout.fillWidth: true
implicitHeight: gridCol.implicitHeight
property int displayYear: timeManager.now.getFullYear()
property int displayMonth: timeManager.now.getMonth()
Connections {
target: calendarPopup
function onPopupOpenChanged() {
if (calendarPopup.popupOpen) {
cal.displayYear = timeManager.now.getFullYear()
cal.displayMonth = timeManager.now.getMonth()
}
}
}
ColumnLayout {
id: gridCol
anchors.fill: parent
spacing: 2
Row {
spacing: 0
Repeater {
model: ["Mo","Tu","We","Th","Fr","Sa","Su"]
Text {
width: (card.width - 32) / 7
text: modelData; color: root.t.clockPopupDim
font.pixelSize: 11; horizontalAlignment: Text.AlignHCenter
}
}
}
Repeater {
model: 6
Row {
id: weekRow
property int weekIndex: index
Repeater {
model: 7
delegate: Item {
width: (card.width - 32) / 7; height: 26
readonly property int dayNum: (weekRow.weekIndex * 7 + index) - ((new Date(cal.displayYear, cal.displayMonth, 1).getDay() + 6) % 7) + 1
readonly property bool inMonth: dayNum >= 1 && dayNum <= new Date(cal.displayYear, cal.displayMonth + 1, 0).getDate()
readonly property bool isToday: inMonth && dayNum === timeManager.now.getDate() && cal.displayYear === timeManager.now.getFullYear() && cal.displayMonth === timeManager.now.getMonth()
Rectangle {
anchors.centerIn: parent; width: 22; height: 22; radius: 11
color: isToday ? root.t.accent : "transparent"
visible: inMonth
}
Text {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: inMonth ? dayNum : ""
color: isToday ? root.t.todayText : (index >= 5 ? root.t.clockPopupDim : root.t.clockPopupText)
font.pixelSize: 12; font.bold: isToday
}
}
}
}
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.calSeparator }
RowLayout {
Layout.fillWidth: true; spacing: 12
Text { text: "󰣐"; color: root.t.clockIcon; font.pixelSize: 16 }
ColumnLayout {
spacing: 0; Layout.fillWidth: true
Text {
text: timeManager.now.toLocaleTimeString(Qt.locale(), "t") || "UTC"
color: root.t.clockPopupText; font.pixelSize: 12; font.bold: true
}
Text {
text: "UTC " + (root.tzOffsetHours >= 0 ? "+" : "") + root.tzOffsetHours
color: root.t.clockPopupDim; font.pixelSize: 10
}
}
Text {
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm:ss")
color: root.t.clockTime; font.pixelSize: 20; font.bold: true; font.family: "monospace"
}
}
Item { height: 4 }
}
}
}
}
}

View File

@@ -0,0 +1,357 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import "../../config" as Cfg
import "../../notifications" as Notif
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
property int unreadCount: 0
property bool historyOpen: historyPopup.popupOpen
width: 35
height: 40
radius: panelRadius
color: hoverHandler.hovered ? t.notifBgHover : t.notifBg
border.color: (hoverHandler.hovered || historyOpen) ? t.notifBorderActive : t.notifBorder
border.width: borderWidth
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
TapHandler {
onTapped: {
if (historyPopup.popupOpen) {
historyPopup.close()
} else {
root.unreadCount = 0
historyPopup.open()
}
}
}
Item {
anchors.centerIn: parent
width: Cfg.Config.notifIconSize + 10
height: Cfg.Config.notifIconSize + 10
Text {
id: bellIcon
anchors.centerIn: parent
text: "󰂚"
color: root.unreadCount > 0 ? t.notifIconActive : t.notifIcon
font.pixelSize: Cfg.Config.notifIconSize
Behavior on color { ColorAnimation { duration: 150 } }
}
Rectangle {
visible: root.unreadCount > 0
anchors.top: parent.top
anchors.right: parent.right
width: Math.max(14, badgeLabel.implicitWidth + 6)
height: 14
radius: 7
color: t.notifBadgeBg
Behavior on width { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
Text {
id: badgeLabel
anchors.centerIn: parent
text: root.unreadCount > 99 ? "99+" : root.unreadCount.toString()
color: t.notifBadgeText
font.pixelSize: 8
font.bold: true
}
}
}
ListModel { id: notifHistory }
ListModel { id: activeToasts }
function removeToast(toastId) {
for (var i = 0; i < activeToasts.count; i++) {
if (activeToasts.get(i).toastId === toastId) {
activeToasts.remove(i)
return
}
}
}
function urgencyColor(urgency) {
if (urgency === NotificationUrgency.Critical) return t.notifUrgencyCritical
if (urgency === NotificationUrgency.Normal) return t.notifUrgencyNormal
return t.notifUrgencyLow
}
function urgencyIcon(urgency) {
if (urgency === NotificationUrgency.Critical) return "󰀪"
if (urgency === NotificationUrgency.Normal) return "󰋼"
return "󰵙"
}
NotificationServer {
id: notifServer
keepOnReload: false
actionIconsSupported: true
bodyMarkupSupported: true
bodySupported: true
persistenceSupported: true
imageSupported: true
onNotification: function(notif) {
notif.tracked = true
var timeStr = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
var toastId = notif.id + "_" + Date.now()
notifHistory.insert(0, {
nid: notif.id,
appName: notif.appName || "Unknown",
summary: notif.summary || "",
body: notif.body || "",
urgency: notif.urgency,
timeStr: timeStr
})
while (notifHistory.count > Cfg.Config.notificationHistorySize)
notifHistory.remove(notifHistory.count - 1)
var cfgTimeout = notif.urgency === NotificationUrgency.Critical
? Cfg.Config.notifTimeoutCritical
: (notif.urgency === NotificationUrgency.Low
? Cfg.Config.notifTimeoutLow
: Cfg.Config.notifTimeoutNormal)
var finalTimeout = (notif.expireTimeout > 0) ? notif.expireTimeout : cfgTimeout
var toastEntry = {
toastId: toastId,
nid: notif.id,
appName: notif.appName || "Unknown",
summary: notif.summary || "",
body: notif.body || "",
urgency: notif.urgency,
timeStr: timeStr,
timeout: finalTimeout
}
if (toastLayer.atBottom) activeToasts.append(toastEntry)
else activeToasts.insert(0, toastEntry)
if (!root.historyOpen) root.unreadCount++
}
}
PanelWindow {
id: toastLayer
screen: root.targetScreen
visible: activeToasts.count > 0
color: "transparent"
readonly property string pos: Cfg.Config.notificationPosition
readonly property bool atRight: pos === "topright" || pos === "bottomright"
readonly property bool atBottom: pos === "bottomleft" || pos === "bottomright"
anchors { top: true; bottom: true; right: atRight; left: !atRight }
implicitWidth: 350 + root.barMargin + 4
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "main-shell-notifications-toasts"
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
mask: Region { item: toastCol }
readonly property bool shareEdge: (root.isTop && !atBottom) || (!root.isTop && atBottom)
readonly property int edgeGap: shareEdge ? root.barMargin + root.barHeight + 10 : root.barMargin + 10
Column {
id: toastCol
x: !toastLayer.atRight ? 4 : 4
y: !toastLayer.atBottom ? toastLayer.edgeGap : parent.height - toastLayer.edgeGap - toastCol.height
spacing: 8
width: 350
Repeater {
model: activeToasts
Notif.NotificationCard {
width: toastCol.width
toastId: model.toastId
appName: model.appName
summary: model.summary
body: model.body
urgency: model.urgency
timeStr: model.timeStr
timeout: model.timeout
t: root.t
urgencyColor: root.urgencyColor
urgencyIcon: root.urgencyIcon
onDismissed: (id) => root.removeToast(id)
}
}
}
}
PanelWindow {
id: historyPopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
function open() { hideTimer.stop(); visible = true; popupOpen = true }
function close() { popupOpen = false; hideTimer.restart() }
Timer {
id: hideTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: historyPopup.visible = false
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-notifications-history"
WlrLayershell.exclusiveZone: -1
mask: popupOpen ? null : _noInput
Region { id: _noInput }
MouseArea {
anchors.fill: parent
onClicked: historyPopup.close()
}
Item {
id: histClipContainer
readonly property int cardW: 340
readonly property int screenPad: root.barMargin + 10
x: {
var cx = root.mapToItem(null, 0, 0).x + root.width / 2
return Math.max(screenPad, Math.min(cx - cardW / 2, parent.width - cardW - screenPad))
}
width: cardW
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
height: parent.height - root.barHeight - 10
clip: true
Rectangle {
id: historyCard
width: parent.width
height: Math.min(histHeader.implicitHeight + 56 + (notifHistory.count > 0 ? notifHistory.count * 76 : 90), 520)
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: historyPopup.popupOpen ? 0 : (root.isTop ? -historyCard.height : historyCard.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration;
easing.type: Easing.OutExpo
}
enabled: historyPopup.visible
}
}
opacity: historyPopup.popupOpen ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 200 } }
color: t.notifHistoryBg
radius: Cfg.Config.popupRadius
border.color: t.notifHistoryBorder
border.width: Cfg.Config.popupBorderWidth
layer.enabled: true
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
RowLayout {
id: histHeader
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.topMargin: 20; anchors.leftMargin: 20; anchors.rightMargin: 20
spacing: 10
Text { text: "󰂚"; color: t.notifToastAppName; font.pixelSize: 18; Layout.alignment: Qt.AlignVCenter }
Text {
text: "Notifications"; color: t.notifToastAppName; font.pixelSize: 15; font.bold: true
Layout.fillWidth: true; Layout.alignment: Qt.AlignVCenter
}
Rectangle {
visible: notifHistory.count > 0
Layout.alignment: Qt.AlignVCenter
width: clearText.implicitWidth + 16
height: 24
radius: 12
color: clearHover.hovered ? t.notifHistoryHover : "transparent"
Behavior on color { ColorAnimation { duration: 150 } }
Text { id: clearText; anchors.centerIn: parent; text: "Clear all"; color: t.notifToastDim; font.pixelSize: 11 }
HoverHandler { id: clearHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: { notifHistory.clear(); root.unreadCount = 0 } }
}
}
Rectangle {
id: histDivider
anchors { left: parent.left; right: parent.right; top: histHeader.bottom }
anchors.topMargin: 12; height: 1; color: t.notifSeparator
}
Flickable {
id: histFlick
anchors { left: parent.left; right: parent.right; top: histDivider.bottom; bottom: parent.bottom }
anchors.leftMargin: 8; anchors.rightMargin: 8
anchors.topMargin: 6; anchors.bottomMargin: 8; clip: true
contentHeight: histCol.implicitHeight
flickableDirection: Flickable.VerticalFlick
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
Column {
id: histCol
width: histFlick.width; spacing: 2; topPadding: 4; bottomPadding: 4; leftPadding: 8; rightPadding: 8
Item {
width: histCol.width - 16; height: 90; visible: notifHistory.count === 0
ColumnLayout {
anchors.centerIn: parent; spacing: 8
Text { text: "󰂚"; color: t.notifHistoryEmpty; font.pixelSize: 30; Layout.alignment: Qt.AlignHCenter }
Text { text: "No notifications"; color: t.notifHistoryEmpty; font.pixelSize: 12; Layout.alignment: Qt.AlignHCenter }
}
}
Repeater {
model: notifHistory
Notif.HistoryCard {
width: histCol.width - 16; x: 8; itemIndex: index
appName: model.appName; summary: model.summary; body: model.body; urgency: model.urgency; timeStr: model.timeStr
t: root.t; urgencyColor: root.urgencyColor; urgencyIcon: root.urgencyIcon
onDismissed: (idx) => notifHistory.remove(idx)
}
}
}
}
}
}
}
}

181
bar/modules/Stats.qml Normal file
View File

@@ -0,0 +1,181 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
width: statsLayout.implicitWidth + 20
height: Cfg.Config.moduleHeight
color: t.statsBg
radius: panelRadius
border.color: t.statsBorder
border.width: borderWidth
Behavior on width {
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
}
HoverHandler { cursorShape: Qt.PointingHandCursor }
property real cpuVal: 0
property real memVal: 0
property real diskVal: 0
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
cpuProc.running = true
memProc.running = true
diskProc.running = true
}
}
Process {
id: cpuProc
command: ["sh", "-c", "top -bn1 | awk '/^%Cpu/ {print 100-$8}'"]
stdout: SplitParser {
onRead: data => {
let val = parseFloat(data.trim())
root.cpuVal = isNaN(val) ? 0 : val
}
}
}
Process {
id: memProc
command: ["sh", "-c", "free | awk '/Mem:/ {print ($2-$7)/$2 * 100}'"]
stdout: SplitParser {
onRead: data => {
let val = parseFloat(data.trim())
root.memVal = isNaN(val) ? 0 : val
}
}
}
Process {
id: diskProc
command: ["sh", "-c", "df / --output=pcent | tail -1 | tr -dc '0-9'"]
stdout: SplitParser {
onRead: data => {
let val = parseFloat(data.trim())
root.diskVal = isNaN(val) ? 0 : val
}
}
}
component StatRing: Item {
id: ring
property real value: 0.0
property string label: ""
property color ringColor: Cfg.Config.theme.accent
property color trackColor: Cfg.Config.theme.statsTrackColor
property color labelColor: Cfg.Config.theme.textMain
Behavior on value {
NumberAnimation {
duration: 800
easing.type: Easing.OutCubic
}
}
readonly property real fraction: Math.max(0, Math.min(value, 100)) / 100
width: Cfg.Config.statsRingSize
height: Cfg.Config.statsRingSize
onFractionChanged: arc.requestPaint()
onRingColorChanged: arc.requestPaint()
Canvas {
id: arc
anchors.fill: parent
antialiasing: true
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.clearRect(0, 0, width, height)
var cx = width / 2
var cy = height / 2
var lw = 2.8
var r = Math.min(cx, cy) - lw / 2 - 0.5
ctx.beginPath()
ctx.arc(cx, cy, r, 0, Math.PI * 2)
ctx.strokeStyle = ring.trackColor
ctx.lineWidth = lw
ctx.stroke()
if (ring.fraction > 0) {
var start = -Math.PI / 2
var end = start + (ring.fraction * Math.PI * 2)
ctx.beginPath()
ctx.arc(cx, cy, r, start, end)
ctx.strokeStyle = ring.ringColor
ctx.lineWidth = lw
ctx.lineCap = "round"
ctx.stroke()
}
}
}
HoverHandler { id: hov }
Text {
anchors.centerIn: parent
text: Cfg.Config.statsShowPercentByDefault
? (hov.hovered ? ring.label : Math.round(ring.value) + "%")
: (hov.hovered ? Math.round(ring.value) + "%" : ring.label)
color: ring.labelColor
font.pixelSize: Cfg.Config.statsRingFontSize
font.bold: true
font.family: "monospace"
}
}
RowLayout {
id: statsLayout
anchors.centerIn: parent
spacing: 10
StatRing {
value: root.cpuVal
label: "cpu"
ringColor: root.t.statsCpuColor
trackColor: root.t.statsTrackColor
labelColor: root.t.statsText
}
StatRing {
value: root.memVal
label: "ram"
ringColor: root.t.statsMemColor
trackColor: root.t.statsTrackColor
labelColor: root.t.statsText
}
StatRing {
value: root.diskVal
label: "disk"
ringColor: root.t.statsDiskColor
trackColor: root.t.statsTrackColor
labelColor: root.t.statsText
}
}
}

161
bar/modules/Tray.qml Normal file
View File

@@ -0,0 +1,161 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "../../config" as Cfg
Item {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property string barSide: "right"
readonly property var t: Cfg.Config.theme
property bool expanded: false
property bool manuallyExpanded: false
property var menuWindow: null
implicitWidth: bgRect.width
implicitHeight: bgRect.height
Rectangle {
id: bgRect
anchors.verticalCenter: parent.verticalCenter
height: Cfg.Config.trayHeight
width: mainRow.width
radius: root.panelRadius
color: bgHover.hovered ? root.t.trayBgHover : root.t.trayBg
border.color: bgHover.hovered ? root.t.trayBorderHover : root.t.trayBorder
border.width: root.borderWidth
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler {
id: bgHover
onHoveredChanged: {
if (!root.manuallyExpanded) {
root.expanded = hovered;
}
}
}
Row {
id: mainRow
anchors.verticalCenter: parent.verticalCenter
leftPadding: root.expanded && root.barSide === "right" ? 8 : 0
rightPadding: root.expanded && root.barSide === "left" ? 8 : 0
Behavior on leftPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on rightPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
layoutDirection: root.barSide === "left" ? Qt.LeftToRight : Qt.RightToLeft
spacing: root.expanded ? 4 : 0
Behavior on spacing { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
Item {
id: toggleArea
width: Cfg.Config.trayHeight
height: Cfg.Config.trayHeight
z: 10
TapHandler {
onTapped: {
root.manuallyExpanded = !root.manuallyExpanded;
root.expanded = root.manuallyExpanded;
}
}
Text {
anchors.centerIn: parent
text: root.expanded
? (root.barSide === "left" ? "󰅁" : "󰅂")
: (root.barSide === "left" ? "󰅂" : "󰅁")
color: root.t.trayIcon
font.pixelSize: Cfg.Config.trayHeight * 0.55
}
}
Row {
id: iconsRow
spacing: 6
clip: true
anchors.verticalCenter: parent.verticalCenter
width: root.expanded ? implicitWidth : 0
opacity: root.expanded ? 1 : 0
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 150 } }
Repeater {
model: SystemTray.items
delegate: MouseArea {
id: iconDelegate
required property SystemTrayItem modelData
property alias item: iconDelegate.modelData
implicitWidth: Cfg.Config.trayIconSize
implicitHeight: Cfg.Config.trayIconSize
width: Cfg.Config.trayIconSize
height: Cfg.Config.trayIconSize
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
onClicked: function(mouse) {
if (mouse.button === Qt.LeftButton) {
item.activate()
} else if (mouse.button === Qt.MiddleButton) {
item.secondaryActivate()
} else if (mouse.button === Qt.RightButton) {
menuAnchor.open()
}
}
onWheel: function(wheel) {
wheel.accepted = true
item.scroll(wheel.angleDelta.y / 120, false)
}
IconImage {
anchors.fill: parent
source: item.icon
}
QsMenuAnchor {
id: menuAnchor
menu: item.menu
anchor.window: root.menuWindow
anchor.adjustment: PopupAdjustment.Flip
anchor.onAnchoring: {
if (!root.menuWindow) return
const global = iconDelegate.mapToGlobal(0, iconDelegate.height)
const local = root.menuWindow.contentItem.mapFromGlobal(
global.x, global.y
)
menuAnchor.anchor.rect = Qt.rect(
local.x, local.y,
iconDelegate.width, 0
)
}
}
}
}
}
}
}
}

530
bar/modules/Volume.qml Normal file
View File

@@ -0,0 +1,530 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Services.Pipewire
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
property int outputMaxVolume: Cfg.Config.volumeOutputMax
property int inputMaxVolume: Cfg.Config.volumeInputMax
readonly property var sink: Pipewire.defaultAudioSink
readonly property var source: Pipewire.defaultAudioSource
PwObjectTracker {
objects: root.sinkNodes.concat(root.sourceNodes)
}
readonly property var sinkNodes: {
if (!Pipewire.nodes) return []
return Pipewire.nodes.values.filter(n => !n.isStream && n.isSink)
}
readonly property var sourceNodes: {
if (!Pipewire.nodes) return []
return Pipewire.nodes.values.filter(n => !n.isStream && !n.isSink)
}
property int _outVol: 0
property int _inVol: 0
readonly property int outputVolume: {
if (sink && sink.ready) { _outVol = Math.round(sink.audio.volume * 100); return _outVol }
return _outVol
}
readonly property int inputVolume: {
if (source && source.ready) { _inVol = Math.round(source.audio.volume * 100); return _inVol }
return _inVol
}
readonly property bool outputMuted: sink && sink.ready ? sink.audio.muted : false
readonly property bool inputMuted: source && source.ready ? source.audio.muted : false
readonly property string outputDeviceName: sink ? (sink.description || sink.nickName || sink.name) : "No device"
readonly property string inputDeviceName: source ? (source.description || source.nickName || source.name) : "No device"
function setOutputVol(pct) { if (sink && sink.ready) sink.audio.volume = pct / 100 }
function setInputVol(pct) { if (source && source.ready) source.audio.volume = pct / 100 }
function toggleOutputMute() { if (sink && sink.ready) sink.audio.muted = !sink.audio.muted }
function toggleInputMute() { if (source && source.ready) source.audio.muted = !source.audio.muted }
Process {
id: defaultSetter
running: false
property string nodeId: "0"
command: ["wpctl", "set-default", nodeId]
}
function setDefaultDevice(node) {
defaultSetter.nodeId = node.id.toString()
defaultSetter.running = true
}
readonly property string iconSpeakerHigh: "󰕾"
readonly property string iconSpeakerMed: "󰖀"
readonly property string iconSpeakerLow: "󰕿"
readonly property string iconSpeakerMuted: "󰖁"
readonly property string iconMic: "󰍬"
readonly property string iconMicMuted: "󰍭"
function speakerGlyph(muted, vol) {
if (muted || vol === 0) return iconSpeakerMuted
if (vol > 60) return iconSpeakerHigh
if (vol > 25) return iconSpeakerMed
return iconSpeakerLow
}
width: chipLayout.implicitWidth + 15
height: Cfg.Config.moduleHeight
radius: panelRadius
color: hoverHandler.hovered ? t.volBgHover : t.volBg
border.color: (hoverHandler.hovered || volumePopup.popupOpen) ? t.volBorderHover : t.volBorder
border.width: borderWidth
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: volumePopup.toggle() }
RowLayout {
id: chipLayout
anchors.centerIn: parent
spacing: 5
Text {
text: root.speakerGlyph(root.outputMuted, root.outputVolume)
color: root.outputMuted ? root.t.volIconMuted : root.t.volIcon
font.pixelSize: Cfg.Config.volumeIconSize
Layout.alignment: Qt.AlignVCenter
}
}
PanelWindow {
id: volumePopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
function open() { hideTimer.stop(); visible = true; popupOpen = true }
function close() { popupOpen = false; hideTimer.restart() }
function toggle() { popupOpen ? close() : open() }
Timer {
id: hideTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: volumePopup.visible = false
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-volume"
WlrLayershell.exclusiveZone: -1
mask: popupOpen ? null : _noInput
Region { id: _noInput }
MouseArea {
anchors.fill: parent
onClicked: volumePopup.close()
}
Item {
id: volClipContainer
readonly property int cardW: Cfg.Config.volumePopupWidth
readonly property int screenPad: root.barMargin + 10
x: {
let cx = root.mapToItem(null, 0, 0).x + root.width / 2
return Math.max(screenPad, Math.min(cx - cardW / 2, parent.width - cardW - screenPad))
}
width: cardW
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
height: parent.height - root.barHeight - 10
clip: true
Rectangle {
id: volCard
width: parent.width
height: popupCol.implicitHeight + 32
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: volumePopup.popupOpen ? 0 : (root.isTop ? -volCard.height : volCard.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration;
easing.type: Easing.OutExpo
}
enabled: volumePopup.visible
}
}
opacity: volumePopup.popupOpen ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 200 } }
radius: Cfg.Config.popupRadius
color: root.t.volPopupBg
border.color: root.t.volPopupBorder
border.width: Cfg.Config.popupBorderWidth
layer.enabled: true
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
ColumnLayout {
id: popupCol
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.margins: 16
anchors.topMargin: 18
spacing: 14
VolumeSection {
id: outputSection
Layout.fillWidth: true
label: "Output"
icon: root.speakerGlyph(root.outputMuted, root.outputVolume)
iconColor: root.t.accent
volume: root.outputVolume
maxVolume: root.outputMaxVolume
muted: root.outputMuted
accentColor: root.t.volHandle
dimColor: root.t.textDim
textColor: root.t.textMain
trackColor: root.t.volTrack
deviceName: root.outputDeviceName
nodes: root.sinkNodes
currentNode: root.sink
onSliderMoved: vol => root.setOutputVol(vol)
onMuteToggled: root.toggleOutputMute()
onDeviceSelected: node => root.setDefaultDevice(node)
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.separator }
VolumeSection {
id: inputSection
Layout.fillWidth: true
label: "Input"
icon: root.inputMuted ? root.iconMicMuted : root.iconMic
iconColor: root.t.statusInfo
volume: root.inputVolume
maxVolume: root.inputMaxVolume
muted: root.inputMuted
accentColor: root.t.statusInfo
dimColor: root.t.textDim
textColor: root.t.textMain
trackColor: root.t.volTrack
deviceName: root.inputDeviceName
nodes: root.sourceNodes
currentNode: root.source
onSliderMoved: vol => root.setInputVol(vol)
onMuteToggled: root.toggleInputMute()
onDeviceSelected: node => root.setDefaultDevice(node)
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.separator }
ColumnLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: "Volume Limits"
color: root.t.textDim
font.pixelSize: 10
font.capitalization: Font.AllUppercase
Layout.fillWidth: true
}
LimitRow {
Layout.fillWidth: true
icon: root.iconSpeakerHigh
iconColor: root.t.accent
labelText: "Output max"
value: root.outputMaxVolume
minValue: 10
maxValue: 300
textColor: root.t.textMain
dimColor: root.t.textDim
borderColor: root.t.volPopupBorder
onDecrement: root.outputMaxVolume = Math.max(10, root.outputMaxVolume - 10)
onIncrement: root.outputMaxVolume = Math.min(300, root.outputMaxVolume + 10)
}
LimitRow {
Layout.fillWidth: true
icon: root.iconMic
iconColor: root.t.statusInfo
labelText: "Input max"
value: root.inputMaxVolume
minValue: 10
maxValue: 300
textColor: root.t.textMain
dimColor: root.t.textDim
borderColor: root.t.volPopupBorder
onDecrement: root.inputMaxVolume = Math.max(10, root.inputMaxVolume - 10)
onIncrement: root.inputMaxVolume = Math.min(300, root.inputMaxVolume + 10)
}
}
Item { height: 2 }
}
}
}
}
component VolumeSection: ColumnLayout {
id: vs
spacing: 8
required property string label
required property string icon
required property color iconColor
required property int volume
required property int maxVolume
required property bool muted
required property color accentColor
required property color dimColor
required property color textColor
required property color trackColor
required property string deviceName
required property var nodes
required property var currentNode
signal sliderMoved(int vol)
signal muteToggled()
signal deviceSelected(var node)
property bool pickerOpen: false
RowLayout {
Layout.fillWidth: true
spacing: 6
Text {
text: vs.icon
color: vs.muted ? vs.dimColor : vs.iconColor
font.pixelSize: Cfg.Config.volumeIconSize
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Cfg.Config.volumeIconSize + 4
horizontalAlignment: Text.AlignHCenter
}
Text {
text: vs.label
color: vs.textColor
font.pixelSize: 13
font.bold: true
Layout.fillWidth: true
}
Text {
text: vs.muted ? "muted" : vs.volume + "%"
color: vs.muted ? vs.dimColor : vs.textColor
font.pixelSize: 12
font.bold: !vs.muted
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: 44
}
Rectangle {
width: 22; height: 22; radius: 6
color: muteHover.hovered ? (vs.muted ? "#33ffffff" : "#22ffffff") : (vs.muted ? root.t.volMuteBg : "transparent")
border.color: vs.muted ? vs.accentColor : vs.dimColor
border.width: 1
Text {
anchors.centerIn: parent
text: vs.muted ? "󰖁" : "󰕾"
color: vs.muted ? vs.accentColor : vs.dimColor
font.pixelSize: 11
}
HoverHandler { id: muteHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: vs.muteToggled() }
}
}
Item {
id: sliderItem
Layout.fillWidth: true
height: 22
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width; height: Cfg.Config.volumeSliderHeight; radius: 2
color: vs.trackColor
Rectangle {
width: Math.min(parent.width, parent.width * (vs.volume / Math.max(1, vs.maxVolume)))
height: parent.height; radius: parent.radius
color: vs.muted ? vs.dimColor : vs.accentColor
}
}
Rectangle {
readonly property real ratio: vs.volume / Math.max(1, vs.maxVolume)
x: Math.min(sliderItem.width - width, Math.max(0, sliderItem.width * ratio - width / 2))
anchors.verticalCenter: parent.verticalCenter
width: Cfg.Config.volumeHandleSize; height: Cfg.Config.volumeHandleSize; radius: width / 2
color: vs.muted ? vs.dimColor : vs.accentColor
border.color: root.t.volHandleBorder
border.width: 1
}
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onPressed: pos => sliderItem.seek(pos.x)
onPositionChanged: pos => { if (pressed) sliderItem.seek(pos.x) }
}
function seek(x) {
let ratio = Math.max(0.0, Math.min(1.0, x / sliderItem.width))
vs.sliderMoved(Math.round(ratio * vs.maxVolume))
}
}
RowLayout {
Layout.fillWidth: true
spacing: 6
Text { text: "󰒓"; color: vs.dimColor; font.pixelSize: 11 }
Text {
text: vs.deviceName
color: vs.textColor
font.pixelSize: 11
elide: Text.ElideRight
Layout.fillWidth: true
}
Rectangle {
width: 20; height: 20; radius: 5
color: chevHover.hovered ? "#22ffffff" : "transparent"
border.color: vs.pickerOpen ? vs.accentColor : root.t.volPopupBorder
border.width: 1
Text {
anchors.centerIn: parent
text: vs.pickerOpen ? "󰅃" : "󰅀"
color: vs.pickerOpen ? vs.accentColor : vs.dimColor
font.pixelSize: 10
}
HoverHandler { id: chevHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: vs.pickerOpen = !vs.pickerOpen }
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: vs.pickerOpen ? Math.min(deviceFlickable.contentHeight, 120) : 0
opacity: vs.pickerOpen ? 1.0 : 0.0
clip: true
Behavior on Layout.preferredHeight { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
Flickable {
id: deviceFlickable
anchors.fill: parent
contentWidth: width
contentHeight: deviceList.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: deviceList
width: deviceFlickable.width
spacing: 2
Repeater {
model: vs.nodes
delegate: Rectangle {
required property var modelData
readonly property bool isActive: vs.currentNode !== null && modelData.id === vs.currentNode.id
Layout.fillWidth: true; height: 28; radius: 7
color: isActive ? Qt.alpha(vs.accentColor, 0.18) : (rowHover.hovered ? root.t.volPickerHover : "transparent")
border.color: isActive ? Qt.alpha(vs.accentColor, 0.45) : "transparent"
border.width: 1
RowLayout {
anchors { fill: parent; leftMargin: 8; rightMargin: 8 }
spacing: 8
Rectangle {
width: 6; height: 6; radius: 3
color: isActive ? vs.accentColor : "transparent"
border.color: isActive ? "transparent" : vs.dimColor
border.width: 1
}
Text {
text: modelData.description || modelData.nickName || modelData.name
color: isActive ? vs.accentColor : vs.textColor
font.pixelSize: 11; font.bold: isActive
elide: Text.ElideRight; Layout.fillWidth: true
}
}
HoverHandler { id: rowHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: { vs.deviceSelected(modelData); vs.pickerOpen = false } }
}
}
}
}
}
}
component LimitRow: RowLayout {
id: lr
spacing: 8
required property string icon
required property color iconColor
required property string labelText
required property int value
required property int minValue
required property int maxValue
required property color textColor
required property color dimColor
required property color borderColor
signal decrement()
signal increment()
Text { text: lr.icon; color: lr.iconColor; font.pixelSize: 13 }
Text {
text: lr.labelText
color: lr.textColor
font.pixelSize: 11
Layout.fillWidth: true
}
Rectangle {
width: 22; height: 22; radius: 5
color: decHover.hovered ? "#22ffffff" : "transparent"
border.color: lr.borderColor; border.width: 1
Text {
anchors.centerIn: parent; text: ""
color: lr.value <= lr.minValue ? lr.dimColor : lr.textColor
font.pixelSize: 14
}
HoverHandler { id: decHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: { if (lr.value > lr.minValue) lr.decrement() } }
}
Text {
text: lr.value + "%"
color: lr.textColor
font.pixelSize: 12; font.bold: true
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: 44
}
Rectangle {
width: 22; height: 22; radius: 5
color: incHover.hovered ? "#22ffffff" : "transparent"
border.color: lr.borderColor; border.width: 1
Text {
anchors.centerIn: parent; text: "+"
color: lr.value >= lr.maxValue ? lr.dimColor : lr.textColor
font.pixelSize: 14
}
HoverHandler { id: incHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: { if (lr.value < lr.maxValue) lr.increment() } }
}
}
}

62
bar/modules/Workspace.qml Normal file
View File

@@ -0,0 +1,62 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
readonly property var t: Cfg.Config.theme
width: workspaceLayout.implicitWidth + 24
height: Cfg.Config.moduleHeight
color: t.wsPanelBg
radius: panelRadius
border.color: t.wsPanelBorder
border.width: borderWidth
RowLayout {
id: workspaceLayout
anchors.centerIn: parent
spacing: 6
Repeater {
model: Hyprland.workspaces
Rectangle {
Layout.preferredWidth: modelData.active ? 34 : 28
Layout.preferredHeight: 22
radius: 11
color: modelData.active
? root.t.wsActiveBg
: (wsHover.containsMouse ? root.t.wsHoverBg : root.t.wsInactiveBg)
Behavior on color { ColorAnimation { duration: 200 } }
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200 } }
Text {
anchors.centerIn: parent
text: modelData.name
font.pixelSize: 11
font.bold: true
color: modelData.active
? root.t.wsActiveText
: (wsHover.containsMouse ? root.t.wsHoverText : root.t.wsInactiveText)
}
MouseArea {
id: wsHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: modelData.activate()
}
}
}
}
}

87
config/config.qml Normal file
View File

@@ -0,0 +1,87 @@
pragma Singleton
import QtQuick
import Quickshell
import "themes"
QtObject {
id: root
readonly property var theme: Everforest
// ────────────────────────────────────────────────────────────────────────
// Widgets
// ────────────────────────────────────────────────────────────────────────
property bool enableBgDate: true
// ────────────────────────────────────────────────────────────────────────
// BAR
// ────────────────────────────────────────────────────────────────────────
// ── Position & Dimensions ─────────────────────────────────────────────────
property string barPosition: "bottom"
property int barHeight: 37
property int margin: 20
property int radius: 26
// ── Module Lists ──────────────────────────────────────────────────────────
property var leftModules: ["ArchLogo", "Stats"]
property var centerModules: ["Workspace"]
property var rightModules: ["Tray", "Notifications", "Volume", "Clock"]
// ── Arch Logo ─────────────────────────────────────────────────────────────
property int logoIconSize: 22 // icon font size; container width scales with it
property string logoIcon: "󱄅" // Nerd Font glyph for the logo
// ── Panel Styling ─────────────────────────────────────────────────────────
property int panelBorderWidth: 1
property int moduleSpacing: 2 // space between modules within a panel
property int panelSpacing: 12 // gap between left / center / right panels
property int moduleHeight: 32 // default chip height for bar modules
// ── Popup Styling ─────────────────────────────────────────────────────────
property int popupRadius: 18
property int popupBorderWidth: 1
property int popupAnimDuration: 500
// ── Tray Styling ─────────────────────────────────────────────────────────
property int trayHeight: 26
property int trayIconSize: 20
// ── Stats Styling ─────────────────────────────────────────────────────────
property int statsRingSize: 25
property int statsRingFontSize: 10
property bool statsShowPercentByDefault: true
// ── Volume Module ─────────────────────────────────────────────────────────
property int volumeIconSize: 20
property int volumeSliderHeight: 4
property int volumeHandleSize: 20
property int volumePopupWidth: 280
property int volumeOutputMax: 100
property int volumeInputMax: 200
// ────────────────────────────────────────────────────────────────────────
// FRAME
// ────────────────────────────────────────────────────────────────────────
property int frameBorderWidth: 1
// ────────────────────────────────────────────────────────────────────────
// NOTIFICATIONS
// ────────────────────────────────────────────────────────────────────────
property string notificationPosition: "topright"
property int notificationHistorySize: 50
// ── Toast timeouts (milliseconds) ─────────────────────────────────────────
property int notifTimeoutLow: 3500
property int notifTimeoutNormal: 5000
property int notifTimeoutCritical: 0
property int notifIconSize: 18
// ────────────────────────────────────────────────────────────────────────
// LAUNCHER
// ────────────────────────────────────────────────────────────────────────
property string launcherScript: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher.sh"
property string launcherCacheDir: "/tmp/qs_launcher"
}

3
config/qmldir Normal file
View File

@@ -0,0 +1,3 @@
module config
singleton Config 1.0 config.qml

View File

@@ -0,0 +1,159 @@
pragma Singleton
import QtQuick
import Quickshell
QtObject {
id: theme
// ── Wallpaper ──────────────────────────────────────────────────────────────
readonly property string wallpaper: Quickshell.env("HOME") + "/.config/quickshell/wallpaper.jpg"
// ── Base Backgrounds ───────────────────────────────────────────────────────
readonly property color bg: "#aa0a1410"
readonly property color bgPanel: "transparent"
readonly property color bgPopup: bg
readonly property color bgFrame: bg
// ── Separators ────────────────────────────────────────────────────────────
readonly property color separator: "#33ffffff"
// ── Text ──────────────────────────────────────────────────────────────────
readonly property color textMain: "#ffdce3de"
readonly property color textDim: "#d97a9480"
readonly property color todayText: "#ffffffff"
// ── Accent & Borders ──────────────────────────────────────────────────────
readonly property color accent: "#ff4a8c5c"
readonly property color border: "transparent"
readonly property color borderPopup: "#ff2d4032"
readonly property color borderToday: accent
// ── ArchLogo Module ────────────────────────────────────────────────────────
readonly property color archLogoBg: bgPanel
readonly property color archLogoBgHover: "#22ffffff"
readonly property color archLogoHover: accent
readonly property color archLogoBorder: border
readonly property color archLogoBorderActive: accent
readonly property color archLogoIcon: textMain
readonly property color archLogoIconActive: accent
// ── Stats Module ───────────────────────────────────────────────────────────
readonly property color statsBg: bgPanel
readonly property color statsBorder: border
readonly property color statsText: textMain
readonly property color statsRingColor: "#ff83c092"
readonly property color statsCpuColor: statsRingColor
readonly property color statsMemColor: statsRingColor
readonly property color statsDiskColor: statsRingColor
readonly property color statsGpuColor: statsRingColor
readonly property color statsTrackColor: "#28ffffff"
// ── Volume Module ──────────────────────────────────────────────────────────
readonly property color volBg: bgPanel
readonly property color volBgHover: "#22ffffff"
readonly property color volBorder: border
readonly property color volBorderHover: accent
readonly property color volPopupBg: bgPopup
readonly property color volPopupBorder: borderPopup
readonly property color volIcon: textMain
readonly property color volIconMuted: textDim
readonly property color volTrack: "#22ffffff"
readonly property color volHandle: accent
readonly property color volHandleBorder: "#33ffffff"
readonly property color volMuteBg: "#22ffffff"
readonly property color volPickerHover: "#15ffffff"
// ── Clock Module ───────────────────────────────────────────────────────────
readonly property color clockBg: bgPanel
readonly property color clockBgHover: "#22ffffff"
readonly property color clockBorder: border
readonly property color clockBorderHover: accent
readonly property color clockTime: textMain
readonly property color clockSeconds: textMain
readonly property color clockIcon: textMain
readonly property color clockSeparator: separator
readonly property color clockText: textMain
readonly property color clockShadow: "#22000000"
// ── Calendar Popup ─────────────────────────────────────────────────────────
readonly property color calCardBg: bgPopup
readonly property color calCardBorder: borderPopup
readonly property color calSeparator: separator
readonly property color calShadow: "#44000000"
readonly property color clockPopupHeader: accent
readonly property color clockPopupText: textMain
readonly property color clockPopupDim: textDim
// ── Workspace Module ───────────────────────────────────────────────────────
readonly property color wsPanelBg: bgPanel
readonly property color wsPanelBorder: "transparent"
readonly property color wsActiveBg: accent
readonly property color wsInactiveBg: "#22ffffff"
readonly property color wsHoverBg: borderPopup
readonly property color wsActiveText: "#ff0e1410"
readonly property color wsInactiveText: textMain
readonly property color wsHoverText: wsActiveText
// ── Tray Module ────────────────────────────────────────────────────────────
readonly property color trayBg: clockBg
readonly property color trayBgHover: clockBgHover
readonly property color trayBorder: clockBorder
readonly property color trayBorderHover: clockBorderHover
readonly property color trayIcon: clockIcon
// ── Launcher ───────────────────────────────────────────────────────────────
readonly property color launcherBg: bgPopup
readonly property color launcherBorder: "#22ffffff"
readonly property color launcherInput: "#330b1210"
readonly property color launcherInputBorderFocus: accent
readonly property color launcherPill: "#4d111a14"
readonly property color launcherText: textMain
readonly property color launcherDim: textDim
readonly property color launcherAccent: accent
readonly property color launcherHover: "#4d2d4032"
readonly property color launcherSelected: launcherHover
readonly property color launcherModeActiveText: todayText
readonly property color launcherModeActiveBg: accent
// ── Status Colors ──────────────────────────────────────────────────────────
readonly property color statusOk: "#ffa7c080"
readonly property color statusWarn: "#ffdbbc7f"
readonly property color statusErr: "#ffe67e80"
readonly property color statusInfo: "#ff7fbbb3"
// ── Notifications Module ───────────────────────────────────────────────────
readonly property color notifBg: bgPanel
readonly property color notifBgHover: "#22ffffff"
readonly property color notifBorder: border
readonly property color notifBorderActive: accent
readonly property color notifIcon: textMain
readonly property color notifIconActive: accent
readonly property color notifBadgeBg: statusErr
readonly property color notifBadgeText: "#ff0e1410"
readonly property color notifSeparator: separator
// ── Toast cards ────────────────────────────────────────────────────────────
readonly property color notifToastBg: bgPopup
readonly property color notifToastBgHover: borderPopup
readonly property color notifToastBorder: borderPopup
readonly property color notifToastShadow: "#55000000"
readonly property color notifToastText: textMain
readonly property color notifToastDim: textDim
readonly property color notifToastAppName: accent
// ── Urgency colours ────────────────────────────────────────────────────────
readonly property color notifUrgencyCritical: statusErr
readonly property color notifUrgencyNormal: statusInfo
readonly property color notifUrgencyLow: textDim
// ── Progress bar ───────────────────────────────────────────────────────────
readonly property color notifProgressBg: "#22ffffff"
readonly property color notifProgressFill: accent
// ── History popup ──────────────────────────────────────────────────────────
readonly property color notifHistoryBg: bgPopup
readonly property color notifHistoryBorder: borderPopup
readonly property color notifHistoryShadow: "#55000000"
readonly property color notifHistoryHover: "#22ffffff"
readonly property color notifHistoryEmpty: textDim
}

3
config/themes/qmldir Normal file
View File

@@ -0,0 +1,3 @@
module themes
singleton Everforest 1.0 everforest.qml

393
launcher/Launcher.qml Normal file
View File

@@ -0,0 +1,393 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Hyprland
import "../config" as Cfg
PanelWindow {
id: root
property bool isTop: true
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
readonly property int launcherGap: 12
GlobalShortcut {
id: launcherShortcut
name: "toggle"
onPressed: root.toggle()
}
property bool _isOpen: false
readonly property bool isOpen: _isOpen
property string mode: "drun"
property bool _keyboardNav: false
property var allItems: []
property bool loading: false
property bool proxyMode: false
readonly property var filteredItems: {
const q = searchInput.text.toLowerCase()
if (!q) return allItems.slice(0, 80)
return allItems.filter(i => i.name.toLowerCase().includes(q))
}
function toggle() { _isOpen ? close() : open() }
function open() {
closeTimer.stop()
visible = true
_isOpen = true
searchInput.text = ""
resultsList.currentIndex = -1
loadItems()
Qt.callLater(() => searchInput.forceActiveFocus())
}
function close() {
_isOpen = false
closeTimer.restart()
}
Connections {
target: root
function onActiveChanged() {
if (!root.active && root._isOpen) root.close()
}
}
function loadItems() {
loading = true
readCache()
updateProc.command = [Cfg.Config.launcherScript, "--update-only"]
updateProc.running = true
}
function readCache() {
const path = Cfg.Config.launcherCacheDir + "/" + (mode === "drun" ? "drun.txt" : "run.txt")
dataProc.rawOutput = ""
dataProc.command = ["bash", "-c", "cat " + path]
dataProc.running = true
}
function launch(item) {
if (!item || !item.exec) return
const cleanExec = item.exec.replace(/%[fFuUik]/g, "").trim()
const finalExec = root.proxyMode ? "proxychains4 " + cleanExec : cleanExec
const logPath = "/tmp/qml_launch.log"
launchProc.command = ["sh", "-c", finalExec + " > " + logPath + " 2>&1 &"]
launchProc.running = true
console.log("Launching: " + finalExec)
if (typeof close === "function") close()
}
onModeChanged: if (_isOpen) loadItems()
visible: false
color: "transparent"
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "main-shell-launcher"
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: _isOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
mask: Region { item: clipContainer }
Timer {
id: closeTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: {
root.visible = false
root.mode = "drun"
root.allItems = []
}
}
Process {
id: updateProc
onRunningChanged: if (!running) root.readCache()
}
Process {
id: dataProc
property string rawOutput: ""
stdout: SplitParser { onRead: data => dataProc.rawOutput += data + "\n" }
onRunningChanged: {
if (!running && rawOutput) {
const lines = rawOutput.trim().split("\n")
const items = []
for (let i = 0; i < lines.length; i++) {
const p = lines[i].split("\t")
if (p.length >= 3) items.push({ name: p[0], exec: p[1], id: p[2], icon: p[3] || "" })
}
root.allItems = items
root.loading = false
}
}
}
Process { id: launchProc }
Item {
id: clipContainer
anchors.horizontalCenter: parent.horizontalCenter
width: 460
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (root.barHeight + root.launcherGap) : 0
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + root.launcherGap)
height: parent.height - root.barHeight - root.launcherGap
clip: true
Rectangle {
id: bubble
width: parent.width
property int targetHeight: Math.min(layout.implicitHeight + 28, 540)
height: targetHeight
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: root._isOpen ? 0 : (root.isTop ? -bubble.height : bubble.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration;
easing.type: Easing.OutExpo
}
enabled: root.visible
}
}
opacity: root._isOpen ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200 } }
radius: Cfg.Config.popupRadius
color: t.launcherBg
border.color: t.launcherBorder
border.width: Cfg.Config.popupBorderWidth
focus: root._isOpen
Keys.onEscapePressed: root.close()
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: 14
spacing: 10
Rectangle {
Layout.fillWidth: true
height: 44
radius: 10
color: t.launcherInput
border.color: searchInput.activeFocus ? t.launcherInputBorderFocus : t.launcherBorder
Behavior on color { NumberAnimation { duration: 180 } }
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 8
spacing: 10
TextInput {
id: searchInput
Layout.fillWidth: true
color: t.launcherText
font.pixelSize: 15
verticalAlignment: TextInput.AlignVCenter
selectionColor: t.launcherAccent
onTextChanged: resultsList.currentIndex = -1
Keys.onReturnPressed: {
if (root.filteredItems.length > 0) {
let idx = resultsList.currentIndex;
if (idx === -1) idx = 0;
root.launch(root.filteredItems[idx]);
}
}
Keys.onDownPressed: {
root._keyboardNav = true
if (resultsList.currentIndex < 0) {
resultsList.currentIndex = 0;
} else {
resultsList.currentIndex = Math.min(resultsList.currentIndex + 1, resultsList.count - 1)
}
resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain)
}
Keys.onUpPressed: {
root._keyboardNav = true
if (resultsList.currentIndex < 0) {
resultsList.currentIndex = resultsList.count - 1;
} else {
resultsList.currentIndex = Math.max(resultsList.currentIndex - 1, 0)
}
resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain)
}
Keys.onTabPressed: root.mode = root.mode === "drun" ? "run" : "drun"
}
Rectangle {
height: 28
width: modeRow.implicitWidth + 10
radius: 8
color: t.launcherPill
border.color: t.launcherBorder
RowLayout {
id: modeRow
anchors.centerIn: parent
spacing: 2
Repeater {
model: ["drun", "run"]
delegate: Rectangle {
height: 22
width: label.implicitWidth + 14
radius: 6
color: root.mode === modelData ? t.launcherModeActiveBg : "transparent"
Text {
id: label
anchors.centerIn: parent
text: modelData
font.pixelSize: 10
font.bold: root.mode === modelData
color: root.mode === modelData ? t.launcherModeActiveText : t.launcherDim
}
MouseArea {
anchors.fill: parent
onClicked: root.mode = modelData
}
}
}
}
}
Rectangle {
id: proxyBtn
height: 28
width: 110
radius: 8
color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.22) : t.launcherPill
border.color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.65) : t.launcherBorder
border.width: 1
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
RowLayout {
id: proxyBtnRow
anchors.centerIn: parent
spacing: 5
Text {
text: "󰒃"
font.pixelSize: 11
color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim
Behavior on color { ColorAnimation { duration: 150 } }
}
Text {
text: "proxychains4"
font.pixelSize: 10
font.bold: root.proxyMode
color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim
Behavior on color { ColorAnimation { duration: 150 } }
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.proxyMode = !root.proxyMode
}
}
}
}
ListView {
id: resultsList
Layout.fillWidth: true
implicitHeight: Math.min(contentHeight, 400)
clip: true
model: root.filteredItems
currentIndex: -1
spacing: 2
onModelChanged: currentIndex = -1
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: Rectangle {
width: resultsList.width
height: 38
radius: 8
property bool hovered: false
color: hovered ? t.launcherHover
: (resultsList.currentIndex === index && index !== -1
? t.launcherSelected : "transparent")
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 12
Item {
width: 22; height: 22
Layout.alignment: Qt.AlignVCenter
Image {
id: appIcon
anchors.fill: parent
source: modelData.icon ? "image://icon/" + modelData.icon : ""
fillMode: Image.PreserveAspectFit
smooth: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !appIcon.visible
text: root.mode === "drun" ? "󰣆" : "󰆍"
color: resultsList.currentIndex === index && index !== -1 ? t.launcherAccent : t.launcherDim
font.pixelSize: 16
}
}
Text {
text: modelData.name
color: t.launcherText
font.pixelSize: 14
Layout.fillWidth: true
elide: Text.ElideRight
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: parent.hovered = true
onExited: parent.hovered = false
onClicked: root.launch(modelData)
}
}
}
}
}
}
}

105
launcher/launcher.sh Executable file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env bash
CACHE_DIR="/tmp/qs_launcher"
DRUN_CACHE="$CACHE_DIR/drun.txt"
RUN_CACHE="$CACHE_DIR/run.txt"
mkdir -p "$CACHE_DIR"
get_desktop_files() {
IFS=':' read -ra DIRS <<< \
"${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
for dir in "${DIRS[@]}"; do
APP_DIR="$dir/applications"
[ -d "$APP_DIR" ] || continue
find "$APP_DIR" \
-maxdepth 1 \
-name "*.desktop" \
2>/dev/null
done | while read -r f; do
name=$(
grep -m1 '^Name=' "$f" \
| cut -d= -f2-
)
exec=$(
grep -m1 '^Exec=' "$f" \
| cut -d= -f2- \
| sed 's/%[fFuUik]//g' \
| xargs
)
type=$(
grep -m1 '^Type=' "$f" \
| cut -d= -f2-
)
nodisplay=$(
grep -m1 '^NoDisplay=' "$f" \
| cut -d= -f2- \
| tr '[:upper:]' '[:lower:]'
)
icon=$(
grep -m1 '^Icon=' "$f" \
| cut -d= -f2-
)
[[ "$type" == "Application" ]] || continue
[[ "$nodisplay" != "true" ]] || continue
[[ -n "$name" ]] || continue
printf "%s\t%s\t%s\t%s\n" \
"$name" \
"$exec" \
"$(basename "$f" .desktop)" \
"$icon"
done
}
get_binaries() {
compgen -c \
| sort -u \
| head -600 \
| awk '{
print $1 "\t" $1 "\t" $1
}'
}
build_drun() {
get_desktop_files \
| sort -f -t$'\t' -k1,1 \
| awk -F'\t' '!seen[$1]++'
}
build_run() {
{
get_desktop_files
get_binaries
} \
| sort -f -t$'\t' -k1,1 \
| awk -F'\t' '!seen[$1]++'
}
update_cache() {
build_drun > "$DRUN_CACHE.tmp"
build_run > "$RUN_CACHE.tmp"
mv "$DRUN_CACHE.tmp" "$DRUN_CACHE"
mv "$RUN_CACHE.tmp" "$RUN_CACHE"
}
if [ "$1" == "--update-only" ]; then
update_cache
exit 0
fi

View File

@@ -0,0 +1,83 @@
import QtQuick
import QtQuick.Layouts
Rectangle {
id: root
property int itemIndex: 0
property string appName: ""
property string summary: ""
property string body: ""
property int urgency: 0
property string timeStr: ""
property var t
property var urgencyColor: function(u) { return "#ffffff" }
property var urgencyIcon: function(u) { return "" }
signal dismissed(int itemIndex)
height: histItemContent.implicitHeight + 24
radius: 12
clip: true
color: histItemHover.hovered ? root.t.notifHistoryHover : "transparent"
Behavior on color { ColorAnimation { duration: 150; easing.type: Easing.OutCubic } }
HoverHandler { id: histItemHover }
SequentialAnimation {
id: dismissAnim
ParallelAnimation {
NumberAnimation { target: root; property: "opacity"; to: 0; duration: 150; easing.type: Easing.OutCubic }
NumberAnimation { target: root; property: "scale"; to: 0.95; duration: 150; easing.type: Easing.OutCubic }
}
NumberAnimation { target: root; property: "height"; to: 0; duration: 150; easing.type: Easing.InOutQuad }
ScriptAction { script: root.dismissed(root.itemIndex) }
}
Rectangle {
width: 3
height: Math.max(12, root.height - 24)
radius: 1.5
anchors { left: parent.left; leftMargin: 10; verticalCenter: parent.verticalCenter }
color: root.urgencyColor(root.urgency)
}
Rectangle {
anchors { right: parent.right; top: parent.top; margins: 8 }
width: 22; height: 22; radius: 11
color: histCloseHover.hovered ? root.t.notifHistoryHover : "transparent"
opacity: histItemHover.hovered ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 150 } }
Behavior on color { ColorAnimation { duration: 150 } }
Text { anchors.centerIn: parent; text: "✕"; color: root.t.notifToastText; font.pixelSize: 10 }
HoverHandler { id: histCloseHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: dismissAnim.start() }
}
ColumnLayout {
id: histItemContent
anchors { left: parent.left; right: parent.right; top: parent.top; leftMargin: 24; rightMargin: 36; topMargin: 12 }
spacing: 4
RowLayout {
Layout.fillWidth: true; spacing: 5
Text { text: root.urgencyIcon(root.urgency); color: root.urgencyColor(root.urgency); font.pixelSize: 11; Layout.alignment: Qt.AlignVCenter }
Text {
text: root.appName; color: root.t.notifToastAppName; font.pixelSize: 11; font.bold: true;
font.capitalization: Font.AllUppercase; Layout.fillWidth: true; elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
}
Text { text: root.timeStr; color: root.t.notifToastDim; font.pixelSize: 11; verticalAlignment: Text.AlignVCenter }
}
Text {
text: root.summary; color: root.t.notifToastText; font.pixelSize: 13; font.bold: true;
Layout.fillWidth: true; elide: Text.ElideRight; textFormat: Text.PlainText; visible: root.summary !== ""
}
Text {
text: root.body; color: root.t.notifToastDim; font.pixelSize: 12; Layout.fillWidth: true;
wrapMode: Text.WordWrap; maximumLineCount: 2; elide: Text.ElideRight; textFormat: Text.PlainText; visible: root.body !== ""; lineHeight: 1.15
}
}
}

View File

@@ -0,0 +1,191 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Notifications
Item {
id: root
property string toastId: ""
property string appName: ""
property string summary: ""
property string body: ""
property int urgency: NotificationUrgency.Normal
property string timeStr: ""
property int timeout: 5000
property var t
property var urgencyColor: function(u) { return "#ffffff" }
property var urgencyIcon: function(u) { return "" }
signal dismissed(string toastId)
width: parent ? parent.width : 350
height: toastCard.height
property real progressVal: 1.0
Timer {
interval: root.timeout
running: root.timeout > 0
repeat: false
onTriggered: root.dismissed(root.toastId)
}
NumberAnimation on progressVal {
from: 1.0; to: 0.0
duration: root.timeout
running: root.urgency === NotificationUrgency.Low && root.timeout > 0
}
opacity: 0
scale: 0.90
Component.onCompleted: {
entranceOp.start()
entranceSc.start()
}
NumberAnimation {
id: entranceOp; target: root
property: "opacity"; from: 0; to: 1
duration: 240; easing.type: Easing.OutCubic
}
NumberAnimation {
id: entranceSc; target: root
property: "scale"; from: 0.90; to: 1.0
duration: 240; easing.type: Easing.OutBack
}
Rectangle {
id: toastCard
width: parent.width
height: toastContent.implicitHeight + (root.urgency === NotificationUrgency.Low ? 30 : 24)
radius: 16
color: cardHover.hovered ? root.t.notifToastBgHover : root.t.notifToastBg
border.color: root.urgency === NotificationUrgency.Critical
? root.t.notifUrgencyCritical
: (cardHover.hovered ? root.t.notifBorderActive : root.t.notifToastBorder)
border.width: 1
layer.enabled: true
Behavior on color { ColorAnimation { duration: 150; easing.type: Easing.OutCubic } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler { id: cardHover }
TapHandler { onTapped: root.dismissed(root.toastId) }
Rectangle {
width: 4
height: Math.max(16, toastCard.height - 24)
radius: 2
anchors {
left: parent.left
leftMargin: 10
verticalCenter: parent.verticalCenter
}
color: root.urgencyColor(root.urgency)
}
ColumnLayout {
id: toastContent
anchors {
left: parent.left
right: parent.right
top: parent.top
leftMargin: 24
rightMargin: 12
topMargin: 12
}
spacing: 4
RowLayout {
Layout.fillWidth: true
spacing: 6
Text {
text: root.urgencyIcon(root.urgency)
color: root.urgencyColor(root.urgency)
font.pixelSize: 12
Layout.alignment: Qt.AlignVCenter
}
Text {
text: root.appName
color: root.t.notifToastAppName
font.pixelSize: 11
font.bold: true
font.capitalization: Font.AllUppercase
Layout.fillWidth: true
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
Text {
text: root.timeStr
color: root.t.notifToastDim
font.pixelSize: 10
verticalAlignment: Text.AlignVCenter
}
Rectangle {
Layout.alignment: Qt.AlignVCenter
width: 20; height: 20; radius: 10
color: closeHover.hovered ? root.t.notifHistoryHover : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: "✕"
color: closeHover.hovered ? root.t.notifToastText : root.t.notifToastDim
font.pixelSize: 10
}
HoverHandler { id: closeHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: root.dismissed(root.toastId) }
}
}
Text {
text: root.summary
color: root.t.notifToastText
font.pixelSize: 14
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
textFormat: Text.PlainText
visible: root.summary !== ""
}
Text {
text: root.body
color: root.t.notifToastDim
font.pixelSize: 12
Layout.fillWidth: true
wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
textFormat: Text.PlainText
visible: root.body !== ""
Layout.bottomMargin: root.urgency === NotificationUrgency.Low ? 6 : 0
lineHeight: 1.15
}
}
Item {
visible: root.urgency === NotificationUrgency.Low && root.timeout > 0
anchors {
left: parent.left; right: parent.right; bottom: parent.bottom
margins: 10; bottomMargin: 10
}
height: 4
Rectangle {
anchors.fill: parent
radius: 2
color: root.t.notifProgressBg
}
Rectangle {
width: parent.width * root.progressVal
height: parent.height
radius: 2
color: root.t.notifProgressFill
}
}
}
}

247
shell.qml Normal file
View File

@@ -0,0 +1,247 @@
//@ pragma UseQApplication
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import "bar"
import "launcher"
import "widgets/bgDate"
import "config" as Cfg
ShellRoot {
id: root
readonly property bool isTop: Cfg.Config.barPosition === "top"
property color frameBackground: Cfg.Config.theme.bgFrame
property color frameBorderColor: Cfg.Config.theme.borderPopup
// ── Spacing Gap: Top / Bottom ────────────────────────────────────────
Variants {
model: Quickshell.screens
PanelWindow {
required property var modelData
screen: modelData
anchors {
top: !root.isTop
bottom: root.isTop
left: true
right: true
}
implicitHeight: Cfg.Config.margin
color: "transparent"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusiveZone: Cfg.Config.margin
mask: Region {}
}
}
// ── Spacing Gap: Left ────────────────────────────────────────────────
Variants {
model: Quickshell.screens
PanelWindow {
required property var modelData
screen: modelData
anchors { left: true; top: true; bottom: true }
implicitWidth: Cfg.Config.margin
color: "transparent"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusiveZone: Cfg.Config.margin
mask: Region {}
}
}
// ── Spacing Gap: Right ───────────────────────────────────────────────
Variants {
model: Quickshell.screens
PanelWindow {
required property var modelData
screen: modelData
anchors { right: true; top: true; bottom: true }
implicitWidth: Cfg.Config.margin
color: "transparent"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusiveZone: Cfg.Config.margin
mask: Region {}
}
}
// ── Wallpaper ────────────────────────────────────────────────────────
Variants {
model: Quickshell.screens
PanelWindow {
required property var modelData
screen: modelData
color: "transparent"
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.namespace: "main-shell-wallpaper"
WlrLayershell.exclusiveZone: -1
mask: Region {}
Image {
anchors.fill: parent
source: {
var p = Cfg.Config.theme.wallpaper
return p.startsWith("file://") ? p : "file://" + p
}
fillMode: Image.PreserveAspectCrop
asynchronous: true
}
}
}
// ── Center Clock (bgDate Widget) ─────────────────────────────────────
Variants {
model: Cfg.Config.enableBgDate ? Quickshell.screens : []
BgDate {
modelData: modelData
}
}
// ── Frame ────────────────────────────────────────────────────────────
Variants {
model: Quickshell.screens
PanelWindow {
required property var modelData
screen: modelData
color: "transparent"
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-frame"
WlrLayershell.exclusiveZone: -1
mask: Region {}
Canvas {
id: frameCanvas
anchors.fill: parent
// Animation Properties
opacity: 0
scale: 0.98
Component.onCompleted: startupAnimation.start()
ParallelAnimation {
id: startupAnimation
NumberAnimation {
target: frameCanvas; property: "opacity"
from: 0; to: 1; duration: 500
easing.type: Easing.OutCubic
}
NumberAnimation {
target: frameCanvas; property: "scale"
from: 0.98; to: 1.0; duration: 600
easing.type: Easing.OutBack
}
}
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var mT = root.isTop ? Cfg.Config.barHeight : Cfg.Config.margin
var mB = root.isTop ? Cfg.Config.margin : Cfg.Config.barHeight
var r = Cfg.Config.radius
var cw = width - Cfg.Config.margin * 2
var ch = height - mT - mB
var x = Cfg.Config.margin
var y = mT
ctx.fillStyle = root.frameBackground
ctx.fillRect(0, 0, width, mT)
ctx.fillRect(0, height - mB, width, mB)
ctx.fillRect(0, mT, x, ch)
ctx.fillRect(x + cw, mT, width - (x + cw), ch)
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x + r, y)
ctx.arc(x + r, y + r, r, -Math.PI / 2, Math.PI, true)
ctx.closePath()
ctx.fill()
ctx.beginPath()
ctx.moveTo(x + cw, y)
ctx.lineTo(x + cw - r, y)
ctx.arc(x + cw - r, y + r, r, -Math.PI / 2, 0, false)
ctx.closePath()
ctx.fill()
ctx.beginPath()
ctx.moveTo(x + cw, y + ch)
ctx.lineTo(x + cw, y + ch - r)
ctx.arc(x + cw - r, y + ch - r, r, 0, Math.PI / 2, false)
ctx.closePath()
ctx.fill()
ctx.beginPath()
ctx.moveTo(x, y + ch)
ctx.lineTo(x + r, y + ch)
ctx.arc(x + r, y + ch - r, r, Math.PI / 2, Math.PI, false)
ctx.closePath()
ctx.fill()
ctx.lineWidth = Cfg.Config.frameBorderWidth
ctx.strokeStyle = root.frameBorderColor
ctx.beginPath()
ctx.moveTo(x + r, y)
ctx.lineTo(x + cw - r, y)
ctx.arcTo(x + cw, y, x + cw, y + r, r)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(x + cw, y + r)
ctx.lineTo(x + cw, y + ch - r)
ctx.arcTo(x + cw, y + ch, x + cw - r, y + ch, r)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(x + cw - r, y + ch)
ctx.lineTo(x + r, y + ch)
ctx.arcTo(x, y + ch, x, y + ch - r, r)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(x, y + ch - r)
ctx.lineTo(x, y + r)
ctx.arcTo(x, y, x + r, y, r)
ctx.stroke()
}
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
Connections {
target: root
function onFrameBackgroundChanged() { frameCanvas.requestPaint() }
function onFrameBorderColorChanged() { frameCanvas.requestPaint() }
}
}
}
}
// ── Launcher ─────────────────────────────────────────────────────────
Variants {
model: Quickshell.screens
Launcher {
required property var modelData
screen: modelData
isTop: root.isTop
Component.onCompleted: modelData.launcher = this
}
}
// ── Bar ──────────────────────────────────────────────────────────────
Variants {
model: Quickshell.screens
Bar {
screen: modelData
isTop: root.isTop
}
}
}

BIN
wallpaper.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

34
widgets/bgDate/BgDate.qml Normal file
View File

@@ -0,0 +1,34 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "../../config" as Cfg
PanelWindow {
required property var modelData
screen: modelData
color: "transparent"
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusiveZone: -1
mask: Region {}
Text {
id: clockText
anchors.centerIn: parent
color: Cfg.Config.theme.clockText
styleColor: Cfg.Config.theme.clockShadow
font.pixelSize: 100
font.weight: Font.Thin
style: Text.Outline
text: new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: clockText.text = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
}
}