first commit
This commit is contained in:
8
README.md
Normal file
8
README.md
Normal 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
179
bar/Bar.qml
Normal 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
91
bar/modules/ArchLogo.qml
Normal 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
331
bar/modules/Clock.qml
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
357
bar/modules/Notifications.qml
Normal file
357
bar/modules/Notifications.qml
Normal 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
181
bar/modules/Stats.qml
Normal 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
161
bar/modules/Tray.qml
Normal 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
530
bar/modules/Volume.qml
Normal 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
62
bar/modules/Workspace.qml
Normal 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
87
config/config.qml
Normal 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
3
config/qmldir
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module config
|
||||||
|
|
||||||
|
singleton Config 1.0 config.qml
|
||||||
159
config/themes/everforest.qml
Normal file
159
config/themes/everforest.qml
Normal 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
3
config/themes/qmldir
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module themes
|
||||||
|
|
||||||
|
singleton Everforest 1.0 everforest.qml
|
||||||
393
launcher/Launcher.qml
Normal file
393
launcher/Launcher.qml
Normal 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
105
launcher/launcher.sh
Executable 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
|
||||||
83
notifications/HistoryCard.qml
Normal file
83
notifications/HistoryCard.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
191
notifications/NotificationCard.qml
Normal file
191
notifications/NotificationCard.qml
Normal 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
247
shell.qml
Normal 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
BIN
wallpaper.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
34
widgets/bgDate/BgDate.qml
Normal file
34
widgets/bgDate/BgDate.qml
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user