Compare commits

..

No commits in common. "master" and "main" have entirely different histories.
master ... main

59 changed files with 1261 additions and 1202 deletions

View File

@ -1,18 +0,0 @@
//@ pragma UseQApplication
import Quickshell
import QtQuick
import qs.modules.Bar
import qs.modules.ipc
import qs.modules.wallpaper
import qs.modules.widgets.wallpicker
import qs.modules.notifications
ShellRoot {
id: root
Bar {}
Ipc {}
Wallpaper {}
WallPicker {}
Notification {}
}

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
./Colors.qml

View File

@ -1,26 +1,29 @@
pragma Singleton pragma Singleton
import QtQuick import QtQuick
import Quickshell
QtObject { Singleton {
// --- The Backgrounds (Darkest to Lightest) --- id: customColors
readonly property string base00: "#1e1e2e" // Default Background // Core Backgrounds
readonly property string base01: "#181825" // Lighter Background (Status bars, panels) readonly property color background: "#24273A"
readonly property string base02: "#313244" // Selection Background readonly property color foreground: "#CAD3F5"
readonly property string base03: "#45475a" // Comments, Invisibles, line highlighting readonly property color cursor: "#CAD3F5"
// --- The Foregrounds (Darkest to Lightest) --- // The 16 Colors of the Apocalypse
readonly property string base04: "#585b70" // Dark Foreground (Used for status bars) readonly property color color0: "#494D64"
readonly property string base05: "#cdd6f4" // Default Foreground, Caret readonly property color color1: "#ED8796"
readonly property string base06: "#f5e0dc" // Light Foreground (Rarely used) readonly property color color2: "#A6DA95"
readonly property string base07: "#b4befe" // Lightest Foreground readonly property color color3: "#EED49F"
readonly property color color4: "#8AADF4"
// --- The Accent Colors --- readonly property color color5: "#F5BDE6"
readonly property string base08: "#f38ba8" // Red (Variables, errors) readonly property color color6: "#8BD5CA"
readonly property string base09: "#fab387" // Orange (Integers, booleans, constants) readonly property color color7: "#B8C0E0"
readonly property string base0A: "#f9e2af" // Yellow (Classes, search text bg, warnings) readonly property color color8: "#5B6078"
readonly property string base0B: "#a6e3a1" // Green (Strings, success states) readonly property color color9: "#ED8796"
readonly property string base0C: "#94e2d5" // Cyan (Support, regex, escape chars) readonly property color color10: "#A6DA95"
readonly property string base0D: "#89b4fa" // Blue (Functions, methods, headings) readonly property color color11: "#EED49F"
readonly property string base0E: "#cba6f7" // Purple/Mauve (Keywords, storage, selectors) readonly property color color12: "#8AADF4"
readonly property string base0F: "#f2cdcd" // Brown/Flamingo (Deprecated, embedded tags) readonly property color color13: "#F5BDE6"
readonly property color color14: "#8BD5CA"
readonly property color color15: "#A5ADCB"
} }

16
Icons.qml Normal file
View File

@ -0,0 +1,16 @@
import QtQuick
import "./modules/settings/"
Text {
property real fill
font.family: "Material Symbols Rounded"
property int grade: 20
color: Colors.foreground
font.pixelSize: 14
font.variableAxes: ({
FILL: fill.toFixed(1),
GRAD: grade,
opsz: Settings.fontSize,
wght: 500
})
}

6
config.json Normal file
View File

@ -0,0 +1,6 @@
{
"currentWall": "file:///home/lucy/.walls/faris.jpg",
"font": "MonaSpiceXe Nerd Font Propo",
"fontSize": 13,
"wallDir": "/home/lucy/.walls/"
}

View File

@ -1,67 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import Quickshell.Widgets
import QtQuick.Layouts
import qs.settings
import qs
Variants {
model: Quickshell.screens
delegate: PanelWindow {
id: root
required property ShellScreen modelData
aboveWindows: true
screen: modelData
anchors {
top: true
left: true
right: true
}
margins {
top: Settings.config.floating ? Settings.config.margins : 0
left: Settings.config.floating ? Settings.config.margins : 0
right: Settings.config.floating ? Settings.config.margins : 0
}
implicitHeight: Settings.config.barHeight
color: "transparent"
Rectangle {
id: bar
anchors.fill: parent
radius: Settings.config.floating ? Settings.config.rounding * 2 : 0
color: Colors.base00
RowLayout {
id: left
spacing: Settings.config.barSpacing
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
Ws {
barScreen: root.modelData
}
MPris {}
Title {}
}
RowLayout {
id: center
spacing: Settings.config.barSpacing
anchors {
centerIn: parent
}
Clock {}
}
RowLayout {
id: right
spacing: Settings.config.barSpacing
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: Settings.config.floating ? Settings.config.barmargins : 10
}
StatusIcons {}
Tray {}
}
}
}
}

View File

@ -1,39 +0,0 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import Qt5Compat.GraphicalEffects
import qs.settings
import qs
import qs.widgets
WrapperRectangle {
id: root
margin: Settings.config.barmargins
layer {
enabled: true
effect: DropShadow {
color: "#111111"
radius: 4
verticalOffset: 2
horizontalOffset: 2
samples: 18
}
}
color: Colors.base02
radius: Settings.config.rounding
implicitWidth: clockText.implicitWidth + 20
implicitHeight: Settings.config.barHeight - margin * 2
SystemClock {
id: clock
precision: SystemClock.Minutes
}
child: Item {
id: textWrap
CText {
id: clockText
text: Qt.formatDateTime(clock.date, "hh:mm")
anchors.centerIn: parent
}
}
}

View File

@ -1,55 +0,0 @@
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Widgets
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Layouts
import qs
import qs.settings
import qs.widgets
WrapperRectangle {
id: root
margin: Settings.config.barmargins
layer {
enabled: true
effect: DropShadow {
color: "#111111"
radius: 4
verticalOffset: 2
horizontalOffset: 2
samples: 18
}
}
color: Colors.base02
implicitWidth: songLayout.implicitWidth + 20
implicitHeight: Settings.config.barHeight - margin * 2
radius: Settings.config.rounding
property var spotify: root.getSpotify()
visible: getSpotify() == null ? false : true
function getSpotify() {
for (var i = 0; i < Mpris.players.values.length; i++) {
if (Mpris.players.values[i].identity == "Spotify" || Mpris.players.values[i] == "spotify") {
return Mpris.players.values[i];
} else {
return null;
}
}
return null;
}
child: Item {
RowLayout {
id: songLayout
anchors.centerIn: parent
CText {
id: playingSong
Layout.maximumWidth: 400
text: root.spotify == null ? "" : root.spotify.trackTitle + " - " + root.spotify.trackArtist
elide: Text.ElideRight
}
}
}
}

View File

