Initial commit

This commit is contained in:
ly-sec 2025-07-11 14:14:28 +02:00
commit a8c2f88654
53 changed files with 9269 additions and 0 deletions

View file

@ -0,0 +1,67 @@
import QtQuick
import Quickshell
import qs.Components
import qs.Settings
Item {
id: activeWindowWrapper
width: parent.width
property int fullHeight: activeWindowTitleContainer.height
y: panel.activeWindowVisible ? barBackground.height : barBackground.height - fullHeight
height: panel.activeWindowVisible ? fullHeight : 1
opacity: panel.activeWindowVisible ? 1 : 0
clip: true
Behavior on height { NumberAnimation { duration: 300; easing.type: Easing.OutQuad } }
Behavior on y { NumberAnimation { duration: 300; easing.type: Easing.OutQuad } }
Behavior on opacity { NumberAnimation { duration: 250 } }
Rectangle {
id: activeWindowTitleContainer
color: Theme.backgroundPrimary
bottomLeftRadius: Math.max(0, width / 2)
bottomRightRadius: Math.max(0, width / 2)
width: Math.min(barBackground.width - 200, activeWindowTitle.implicitWidth + 24)
height: activeWindowTitle.implicitHeight + 12
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
Text {
id: activeWindowTitle
text: panel.displayedWindowTitle && panel.displayedWindowTitle.length > 60
? panel.displayedWindowTitle.substring(0, 60) + "..."
: panel.displayedWindowTitle
font.pixelSize: 12
color: Theme.textSecondary
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.fill: parent
anchors.margins: 6
maximumLineCount: 1
}
}
Corners {
id: activeCornerRight
position: "bottomleft"
size: 1.1
fillColor: Theme.backgroundPrimary
offsetX: activeWindowTitleContainer.x + activeWindowTitleContainer.width - 33
offsetY: 0
anchors.top: activeWindowTitleContainer.top
}
Corners {
id: activeCornerLeft
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.top: activeWindowTitleContainer.top
x: activeWindowTitleContainer.x + 33 - width
offsetY: 0
}
}

364
Bar/Modules/Applauncher.qml Normal file
View file

