initial commit

This commit is contained in:
Niklas Kapelle 2025-09-09 12:03:05 +02:00
commit 2d5299c8fa
Signed by: niklas
GPG Key ID: 4EB651B36D841D16
15 changed files with 622 additions and 0 deletions

57
ActivateLinux.qml Normal file
View File

@ -0,0 +1,57 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import "root:/bar"
PanelWindow {
id: activateLinux
required property ShellScreen modelData
screen: modelData
anchors {
right: true
bottom: true
}
margins {
right: 50
bottom: 50
}
implicitWidth: content.width
implicitHeight: content.height
color: "transparent"
// Give the window an empty click mask so all clicks pass through it.
mask: Region {}
// Use the wlroots specific layer property to ensure it displays over
// fullscreen windows.
WlrLayershell.layer: WlrLayer.Overlay
Pill {
Text {
text: "Ahhh"
}
}
ColumnLayout {
id: content
Text {
text: "Activate Linux"
color: "#50ffffff"
font.pointSize: 22
}
Text {
text: "Go to Settings to activate Linux"
color: "#50ffffff"
font.pointSize: 14
}
}
}

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# Usefull links
## Docs
https://doc.qt.io/qt-6/qml-qtquick-repeater.html
https://quickshell.outfoxxed.me/
## Example dotfiles
https://github.com/caelestia-dots/shell
https://github.com/end-4/dots-hyprland
https://github.com/Ly-sec/nixos/tree/main/home/quickshell

30
bar/Music.qml Normal file
View File

@ -0,0 +1,30 @@
import QtQuick
import Quickshell.Services.Mpris
Text {
required property MprisPlayer player
text: `${player.trackTitle} - ${player.trackArtist || "Unknown Artist"}`
color: "white"
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.RightButton) {
parent.player.canGoNext && parent.player.next()
} else if (mouse.button === Qt.LeftButton) {
parent.player.canTogglePlaying && parent.player.togglePlaying()
}
}
onWheel: (event) => {
if (!parent.player.volumeSupported || !parent.player.canControl){
return
}
parent.player.volume = parent.player.volume + (event.angleDelta.y / 120 * 0.05)
}
}
}

24
bar/Pill.qml Normal file
View File

@ -0,0 +1,24 @@
import QtQuick
import QtQuick.Shapes
import QtQuick.Layouts
Rectangle {
id: pill
default property alias content: container.data
// Center in the middle of the bar
anchors.verticalCenter: parent.verticalCenter
implicitWidth: container.childrenRect.width + 15
implicitHeight: container.childrenRect.height + 12
color: "#939ab7"
radius: 18
Loader {
id: container
width: childrenRect.width
height: childrenRect.height
anchors.centerIn: parent
}
}

54
bar/StatusBar.qml Normal file
View File

@ -0,0 +1,54 @@
import Quickshell
import QtQuick
import Quickshell.Io
import Quickshell.Hyprland
import QtQuick.Layouts
import Quickshell.Services.Mpris
import "root:/singeltons"
PanelWindow {
required property ShellScreen modelData
screen: modelData
anchors {
top: true
left: true
right: true
}
height: 30
color: "transparent"
Pill {
Workspaces {}
}
Pill {
anchors.centerIn: parent
Text {
text: Time.time
color: "white"
font.pixelSize: 14
}
}
Pill {
anchors.verticalCenter: parent.verticalCenter
anchors.right: music.left
anchors.rightMargin: 10
Volume {}
}
Pill {
id: music
anchors.right: parent.right
anchors.rightMargin: 10
Music {
player: Mpris.players.values[0]
}
}
}

16
bar/Volume.qml Normal file
View File

@ -0,0 +1,16 @@
import QtQuick
import "root:singeltons"
Text {
text: `${Pipewire.volumePercent}% ${Pipewire.micMuted}`
color: "white"
MouseArea {
anchors.fill: parent
onWheel: (event) => {
Pipewire.setVolume(Pipewire.volume + (event.angleDelta.y / 120 * 0.05))
}
}
}

51
bar/Workspaces.qml Normal file
View File