@ -1,80 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Niri
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import qs
import qs.settings
import qs.widgets
Rectangle {
id: wsWrap
Niri {
id: niri
Component.onCompleted: connect()
onConnected: console.log("Connected to niri")
onErrorOccurred: function (error) {
console.error("Error:", error);
}
}
required property ShellScreen barScreen
color: "transparent"
radius: Settings.config.rounding
implicitWidth: wsLayout.implicitWidth + 6
implicitHeight: wsLayout.implicitHeight + 6
RowLayout {
id: wsLayout
spacing: 6
anchors.centerIn: parent
Repeater {
id: wsRep
model: niri.workspaces
delegate: Rectangle {
id: wsRect
layer {
enabled: true
effect: DropShadow {
color: Colors.base01
radius: 8
verticalOffset: 1
horizontalOffset: 1
samples: 18
}
}
implicitWidth: modelData.isFocused ? Settings.config.barHeight * 1.5 : Settings.config.barHeight / 2 + 10
implicitHeight: Settings.config.barHeight / 1.5
visible: modelData.id < 0 ? false : modelData.output == wsWrap.barScreen.name
required property var modelData
color: modelData.isFocused ? Colors.base0D : Colors.base02
radius: Settings.config.rounding
CText {
id: wsText
anchors.centerIn: parent
text: wsRect.modelData.index
color: parent.modelData.isFocused ? Colors.base01 : Colors.base07
opacity: parent.modelData.isFocused ? 1 : 0.5
}
Behavior on implicitWidth {
NumberAnimation {
easing {
type: Easing.OutBack
overshoot: 2
}
duration: 400
}
}
MouseArea {
id: mouseHandler
acceptedButtons: Qt.LeftButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
niri.focusWorkspace(wsRect.modelData.index);
}
}
}
}
}
}

View File

@ -1,103 +0,0 @@
import Quickshell.Services.UPower
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Layouts
import qs
import qs.settings
import qs.widgets
WrapperRectangle {
id: root
margin: Settings.config.barmargins
layer {
enabled: true
effect: DropShadow {
color: "#111111"
radius: 4
verticalOffset: 2
horizontalOffset: 2
samples: 18
}
}
color: Colors.base02
implicitWidth: iconLayout.implicitWidth + 14
implicitHeight: Settings.config.barHeight - margin * 2
radius: Settings.config.rounding
property var battery: UPower.displayDevice.isLaptopBattery ? UPower.displayDevice : null
property var percentage: UPower.displayDevice.isLaptopBattery ? UPower.displayDevice.percentage : null
property bool charging: UPower.displayDevice.isLaptopBattery ? UPower.displayDevice.state == UPowerDeviceState.Charging : null
property bool hasBattery: UPower.displayDevice.isLaptopBattery
property var audio: Pipewire.ready ? Pipewire.defaultAudioSink : ""
property var audioPercentage: Pipewire.ready ? Pipewire.defaultAudioSink.audio.volume : 0
property bool audioMute: Pipewire.ready ? Pipewire.defaultAudioSink.audio.muted : false
function getBatteryIcon() {
if (charging) {
return "\uf250";
}
if (percentage <= 0.12) {
return "\uf251";
}
if (percentage <= 0.24) {
return "\uf257";
}
if (percentage <= 0.36) {
return "\uf256";
}
if (percentage <= 0.48) {
return "\uf255";
}
if (percentage <= 0.60) {
return "\uf254";
}
if (percentage <= 0.72) {
return "\uf253";
}
if (percentage <= 0.84) {
return "\uf252";
}
if (percentage >= 0.84) {
return "\uf24f";
} else {
return "";
}
}
function getVolumeIcon() {
if (audioMute) {
return "\ue04f";
}
if (audioPercentage <= 0.33) {
return "\ue04e";
}
if (audioPercentage <= 0.66) {
return "\ue04d";
}
if (audioPercentage >= 0.66) {
return "\ue050";
} else {
return "";
}
}
child: Item {
RowLayout {
id: iconLayout
anchors.centerIn: parent
CIcon {
id: batteryIcon
Layout.leftMargin: 2
visible: root.hasBattery
text: root.getBatteryIcon()
}
CIcon {
id: volIcon
text: root.getVolumeIcon()
PwObjectTracker {
id: audioTracker
objects: Pipewire.ready ? Pipewire.defaultAudioSink : []
}
}
}
}
}

View File

@ -1,43 +0,0 @@
import Quickshell
import Qt5Compat.GraphicalEffects
import Quickshell.Wayland
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs
import qs.widgets
import qs.settings
WrapperRectangle {
id: root
margin: Settings.config.barmargins
layer {
enabled: true
effect: DropShadow {
color: "#111111"
radius: 4
verticalOffset: 2
horizontalOffset: 2
samples: 18
}
}
property var activeWindow: ToplevelManager.activeToplevel
property bool active: activeWindow ? activeWindow.activated ? true : false : false
radius: Settings.config.rounding
color: active ? Colors.base02 : "transparent"
implicitWidth: titleText.width + 40
implicitHeight: Settings.config.barHeight - margin * 2
child: Item {
RowLayout {
anchors.centerIn: parent
CText {
id: titleText
Layout.maximumWidth: 250
text: root.activeWindow ? root.activeWindow.activated ? root.activeWindow.title : "" : ""
elide: Text.ElideRight // Allows wrapping
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
}
}
Layout.alignment: Qt.AlignHCenter
}
}

View File

@ -1,39 +0,0 @@
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import QtQuick
import Qt5Compat.GraphicalEffects
import qs
import qs.settings
import QtQuick.Layouts
WrapperRectangle {
id: root
margin: Settings.config.barmargins
layer {
enabled: true
effect: DropShadow {
color: "#111111"
radius: 4
verticalOffset: 2
horizontalOffset: 2
samples: 18
}
}
implicitWidth: trayRow.implicitWidth + 14
implicitHeight: Settings.config.barHeight - margin * 2
visible: trayRep.count > 0
color: Colors.base02
radius: Settings.config.rounding
child: Item {
RowLayout {
id: trayRow
anchors.centerIn: parent
Repeater {
id: trayRep
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
model: SystemTray.items
delegate: TrayItem {}
}
}
}
}

View File

@ -1,76 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Layouts
import qs
import qs.settings
import qs.widgets
WrapperRectangle {
id: wsWrap
margin: Settings.config.barmargins
leftMargin: margin * 2
required property ShellScreen barScreen
color: "transparent"
radius: Settings.config.rounding
implicitWidth: wsLayout.implicitWidth + 6
implicitHeight: Settings.config.barHeight - margin * 2
child: Item {
RowLayout {
id: wsLayout
spacing: 6
anchors.centerIn: parent
Repeater {
id: wsRep
model: Hyprland.workspaces
delegate: Rectangle {
id: wsRect
layer {
enabled: true
effect: DropShadow {
color: "#111111"
radius: 0
verticalOffset: 2
horizontalOffset: 2
samples: 16
}
}
implicitWidth: modelData.focused ? Settings.config.barHeight * 1.5 : Settings.config.barHeight / 2 + 10
implicitHeight: Settings.config.barHeight - wsWrap.margin * 2
visible: modelData.id < 0 ? false : modelData.monitor?.name == wsWrap.barScreen.name
required property var modelData
color: modelData.focused ? Colors.base0D : Colors.base02
radius: Settings.config.rounding
CText {
id: wsText
anchors.centerIn: parent
text: wsRect.modelData.id
opacity: 1
color: parent.modelData.focused ? Colors.base00 : Colors.base05
}
Behavior on implicitWidth {
NumberAnimation {
easing {
type: Easing.OutBack
overshoot: 2
}
duration: 400
}
}
MouseArea {
id: mouseHandler
acceptedButtons: Qt.LeftButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
wsRect.modelData.activate();
}
}
}
}
}
}
}