@ -0,0 +1,364 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Components
import qs.Settings
import Qt5Compat.GraphicalEffects
import Quickshell.Wayland
import "../../Helpers/Fuzzysort.js" as Fuzzysort
PanelWindow {
id: appLauncherPanel
implicitWidth: 460
implicitHeight: 640
color: "transparent"
visible: false
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
screen: (typeof modelData !== 'undefined' ? modelData : null)
property bool shouldBeVisible: false
anchors.top: true
margins.top: -26
function showAt() {
visible = true;
shouldBeVisible = true;
searchField.forceActiveFocus()
}
function hidePanel() {
shouldBeVisible = false;
}
Rectangle {
id: root
width: 400
height: 640
x: (parent.width - width) / 2 // Horizontally centered
color: Theme.backgroundPrimary
bottomLeftRadius: 28
bottomRightRadius: 28
property var appModel: DesktopEntries.applications.values
property var filteredApps: []
property int selectedIndex: 0
property int targetY: (parent.height - height) / 2 // Centered vertically
y: appLauncherPanel.shouldBeVisible ? targetY : -height // Slide from above
Behavior on y {
NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
}
scale: appLauncherPanel.shouldBeVisible ? 1 : 0
Behavior on scale {
NumberAnimation { duration: 200; easing.type: Easing.InOutCubic }
}
onScaleChanged: {
if (scale === 0 && !appLauncherPanel.shouldBeVisible) {
appLauncherPanel.visible = false;
}
}
function isMathExpression(str) {
// Allow numbers, operators, parentheses, decimal points, spaces
return /^[-+*/().0-9\s]+$/.test(str);
}
function safeEval(expr) {
// Only allow safe math expressions
try {
// eslint-disable-next-line no-eval
return Function('return (' + expr + ')')();
} catch (e) {
return undefined;
}
}
function updateFilter() {
var query = searchField.text ? searchField.text.toLowerCase() : "";
var apps = root.appModel;
var results = [];
// Calculator mode: starts with '='
if (query.startsWith("=")) {
var expr = searchField.text.slice(1).trim();
if (expr && isMathExpression(expr)) {
var value = safeEval(expr);
if (value !== undefined && value !== null && value !== "") {
results.push({
isCalculator: true,
name: `Calculator: ${expr} = ${value}`,
result: value,
expr: expr,
icon: "calculate"
});
}
}
}
// Normal app search
if (!query || query.startsWith("=")) {
results = results.concat(apps);
} else {
var fuzzyResults = Fuzzysort.go(query, apps, { keys: ["name", "comment", "genericName"] });
results = results.concat(fuzzyResults.map(function(r) { return r.obj; }));
}
root.filteredApps = results;
root.selectedIndex = 0; // reset selection
}
function selectNext() {
if (filteredApps.length > 0)
selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1);
}
function selectPrev() {
if (filteredApps.length > 0)
selectedIndex = Math.max(selectedIndex - 1, 0);
}
function activateSelected() {
if (filteredApps.length === 0)
return;
var modelData = filteredApps[selectedIndex];
if (modelData.isCalculator) {
Qt.callLater(function() {
Quickshell.clipboardText = String(modelData.result);
Quickshell.execDetached([
"notify-send",
"Calculator Result",
`${modelData.expr} = ${modelData.result} (copied to clipboard)`
]);
});
} else if (modelData.execString) {
Quickshell.execDetached(["sh", "-c", modelData.execString]);
} else if (modelData.exec) {
Quickshell.execDetached(["sh", "-c", modelData.exec]);
} else {
if (!modelData.isCalculator)
console.warn("Cannot launch app:", modelData.name, "missing execString or exec", modelData);
}
appLauncherPanel.hidePanel();
}
Component.onCompleted: updateFilter()
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: 32
spacing: 18
Rectangle {
Layout.fillWidth: true
height: 1.5
color: Theme.outline
opacity: 0.10
}
// Search Bar
Rectangle {
id: searchBar
color: Theme.surfaceVariant
radius: 22
height: 48
Layout.fillWidth: true
//border.color: searchField.activeFocus ? Theme.accentPrimary : Theme.outline
//border.width: searchField.activeFocus ? 2.5 : 1.5
layer.enabled: searchField.activeFocus
layer.effect: DropShadow {
color: Theme.accentPrimary
radius: 12
samples: 16
verticalOffset: 0
horizontalOffset: 0
opacity: 0.10
}
RowLayout {
anchors.fill: parent
spacing: 10
anchors.margins: 14
Text {
text: "search"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: searchField.activeFocus ? Theme.accentPrimary : Theme.textSecondary
verticalAlignment: Text.AlignVCenter
}
TextField {
id: searchField
placeholderText: "Search apps..."
color: Theme.textPrimary
placeholderTextColor: Theme.textSecondary
background: null
font.pixelSize: 17
Layout.fillWidth: true
onTextChanged: root.updateFilter()
selectedTextColor: Theme.onAccent
selectionColor: Theme.accentPrimary
padding: 2
verticalAlignment: TextInput.AlignVCenter
leftPadding: 0
rightPadding: 0
font.bold: true
Keys.onDownPressed: root.selectNext()
Keys.onUpPressed: root.selectPrev()
Keys.onEnterPressed: root.activateSelected()
Keys.onReturnPressed: root.activateSelected()
Keys.onEscapePressed: appLauncherPanel.hidePanel()
}
}
Behavior on border.color { ColorAnimation { duration: 120 } }
Behavior on border.width { NumberAnimation { duration: 120 } }
}
// App List Card
Rectangle {
color: Theme.surface
radius: 20
//border.color: Theme.outline
//border.width: 1
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
anchors.margins: 0
property int innerPadding: 16
// Add an Item for padding
Item {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: parent.innerPadding
visible: false // for layout only
}
ListView {
id: appList
anchors.fill: parent
anchors.margins: parent.innerPadding
spacing: 2
model: root.filteredApps
currentIndex: root.selectedIndex
delegate: Item {
id: appDelegate
width: appList.width
height: 48
property bool hovered: mouseArea.containsMouse
property bool isSelected: index === root.selectedIndex
Rectangle {
anchors.fill: parent
color: hovered || isSelected ? Theme.accentPrimary : "transparent"
radius: 12
border.color: hovered || isSelected ? Theme.accentPrimary : "transparent"
border.width: hovered || isSelected ? 2 : 0
Behavior on color { ColorAnimation { duration: 120 } }
Behavior on border.color { ColorAnimation { duration: 120 } }
Behavior on border.width { NumberAnimation { duration: 120 } }
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 10
Item {
width: 28; height: 28
property bool iconLoaded: !modelData.isCalculator && iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error
Image {
id: iconImg
anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
cache: false
asynchronous: true
source: modelData.isCalculator ? "qrc:/icons/calculate.svg" : Quickshell.iconPath(modelData.icon, "")
visible: modelData.isCalculator || parent.iconLoaded
}
Text {
anchors.centerIn: parent
visible: !modelData.isCalculator && !parent.iconLoaded
text: "broken_image"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: Theme.accentPrimary
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Text {
text: modelData.name
color: hovered || isSelected ? Theme.onAccent : Theme.textPrimary
font.pixelSize: 14
font.bold: hovered || isSelected
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : (modelData.comment || modelData.genericName || "")
color: hovered || isSelected ? Theme.onAccent : Theme.textSecondary
font.pixelSize: 11
elide: Text.ElideRight
Layout.fillWidth: true
}
}
Item { Layout.fillWidth: true }
Text {
text: modelData.isCalculator ? "content_copy" : "chevron_right"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: hovered || isSelected ? Theme.onAccent : Theme.textSecondary
verticalAlignment: Text.AlignVCenter
}
}
Rectangle {
id: ripple
anchors.fill: parent
color: Theme.onAccent
opacity: 0.0
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
ripple.opacity = 0.18
rippleNumberAnimation.start()
root.selectedIndex = index // update selection on click
root.activateSelected()
}
onPressed: ripple.opacity = 0.18
onReleased: ripple.opacity = 0.0
}
NumberAnimation {
id: rippleNumberAnimation
target: ripple
property: "opacity"
to: 0.0
duration: 320
}
// Divider (except last item)
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: Theme.outline
opacity: index === appList.count - 1 ? 0 : 0.10
}
}
}
}
}
}
Corners {
id: launcherCornerRight
position: "bottomleft"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.top: root.top
offsetX: 397
offsetY: 0
}
Corners {
id: launcherCornerLeft
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.top: root.top
offsetX: -397
offsetY: 0
}
}

