feat: basic quickshell setup

This commit is contained in:
2026-06-14 14:30:09 +02:00
parent d2e569114f
commit 940ee3e988
23 changed files with 477 additions and 0 deletions
+4
View File
@@ -20,3 +20,7 @@ output_path = '~/.config/gtk-3.0/gtk.css'
[templates.gtk4]
input_path = '~/.config/matugen/templates/gtk-4.0/gtk.css'
output_path = '~/.config/gtk-4.0/gtk.css'
[templates.wlogout]
input_path = '~/.config/matugen/templates/wlogout/style.css'
output_path = '~/.config/wlogout/style.css'
+3
View File
@@ -1 +1,4 @@
# Quickshell Status bar
Many of the components were taken and adapted from the [End-4 configs](https://github.com/end-4/dots-hyprland).
All of the files in `utils/` are taken from the End-4 configs
+35
View File
@@ -0,0 +1,35 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import "./widgets/"
import "../config"
Scope {
Variants {
model: Quickshell.screens
PanelWindow {
id: barRoot
property var modelData
screen: modelData
anchors {
top: true
left: true
right: true
}
color: Appearance.colors.m3background
implicitHeight: 40
RowLayout {
anchors.fill: parent
spacing: 0
Clock {}
}
}
}
}
+63
View File
@@ -0,0 +1,63 @@
import QtQuick
import QtQuick.Layouts
import "../config/"
// Mostly from here: https://github.com/ChrisTitusTech/quickshell/blob/main/bar/BarBlock.qml
Rectangle {
id: root
radius: Appearance.rounding.barItems
Layout.preferredWidth: contentContainer.implicitWidth + 20
Layout.preferredHeight: 30
property Item content: Text {
text: "No content"
}
property bool hoverEnabled: false
property Item mouseArea: mouseArea
property bool dim: false
property var onClicked: function () {}
property int leftPadding
property int rightPadding
// Background color
color: {
if (mouseArea.containsMouse)
return Appearance.colors.m3primary;
return Appearance.colors.m3background;
}
states: [
State {
when: mouseArea.containsMouse
PropertyChanges {
target: root
}
}
]
Behavior on color {
ColorAnimation {
duration: 200
}
}
Item {
// Contents of the bar block
id: contentContainer
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
anchors.centerIn: parent
children: content
}
MouseArea {
enabled: root.hoverEnabled
id: mouseArea
anchors.fill: root
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onClicked: root.onClicked()
}
}
+2
View File
@@ -0,0 +1,2 @@
Bar 1.0 Bar.qml
WidgetWrapper 1.0 WidgetWrapper.qml
@@ -0,0 +1,4 @@
import QtQuick
import Quickshell
Scope {}
+10
View File
@@ -0,0 +1,10 @@
import QtQuick
import QtQuick.Layouts
import "."
import "../../config"
Text {
text: `${ClockHandler.date}, ${ClockHandler.time}`
color: Appearance.colors.m3onBackground
Layout.leftMargin: 10
}
@@ -0,0 +1,30 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
property string time
property string date
Process {
id: dateProc
command: ["date", "+%a %e %b|%T"]
running: true
stdout: SplitParser {
onRead: data => {
date = data.split("|")[0];
time = data.split("|")[1];
}
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: dateProc.running = true
}
}
+6
View File
@@ -0,0 +1,6 @@
import Quickshell
import QtQuick
Scope {
id: root
}
+2
View File
@@ -0,0 +1,2 @@
Clock 1.0 Clock.qml
singleton ClockHandler 1.0 ClockHandler.qml
+114
View File
@@ -0,0 +1,114 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property QtObject colors
property QtObject fonts
property QtObject fontsizes
property QtObject rounding
// From end-4 configs
colors: QtObject {
property color m3background: "#141313"
property color m3onBackground: "#e6e1e1"
property color m3surface: "#141313"
property color m3surfaceDim: "#141313"
property color m3surfaceBright: "#3a3939"
property color m3surfaceContainerLowest: "#0f0e0e"
property color m3surfaceContainerLow: "#1c1b1c"
property color m3surfaceContainer: "#201f20"
property color m3surfaceContainerHigh: "#2b2a2a"
property color m3surfaceContainerHighest: "#363435"
property color m3onSurface: "#e6e1e1"
property color m3surfaceVariant: "#49464a"
property color m3onSurfaceVariant: "#cbc5ca"
property color m3inverseSurface: "#e6e1e1"
property color m3inverseOnSurface: "#313030"
property color m3outline: "#948f94"
property color m3outlineVariant: "#49464a"
property color m3shadow: "#000000"
property color m3scrim: "#000000"
property color m3surfaceTint: "#cbc4cb"
property color m3primary: "#cbc4cb"
property color m3onPrimary: "#322f34"
property color m3primaryContainer: "#2d2a2f"
property color m3onPrimaryContainer: "#bcb6bc"
property color m3inversePrimary: "#615d63"
property color m3secondary: "#cac5c8"
property color m3onSecondary: "#323032"
property color m3secondaryContainer: "#4d4b4d"
property color m3onSecondaryContainer: "#ece6e9"
property color m3tertiary: "#d1c3c6"
property color m3onTertiary: "#372e30"
property color m3tertiaryContainer: "#31292b"
property color m3onTertiaryContainer: "#c1b4b7"
property color m3error: "#ffb4ab"
property color m3onError: "#690005"
property color m3errorContainer: "#93000a"
property color m3onErrorContainer: "#ffdad6"
property color m3primaryFixed: "#e7e0e7"
property color m3primaryFixedDim: "#cbc4cb"
property color m3onPrimaryFixed: "#1d1b1f"
property color m3onPrimaryFixedVariant: "#49454b"
property color m3secondaryFixed: "#e6e1e4"
property color m3secondaryFixedDim: "#cac5c8"
property color m3onSecondaryFixed: "#1d1b1d"
property color m3onSecondaryFixedVariant: "#484648"
property color m3tertiaryFixed: "#eddfe1"
property color m3tertiaryFixedDim: "#d1c3c6"
property color m3onTertiaryFixed: "#211a1c"
property color m3onTertiaryFixedVariant: "#4e4447"
property color m3success: "#B5CCBA"
property color m3onSuccess: "#213528"
property color m3successContainer: "#374B3E"
property color m3onSuccessContainer: "#D1E9D6"
}
rounding: QtObject {
property int barItems: 10
}
function reapplyTheme() {
themeFileView.reload();
}
function applyTheme(fileContent) {
const json = JSON.parse(fileContent);
for (const key in json) {
if (json.hasOwnProperty(key)) {
// Convert snake_case to CamelCase
const camelCaseKey = key.replace(/_([a-z])/g, g => g[1].toUpperCase());
const m3Key = `${camelCaseKey}`;
root.colors[m3Key] = json[key];
}
}
}
Timer {
id: delayedFileRead
interval: 1000
repeat: false
running: false
onTriggered: {
root.applyTheme(themeFileView.text());
}
}
FileView {
id: themeFileView
path: Qt.resolvedUrl(Paths.materialColorsPath)
watchChanges: true
onFileChanged: {
this.reload();
delayedFileRead.start();
}
onLoadedChanged: {
const fileContent = themeFileView.text();
root.applyTheme(fileContent);
}
}
}
+14
View File
@@ -0,0 +1,14 @@
pragma Singleton
import Quickshell
import QtCore
import "../utils/"
Singleton {
readonly property string home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
property string materialColorsPath: FileUtils.trimFileProtocol(`${Paths.state}/user/generated/colors.json`)
}
+2
View File
@@ -0,0 +1,2 @@
singleton Appearance 1.0 Appearance.qml
singleton Paths 1.0 Paths.qml
+95
View File
@@ -0,0 +1,95 @@
// From https://github.com/quickshell-mirror/quickshell-examples/blob/master/volume-osd/shell.qml
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import "../config/"
Scope {
id: root
// Bind the pipewire node so its volume will be tracked
PwObjectTracker {
objects: [Pipewire.defaultAudioSink]
}
Connections {
target: Pipewire.defaultAudioSink?.audio
function onVolumeChanged() {
root.shouldShowOsd = true;
hideTimer.restart();
}
}
property bool shouldShowOsd: false
Timer {
id: hideTimer
interval: 1000
onTriggered: root.shouldShowOsd = false
}
// The OSD window will be created and destroyed based on shouldShowOsd.
// PanelWindow.visible could be set instead of using a loader, but using
// a loader will reduce the memory overhead when the window isn't open.
LazyLoader {
active: root.shouldShowOsd
PanelWindow {
// Since the panel's screen is unset, it will be picked by the compositor
// when the window is created. Most compositors pick the current active monitor.
anchors.bottom: true
margins.bottom: screen.height / 5
exclusiveZone: 0
implicitWidth: 300
implicitHeight: 50
color: "transparent"
// An empty click mask prevents the window from blocking mouse events.
mask: Region {}
Rectangle {
anchors.fill: parent
radius: height / 2
color: Appearance.colors.m3background
RowLayout {
anchors {
fill: parent
leftMargin: 10
rightMargin: 15
}
IconImage {
implicitSize: 30
source: Quickshell.iconPath("audio-volume-high-symbolic")
}
Rectangle {
// Stretches to fill all left-over space
Layout.fillWidth: true
implicitHeight: 10
radius: 20
color: "#50ffffff"
Rectangle {
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0)
radius: parent.radius
}
}
}
}
}
}
}
+1
View File
@@ -0,0 +1 @@
Volume 1.0 Volume.qml
+15
View File
@@ -0,0 +1,15 @@
import Quickshell
import QtQuick
import "config"
import "bar"
import "osd"
ShellRoot {
Component.onCompleted: {
// Load themes
Appearance.reapplyTheme();
}
Bar {}
Volume {}
}
+72
View File
@@ -0,0 +1,72 @@
pragma Singleton
import Quickshell
Singleton {
id: root
/**
* Trims the File protocol off the input string
* @param {string} str
* @returns {string}
*/
function trimFileProtocol(str) {
let s = str;
if (typeof s !== "string") s = str.toString(); // Convert to string if it's an url or whatever
return s.startsWith("file://") ? s.slice(7) : s;
}
/**
* Extracts the file name from a file path
* @param {string} str
* @returns {string}
*/
function fileNameForPath(str) {
if (typeof str !== "string") return "";
const trimmed = trimFileProtocol(str);
return trimmed.split(/[\\/]/).pop();
}
/**
* Extracts the folder name from a directory path
* @param {string} str
* @returns {string}
*/
function folderNameForPath(str) {
if (typeof str !== "string") return "";
const trimmed = trimFileProtocol(str);
// Remove trailing slash if present
const noTrailing = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
if (!noTrailing) return "";
return noTrailing.split(/[\\/]/).pop();
}
/**
* Removes the file extension from a file path or name
* @param {string} str
* @returns {string}
*/
function trimFileExt(str) {
if (typeof str !== "string") return "";
const trimmed = trimFileProtocol(str);
const lastDot = trimmed.lastIndexOf(".");
if (lastDot > -1 && lastDot > trimmed.lastIndexOf("/")) {
return trimmed.slice(0, lastDot);
}
return trimmed;
}
/**
* Returns the parent directory of a given file path
* @param {string} str
* @returns {string}
*/
function parentDirectory(str) {
if (typeof str !== "string") return "";
const trimmed = trimFileProtocol(str);
const parts = trimmed.split(/[\\/]/);
if (parts.length <= 1) return "";
parts.pop();
return parts.join("/");
}
}
+1
View File
@@ -0,0 +1 @@
singleton FileUtils 1.0 FileUtils.qml
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
mkdir ~/.config/quickshell | true
cp -r ./config/quickshell/* ~/.config/quickshell/