46
modules/bar/Bar.qml Normal file
View File

@ -0,0 +1,46 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import "../../"
PanelWindow {
id: root
required property var modelData
implicitHeight: 36
color: Colors.background
anchors {
top: true
left: true
right: true
}
RowLayout {
id: leftLayout
spacing: 40
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Clock {
Layout.leftMargin: 30
}
Mpris {}
}
RowLayout {
id: centerLayout
anchors.centerIn: parent
Workspaces {}
}
RowLayout {
id: rightLayout
spacing: 40
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Battery {}
Volume {}
PowerProfiles {}
SystemTray {
Layout.rightMargin: 30
}
}
}

50
modules/bar/Battery.qml Normal file
View File

@ -0,0 +1,50 @@
pragma ComponentBehavior: Bound
import Quickshell.Services.UPower
import QtQuick
import "../../"
import "../settings/"
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
Item {
id: root
visible: UPower.displayDevice.isLaptopBattery
implicitWidth: masterLayout.implicitWidth
height: 34
property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging
ColumnLayout {
id: masterLayout
anchors.centerIn: parent
implicitWidth: botText.width
spacing: 0
Row {
spacing: 5
Text {
id: topText
font.weight: 900
font.family: Settings.font
font.pixelSize: Settings.fontSize
text: Math.round(UPower.displayDevice.percentage * 100) + "%"
color: Colors.foreground
}
IconImage {
anchors.verticalCenter: parent.verticalCenter
implicitSize: 12
source: Quickshell.iconPath(UPower.displayDevice.iconName)
}
}
Text {
id: botText
property var timeToEmpty: UPower.displayDevice.timeToEmpty / 60 / 60
property var timeToFull: UPower.displayDevice.timeToFull / 60 / 60
property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging
font.weight: 600
font.family: Settings.font
font.pixelSize: Settings.fontSize - 2
opacity: 0.7
color: Colors.foreground
text: isCharging ? timeToFull.toFixed(1) + "h to full" : timeToEmpty.toFixed(1) + "h left"
}
}
}

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

@ -0,0 +1,47 @@
import QtQuick
import Quickshell
import QtQuick.Layouts
import "../settings/"
import "../../"
Item {
id: root
implicitWidth: clockLayout.implicitWidth
implicitHeight: 35
ColumnLayout {
id: clockLayout
anchors.centerIn: parent
spacing: 0
Text {
id: clockHoursText
font.weight: 900
font.family: Settings.font
font.pixelSize: Settings.fontSize
color: Colors.foreground
text: Qt.formatDateTime(clockHours.date, "hh:mm")
SystemClock {
id: clockHours
precision: SystemClock.Minutes
}
}
Text {
id: clockDateText
font.weight: 900
opacity: 0.7
font.family: Settings.font
font.pixelSize: Settings.fontSize - 2
color: Colors.foreground
text: Qt.formatDateTime(clockDate.date, "dd.MM.yy")
SystemClock {
id: clockDate
precision: SystemClock.Minutes
}
}
}
}

110
modules/bar/Mpris.qml Normal file
View File

@ -0,0 +1,110 @@
// Ensure Colors is imported
// import "../../"
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Widgets
import "../settings/"
import "../../"
Item {
id: root
implicitWidth: mprisRepeater.implicitWidth + 10
implicitHeight: 34
// 1. Let Repeater loop through the ObjectModel for us
Repeater {
id: mprisRepeater
model: Mpris.players
delegate: Item {
id: delegateItem
required property var modelData
implicitHeight: 34
implicitWidth: delegateLayout.implicitWidth
MouseArea {
id: playbackControl
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button == Qt.LeftButton) {
console.log("Left button press");
}
if (mouse.button == Qt.RightButton) {
parent.modelData.togglePlaying();
}
}
onDoubleClicked: mouse => {
if (mouse.button == Qt.LeftButton) {
parent.modelData.next();
}
}
}
RowLayout {
id: delegateLayout
anchors.centerIn: parent
// 2. 🕵 FILTER LOGIC
// Check if this specific player is Spotify.
// We verify 'modelData' exists and check the name.
property bool isSpotify: delegateItem.modelData && delegateItem.modelData.identity.toLowerCase().includes("spotify")
// 3. 👻 HIDE NON-SPOTIFY PLAYERS
visible: isSpotify
// If hidden, take up ZERO space
Layout.preferredWidth: isSpotify ? Math.min(implicitWidth, 400) : 0
Layout.fillHeight: true
property string title: delegateItem.modelData.trackTitle
property string artist: delegateItem.modelData.trackArtist
property string artUrl: delegateItem.modelData.trackArtUrl
property bool isPlaying: delegateItem.modelData.isPlaying
spacing: 10
// 🖼 ALBUM ART
ClippingWrapperRectangle {
Layout.alignment: Qt.AlignVCenter
radius: 20
IconImage {
source: delegateLayout.artUrl // Access property from delegate
asynchronous: true
implicitSize: root.implicitHeight * 0.6
}
}
// 📝 TEXT INFO
ColumnLayout {
spacing: 0
visible: parent.visible
Text {
text: delegateLayout.title
color: Colors.foreground
font.bold: true
font.pixelSize: Settings.fontSize
font.family: Settings.font
elide: Text.ElideRight
Layout.preferredWidth: implicitWidth
}
Text {
font.pixelSize: Settings.fontSize - 2
font.family: Settings.font
text: delegateLayout.artist
color: Colors.foreground
opacity: 0.7
Layout.preferredWidth: implicitWidth
}
}
}
}
}
}

View File

@ -0,0 +1,48 @@
import QtQuick
import Quickshell.Services.UPower
import QtQuick.Layouts
import "../settings/"
import "../../"
Item {
id: root
width: powerLayout.implicitWidth
implicitHeight: 34
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
const modes = [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance];
let current = PowerProfiles.profile;
let currentIndex = modes.indexOf(current);
let nextIndex = (currentIndex + 1) % modes.length;
let prevIndex = (currentIndex - 1) % modes.length;
if (mouse.button == Qt.LeftButton)
PowerProfiles.profile = modes[nextIndex];
if (mouse.button == Qt.RightButton)
PowerProfiles.profile = modes[prevIndex];
}
}
ColumnLayout {
id: powerLayout
anchors.centerIn: parent
spacing: 0
Text {
id: powerProfile
text: PowerProfile.toString(PowerProfiles.profile)
font.weight: 900
color: Colors.foreground
font.family: Settings.font
font.pixelSize: Settings.fontSize
}
Text {
text: "Profile"
font.weight: 900
color: Colors.foreground
font.family: Settings.font
font.pixelSize: Settings.fontSize - 2
opacity: 0.7
}
}
}