221
Bar/Modules/Bluetooth.qml Normal file
View file

@ -0,0 +1,221 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs.Settings
import qs.Components
Item {
id: bluetoothDisplay
width: 22
height: 22
property color hoverColor: Theme.rippleEffect
property real hoverOpacity: 0.0
property bool isActive: mouseArea.containsMouse || (bluetoothPopup && bluetoothPopup.visible)
// Show the Bluetooth popup when clicked
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
if (bluetoothPopup.visible) {
bluetoothPopup.hidePopup();
} else {
bluetoothPopup.showAt(this, 0, parent.height);
}
}
onEntered: bluetoothDisplay.hoverOpacity = 0.18
onExited: bluetoothDisplay.hoverOpacity = 0.0
}
Rectangle {
anchors.fill: parent
anchors.margins: -4 // Make hover area larger than icon
color: hoverColor
opacity: isActive ? 0.18 : hoverOpacity
radius: height / 2
z: 0
visible: opacity > 0.01
}
Text {
anchors.centerIn: parent
text: "bluetooth"
font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 18
color: bluetoothPopup.visible ? Theme.accentPrimary : Theme.textPrimary
z: 1
}
Behavior on hoverOpacity {
NumberAnimation {
duration: 120
easing.type: Easing.OutQuad
}
}
// The popup window for device list
PopupWindow {
id: bluetoothPopup
implicitWidth: 350
//property int deviceCount: (typeof Bluetooth.devices.count === 'number' && Bluetooth.devices.count >= 0) ? Bluetooth.devices.count : 0
//implicitHeight: Math.max(100, Math.min(420, 56 + (deviceCount * 36) + 24))
implicitHeight: 400
visible: false
color: "transparent"
property var anchorItem: null
property real anchorX
property real anchorY
anchor.item: anchorItem ? anchorItem : null
anchor.rect.x: anchorX - (implicitWidth / 2) + (anchorItem ? anchorItem.width / 2 : 0)
anchor.rect.y: anchorY + 8 // Move popup further down
function showAt(item, x, y) {
if (!item) {
console.warn("Bluetooth: anchorItem is undefined, not showing popup.")
return
}
anchorItem = item
anchorX = x
anchorY = y
visible = true
forceActiveFocus()
}
function hidePopup() {
visible = false
}
Item {
anchors.fill: parent
Keys.onEscapePressed: bluetoothPopup.hidePopup()
}
Rectangle {
id: bg
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 12
border.width: 1
border.color: Theme.surfaceVariant
z: 0
}
// Header
Rectangle {
id: header
width: parent.width
height: 56
color: "transparent"
Text {
text: "Bluetooth"
font.pixelSize: 18
font.bold: true
color: Theme.textPrimary
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 16
}
}
// Device list container with proper margins
Rectangle {
id: listContainer
anchors.top: header.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 8
color: "transparent"
clip: true
ListView {
id: deviceListView
anchors.fill: parent
spacing: 4
boundsBehavior: Flickable.StopAtBounds
model: Bluetooth.devices
delegate: Rectangle {
width: parent.width
height: 42
color: "transparent"
radius: 8
Rectangle {
anchors.fill: parent
radius: 8
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.18)
: (deviceMouseArea.containsMouse ? Theme.highlight : "transparent")
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 12
Text {
text: modelData.connected ? "bluetooth" : "bluetooth_disabled"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary
verticalAlignment: Text.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Text {
text: modelData.name || "Unknown Device"
color: modelData.connected ? Theme.accentPrimary : Theme.textPrimary
font.pixelSize: 14
elide: Text.ElideRight
}
Text {
text: modelData.address
color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary
font.pixelSize: 11
elide: Text.ElideRight
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (modelData.connected) {
modelData.disconnect()
} else {
modelData.connect()
}
}
}
}
}
}
// Scroll indicator when needed
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: listContainer.top
anchors.bottom: listContainer.bottom
width: 4
radius: 2
color: Theme.textSecondary
opacity: deviceListView.contentHeight > deviceListView.height ? 0.3 : 0
visible: opacity > 0
}
}
}