@ -0,0 +1,51 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import QtQuick.Layouts
Rectangle {
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
color: "transparent"
Row {
id: content
anchors.fill: parent
Repeater {
model: ScriptModel {
values: Hyprland.workspaces.values.filter(e => e.id >= 0)
}
Rectangle {
required property HyprlandWorkspace modelData
property bool hovered: false
width: 25
height: 20
color: "transparent"
Text {
text: modelData.name
color: modelData.active ? "#09608c" : "#cccccc"
anchors.centerIn: parent
font.pixelSize: 14
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
modelData.activate()
parent.hovered = false
}
onEntered: parent.hovered = !modelData.active
onExited: parent.hovered = false
}
}
}
}
}

61
flake.lock generated Normal file
View File

@ -0,0 +1,61 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1751741127,
"narHash": "sha256-t75Shs76NgxjZSgvvZZ9qOmz5zuBE8buUaYD28BMTxg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "29e290002bfff26af1db6f64d070698019460302",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1749285348,
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"quickshell": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1751880110,
"narHash": "sha256-5fQ2cetL3rTHqXe2VM3puawL/8u5j6ujBr6Gdt7Iues=",
"owner": "quickshell-mirror",
"repo": "quickshell",
"rev": "5d7e07508ae3e5487edb1ac5a152120434f091d5",
"type": "github"
},
"original": {
"owner": "quickshell-mirror",
"repo": "quickshell",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"quickshell": "quickshell"
}
}
},
"root": "root",
"version": 7
}

24
flake.nix Normal file
View File

@ -0,0 +1,24 @@
{
description = "Dev shell with quickshell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
quickshell.url = "github:quickshell-mirror/quickshell";
};
outputs = { self, nixpkgs, quickshell, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
devShells.${system}.default = pkgs.mkShell {
packages = [
quickshell.packages.${system}.default
];
shellHook = ''
exec zsh
'';
};
};
}

136
launcher/Launcher.qml Normal file
View File

@ -0,0 +1,136 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell.Wayland
import Quickshell.Hyprland
import "root:/singeltons"
PanelWindow {
id: root
property bool show: true
implicitWidth: 600
implicitHeight: 400
visible: show
focusable: true
color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.layer: WlrLayer.Top
HyprlandWindow.opacity: 0.9
// Main window
Rectangle {
anchors.fill: parent
anchors.centerIn: parent
color: Qt.hsla(0, 0, 1, 0.5)
border.color: Qt.hsla(0, 0, 1, 0.2)
border.width: 2
radius: 12
// Main window layout
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 20
// Search bar
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 40
spacing: 8
TextField {
id: searchInput
Layout.fillWidth: true
focus: true
placeholderText: "Search..."
font.pixelSize: 20
background: null
color: "white"
selectByMouse: true
Keys.onPressed: (event) => {
switch (event.key){
case (Qt.Key_Escape):
root.show = false
event.accepted = true
break
case (Qt.Key_Return):
case (Qt.Key_Enter):
if (resultsList.currentIndex >= 0) {
const item = resultsList.model[resultsList.currentIndex]
DesktopApps.launch(item)
}
root.show = false
event.accepted = true
break
case (Qt.Key_Down):
case (Qt.Key_Tab):
resultsList.currentIndex = Math.min(resultsList.count - 1, resultsList.currentIndex + 1)
event.accepted = true
break
case (Qt.Key_Up):
case (Qt.Key_Backtab):
resultsList.currentIndex = Math.max(0, resultsList.currentIndex - 1)
break
}
}
}
}
// Results
ListView {
id: resultsList
model: DesktopApps.fuzzySearch(searchInput.text)
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
keyNavigationWraps: true
// Result item
delegate: Rectangle {
required property DesktopEntry modelData
width: parent.width
height: 50
radius: 6
color: ListView.isCurrentItem ? "#3a3a3c" : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
Image {
source: parent.parent.modelData.icon !== "" ? Quickshell.iconPath(parent.parent.modelData.icon,true) : ""
visible: parent.parent.modelData.icon !== ""
fillMode: Image.PreserveAspectFit
sourceSize.width: 30
sourceSize.height: 30
}
Text {
text: parent.parent.modelData.name
Layout.fillWidth: true
font.pixelSize: 18
color: "white"
}
}
}
}
}
}
}

21
shell.qml Normal file
View File