View File

@ -0,0 +1,23 @@
import Quickshell.Services.SystemTray
import QtQuick
Item {
id: root
clip: true
implicitWidth: layout.implicitWidth < 0 ? 0 : layout.implicitWidth
implicitHeight: 34
visible: layout.children.length > 0
Row {
id: layout
anchors.centerIn: parent
spacing: 10 // Spacing between icons
Repeater {
model: SystemTray.items
delegate: TrayItem {}
}
}
}

View File

@ -2,24 +2,16 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets import Quickshell.Widgets
import qs
MouseArea { MouseArea {
id: root id: root
property var bar: root.QsWindow.window
required property SystemTrayItem modelData required property SystemTrayItem modelData
implicitWidth: trayIcon.implicitWidth
implicitHeight: trayIcon.implicitHeight
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
IconImage { implicitWidth: 16
id: trayIcon implicitHeight: 16
implicitSize: 16
source: parent.modelData.icon
}
QsMenuAnchor {
id: menu
menu: root.modelData.hasMenu ? root.modelData.menu : null
anchor.item: root
}
onClicked: event => { onClicked: event => {
if (event.button === Qt.LeftButton) { if (event.button === Qt.LeftButton) {
modelData.activate(); modelData.activate();
@ -27,4 +19,18 @@ MouseArea {
menu.open(); menu.open();
} }
} }
QsMenuAnchor {
id: menu
menu: root.modelData.menu
anchor.item: root
}
IconImage {
id: trayIcon
width: parent.implicitWidth
height: parent.implicitHeight
source: root.modelData.icon
anchors.centerIn: parent
}
} }

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

@ -0,0 +1,93 @@
import QtQuick
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import "../../"
import "../settings/"
Item {
id: root
implicitWidth: styleLayout.implicitWidth
height: 34
property var sink: Pipewire.defaultAudioSink
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
pavu.startDetached();
}
}
}
Process {
id: pavu
command: ["pavucontrol"] // The command and args list
}
// Logic to pick the correct icon name
function getVolumeIcon() {
// Safety check: if Pipewire is dead or sink is missing
if (!sink)
return "audio-volume-muted-symbolic";
// If muted, show the hush icon
if (sink.audio.muted)
return "audio-volume-muted-symbolic";
// Volume is usually 0.0 to 1.0 (0% to 100%)
const vol = sink.audio.volume;
if (vol <= 0.25)
return "audio-volume-low-symbolic";
if (vol < 0.75)
return "audio-volume-medium-symbolic";
if (vol <= 1.00)
return "audio-volume-high-symbolic";
// If it's loud, prepare the ears!
return "audio-volume-high-danger-symbolic";
}
ColumnLayout {
id: styleLayout
anchors.centerIn: parent
spacing: 0
implicitWidth: topText.width
Row {
spacing: 5
Text {
id: topText
anchors.verticalCenter: parent.verticalCenter
PwObjectTracker {
objects: Pipewire.ready ? root.sink : []
}
font.weight: 900
color: Colors.foreground
font.family: Settings.font
font.pixelSize: Settings.fontSize
text: Pipewire.ready ? root.sink.audio.volume.toFixed(2) + "%" : "0%"
onTextChanged: console.log(Quickshell.iconPath)
}
IconImage {
id: icon
anchors.verticalCenter: parent.verticalCenter
implicitSize: 12
source: Quickshell.iconPath(root.getVolumeIcon())
}
}
Text {
id: botText
font.weight: 900
color: Colors.foreground
font.family: Settings.font
font.pixelSize: Settings.fontSize - 2
opacity: 0.7
text: Pipewire.ready ? Pipewire.defaultAudioSink.nickname : "failure"
}
}
}

View File

@ -0,0 +1,47 @@
pragma ComponentBehavior: Bound
import Quickshell.Hyprland
import QtQuick
import "../../"
import "../settings/"
Item {
id: root
implicitWidth: workspaceRow.implicitWidth
height: 30
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: 10 // Slightly increase spacing between workspace buttons
Repeater {
id: wsRepeater
model: Hyprland.workspaces
anchors.centerIn: parent
Rectangle {
id: workspaceNumber
required property var modelData
width: 16
height: 16
radius: 20
color: modelData.active ? Colors.foreground : "transparent"
Text {
font.weight: 900
font.family: Settings.font
font.pixelSize: Settings.fontSize
anchors.centerIn: workspaceNumber
text: parent.modelData.id
color: parent.modelData.active ? Colors.background : Colors.foreground // Set contrasting color for workspace number
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: {
parent.modelData.activate();
}
}
}
}
}
}

View File

@ -1,14 +0,0 @@
import Quickshell
import QtQuick
import Quickshell.Io
import qs
import qs.settings
Item {
IpcHandler {
target: "settings"
function toggleWall() {
Settings.config.wallswitchershown = !Settings.config.wallswitchershown;
}
}
}

View File