View file

@ -0,0 +1,50 @@
import QtQuick
import Quickshell.Io
import qs.Settings
import qs.Components
Item {
id: brightnessDisplay
property int brightness: -1
width: pill.width
height: pill.height
FileView {
id: brightnessFile
path: "/tmp/brightness_osd_level"
watchChanges: true
blockLoading: true
onLoaded: updateBrightness()
onFileChanged: {
brightnessFile.reload()
updateBrightness()
}
function updateBrightness() {
const val = parseInt(brightnessFile.text())
if (!isNaN(val) && val !== brightnessDisplay.brightness) {
brightnessDisplay.brightness = val
pill.text = brightness + "%"
pill.show()
}
}
}
PillIndicator {
id: pill
icon: "brightness_high"
text: brightness >= 0 ? brightness + "%" : ""
pillColor: Theme.surfaceVariant
iconCircleColor: Theme.accentPrimary
iconTextColor: Theme.backgroundPrimary
textColor: Theme.textPrimary
}
Component.onCompleted: {
if (brightness >= 0) {
pill.show()
}
}
}

View file

@ -0,0 +1,18 @@
import QtQuick
import qs.Settings
Rectangle {
width: textItem.paintedWidth
height: textItem.paintedHeight
color: "transparent"
Text {
id: textItem
text: Time.time
font.family: "Roboto"
font.weight: Font.Bold
font.pixelSize: 14
color: Theme.textPrimary
anchors.centerIn: parent
}
}

View file