@ -0,0 +1,21 @@
import Quickshell
import QtQuick
import Quickshell.Io
import "./bar"
import "./launcher/"
ShellRoot{
// Variants{
// model: Quickshell.screens
//
// ActivateLinux {}
// }
//
Variants {
model: Quickshell.screens
StatusBar {}
}
// Launcher {}
}

View File

@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import "root:/utils/fuzzySearch.js" as FuzzySearch
Singleton {
id: root
readonly property list<DesktopEntry> all: DesktopEntries.applications.values
function launch(entry: DesktopEntry): void {
entry.execute()
}
function fuzzySearch(query: string): var {
return FuzzySearch.fuzzySearch(query, all);
}
}

30
singeltons/Pipewire.qml Normal file
View File

@ -0,0 +1,30 @@
pragma Singleton
import Quickshell
import Quickshell.Services.Pipewire
Singleton {
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
readonly property bool muted: sink?.audio?.muted ?? false
readonly property real volume: sink?.audio?.volume ?? 0
readonly property int volumePercent: Math.round(volume * 100)
readonly property bool micMuted: source?.audio?.muted ?? false
function setVolume(volume: real): void {
if (sink?.ready && sink?.audio) {
sink.audio.muted = false;
sink.audio.volume = volume;
}
}
PwObjectTracker {
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
}
}

17
singeltons/Time.qml Normal file
View File

@ -0,0 +1,17 @@
// Time.qml
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: root
readonly property string time: {
Qt.formatDateTime(clock.date, "hh:mm")
}
SystemClock {
id: clock
precision: SystemClock.Seconds
}
}

70
utils/fuzzySearch.js Normal file
View File

@ -0,0 +1,70 @@
/**
* Fuzzy search for list of objects.
* - Each object in `items` must have a `value`. Or set the search key in options.
* - Returns an array of original objects ordered by their relevance.
* - Implements a cutoff for non-matching strings based on minimum relevance. Can be set in the options.
*
* @param {string} query - The search string.
* @param {Object[]} items - The array of objects to search. Each must have a 'value' property (string).
* @param {object} [options] - Optional settings.
* @param {number} [options.cutoff=0.2] - Minimum relevance threshold (0 to 1).
* @param {string} [options.key='value'] - The property name to match against (default: 'value').
* @param {boolean} [options.allOnEmpty=true] - Return all items on empty query.
* @param {Object} [options.boostMap] - An object mapping item keys (by 'value') to normalized boost values (0 to 1).
* @param {number} [options.boostWeight=0.1] - The maximum boost to apply for boostMap value 1.0.
* @returns {Object[]} - Array of matching objects sorted by relevance.
*/
function fuzzySearch(query, items, options = {}) {
const cutoff = options.cutoff ?? 0.2;
const key = options.key ?? 'name';
const boostMap = options.boostMap || {};
const boostWeight = options.boostWeight ?? 0.1;
const allOnEmpty = options.allOnEmpty == undefined || options.allOnEmpty == true;
if (!query) return allOnEmpty ? items : []
const q = query.toLowerCase();
return items
.map(item => {
const text = (item[key] || '').toLowerCase();
let score = 0;
// Perfect match
if (text === q) score = 1.0;
// Starts with
else if (text.startsWith(q)) score = 0.9 + 0.01 * (1 - q.length / (text.length || 1));
// Substring match
else {
const idx = text.indexOf(q);
if (idx !== -1) {
const startScore = 0.7 - 0.1 * (idx / (text.length || 1));
const lengthScore = 0.2 * (q.length / (text.length || 1));
score = startScore + lengthScore;
} else {
// Fuzzy: all query chars in order
let lastIdx = -1;
let found = true;
for (let c of q) {
lastIdx = text.indexOf(c, lastIdx + 1);
if (lastIdx === -1) {
found = false;
break;
}
}
if (found) {
score = 0.5 * (q.length / (text.length || 1));
}
}
}
// Boost: expects normalized boostMap (values from 0 to 1)
const boost = Math.max(0, Math.min(1, boostMap[item[key]] || 0)) * boostWeight;
return { item, score: score + boost };
})
.filter(result => result.score >= cutoff)
.sort((a, b) => b.score - a.score)
.map(result => result.item);
}