@ -0,0 +1,172 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import "."
import "../../"
import QtQuick.Layouts
import Quickshell.Widgets
import "../settings/"
WlrLayershell {
id: root
required property var modelData
screen: {
// Iterate through all connected Quickshell screens
for (let i = 0; i < Quickshell.screens.length; i++) {
let screenCandidate = Quickshell.screens[i];
// Ask: "Is this screen the one Hyprland is currently focusing?"
if (Hyprland.monitorFor(screenCandidate) === Hyprland.focusedMonitor) {
return screenCandidate;
}
}
return null; // Fallback (should rarely happen)
}
// 1. Position: Top Right Corner, covering the full height
// We make it a fixed width (e.g., 400px) so it doesn't block the whole screen
anchors {
top: true
right: true
}
margins {
top: 36
right: 00
}
implicitWidth: 300
implicitHeight: notifList.contentHeight + 20
Behavior on implicitHeight {
NumberAnimation {
duration: 300
easing.type: Easing.OutQuad
}
}
// 2. Layer: Put it ABOVE normal windows
layer: WlrLayer.Overlay
exclusionMode: ExclusionMode.Ignore
// 3. CRITICAL: Make the window itself invisible!
// We only want to see the
// notifications, not the container.
color: "transparent"
// 4. Input: Let clicks pass through empty areas
// (This is default behavior if the background is transparent in some compositors,
// but usually you need to be careful with handling mouse events here)
// THE SPAWNER
ListView {
id: notifList
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 0
// Use 'spacing' to put gaps between notifications
spacing: 00
height: contentHeight
model: NotifServer.trackedNotifications
delegate: Item {
id: notifyItem
required property var index
readonly property bool isLast: index === (ListView.view.count - 1)
implicitWidth: ListView.view.width
implicitHeight: 85 // Fixed height is usually better for icon layouts
height: implicitHeight
required property var modelData
Timer {
id: timout
interval: 3000
running: true
onTriggered: notifyItem.modelData.dismiss()
}
Rectangle {
anchors.fill: parent
color: Colors.background
bottomLeftRadius: notifyItem.isLast ? 20 : 0
border.color: Colors.color5
border.width: 0
// 2. Use RowLayout to put Image | Text side-by-side
RowLayout {
id: fullLayout
anchors.margins: 10
anchors.fill: parent
spacing: 10
// 🖼 THE IMAGE ON THE LEFT
ClippingWrapperRectangle {
radius: 10
implicitWidth: 64
implicitHeight: 64
visible: notifyItem.modelData.image !== ""
IconImage {
// Use the image if available, otherwise hide this space?
// Or you could use an icon fallback.
source: notifyItem.modelData.image
// Hide if no image exists so text takes full width
visible: notifyItem.modelData.image !== ""
// Fixed size for consistency
implicitSize: 30
// Crop it nicely so it doesn't stretch
// Optional: Cache it for performance
asynchronous: true
}
}
// 📝 THE TEXT ON THE RIGHT
ColumnLayout {
id: textLayout
// Take up all remaining width
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter // Center vertically
spacing: 2
Text {
text: notifyItem.modelData.summary
color: Colors.foreground
font.family: Settings.font
font.pixelSize: Settings.fontSize
font.bold: true
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: notifyItem.modelData.body
color: Colors.foreground
// Limit to 2 lines
font.family: Settings.font
font.pixelSize: Settings.fontSize - 2
maximumLineCount: 3
wrapMode: Text.WordWrap
elide: Text.ElideRight
Layout.fillWidth: true
}
}
}
// (Your MouseArea for closing can still go here covering the whole thing)
MouseArea {
anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
acceptedButtons: Qt.LeftButton
onClicked: notifyItem.modelData.dismiss()
}
}
}
}
}

View File

@ -1,73 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.settings
Variants {
model: Quickshell.screens
delegate: WlrLayershell {
id: root
required property var modelData
screen: modelData
anchors {
top: true
right: true
bottom: true
}
margins {
top: Settings.config.floating ? Settings.config.barHeight + Settings.config.margins + 10 : Settings.config.barHeight + 10
right: 10
left: 10
}
mask: Region {
item: notifList
}
implicitHeight: notifList.contentHeight + 20
implicitWidth: modelData.width / 6
layer: WlrLayer.Overlay
exclusionMode: ExclusionMode.Ignore
color: "transparent"
ListView {
id: notifList
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 10
height: contentHeight
model: NotiServer.trackedNotifications
delegate: NotificationCard {}
add: Transition {
NumberAnimation {
property: "x"
from: notifList.width
to: 0
duration: 400
easing.type: Easing.OutExpo
}
}
remove: Transition {
NumberAnimation {
property: "x"
from: 0
to: notifList.width
duration: 400
easing.type: Easing.OutExpo
}
}
move: Transition {
NumberAnimation {
properties: "y"
duration: 300
}
}
}
}
}

View File

@ -1,99 +0,0 @@
import QtQuick
import qs.settings
import QtQuick.Layouts
import qs
import Quickshell
import qs.widgets
import Quickshell.Widgets
Rectangle {
id: notifyItem
required property var modelData
implicitWidth: ListView.view ? ListView.view.width : 500
implicitHeight: fullLayout.implicitHeight + 40
color: dismissArea.containsMouse ? Colors.base02 : Colors.base00
radius: Settings.config.rounding
border.width: 2
border.color: Colors.base0D
Timer {
id: dismissTimer
interval: 5000
running: true
onTriggered: notifyItem.modelData.expire()
}
RowLayout {
id: fullLayout
anchors.margins: 20
anchors.fill: parent
spacing: 10
ColumnLayout {
id: textLayout
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: 0
// New RowLayout to hold the Icon and App Name together
RowLayout {
id: iconTextLayout
spacing: 8
ClippingWrapperRectangle {
id: notiIconWrapper
radius: notifyItem.radius - notifyItem.radius / 3
implicitWidth: notiIcon.implicitSize
implicitHeight: notiIcon.implicitSize
color: "transparent"
child: IconImage {
id: notiIcon
// Keep your existing source logic
source: notifyItem.modelData.image !== "" ? notifyItem.modelData.image : Quickshell.iconPath("/usr/share/icons/Papirus/24x24/panel/notifications.svg")
implicitSize: 22 // Slightly smaller to match text height
asynchronous: true
}
}
CText {
id: appName
text: notifyItem.modelData.appName
opacity: 0.5
font.pixelSize: 10
}
}
ColumnLayout {
spacing: 0
Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: notiIcon.implicitWidth + iconTextLayout.spacing
CText {
id: summary
text: notifyItem.modelData.summary
font.bold: true
elide: Text.ElideRight
Layout.fillWidth: true
Layout.topMargin: 5
}
CText {
text: notifyItem.modelData.body
font.pixelSize: Settings.config.fontSize - 2
maximumLineCount: 1
opacity: 0.3
wrapMode: Text.WordWrap
elide: Text.ElideRight
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
}
}
}
}
MouseArea {
id: dismissArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: notifyItem.modelData.dismiss()
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}

View File

@ -0,0 +1,2 @@
singleton NotifServer 1.0 NotifServer.qml
NotiPopup 1.0 NotiPopup.qml

View File

@ -0,0 +1,36 @@
pragma ComponentBehavior: Bound
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
property alias currentWall: jsonAdapter.currentWall
property alias font: jsonAdapter.font
property alias fontSize: jsonAdapter.fontSize
property alias wallDir: jsonAdapter.wallDir
onCurrentWallChanged: settingsView.writeAdapter()
onWallDirChanged: settingsView.writeAdapter()
onFontChanged: {
Quickshell.reload();
settingsView.writeAdapter();
}
onFontSizeChanged: settingsView.writeAdapter()
FileView {
id: settingsView
path: "/home/lucy/.config/quickshell/modules/settings/config.json"
watchChanges: true
onAdapterChanged: reload()
onAdapterUpdated: reload()
adapter: JsonAdapter {
id: jsonAdapter
property string currentWall: ""
property string wallDir: "/home/lucy/.walls/"
property string font: "Google Sans Code"
property real fontSize: 14
}
}
}

View File

@ -0,0 +1,6 @@
{
"currentWall": "file:///home/lucy/.walls/lain_room.png",
"font": "Google Sans Code",
"fontSize": 14,
"wallDir": "/home/lucy/.walls/"
}

1
modules/settings/qmldir Normal file
View File

@ -0,0 +1 @@
singleton Settings 1.0 Settings.qml

View File