@ -0,0 +1,137 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Settings
PopupWindow {
id: trayMenu
implicitWidth: 180
implicitHeight: Math.max(40, listView.contentHeight + 12)
visible: false
color: "transparent"
property QsMenuHandle menu
property var anchorItem: null
property real anchorX
property real anchorY
anchor.item: anchorItem ? anchorItem : null
anchor.rect.x: anchorX
anchor.rect.y: anchorY
function showAt(item, x, y) {
if (!item) {
console.warn("CustomTrayMenu: anchorItem is undefined, not showing menu.");
return;
}
anchorItem = item
anchorX = x
anchorY = y
visible = true
forceActiveFocus()
Qt.callLater(() => trayMenu.anchor.updateAnchor())
}
function hideMenu() {
visible = false
}
Item {
anchors.fill: parent
Keys.onEscapePressed: trayMenu.hideMenu()
}
QsMenuOpener {
id: opener
menu: trayMenu.menu
}
Rectangle {
id: bg
anchors.fill: parent
color: Theme.backgroundPrimary
border.color: Theme.accentPrimary
border.width: 2
radius: 20
z: 0
}
ListView {
id: listView
anchors.fill: parent
anchors.margins: 6
spacing: 2
interactive: false
enabled: trayMenu.visible
clip: true
model: ScriptModel {
values: opener.children ? [...opener.children.values] : []
}
delegate: Rectangle {
id: entry
required property var modelData
width: listView.width
height: (modelData?.isSeparator) ? 8 : 28
color: "transparent"
radius: 6
Rectangle {
anchors.centerIn: parent
width: parent.width - 20
height: 1
color: Qt.darker(Theme.backgroundPrimary, 1.4)
visible: modelData?.isSeparator ?? false
}
Rectangle {
anchors.fill: parent
color: mouseArea.containsMouse ? Theme.highlight : "transparent"
radius: 6
visible: !(modelData?.isSeparator ?? false)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 8
Text {
Layout.fillWidth: true
color: (modelData?.enabled ?? true) ? Theme.textPrimary : Theme.textDisabled
text: modelData?.text ?? ""
font.pixelSize: 13
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
Image {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && trayMenu.visible
onClicked: {
if (modelData && !modelData.isSeparator) {
modelData.triggered()
trayMenu.hideMenu()
}
}
}
}
}
}
}

128
Bar/Modules/SystemTray.qml Normal file
View file

@ -0,0 +1,128 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.SystemTray
Row {
property var bar
property var shell
property var trayMenu
spacing: 8
Layout.alignment: Qt.AlignVCenter
property bool containsMouse: false
property var systemTray: SystemTray
Repeater {
model: systemTray.items
delegate: Item {
width: 24
height: 24
// Hide Spotify icon, or adjust to your liking
visible: modelData && modelData.id !== "spotify"
property bool isHovered: trayMouseArea.containsMouse
// Hover scale animation
scale: isHovered ? 1.15 : 1.0
Behavior on scale {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
// Subtle rotation on hover
rotation: isHovered ? 5 : 0
Behavior on rotation {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Image {
id: trayIcon
anchors.centerIn: parent
width: 18
height: 18
sourceSize.width: 18
sourceSize.height: 18
smooth: false // Memory savings
asynchronous: true
cache: false
source: {
let icon = modelData?.icon || "";
if (!icon) return "";
// Process icon path
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Component.onCompleted: {
// console.log("Tray icon for", modelData?.id, ":", modelData?.icon)
}
}
MouseArea {
id: trayMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: (mouse) => {
if (!modelData) return;
if (mouse.button === Qt.LeftButton) {
// Close any open menu first
if (trayMenu && trayMenu.visible) {
trayMenu.hideMenu()
}
if (!modelData.onlyMenu) {
modelData.activate()
}
} else if (mouse.button === Qt.MiddleButton) {
// Close any open menu first
if (trayMenu && trayMenu.visible) {
trayMenu.hideMenu()
}
modelData.secondaryActivate && modelData.secondaryActivate()
} else if (mouse.button === Qt.RightButton) {
console.log("Right click on", modelData.id, "hasMenu:", modelData.hasMenu, "menu:", modelData.menu)
// If menu is already visible, close it
if (trayMenu && trayMenu.visible) {
trayMenu.hideMenu()
return
}
if (modelData.hasMenu && modelData.menu && trayMenu) {
// Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.width / 2);
const menuY = height + 20;
trayMenu.menu = modelData.menu;
trayMenu.showAt(parent, menuX, menuY);
} else {
// console.log("No menu available for", modelData.id, "or trayMenu not set")
}
}
}
}
Component.onDestruction: {
// No cache cleanup needed
}
}
}
}

16
Bar/Modules/Time.qml Normal file
View file

@ -0,0 +1,16 @@
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
}
}

44
Bar/Modules/Volume.qml Normal file
View file

@ -0,0 +1,44 @@
import QtQuick
import Quickshell
import qs.Settings
import qs.Components
Item {
id: volumeDisplay
property var shell
property int volume: 0
// The total width will match the pill's width
width: pillIndicator.width
height: pillIndicator.height
PillIndicator {
id: pillIndicator
icon: volume === 0 ? "volume_off" : (volume < 30 ? "volume_down" : "volume_up")
text: volume + "%"
pillColor: Theme.surfaceVariant
iconCircleColor: Theme.accentPrimary
iconTextColor: Theme.backgroundPrimary
textColor: Theme.textPrimary
}
Connections {
target: shell ?? null
function onVolumeChanged() {
if (shell && shell.volume !== volume) {
volume = shell.volume
pillIndicator.text = volume + "%"
pillIndicator.icon = volume === 0 ? "volume_off" : (volume < 30 ? "volume_down" : "volume_up")
pillIndicator.show()
}
}
}
Component.onCompleted: {
if (shell && shell.volume !== undefined) {
volume = shell.volume
pillIndicator.show()
}
}
}

430
Bar/Modules/Wifi.qml Normal file
View file

@ -0,0 +1,430 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Settings
Item {
id: wifiDisplay
width: 22
height: 22
property color hoverColor: Theme.rippleEffect
property real hoverOpacity: 0.0
property bool isActive: mouseArea.containsMouse || (wifiPanel && wifiPanel.visible)
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
if (wifiPanel.visible) {
wifiPanel.hidePopup();
} else {
wifiPanel.showAt(this, 0, parent.height);
}
}
onEntered: wifiDisplay.hoverOpacity = 0.18
onExited: wifiDisplay.hoverOpacity = 0.0
}
Rectangle {
anchors.fill: parent
anchors.margins: -4
color: hoverColor
opacity: isActive ? 0.18 : hoverOpacity
radius: height / 2
z: 0
visible: opacity > 0.01
}
Text {
anchors.centerIn: parent
text: "wifi"
font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 18
color: wifiPanel.visible ? Theme.accentPrimary : Theme.textPrimary
z: 1
}
PanelWindow {
id: wifiPanel
implicitWidth: 350
implicitHeight: 400
color: "transparent"
visible: false
property var anchorItem: null
property real anchorX
property real anchorY
property var networks: [] // { ssid, signal, security, connected }
property string connectingSsid: ""
property string errorMsg: ""
property string passwordPromptSsid: ""
property string passwordInput: ""
property bool showPasswordPrompt: false
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
function showAt(item, x, y) {
if (!item) {
console.warn("Wifi: anchorItem is undefined, not showing panel.");
return;
}
anchorItem = item
anchorX = x
anchorY = y
visible = true
refreshNetworks()
Qt.callLater(() => {
wifiPanel.x = anchorX - (wifiPanel.width / 2) + (anchorItem ? anchorItem.width / 2 : 0)
wifiPanel.y = anchorY + 8
})
}
function hidePopup() {
visible = false
showPasswordPrompt = false
errorMsg = ""
}
// Scan for networks
Process {
id: scanProcess
running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
stdout: StdioCollector {
onStreamFinished: {
var lines = text.split("\n");
var nets = [];
var seen = {};
for (var i = 0; i < lines.length; ++i) {
var line = lines[i].trim();
if (!line) continue;
var parts = line.split(":");
var ssid = parts[0];
var security = parts[1];
var signal = parseInt(parts[2]);
var inUse = parts[3] === "*";
if (ssid && !seen[ssid]) {
nets.push({ ssid: ssid, security: security, signal: signal, connected: inUse });
seen[ssid] = true;
}
}
wifiPanel.networks = nets;
}
}
}
function refreshNetworks() {
scanProcess.running = true;
}
// Connect to a network
Process {
id: connectProcess
property string ssid: ""
property string password: ""
running: false
command: password ? ["nmcli", "device", "wifi", "connect", ssid, "password", password] : ["nmcli", "device", "wifi", "connect", ssid]
stdout: StdioCollector {
onStreamFinished: {
wifiPanel.connectingSsid = "";
wifiPanel.showPasswordPrompt = false;
wifiPanel.errorMsg = "";
refreshNetworks();
}
}
stderr: StdioCollector {
onStreamFinished: {
wifiPanel.connectingSsid = "";
wifiPanel.errorMsg = text;
wifiPanel.showPasswordPrompt = false;
}
}
}
function connectNetwork(ssid, security) {
if (security && security !== "--") {
// Prompt for password
passwordPromptSsid = ssid;
passwordInput = "";
showPasswordPrompt = true;
} else {
connectingSsid = ssid;
connectProcess.ssid = ssid;
connectProcess.password = "";
connectProcess.running = true;
}
}
function submitPassword() {
connectingSsid = passwordPromptSsid;
connectProcess.ssid = passwordPromptSsid;
connectProcess.password = passwordInput;
connectProcess.running = true;
}
// Disconnect
Process {
id: disconnectProcess
property string ssid: ""
running: false
command: ["nmcli", "connection", "down", "id", ssid]
onRunningChanged: {
if (!running) {
refreshNetworks();
}
}
}
function disconnectNetwork(ssid) {
disconnectProcess.ssid = ssid;
disconnectProcess.running = true;
}
// UI
Rectangle {
id: bg
anchors.fill: parent
radius: 12
border.width: 1
border.color: Theme.surfaceVariant
color: Theme.backgroundPrimary
z: 0
}
// Header
Rectangle {
id: header
width: parent.width
height: 56
color: "transparent"
Text {
text: "Wi-Fi"
font.pixelSize: 18
font.bold: true
color: Theme.textPrimary
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 16
}
// Refresh button
Rectangle {
id: refreshBtn
width: 36
height: 36
radius: 18
color: Theme.surfaceVariant
border.color: refreshMouseArea.containsMouse ? Theme.accentPrimary : Theme.outline
border.width: 1
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 12
MouseArea {
id: refreshMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: wifiPanel.refreshNetworks()
}
Text {
anchors.centerIn: parent
text: "refresh"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Theme.accentPrimary
}
}
}
// Error message
Text {
visible: wifiPanel.errorMsg.length > 0
text: wifiPanel.errorMsg
color: Theme.error
font.pixelSize: 12
anchors.top: header.bottom
anchors.left: parent.left
anchors.leftMargin: 16
anchors.topMargin: 2
}
// Device list container
Rectangle {
id: listContainer
anchors.top: header.bottom
anchors.topMargin: wifiPanel.showPasswordPrompt ? 68 : 0
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 8
color: "transparent"
clip: true
ListView {
id: networkListView
anchors.fill: parent
spacing: 4
boundsBehavior: Flickable.StopAtBounds
model: wifiPanel.networks
delegate: Rectangle {
id: networkEntry
width: parent.width
height: modelData.ssid === wifiPanel.passwordPromptSsid && wifiPanel.showPasswordPrompt ? 110 : 42
color: "transparent"
radius: 8
Rectangle {
anchors.fill: parent
radius: 8
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.18)
: ((networkMouseArea.containsMouse || (modelData.ssid === wifiPanel.passwordPromptSsid && wifiPanel.showPasswordPrompt)) ? Theme.highlight : "transparent")
z: 0
}
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 0
anchors.bottomMargin: 0
anchors.topMargin: 0
height: 42
spacing: 12
// Signal icon
Text {
text: signalIcon(modelData.signal)
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: hovered ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
verticalAlignment: Text.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Text {
text: modelData.ssid || "Unknown Network"
color: hovered ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary)
font.pixelSize: 14
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: modelData.security && modelData.security !== "--" ? modelData.security : "Open"
color: hovered ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
font.pixelSize: 11
elide: Text.ElideRight
Layout.fillWidth: true
}
}
// Status
Text {
visible: modelData.connected
text: "connected"
color: hovered ? Theme.backgroundPrimary : Theme.accentPrimary
font.pixelSize: 11
verticalAlignment: Text.AlignVCenter
}
}
// Password prompt dropdown (only for selected network)
Rectangle {
visible: modelData.ssid === wifiPanel.passwordPromptSsid && wifiPanel.showPasswordPrompt
width: parent.width
height: 60
radius: 8
color: Theme.surfaceVariant
border.color: passwordField.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
anchors.bottom: parent.bottom
z: 2
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 8
color: Theme.surfaceVariant
border.color: passwordField.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: passwordField
anchors.fill: parent
anchors.margins: 12
text: wifiPanel.passwordInput
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
echoMode: TextInput.Password
onTextChanged: wifiPanel.passwordInput = text
onAccepted: wifiPanel.submitPassword()
MouseArea {
id: passwordMouseArea
anchors.fill: parent
onClicked: passwordField.forceActiveFocus()
}
}
}
Rectangle {
width: 80
height: 36
radius: 18
color: Theme.accentPrimary
border.color: Theme.accentPrimary
border.width: 0
MouseArea {
anchors.fill: parent
onClicked: wifiPanel.submitPassword()
cursorShape: Qt.PointingHandCursor
}
Text {
anchors.centerIn: parent
text: "Connect"
color: Theme.backgroundPrimary
font.pixelSize: 14
font.bold: true
}
}
}
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (modelData.connected) {
wifiPanel.disconnectNetwork(modelData.ssid)
} else if (modelData.security && modelData.security !== "--") {
wifiPanel.passwordPromptSsid = modelData.ssid;
wifiPanel.passwordInput = "";
wifiPanel.showPasswordPrompt = true;
} else {
wifiPanel.connectNetwork(modelData.ssid, modelData.security)
}
}
}
// Helper for hover text color
property bool hovered: networkMouseArea.containsMouse || (modelData.ssid === wifiPanel.passwordPromptSsid && wifiPanel.showPasswordPrompt)
}
}
}
// Scroll indicator
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: listContainer.top
anchors.bottom: listContainer.bottom
width: 4
radius: 2
color: Theme.textSecondary
opacity: networkListView.contentHeight > networkListView.height ? 0.3 : 0
visible: opacity > 0
}
}
// Helper for signal icon
function signalIcon(signal) {
if (signal >= 80) return "network_wifi_4_bar";
if (signal >= 60) return "network_wifi_3_bar";
if (signal >= 40) return "network_wifi_2_bar";
if (signal >= 20) return "network_wifi_1_bar";
return "wifi_0_bar";
}
}