@ -1,51 +0,0 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import qs
import qs.settings
import qs.widgets
Item {
id: root
property int gaps: 10
implicitWidth: wrapper.width + gaps
implicitHeight: wrapper.height + gaps
ClippingWrapperRectangle {
id: wrapper
layer {
enabled: true
effect: DropShadow {
color: "#111111"
horizontalOffset: 7
verticalOffset: 8
radius: 12
samples: 14
}
}
SystemClock {
id: clock
precision: SystemClock.Minutes
}
color: Colors.base01
radius: Settings.config.rounding
anchors.centerIn: parent
margin: 10
rightMargin: 15
child: Column {
id: dataLayout
spacing: 0
anchors.margins: 0
CText {
text: Qt.formatDateTime(clock.date, "hh:mm")
font.pixelSize: 48
}
CText {
text: Qt.formatDateTime(clock.date, "dd.MM.yy")
opacity: 0.6
font.pixelSize: 24
}
}
}
}

View File

@ -0,0 +1,66 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import "../../"
import "."
WlrLayershell {
id: overlayRoot
required property var modelData
property var padding: 5
property var rounding: 25
property var hyprgaps: 5
onPaddingChanged: {
hyprGaps.exec(hyprGaps.command);
console.log(hyprGaps.command);
}
Process {
id: hyprGaps
running: true
property bool isZero: overlayRoot.padding === 0
property var top: overlayRoot.hyprgaps
property var sides: isZero ? overlayRoot.hyprgaps : overlayRoot.padding + overlayRoot.hyprgaps
property var gaps: top + "," + sides + "," + sides + "," + sides
command: ["hyprctl", "keyword", "general:gaps_out", gaps]
onStarted: console.log("set gaps to ", gaps)
}
Process {
id: hyprRounding
property var rounding: overlayRoot.rounding - 4
running: true
command: ["hyprctl", "keyword", "decoration:rounding", rounding]
onStarted: console.log("set rounding to ", overlayRoot.rounding)
}
// 1. Fill the entire screen
anchors {
top: true
bottom: true
left: true
right: true
}
// 2. Sit on top of EVERYTHING (even fullscreen apps if compositor allows)
layer: WlrLayer.Top
// 3. Invisible background
color: "transparent"
// 4. 👻 GHOST MODE ENABLED 👻
// An empty Region means "I accept mouse events nowhere".
// This guarantees you can click through the black corners.
mask: Region {}
// 5. Load the corners!
ScreenPadding {
paddingWidth: overlayRoot.padding
paddingColor: Colors.background
}
ScreenCorners {
// Adjust these to match your screen's aesthetic
cornerRadius: overlayRoot.rounding
margin: overlayRoot.padding
cornerColor: Colors.background
}
}

View File

@ -1,218 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell.Services.Mpris
import Quickshell.Widgets
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Layouts
import QtQuick.Controls
import qs
import qs.settings
import qs.widgets
Rectangle {
id: root
color: Colors.base00
radius: Settings.config.rounding
implicitWidth: 600
implicitHeight: 200
visible: getSpotify() != null
layer {
enabled: true
effect: DropShadow {
color: "#111111"
horizontalOffset: 7
verticalOffset: 8
radius: 12
samples: 14
}
}
MouseArea {
id: hoverDetect
hoverEnabled: true
anchors.fill: parent
onExited: title.x = 0
}
function getSpotify() {
for (var i = 0; i < Mpris.players.values.length; i++) {
if (Mpris.players.values[i].identity == "Spotify" || Mpris.players.values[i] == "spotify") {
return Mpris.players.values[i];
} else {
return null;
}
}
return null;
}
property var spotify: getSpotify() != null ? getSpotify() : null
property var title: getSpotify() != null ? getSpotify().trackTitle : ""
property var album: getSpotify() != null ? getSpotify().trackAlbum : ""
property var art: getSpotify() != null ? getSpotify().trackArtUrl : ""
property var artist: getSpotify() != null ? getSpotify().trackArtist : ""
ClippingWrapperRectangle {
id: songWrapper
radius: Settings.config.rounding / 1.5
anchors.margins: 8
margin: 0
anchors.fill: parent
color: Colors.base00
RowLayout {
id: songLayout
spacing: 10
ClippingWrapperRectangle {
id: coverRounder
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: songCover.sourceSize.width
radius: Settings.config.rounding / 1.5
Image {
id: songCover
source: root.art
sourceSize {
width: 180
height: 180
}
}
}
WrapperRectangle {
color: Colors.base01
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
radius: Settings.config.rounding / 1.5
margin: 20
child: ColumnLayout {
id: songInfo
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 20
Layout.topMargin: 2
Item {
id: titleContainer
Layout.fillWidth: true
x: 0
implicitHeight: title.implicitHeight
clip: true // Keeps the text inside this "window"
CText {
id: title
Layout.maximumWidth: 300
text: root.title
font.pixelSize: 30
SequentialAnimation on x {
id: scrollAnimation
running: hoverDetect.containsMouse && title.width > titleContainer.width
loops: Animation.Infinite
// Scroll to the end
NumberAnimation {
to: titleContainer.width - title.width
duration: Math.max(2000, (title.width - titleContainer.width) * 30)
easing.type: Easing.InOutQuad
}
// Scroll back to the start
NumberAnimation {
to: 0
duration: Math.max(2000, (title.width - titleContainer.width) * 30)
easing.type: Easing.InOutQuad
}
}
}
}
CText {
id: album
text: root.album + " - " + root.artist
opacity: 0.6
Layout.maximumWidth: 250
Layout.alignment: Qt.AlignTop
elide: Text.ElideRight
}
ProgressBar {
id: songProgress
FrameAnimation {
// only emit the signal when the position is actually changing.
running: root.spotify ? root.spotify.playbackState == MprisPlaybackState.Playing || root.visible : false
// emit the positionChanged signal every frame.
onTriggered: root.spotify.positionChanged()
}
implicitWidth: 200
implicitHeight: 10
from: 0
to: root.spotify != null ? root.spotify.length : 0
value: root.spotify != null ? root.spotify.position : 0
background: Rectangle {
implicitWidth: 200
implicitHeight: 6
color: Colors.base02
radius: Settings.config.rounding
}
contentItem: Item {
implicitWidth: 200
implicitHeight: 4
// Progress indicator for determinate state.
Rectangle {
width: songProgress.visualPosition * parent.width
height: parent.height
radius: Settings.config.rounding
color: Colors.base07
visible: !songProgress.indeterminate
}
}
}
RowLayout {
id: playerControls
Layout.maximumWidth: 200
CIcon {
id: previous
text: "\ue045"
Layout.alignment: Qt.AlignLeft
MouseArea {
id: prevHandler
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: {
root.spotify.previous();
title.x = 0;
}
}
}
CIcon {
id: pause
text: root.spotify ? root.spotify.isPlaying ? "\ue034" : "\ue037" : ""
Layout.alignment: Qt.AlignHCenter
MouseArea {
id: pauseHandler
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: {
root.spotify.togglePlaying();
title.x = 0;
}
}
}
CIcon {
id: next
Layout.alignment: Qt.AlignRight
text: "\ue044"
MouseArea {
id: nextHandler
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: {
root.spotify.next();
title.x = 0;
}
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,177 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
Item {
id: root
anchors.fill: parent
// ---------------------------------------------------------
// 🛠 CONFIGURATION (Tweaked to match your setup)
// ---------------------------------------------------------
// How round do you want the screen?
property real cornerRadius: 20
// What color should the corners be? (Usually black to match the bezel)
// You can change this to "transparent" or a theme color if you want.
property color cornerColor
// Enable/Disable toggle
property bool shouldShow: true
property real margin
// ---------------------------------------------------------
// Wrapper with layer caching to reduce GPU usage
Item {
anchors.fill: parent
layer.enabled: true
Shape {
id: cornersShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
enabled: false // Click-through
ShapePath {
id: cornersPath
// Map our local properties to the variables the code expects
readonly property real cornerRadius: root.cornerRadius
readonly property real cornerSize: root.cornerRadius // Usually same as radius
// Margins (Leave 0 unless your bar overlaps)
readonly property real topMargin: 0
readonly property real bottomMargin: root.margin
readonly property real leftMargin: root.margin
readonly property real rightMargin: root.margin
readonly property real screenWidth: cornersShape.width
readonly property real screenHeight: cornersShape.height
strokeWidth: -1 // No outline
fillColor: root.cornerColor
// Smooth fade if you toggle it
// ==========================================
// 📐 GEOMETRY LOGIC (Untouched)
// ==========================================
// Top-Left
startX: leftMargin
startY: topMargin
PathLine {
relativeX: cornersPath.cornerSize
relativeY: 0
}
PathLine {
relativeX: 0
relativeY: cornersPath.cornerSize - cornersPath.cornerRadius
}
PathArc {
relativeX: -cornersPath.cornerRadius
relativeY: cornersPath.cornerRadius
radiusX: cornersPath.cornerRadius
radiusY: cornersPath.cornerRadius
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius)
relativeY: 0
}
PathLine {
relativeX: 0
relativeY: -cornersPath.cornerSize
}
// Top-Right
PathMove {
x: cornersPath.screenWidth - cornersPath.rightMargin - cornersPath.cornerSize
y: cornersPath.topMargin
}
PathLine {
relativeX: cornersPath.cornerSize
relativeY: 0
}
PathLine {
relativeX: 0
relativeY: cornersPath.cornerSize
}
PathLine {
relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius)
relativeY: 0
}
PathArc {
relativeX: -cornersPath.cornerRadius
relativeY: -cornersPath.cornerRadius
radiusX: cornersPath.cornerRadius
radiusY: cornersPath.cornerRadius
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius)
}
// Bottom-Left
PathMove {
x: cornersPath.leftMargin
y: cornersPath.screenHeight - cornersPath.bottomMargin - cornersPath.cornerSize
}
PathLine {
relativeX: cornersPath.cornerSize - cornersPath.cornerRadius
relativeY: 0
}
PathArc {
relativeX: cornersPath.cornerRadius
relativeY: cornersPath.cornerRadius
radiusX: cornersPath.cornerRadius
radiusY: cornersPath.cornerRadius
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: cornersPath.cornerSize - cornersPath.cornerRadius
}
PathLine {
relativeX: -cornersPath.cornerSize
relativeY: 0
}
PathLine {
relativeX: 0
relativeY: -cornersPath.cornerSize
}
// Bottom-Right
PathMove {
x: cornersPath.screenWidth - cornersPath.rightMargin
y: cornersPath.screenHeight - cornersPath.bottomMargin
}
PathLine {
relativeX: -cornersPath.cornerSize
relativeY: 0
}
PathLine {
relativeX: 0
relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius)
}
PathArc {
relativeX: cornersPath.cornerRadius
relativeY: -cornersPath.cornerRadius
radiusX: cornersPath.cornerRadius
radiusY: cornersPath.cornerRadius
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: cornersPath.cornerSize - cornersPath.cornerRadius
relativeY: 0
}
PathLine {
relativeX: 0
relativeY: cornersPath.cornerSize
}
}
}
}
}

View File

@ -0,0 +1,67 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
Item {
id: root
// Make sure this fills the screen!
anchors.fill: parent
property real paddingWidth // Example default
property color paddingColor
Shape {
anchors.fill: parent
// 1. LEFT PADDING (Your existing one, cleaned up)
ShapePath {
strokeWidth: root.paddingWidth * 2
strokeColor: root.paddingColor
fillColor: "transparent" // We only want the stroke
// Start at Top-Left (x=0, y=0)
// We use '0' to align center with edge, so half is in, half is out
startX: 0
startY: 0
PathLine {
x: root.paddingWidth - root.paddingWidth
y: root.height + root.paddingWidth// Go to Bottom-Left
}
}
// 2. RIGHT PADDING
ShapePath {
strokeWidth: root.paddingWidth * 2
strokeColor: root.paddingColor
fillColor: "transparent"
// Start at Top-Right
startX: root.width
startY: 0
PathLine {
x: root.width
y: root.height // Go to Bottom-Right
}
}
// 3. BOTTOM PADDING (The one you wanted!)
ShapePath {
strokeWidth: root.paddingWidth * 2
strokeColor: root.paddingColor
fillColor: "transparent"
// Start at Bottom-Left
startX: 0
startY: root.height
PathLine {
// Draw to Bottom-Right
x: root.width
y: root.height
}
}
}
}

View File

@ -0,0 +1,94 @@
pragma ComponentBehavior: Bound
import QtQuick
import Qt.labs.folderlistmodel 2.15 // <--- The magic file scanner!
import Quickshell
import Quickshell.Hyprland
import "../../"
import "../settings/"
FloatingWindow {
id: root
title: "quickshell-WallSwitcher"
visible: false
implicitWidth: 840
implicitHeight: 640
GlobalShortcut {
// This is the "Secret Password" Hyprland will use
name: "toggle-walls"
onPressed: {
// Toggle visibility!
root.visible = !root.visible;
}
}
// Make it float above everything else
Text {
id: titleText
text: "Wallpapers in " + Settings.wallDir.replace("file://", "")
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
font.pixelSize: 20
topPadding: 20
bottomPadding: 10
font.family: Settings.font
color: Colors.foreground
}
color: Colors.background // Dark background
// 1. The File Scanner
FolderListModel {
id: folderModel
folder: "file://" + Settings.wallDir // <--- Your stash!
nameFilters: ["*.png", "*.jpg", "*.jpeg"]
showDirs: false
}
// 2. The Grid Display
GridView {
anchors.top: titleText.bottom // Sit below the title!
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 20
cellWidth: 200
cellHeight: 100
clip: true
model: folderModel
delegate: Item {
property string cleanPath: modelData.fileUrl.toString().replace("file://", "")
required property var modelData
width: 200
height: 100
Image {
id: wallImage
width: 180
height: 90
anchors.centerIn: parent
// "fileUrl" is provided by FolderListModel
source: parent.modelData.fileUrl
// IMPORTANT: Downscale the image for the thumbnail!
// If you don't do this, loading 50 4K images will eat your RAM
sourceSize.width: 140
sourceSize.height: 90
fillMode: Image.PreserveAspectCrop
}
MouseArea {
anchors.fill: parent
onClicked: {
let cleanPath = parent.modelData.fileUrl.toString().replace("file://", "");
Settings.currentWall = parent.modelData.fileUrl.toString();
console.log(Settings.currentWall);
}
}
}
}
}

View File

@ -1,43 +1,75 @@
import Quickshell pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import qs import QtQuick.Controls // <--- Needed for StackView
import qs.settings
import Quickshell.Wayland import Quickshell.Wayland
import "../settings/"
Variants { WlrLayershell {
model: Quickshell.screens id: root
delegate: WlrLayershell { layer: WlrLayer.Background
id: wallpaperShell keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore anchors {
aboveWindows: false top: true
required property ShellScreen modelData bottom: true
layer: WlrLayer.Background left: true
screen: modelData right: true
anchors { }
top: true
bottom: true // We need to accept the screen from Variants
left: true required property var modelData
right: true
} // 1. The StackView manages the images
Image { StackView {
id: wallpaper id: wallStack
source: Settings.config.currentWall ? Settings.config.currentWall : "" width: parent.width
anchors.fill: parent height: parent.height
}
DesktopClock { // 2. Define what a "Wallpaper" looks like
anchors { Component {
bottom: parent.bottom id: wallComponent
right: parent.right Image {
margins: 25 fillMode: Image.PreserveAspectCrop
width: wallStack.width
height: wallStack.height
asynchronous: true // VERY IMPORTANT: Prevents lag while loading!
} }
} }
PlayerWidget {
anchors { // 4. THE ANIMATIONS 🎬
top: parent.top // When a new wall replaces the old one:
left: parent.left
margins: 35 // New One: Fades In (0 -> 1)
topMargin: Settings.config.barHeight + 35 + (Settings.config.floating ? Settings.config.margins : 0) replaceEnter: Transition {
NumberAnimation {
property: "x"
from: wallStack.width
to: 0
duration: 800 // Slower = Smoother
easing.type: Easing.OutQuad
}
}
// Old One: Fades Out (1 -> 0)
replaceExit: Transition {
NumberAnimation {
property: "x"
from: 0
to: -wallStack.width
duration: 800
easing.type: Easing.OutQuad
} }
} }
} }
// 5. The Trigger 🔫
// We listen for the singleton to change, then tell the Stack to update
Connections {
target: Settings
function onCurrentWallChanged() {
wallStack.replace(wallComponent, {
"source": Settings.currentWall
});
}
}
} }

5
modules/wallpaper/qmldir Normal file
View File

@ -0,0 +1,5 @@
Wallpaper 1.0 Wallpaper.qml
WallSwitcher 1.0 WallSwitcher.qml
Overlay 1.0 Overlay.qml
ScreenCorners 1.0 ScreenCorners.qml
ScreenPadding 1.0 ScreenPadding.qml

View File

@ -1,73 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import QtQuick
import Qt.labs.folderlistmodel 2.10
import qs
import qs.settings
Loader {
active: Settings.config.wallswitchershown
sourceComponent: root
Component {
id: root
FloatingWindow {
implicitWidth: 700
title: "qs-wallpicker"
implicitHeight: 600
color: Colors.base00
visible: Settings.config.wallswitchershown
onClosed: Settings.config.wallswitchershown = false
Rectangle {
id: container
radius: Settings.config.rounding
anchors {
fill: parent
margins: 8
}
color: Colors.base02
FolderListModel {
id: wpModel
folder: "file:///home/lucy/.walls/"
nameFilters: ["*.png"]
}
Component {
id: wallDelegate
Rectangle {
id: wpPreview
required property var filePath
implicitWidth: 80
implicitHeight: 60
color: "transparent"
Image {
asynchronous: true
anchors.fill: parent
source: wpPreview.filePath ? wpPreview.filePath : null
}
MouseArea {
id: updater
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
Settings.config.currentWall = wpPreview.filePath;
}
}
}
}
GridView {
id: wallLayout
anchors.centerIn: parent
anchors.margins: 20
anchors.leftMargin: 40
anchors.fill: parent
clip: true
model: wpModel
delegate: wallDelegate
}
}
}
}
}

2
qmldir Normal file
View File

@ -0,0 +1,2 @@
singleton Colors 1.0 Colors.qml
Icons 1.0 Icons.qml

View File

@ -1,31 +0,0 @@
pragma ComponentBehavior: Bound
pragma Singleton
import Quickshell
import Quickshell.Io
Singleton {
id: root
property alias config: settingsAdapter
FileView {
id: settingsView
path: "file:///home/lucy/.config/quickshell/settings/settings.json"
onFileChanged: reload()
onAdapterUpdated: writeAdapter()
watchChanges: true
adapter: JsonAdapter {
id: settingsAdapter
property int barHeight
property int rounding
property bool floating
property string font
property int fontSize
property int margins
property var currentWall
property bool wallswitchershown
property int barmargins
property int barSpacing
}
}
}

View File

@ -1,12 +0,0 @@
{
"barHeight": 32,
"barSpacing": 10,
"barmargins": 6,
"currentWall": "/home/lucy/.walls/frierensuff.png",
"floating": true,
"font": "Google Sans",
"fontSize": 13,
"margins": 10,
"rounding": 26,
"wallswitchershown": false
}

View File

@ -1,17 +1,42 @@
//@ pragma UseQApplication //@ pragma UseQApplication
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import Quickshell.Io
import qs.modules.Bar import "./modules/bar/"
import qs.modules.ipc import "./modules/wallpaper/"
import qs.modules.wallpaper import "./modules/notifications/"
import qs.modules.widgets.wallpicker
import qs.modules.notifications
ShellRoot { ShellRoot {
id: root id: shellRoot
Ipc {}
Bar {} Variants {
Wallpaper {} id: barVariants
WallPicker {} model: Quickshell.screens
Notification {} delegate: Bar {
screen: modelData
}
}
Variants {
id: overlayVariants
model: Quickshell.screens
delegate: Overlay {
screen: modelData
}
}
Variants {
id: wallVariants
model: Quickshell.screens
delegate: Wallpaper {
screen: modelData
}
}
Variants {
id: notiVariants
model: Quickshell.screens
delegate: NotiPopup {
screen: modelData
}
}
WallSwitcher {}
} }

View File

@ -1,21 +0,0 @@
import QtQuick
import qs
import qs.settings
Text {
id: root
color: Colors.base05
property real iconSize: 18
property var fill: true
renderType: Text.NativeRendering
font {
hintingPreference: Font.PreferNoHinting
family: "Material Symbols Rounded"
pixelSize: iconSize
weight: Font.Normal + (Font.DemiBold - Font.Normal) * fill
variableAxes: {
"FILL": fill,
"opsz": iconSize
}
}
}

View File

@ -1,10 +0,0 @@
import QtQuick
import qs
import qs.settings
Text {
font.family: Settings.config.font
font.pixelSize: Settings.config.fontSize
color: Colors.base05
font.weight: 500
}