229
Bar/Modules/Workspace.qml Normal file
View file

@ -0,0 +1,229 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import qs.Settings
import qs.Services
Item {
id: root
property ListModel workspaces: ListModel {}
property bool isDestroying: false
property bool hovered: false
signal workspaceChanged(int workspaceId, color accentColor)
property real masterProgress: 0.0
property bool effectsActive: false
property color effectColor: Theme.accentPrimary
property int horizontalPadding: 16
property int spacingBetweenPills: 8
width: {
let total = 0
for (let i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i)
if (ws.isFocused) total += 44
else if (ws.isActive) total += 28
else total += 16
}
total += Math.max(workspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
height: 36
Component.onCompleted: updateWorkspaceList()
Connections {
target: Niri
function onWorkspacesChanged() { updateWorkspaceList(); }
function onFocusedWorkspaceIndexChanged() { updateWorkspaceFocus(); }
}
function triggerUnifiedWave() {
effectColor = Theme.accentPrimary
masterAnimation.restart()
}
SequentialAnimation {
id: masterAnimation
PropertyAction { target: root; property: "effectsActive"; value: true }
NumberAnimation {
target: root
property: "masterProgress"
from: 0.0
to: 1.0
duration: 1000
easing.type: Easing.OutQuint
}
PropertyAction { target: root; property: "effectsActive"; value: false }
PropertyAction { target: root; property: "masterProgress"; value: 0.0 }
}
function updateWorkspaceList() {
const newList = Niri.workspaces || []
workspaces.clear()
for (let i = 0; i < newList.length; i++) {
const ws = newList[i]
workspaces.append({
id: ws.id,
idx: ws.idx,
name: ws.name || "",
output: ws.output,
isActive: ws.is_active,
isFocused: ws.is_focused,
isUrgent: ws.is_urgent
})
}
updateWorkspaceFocus()
}
function updateWorkspaceFocus() {
const focusedId = Niri.workspaces?.[Niri.focusedWorkspaceIndex]?.id ?? -1
for (let i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i)
const isFocused = ws.id === focusedId
const isActive = isFocused
if (ws.isFocused !== isFocused || ws.isActive !== isActive) {
workspaces.setProperty(i, "isFocused", isFocused)
workspaces.setProperty(i, "isActive", isActive)
if (isFocused) {
root.triggerUnifiedWave()
root.workspaceChanged(ws.id, Theme.accentPrimary)
}
}
}
}
Rectangle {
id: workspaceBackground
width: parent.width - 15
height: 26
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
radius: 12
color: Theme.surfaceVariant
border.color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.1)
border.width: 1
layer.enabled: true
layer.effect: DropShadow {
color: "black"
radius: 12
samples: 24
verticalOffset: 0
horizontalOffset: 0
opacity: 0.10
}
}
Row {
id: pillRow
spacing: spacingBetweenPills
anchors.verticalCenter: workspaceBackground.verticalCenter
width: root.width - horizontalPadding * 2
x: horizontalPadding
Repeater {
model: root.workspaces
Rectangle {
id: workspacePill
height: 12
width: {
if (model.isFocused) return 44
else if (model.isActive) return 28
else return 16
}
radius: {
if (model.isFocused) return 12 // half of focused height (if you want to animate this too)
else return 6
}
color: {
if (model.isFocused) return Theme.accentPrimary
if (model.isActive) return Theme.accentPrimary.lighter(130)
if (model.isUrgent) return Theme.error
return Qt.lighter(Theme.surfaceVariant, 1.6)
}
scale: model.isFocused ? 1.0 : 0.9
z: 0
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
Behavior on scale {
NumberAnimation {
duration: 300
easing.type: Easing.OutBack
}
}
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
Behavior on opacity {
NumberAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
Behavior on radius {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
// Burst effect overlay for focused pill (smaller outline)
Rectangle {
id: pillBurst
anchors.centerIn: parent
width: parent.width + 18 * root.masterProgress
height: parent.height + 18 * root.masterProgress
radius: width / 2
color: "transparent"
border.color: root.effectColor
border.width: 2 + 6 * (1.0 - root.masterProgress)
opacity: root.effectsActive && model.isFocused
? (1.0 - root.masterProgress) * 0.7
: 0
visible: root.effectsActive && model.isFocused
z: 1
}
}
}
}
// MouseArea to open/close Applauncher
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (appLauncherPanel && appLauncherPanel.visible) {
appLauncherPanel.hidePanel();
} else if (appLauncherPanel) {
appLauncherPanel.showAt();
}
}
z: 1000 // ensure it's above other content
hoverEnabled: true
onEntered: root.hovered = true
onExited: root.hovered = false
}
Component.onDestruction: {
root.isDestroying = true
}
}