quickshell and hyprland additions

This commit is contained in:
2026-03-15 13:56:00 +02:00
parent c9c27d1554
commit 1ad06b82a6
509 changed files with 68371 additions and 19 deletions

View File

@@ -0,0 +1,267 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.config
import qs.modules.functions
import qs.modules.components
import qs.services
Scope {
id: root
Variants {
model: Quickshell.screens
PanelWindow {
id: backgroundContainer
required property var modelData
property string displayName: modelData.name
property url wallpaperPath: {
const displays = Config.runtime.monitors
const fallback = Config.runtime.appearance.background.defaultPath
if (!displays)
return fallback
const monitor = displays?.[displayName]
return monitor?.wallpaper ?? fallback
}
// parallax config
property bool parallaxEnabled: Config.runtime.appearance.background.parallax.enabled
property real parallaxZoom: Config.runtime.appearance.background.parallax.zoom
property int workspaceRange: Config.runtime.bar.modules.workspaces.workspaceIndicators
// hyprland
property int activeWorkspaceId: Hyprland.focusedWorkspace?.id ?? 1
// wallpaper geometry
property real wallpaperWidth: bgImg.implicitWidth
property real wallpaperHeight: bgImg.implicitHeight
property real wallpaperToScreenRatio: {
if (wallpaperWidth <= 0 || wallpaperHeight <= 0)
return 1
return Math.min(
wallpaperWidth / width,
wallpaperHeight / height
)
}
property real effectiveScale: parallaxEnabled ? parallaxZoom : 1
property real movableXSpace: Math.max(
0,
((wallpaperWidth / wallpaperToScreenRatio * effectiveScale) - width) / 2
)
// workspace mapping
property int lowerWorkspace: Math.floor((activeWorkspaceId - 1) / workspaceRange) * workspaceRange + 1
property int upperWorkspace: lowerWorkspace + workspaceRange
property int workspaceSpan: Math.max(1, upperWorkspace - lowerWorkspace)
property real valueX: {
if (!parallaxEnabled)
return 0.5
return (activeWorkspaceId - lowerWorkspace) / workspaceSpan
}
// sidebar globals
property bool sidebarLeftOpen: Globals.visiblility.sidebarLeft
&& Config.runtime.appearance.background.parallax.enableSidebarLeft
property bool sidebarRightOpen: Globals.visiblility.sidebarRight
&& Config.runtime.appearance.background.parallax.enableSidebarRight
property real sidebarOffset: {
if (sidebarLeftOpen && !sidebarRightOpen)
if (Config.runtime.bar.position === "right")
return 0.15
else return -0.15
if (sidebarRightOpen && !sidebarLeftOpen)
if (Config.runtime.bar.position === "left")
return -0.15
else return 0.15
return 0
}
property real effectiveValueX: Math.max(
0,
Math.min(
1,
valueX + sidebarOffset
)
)
// window
color: (bgImg.status === Image.Error) ? Appearance.colors.colLayer2 : "transparent"
WlrLayershell.namespace: "nucleus:background"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Background
screen: modelData
visible: Config.initialized && Config.runtime.appearance.background.enabled
anchors {
top: true
left: true
right: true
bottom: true
}
// wallpaper picker
Process {
id: wallpaperProc
command: ["bash", "-c", Directories.scriptsPath + "/interface/changebg.sh"]
stdout: StdioCollector {
onStreamFinished: {
const out = text.trim()
if (out !== "null" && out.length > 0) {
const parts = out.split("|")
if (parts.length === 2) {
const monitor = parts[0]
const wallpaper = parts[1]
Config.updateKey(
"monitors." + monitor + ".wallpaper",
wallpaper
)
}
}
Quickshell.execDetached([
"nucleus", "ipc", "call", "clock", "changePosition"
])
if (Config.runtime.appearance.colors.autogenerated) {
Quickshell.execDetached([
"nucleus", "ipc", "call", "global", "regenColors"
]);
}
}
}
}
// wallpaper
Item {
anchors.fill: parent
clip: true
StyledImage {
id: bgImg
visible: status === Image.Ready
smooth: false
cache: false
fillMode: Image.PreserveAspectCrop
source: wallpaperPath + "?t=" + Date.now()
width: wallpaperWidth / wallpaperToScreenRatio * effectiveScale
height: wallpaperHeight / wallpaperToScreenRatio * effectiveScale
x: -movableXSpace - (effectiveValueX - 0.5) * 2 * movableXSpace
y: 0
Behavior on x {
NumberAnimation {
duration: Metrics.chronoDuration(600)
easing.type: Easing.OutCubic
}
}
onStatusChanged: {
if (status === Image.Ready) {
backgroundContainer.wallpaperWidth = implicitWidth
backgroundContainer.wallpaperHeight = implicitHeight
}
}
}
MouseArea {
id: widgetCanvas
anchors.fill: parent
}
// error ui
Item {
anchors.centerIn: parent
visible: bgImg.status === Image.Error
Rectangle {
width: 550
height: 400
radius: Appearance.rounding.windowRounding
color: "transparent"
anchors.centerIn: parent
ColumnLayout {
anchors.centerIn: parent
anchors.margins: Metrics.margin("normal")
spacing: Metrics.margin("small")
MaterialSymbol {
text: "wallpaper"
font.pixelSize: Metrics.fontSize("wildass")
color: Appearance.colors.colOnLayer2
Layout.alignment: Qt.AlignHCenter
}
StyledText {
text: "Wallpaper Missing"
font.pixelSize: Metrics.fontSize("hugeass")
font.bold: true
color: Appearance.colors.colOnLayer2
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
StyledText {
text: "Seems like you haven't set a wallpaper yet."
font.pixelSize: Metrics.fontSize("small")
color: Appearance.colors.colSubtext
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter
}
Item { Layout.fillHeight: true }
StyledButton {
text: "Set wallpaper"
icon: "wallpaper"
secondary: true
radius: Metrics.radius("large")
Layout.alignment: Qt.AlignHCenter
onClicked: wallpaperProc.running = true
}
}
}
}
}
IpcHandler {
target: "background"
function change() {
wallpaperProc.running = true
}
function next() {
WallpaperSlideshow.nextWallpaper()
}
}
}
}
Clock {
id: clock
}
}

View File

@@ -0,0 +1,287 @@
import "../../components/morphedPolygons/geometry/offset.js" as Offset
import "../../components/morphedPolygons/material-shapes.js" as MaterialShapes // For polygons
import "../../components/morphedPolygons/shapes/corner-rounding.js" as CornerRounding
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.config
import qs.modules.components
import qs.modules.components.morphedPolygons
import qs.services
Scope {
id: root
property bool imageFailed: false
Variants {
model: Quickshell.screens
PanelWindow {
id: clock
required property var modelData
property int padding: Config.runtime.appearance.background.clock.edgeSpacing
property int clockHeight: Config.runtime.appearance.background.clock.isAnalog ? 250 : 160
property int clockWidth: Config.runtime.appearance.background.clock.isAnalog ? 250 : 360
function setRandomPosition() {
const x = Math.floor(Math.random() * (width - clockWidth));
const y = Math.floor(Math.random() * (height - clockHeight));
animX.to = x;
animY.to = y;
moveAnim.start();
Config.updateKey("appearance.background.clock.xPos", x);
Config.updateKey("appearance.background.clock.yPos", y);
}
color: "transparent"
visible: (Config.runtime.appearance.background.clock.enabled && Config.initialized && !imageFailed)
exclusiveZone: 0
WlrLayershell.layer: WlrLayer.Bottom
screen: modelData
ParallelAnimation {
id: moveAnim
NumberAnimation {
id: animX
target: rootContentContainer
property: "x"
duration: Metrics.chronoDuration(400)
easing.type: Easing.InOutCubic
}
NumberAnimation {
id: animY
target: rootContentContainer
property: "y"
duration: Metrics.chronoDuration(400)
easing.type: Easing.InOutCubic
}
}
anchors {
top: true
bottom: true
left: true
right: true
}
margins {
top: padding
bottom: padding
left: padding
right: padding
}
Item {
id: rootContentContainer
property real releasedX: 0
property real releasedY: 0
height: clockHeight
width: clockWidth
Component.onCompleted: {
Qt.callLater(() => {
x = Config.runtime.appearance.background.clock.xPos;
y = Config.runtime.appearance.background.clock.yPos;
});
}
MouseArea {
id: ma
anchors.fill: parent
drag.target: rootContentContainer
drag.axis: Drag.XAndYAxis
acceptedButtons: Qt.RightButton
onReleased: {
if (ma.button === Qt.RightButton)
return
Config.updateKey("appearance.background.clock.xPos", rootContentContainer.x);
Config.updateKey("appearance.background.clock.yPos", rootContentContainer.y);
}
}
Item {
id: digitalClockContainer
visible: !Config.runtime.appearance.background.clock.isAnalog
Column {
spacing: Metrics.spacing(-40)
StyledText {
animate: false
text: Time.format("hh:mm")
font.pixelSize: Metrics.fontSize(Appearance.font.size.wildass * 3)
font.family: Metrics.fontFamily("main")
font.bold: true
}
StyledText {
anchors.left: parent.left
anchors.leftMargin: Metrics.margin(8)
animate: false
text: Time.format("dddd, dd/MM")
font.pixelSize: Metrics.fontSize(32)
font.family: Metrics.fontFamily("main")
font.bold: true
}
}
}
Item {
id: analogClockContainer
property int hours: parseInt(Time.format("hh"))
property int minutes: parseInt(Time.format("mm"))
property int seconds: parseInt(Time.format("ss"))
readonly property real cx: width / 2
readonly property real cy: height / 2
property var shapes: [MaterialShapes.getCookie7Sided, MaterialShapes.getCookie9Sided, MaterialShapes.getCookie12Sided, MaterialShapes.getPixelCircle, MaterialShapes.getCircle, MaterialShapes.getGhostish]
anchors.fill: parent
visible: Config.runtime.appearance.background.clock.isAnalog
width: clock.width / 1.1
height: clock.height / 1.1
// Polygon
MorphedPolygon {
id: shapeCanvas
anchors.fill: parent
color: Appearance.m3colors.m3secondaryContainer
roundedPolygon: analogClockContainer.shapes[Config.runtime.appearance.background.clock.shape]()
transform: Rotation {
origin.x: shapeCanvas.width / 2
origin.y: shapeCanvas.height / 2
angle: shapeCanvas.rotation
}
NumberAnimation on rotation {
from: 0
to: 360
running: Config.runtime.appearance.animations.enabled && Config.runtime.appearance.background.clock.rotatePolygonBg
duration: Config.runtime.appearance.background.clock.rotationDuration * 1000
loops: Animation.Infinite
}
}
ClockDial {
id: dial
anchors.fill: parent
anchors.margins: parent.width * 0.12
color: Appearance.colors.colOnSecondaryContainer
z: 0
}
// Hour hand
StyledRect {
z: 2
width: 10
height: parent.height * 0.3
radius: Metrics.radius("full")
color: Qt.darker(Appearance.m3colors.m3secondary, 0.8)
x: analogClockContainer.cx - width / 2
y: analogClockContainer.cy - height
transformOrigin: Item.Bottom
rotation: (analogClockContainer.hours % 12 + analogClockContainer.minutes / 60) * 30
}
StyledRect {
anchors.centerIn: parent
width: 16
height: 16
radius: width / 2
color: Appearance.m3colors.m3secondary
z: 99 // Ensures its on top of everthing
// Inner dot
StyledRect {
width: parent.width / 2
height: parent.height / 2
radius: width / 2
anchors.centerIn: parent
z: 100
color: Appearance.m3colors.m3primaryContainer
}
}
// Minute hand
StyledRect {
width: 18
height: parent.height * 0.35
radius: Metrics.radius("full")
color: Appearance.m3colors.m3secondary
x: analogClockContainer.cx - width / 2
y: analogClockContainer.cy - height
transformOrigin: Item.Bottom
rotation: analogClockContainer.minutes * 6
z: 10 // On top of all hands
}
// Second hand
StyledRect {
visible: true
width: 4
height: parent.height * 0.28
radius: Metrics.radius("full")
color: Appearance.m3colors.m3error
x: analogClockContainer.cx - width / 2
y: analogClockContainer.cy - height
transformOrigin: Item.Bottom
rotation: analogClockContainer.seconds * 6
z: 2
}
StyledText {
text: Time.format("hh")
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Metrics.margin(30)
font.pixelSize: Metrics.fontSize(80)
font.bold: true
opacity: 0.3
animate: false
}
StyledText {
text: Time.format("mm")
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Metrics.margin(110)
font.pixelSize: Metrics.fontSize(80)
font.bold: true
opacity: 0.3
animate: false
}
IpcHandler {
function changePosition() {
clock.setRandomPosition();
}
target: "clock"
}
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
import QtQuick
import qs.modules.components
Item {
id: root
property color color: "white"
readonly property real cx: width / 2
readonly property real cy: height / 2
readonly property real radius: Math.min(width, height) / 2
opacity: 0.4
// Hour marks (12 ticks)
Repeater {
model: 12
Item {
width: root.width
height: root.height
anchors.centerIn: parent
rotation: index * 30
transformOrigin: Item.Center
Rectangle {
width: 3 // thickness of tick
height: 15 // length of tick
color: root.color
anchors.horizontalCenter: parent.horizontalCenter
y: -root.radius * 0.15 / 2
radius: width / 2
}
}
}
// Minute marks (60 ticks)
Repeater {
model: 60
Item {
width: root.width
height: root.height
anchors.centerIn: parent
rotation: index * 6
transformOrigin: Item.Center
Rectangle {
width: index % 5 === 0 ? 3 : 2 // thicker for 5-minute marks
height: index % 5 === 0 ? 15 : 8 // longer for 5-minute marks
color: root.color
anchors.horizontalCenter: parent.horizontalCenter
y: -root.radius * 0.15 / 2
radius: width / 2
}
}
}
}

View File

@@ -0,0 +1,163 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.config
import qs.services
import qs.modules.components
Scope {
id: root
GothCorners {
opacity: ConfigResolver.bar(bar.displayName).gothCorners && !ConfigResolver.bar(bar.displayName).floating && ConfigResolver.bar(bar.displayName).enabled && !ConfigResolver.bar(bar.displayName).merged ? 1 : 0
}
Variants {
model: Quickshell.screens
PanelWindow {
// some exclusiveSpacing so it won't look like its sticking into the window when floating
id: bar
required property var modelData
property string displayName: modelData.name
property int rd: ConfigResolver.bar(displayName).radius * Config.runtime.appearance.rounding.factor // So it won't be modified when factor is 0
property int margin: ConfigResolver.bar(displayName).margins
property bool floating: ConfigResolver.bar(displayName).floating
property bool merged: ConfigResolver.bar(displayName).merged
property string pos: ConfigResolver.bar(displayName).position
property bool vertical: pos === "left" || pos === "right"
// Simple position properties
property bool attachedTop: pos === "top"
property bool attachedBottom: pos === "bottom"
property bool attachedLeft: pos === "left"
property bool attachedRight: pos === "right"
screen: modelData // Show bar on all screens
visible: ConfigResolver.bar(displayName).enabled && Config.initialized
WlrLayershell.namespace: "nucleus:bar"
exclusiveZone: ConfigResolver.bar(displayName).floating ? ConfigResolver.bar(displayName).density + Metrics.margin("tiny") : ConfigResolver.bar(displayName).density
implicitHeight: ConfigResolver.bar(displayName).density // density === height. (horizontal orientation)
implicitWidth: ConfigResolver.bar(displayName).density // density === width. (vertical orientation)
color: "transparent" // Keep panel window's color transparent, so that it can be modified by background rect
// This is probably a little weird way to set anchors but I think it's the best way. (and it works)
anchors {
top: ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right"
bottom: ConfigResolver.bar(displayName).position === "bottom" || ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right"
left: ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom"
right: ConfigResolver.bar(displayName).position === "right" || ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom"
}
margins {
top: {
if (floating)
return margin;
if (merged && vertical)
return margin;
return 0;
}
bottom: {
if (floating)
return margin;
if (merged && vertical)
return margin;
return 0;
}
left: {
if (floating)
return margin;
if (merged && !vertical)
return margin;
return 0;
}
right: {
if (floating)
return margin;
if (merged && !vertical)
return margin;
return 0;
}
}
StyledRect {
id: background
color: Appearance.m3colors.m3background
anchors.fill: parent
topLeftRadius: {
if (floating)
return rd;
if (!merged)
return 0;
return attachedBottom || attachedRight ? rd : 0;
}
topRightRadius: {
if (floating)
return rd;
if (!merged)
return 0;
return attachedBottom || attachedLeft ? rd : 0;
}
bottomLeftRadius: {
if (floating)
return rd;
if (!merged)
return 0;
return attachedTop || attachedRight ? rd : 0;
}
bottomRightRadius: {
if (floating)
return rd;
if (!merged)
return 0;
return attachedTop || attachedLeft ? rd : 0;
}
BarContent {
anchors.fill: parent
}
Behavior on bottomLeftRadius {
enabled: Config.runtime.appearance.animations.enabled
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on topLeftRadius {
enabled: Config.runtime.appearance.animations.enabled
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on bottomRightRadius {
enabled: Config.runtime.appearance.animations.enabled
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on topRightRadius {
enabled: Config.runtime.appearance.animations.enabled
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
}
}

View File

@@ -0,0 +1,178 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "content/"
import qs.config
import qs.services
import qs.modules.components
Item {
property string displayName: screen?.name ?? ""
property bool isHorizontal: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
Row {
id: hCenterRow
visible: isHorizontal
anchors.centerIn: parent
spacing: Metrics.spacing(4)
SystemUsageModule {}
MediaPlayerModule {}
ActiveWindowModule {}
ClockModule {}
BatteryIndicatorModule {}
}
RowLayout {
id: hLeftRow
visible: isHorizontal
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Metrics.spacing(4)
anchors.leftMargin: ConfigResolver.bar(displayName).density * 0.3
ToggleModule {
icon: "menu"
iconSize: Metrics.iconSize(22)
iconColor: Appearance.m3colors.m3error
toggle: Globals.visiblility.sidebarLeft
onToggled: function(value) {
Globals.visiblility.sidebarLeft = value
}
}
WorkspaceModule {}
}
RowLayout {
id: hRightRow
visible: isHorizontal
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Metrics.spacing(4)
anchors.rightMargin: ConfigResolver.bar(displayName).density * 0.3
SystemTray {
id: sysTray
}
StyledText {
id: seperator
visible: (sysTray.items.count > 0) && ConfigResolver.bar(displayName).modules.statusIcons.enabled
Layout.alignment: Qt.AlignLeft
font.pixelSize: Metrics.fontSize("hugeass")
text: "·"
}
StatusIconsModule {}
StyledText {
id: seperator2
Layout.alignment: Qt.AlignLeft
font.pixelSize: Metrics.fontSize("hugeass")
text: "·"
}
ToggleModule {
icon: "power_settings_new"
iconSize: Metrics.iconSize(22)
iconColor: Appearance.m3colors.m3error
toggle: Globals.visiblility.powermenu
onToggled: function(value) {
Globals.visiblility.powermenu = value
}
}
}
// Vertical Layout
Item {
visible: !isHorizontal
anchors.top: parent.top
anchors.topMargin: ConfigResolver.bar(displayName).density * 0.1
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: vRow.implicitHeight
implicitHeight: vRow.implicitWidth
Row {
id: vRow
anchors.centerIn: parent
spacing: Metrics.spacing(8)
rotation: 90
ToggleModule {
icon: "menu"
iconSize: Metrics.iconSize(22)
iconColor: Appearance.m3colors.m3error
toggle: Globals.visiblility.sidebarLeft
rotation: 270
onToggled: function(value) {
Globals.visiblility.sidebarLeft = value
}
}
SystemUsageModule {}
MediaPlayerModule {}
SystemTray {
rotation: 0
}
}
}
Item {
visible: !isHorizontal
anchors.centerIn: parent
anchors.verticalCenterOffset: 35
implicitWidth: centerRow.implicitHeight
implicitHeight: centerRow.implicitWidth
Row {
id: centerRow
anchors.centerIn: parent
WorkspaceModule {
rotation: 90
}
}
}
Item {
visible: !isHorizontal
anchors.bottom: parent.bottom
anchors.bottomMargin: ConfigResolver.bar(displayName).density * 0.1
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: row.implicitHeight
implicitHeight: row.implicitWidth
Row {
id: row
anchors.centerIn: parent
spacing: Metrics.spacing(6)
rotation: 90
ClockModule {
rotation: 270
}
StatusIconsModule {}
BatteryIndicatorModule {}
ToggleModule {
icon: "power_settings_new"
iconSize: Metrics.iconSize(22)
iconColor: Appearance.m3colors.m3error
toggle: Globals.visiblility.powermenu
rotation: 270
onToggled: function(value) {
Globals.visiblility.powermenu = value
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Wayland
import qs.config
import qs.modules.components
PanelWindow {
id: root
property int opacity: 0
color: "transparent"
visible: Config.initialized
WlrLayershell.layer: WlrLayer.Top
anchors {
top: true
left: true
bottom: true
right: true
}
Item {
id: container
anchors.fill: parent
StyledRect {
anchors.fill: parent
color: Appearance.m3colors.m3background
layer.enabled: true
opacity: root.opacity
layer.effect: MultiEffect {
maskSource: mask
maskEnabled: true
maskInverted: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Behavior on opacity {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration("large")
easing.type: Easing.InOutExpo
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
StyledRect {
anchors.fill: parent
anchors.topMargin: Config.runtime.bar.position === "bottom" ? -15 : 0
anchors.bottomMargin: Config.runtime.bar.position === "top" ? -15 : 0
anchors.leftMargin: Config.runtime.bar.position === "right" ? -15 : 0
anchors.rightMargin: Config.runtime.bar.position === "left" ? -15 : 0
radius: Metrics.radius("normal")
}
}
}
mask: Region {
item: container
intersection: Intersection.Xor
}
}

View File

@@ -0,0 +1,136 @@
import qs.config
import qs.modules.components
import qs.modules.functions
import qs.services
import QtQuick
import Quickshell
import Quickshell.Wayland
import QtQuick.Layouts
Item {
id: container
property string displayName: screen?.name ?? ""
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
property Toplevel activeToplevel: Compositor.isWorkspaceOccupied(Compositor.focusedWorkspaceId)
? Compositor.activeToplevel
: null
implicitWidth: row.implicitWidth + 30
implicitHeight: ConfigResolver.bar(displayName).modules.height
function simplifyTitle(title) {
if (!title)
return ""
title = title.replace(/[●⬤○◉◌◎]/g, "") // Symbols to remove
// Normalize separators
title = title
.replace(/\s*[|—]\s*/g, " - ")
.replace(/\s+/g, " ")
.trim()
const parts = title.split(" - ").map(p => p.trim()).filter(Boolean)
if (parts.length === 1)
return parts[0]
// Known app names (extend freely my fellow contributors)
const apps = [
"Firefox", "Mozilla Firefox",
"Chromium", "Google Chrome",
"Neovim", "VS Code", "Code",
"Kitty", "Alacritty", "Terminal",
"Discord", "Spotify", "Steam",
"Settings - Nucleus", "Settings"
]
let app = ""
for (let i = parts.length - 1; i >= 0; i--) { // loop over
for (let a of apps) {
if (parts[i].includes(a)) {
app = a
break
}
}
if (app) break
}
if (!app)
app = parts[parts.length - 1]
const context = parts.find(p => p !== app)
return context ? `${app} · ${context}` : app
}
function formatAppId(appId) { // Random ass function to make it look good
if (!appId || appId.length === 0)
return "";
// split on dashes/underscores
const parts = appId.split(/[-_]/);
// capitalize each segment
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
parts[i] = p.charAt(0).toUpperCase() + p.slice(1);
}
return parts.join("-");
}
/* Column {
id: col
anchors.centerIn: parent
StyledText {
id: workspaceText
font.pixelSize: Metrics.fontSize("smallie")
text: {
if (!activeToplevel)
return "Desktop"
const id = activeToplevel.appId || ""
return id // Just for aesthetics
}
horizontalAlignment: Text.AlignHCenter
}
StyledText {
id: titleText
text: StringUtils.shortText(simplifyTitle(activeToplevel?.title, 24) || `Workspace ${Hyprland.focusedWorkspaceId}`)
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Metrics.fontSize("smalle")
}
} */
Rectangle {
visible: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
color: Appearance.m3colors.m3paddingContainer
anchors.fill: parent
height: 34
width: row.height + 30
radius: ConfigResolver.bar(displayName).modules.radius
}
RowLayout {
id: row
spacing: 12
anchors.centerIn: parent
MaterialSymbol {
icon: "desktop_windows"
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
}
StyledText {
text: StringUtils.shortText(simplifyTitle(activeToplevel?.title), 24) || `Workspace ${Hyprland.focusedWorkspaceId}`
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.size.small
}
}
}

View File

@@ -0,0 +1,57 @@
import QtQuick
import QtQuick.Layouts
import qs.config
import qs.modules.components
import qs.services
Item {
id: batteryIndicatorModuleContainer
visible: UPower.batteryPresent
Layout.alignment: Qt.AlignVCenter
// Determine if bar is isVertical
property bool isVertical: ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right"
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
Rectangle {
id: bgRect
color: isVertical ? Appearance.m3colors.m3primary : Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor // No need to use metrics here...
implicitWidth: child.implicitWidth + Appearance.margin.large - (isVertical ? 10 : 0)
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
}
RowLayout {
id: child
anchors.centerIn: parent
spacing: isVertical ? 0 : Metrics.spacing(8)
// Icon for isVertical bars
MaterialSymbol {
visible: isVertical
icon: UPower.battIcon
iconSize: Metrics.iconSize(20)
}
// Battery percentage text
StyledText {
animate: false
font.pixelSize: Metrics.fontSize(16)
rotation: isVertical ? 270 : 0
text: (isVertical ? UPower.percentage : UPower.percentage + "%")
}
// Circular progress for horizontal bars
CircularProgressBar {
visible: !isVertical
value: UPower.percentage / 100
icon: UPower.battIcon
iconSize: Metrics.iconSize(18)
Layout.bottomMargin: Metrics.margin(2)
}
}
}

View File

@@ -0,0 +1,27 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.config
import qs.modules.components
Item {
id: clockContainer
property string format: isVertical ? "hh\nmm\nAP" : "hh:mm • dd/MM"
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
Layout.alignment: Qt.AlignVCenter
implicitWidth: 37
implicitHeight: 30
AnimatedImage {
id: art
anchors.fill: parent
source: Directories.assetsPath + "/gifs/bongo-cat.gif"
cache: false // this is important
smooth: true // smooooooth
rotation: isVertical ? 270 : 0
}
}

View File

@@ -0,0 +1,36 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.config
import qs.modules.components
Item {
id: clockContainer
property string format: isVertical ? "hh\nmm\nAP" : "hh:mm • dd/MM"
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
Layout.alignment: Qt.AlignVCenter
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
// Let the layout compute size automatically
Rectangle {
id: bgRect
color: isVertical ? "transparent" : Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor
// Padding around the text
implicitWidth: isVertical ? textItem.implicitWidth + 40 : textItem.implicitWidth + Metrics.margin("large")
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
}
StyledText {
id: textItem
anchors.centerIn: parent
animate: false
text: Time.format(clockContainer.format)
}
}

View File

@@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import qs.config
import qs.modules.functions
import qs.modules.components
import qs.services
Item {
id: mediaPlayer
property bool isVertical: (
ConfigResolver.bar(screen?.name ?? "").position === "left" ||
ConfigResolver.bar(screen?.name ?? "").position === "right"
)
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
Rectangle {
id: bgRect
color: Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius *
Config.runtime.appearance.rounding.factor
implicitWidth: isVertical
? row.implicitWidth + Metrics.margin("large") - 10
: row.implicitWidth + Metrics.margin("large")
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
}
Row {
id: row
anchors.centerIn: parent
spacing: Metrics.margin("small")
ClippingRectangle {
id: iconButton
width: 24
height: 24
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius / 1.2
color: Appearance.colors.colLayer1Hover
opacity: 0.9
clip: true
layer.enabled: true
Item {
anchors.fill: parent
Image {
id: art
anchors.fill: parent
visible: Mpris.artUrl !== ""
source: Mpris.artUrl
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
}
MaterialSymbol {
anchors.centerIn: parent
visible: Mpris.artUrl === ""
icon: "music_note"
iconSize: 18
color: Config.runtime.appearance.theme === "dark"
? "#b1a4a4"
: "grey"
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: Mpris.playPause()
onEntered: iconButton.opacity = 1
onExited: iconButton.opacity = 0.9
}
RotationAnimation on rotation {
from: 0
to: 360
duration: Metrics.chronoDuration(4000)
loops: Animation.Infinite
running: Mpris.isPlaying &&
Config.runtime.appearance.animations.enabled
}
}
StyledText {
id: textItem
anchors.verticalCenter: parent.verticalCenter
text: StringUtils.shortText(Mpris.title, 16)
visible: !mediaPlayer.isVertical
}
}
}

View File

@@ -0,0 +1,65 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.config
import qs.modules.components
import qs.services
Item {
id: statusIconsContainer
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
Layout.alignment: Qt.AlignVCenter
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.enabled
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
StyledRect {
id: bgRect
color: Globals.visiblility.sidebarRight ? Appearance.m3colors.m3paddingContainer : "transparent"
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor
implicitWidth: isVertical ? contentRow.implicitWidth + Metrics.margin("large") - 8 : contentRow.implicitWidth + Metrics.margin("large")
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
RowLayout {
id: contentRow
anchors.centerIn: parent
spacing: isVertical ? Metrics.spacing(8) : Metrics.spacing(16)
MaterialSymbol {
id: wifi
animate: false
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.networkStatusEnabled
rotation: isVertical ? 270 : 0
icon: Network.icon
iconSize: Metrics.fontSize("huge")
}
MaterialSymbol {
id: btIcon
animate: false
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.bluetoothStatusEnabled
rotation: isVertical ? 270 : 0
icon: Bluetooth.icon
iconSize: Metrics.fontSize("huge")
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (Globals.visiblility.sidebarLeft)
return
Globals.visiblility.sidebarRight = !Globals.visiblility.sidebarRight
}
}
}
}

View File

@@ -0,0 +1,165 @@
import qs.modules.components
import qs.config
import qs.services
import Quickshell.Services.SystemTray
import QtQuick
import Quickshell
import Quickshell.Widgets
import QtQuick.Layouts
Item {
id: root
readonly property Repeater items: items
property bool horizontalMode: (ConfigResolver.bar(screen?.name ?? "").position === "top" || ConfigResolver.bar(screen?.name ?? "").position === "bottom")
clip: true
implicitWidth: layout.implicitWidth + Metrics.margin("verylarge")
implicitHeight: 34
Rectangle {
visible: (items.count > 0) ? 1 : 0
id: padding
implicitHeight: padding.height
anchors.fill: parent
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius
color: "transparent"
}
GridLayout {
id: layout
anchors.centerIn: parent
rows: 1
columns: items.count
rowSpacing: Metrics.spacing(10)
columnSpacing: Metrics.spacing(10)
Repeater {
id: items
model: SystemTray.items
delegate: Item {
id: trayItemRoot
required property SystemTrayItem modelData
implicitWidth: 20
implicitHeight: 20
IconImage {
visible: trayItemRoot.modelData.icon !== ""
source: trayItemRoot.modelData.icon
asynchronous: true
anchors.fill: parent
rotation: root.horizontalMode ? 0 : 270
}
HoverHandler {
id: hover
}
QsMenuOpener {
id: menuOpener
menu: trayItemRoot.modelData.menu
}
StyledPopout {
id: popout
hoverTarget: hover
interactable: true
hCenterOnItem: true
requiresHover: false
Component {
Item {
width: childColumn.implicitWidth
height: childColumn.height
ColumnLayout {
id: childColumn
spacing: Metrics.spacing(5)
Repeater {
model: menuOpener.children
delegate: TrayMenuItem {
parentColumn: childColumn
Layout.preferredWidth: childColumn.width > 0 ? childColumn.width : implicitWidth
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
if (popout.isVisible)
popout.hide();
else
popout.show();
}
}
}
}
}
component TrayMenuItem: Item {
id: itemRoot
required property QsMenuEntry modelData
required property ColumnLayout parentColumn
Layout.fillWidth: true
implicitWidth: rowLayout.implicitWidth + 10
implicitHeight: !itemRoot.modelData.isSeparator ? rowLayout.implicitHeight + 10 : 1
MouseArea {
id: hover
hoverEnabled: itemRoot.modelData.enabled
anchors.fill: parent
onClicked: {
if (!itemRoot.modelData.hasChildren)
itemRoot.modelData.triggered();
}
}
Rectangle {
id: itemBg
anchors.fill: parent
opacity: itemRoot.modelData.isSeparator ? 0.5 : 1
color: itemRoot.modelData.isSeparator ? Appearance.m3colors.m3outline : hover.containsMouse ? Appearance.m3colors.m3surfaceContainer : Appearance.m3colors.m3surface
}
RowLayout {
id: rowLayout
visible: !itemRoot.modelData.isSeparator
opacity: itemRoot.modelData.isSeparator ? 0.5 : 1
spacing: Metrics.spacing(5)
anchors {
left: itemBg.left
leftMargin: Metrics.margin(5)
top: itemBg.top
topMargin:Metrics.margin(5)
}
IconImage {
visible: itemRoot.modelData.icon !== ""
source: itemRoot.modelData.icon
width: 15
height: 15
}
StyledText {
text: itemRoot.modelData.text
font.pixelSize: Metrics.fontSize(14)
color: Appearance.m3colors.m3onSurface
}
MaterialSymbol {
visible: itemRoot.modelData.hasChildren
icon: "chevron_right"
iconSize: Metrics.iconSize(16)
color: Appearance.m3colors.m3onSurface
}
}
}
}

View File

@@ -0,0 +1,99 @@
import QtQuick
import QtQuick.Layouts
import qs.config
import qs.modules.components
import qs.services
Item {
id: systemUsageContainer
property string displayName: screen?.name ?? ""
property bool isHorizontal: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
visible: ConfigResolver.bar(displayName).modules.systemUsage.enabled && haveWidth
Layout.alignment: Qt.AlignVCenter
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
property bool haveWidth:
ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled ||
ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled ||
ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled
// Normalize values so UI always receives correct ranges
function normalize(v) {
if (v > 1) return v / 100
return v
}
function percent(v) {
if (v <= 1) return Math.round(v * 100)
return Math.round(v)
}
Rectangle {
id: bgRect
color: Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
implicitWidth: child.implicitWidth + Metrics.margin("large")
implicitHeight: ConfigResolver.bar(displayName).modules.height
}
RowLayout {
id: child
anchors.centerIn: parent
spacing: Metrics.spacing(4)
// CPU
CircularProgressBar {
rotation: !isHorizontal ? 270 : 0
icon: "developer_board"
visible: ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled
iconSize: Metrics.iconSize(14)
value: normalize(SystemDetails.cpuPercent)
Layout.bottomMargin: Metrics.margin(2)
}
StyledText {
visible: ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled && isHorizontal
animate: false
text: percent(SystemDetails.cpuPercent) + "%"
}
// RAM
CircularProgressBar {
rotation: !isHorizontal ? 270 : 0
Layout.leftMargin: Metrics.margin(4)
icon: "memory_alt"
visible: ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled
iconSize: Metrics.iconSize(14)
value: normalize(SystemDetails.ramPercent)
Layout.bottomMargin: Metrics.margin(2)
}
StyledText {
visible: ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled && isHorizontal
animate: false
text: percent(SystemDetails.ramPercent) + "%"
}
// Temperature
CircularProgressBar {
rotation: !isHorizontal ? 270 : 0
visible: ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled
Layout.leftMargin: Metrics.margin(4)
icon: "device_thermostat"
iconSize: Metrics.iconSize(14)
value: normalize(SystemDetails.cpuTempPercent)
Layout.bottomMargin: Metrics.margin(2)
}
StyledText {
visible: ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled && isHorizontal
animate: false
text: percent(SystemDetails.cpuTempPercent) + "%"
}
}
}

View File

@@ -0,0 +1,43 @@
import qs.config
import qs.modules.components
import QtQuick
import Quickshell
import QtQuick.Layouts
StyledRect {
id: bg
property string icon
property color iconColor: Appearance.syntaxHighlightingTheme
property int iconSize
property bool toggle
property bool transparentBg: false
signal toggled(bool value)
color: (ma.containsMouse && !transparentBg)
? Appearance.m3colors.m3paddingContainer
: "transparent"
radius: Metrics.radius("childish")
implicitWidth: textItem.implicitWidth + 12
implicitHeight: textItem.implicitHeight + 6
MaterialSymbol {
id: textItem
anchors.centerIn: parent
anchors.verticalCenterOffset: 0.4
anchors.horizontalCenterOffset: 0.499
iconSize: bg.iconSize
icon: bg.icon
color: bg.iconColor
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
onClicked: bg.toggled(!bg.toggle)
}
}

View File

@@ -0,0 +1,254 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.modules.functions
import qs.services
Item {
id: workspaceContainer
property string displayName: screen?.name ?? ""
property int numWorkspaces: ConfigResolver.bar(displayName).modules.workspaces.workspaceIndicators
property var workspaceOccupied: []
property var occupiedRanges: []
function japaneseNumber(num) {
var kanjiMap = {
"0": "零",
"1": "一",
"2": "二",
"3": "三",
"4": "四",
"5": "五",
"6": "六",
"7": "七",
"8": "八",
"9": "九",
"10": "十"
};
return kanjiMap[num] !== undefined ? kanjiMap[num] : "Number out of range";
}
function updateWorkspaceOccupied() {
const offset = 1;
workspaceOccupied = Array.from({
"length": numWorkspaces
}, (_, i) => {
return Compositor.isWorkspaceOccupied(i + 1);
});
const ranges = [];
let start = -1;
for (let i = 0; i < workspaceOccupied.length; i++) {
if (workspaceOccupied[i]) {
if (start === -1)
start = i;
} else if (start !== -1) {
ranges.push({
"start": start,
"end": i - 1
});
start = -1;
}
}
if (start !== -1)
ranges.push({
"start": start,
"end": workspaceOccupied.length - 1
});
occupiedRanges = ranges;
}
visible: ConfigResolver.bar(displayName).modules.workspaces.enabled
implicitWidth: bg.implicitWidth
implicitHeight: ConfigResolver.bar(displayName).modules.height
Component.onCompleted: updateWorkspaceOccupied()
Connections {
function onStateChanged() {
updateWorkspaceOccupied();
}
target: Compositor
}
Rectangle {
id: bg
color: Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
implicitWidth: workspaceRow.implicitWidth + Metrics.margin("large") - 8
implicitHeight: ConfigResolver.bar(displayName).modules.height
// occupied background highlight
Item {
id: occupiedStretchLayer
anchors.centerIn: workspaceRow
width: workspaceRow.width
height: 26
z: 0
visible: Compositor.require("hyprland") // Hyprland only
Repeater {
model: occupiedRanges
Rectangle {
height: 26
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
color: ColorUtils.mix(Appearance.m3colors.m3tertiary, Appearance.m3colors.m3surfaceContainerLowest)
opacity: 0.8
x: modelData.start * (26 + workspaceRow.spacing)
width: (modelData.end - modelData.start + 1) * 26 + (modelData.end - modelData.start) * workspaceRow.spacing
}
}
}
// workspace highlight
Rectangle {
id: highlight
property int offset: Compositor.require("hyprland") ? 1 : 0
property int index: Math.max(0, Compositor.focusedWorkspaceId - 1 - offset)
property real itemWidth: 26
property real spacing: workspaceRow.spacing
property int highlightIndex: {
if (!Compositor.focusedWorkspaceId)
return 0;
if (Compositor.require("hyprland"))
return Compositor.focusedWorkspaceId - 1;
// Hyprland starts at 2 internally
return Compositor.focusedWorkspaceId - 2; // Niri or default
}
property real targetX: Math.min(highlightIndex, numWorkspaces - 1) * (itemWidth + spacing) + 7.3
property real animatedX1: targetX
property real animatedX2: targetX
x: Math.min(animatedX1, animatedX2)
anchors.verticalCenter: parent.verticalCenter
width: Math.abs(animatedX2 - animatedX1) + itemWidth - 1
height: 24
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
color: Appearance.m3colors.m3tertiary
onTargetXChanged: {
animatedX1 = targetX;
animatedX2 = targetX;
}
Behavior on animatedX1 {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration(400)
easing.type: Easing.OutSine
}
}
Behavior on animatedX2 {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration(133)
easing.type: Easing.OutSine
}
}
}
RowLayout {
id: workspaceRow
anchors.centerIn: parent
spacing: Metrics.spacing(10)
Repeater {
model: numWorkspaces
Item {
property int wsIndex: index + 1
property bool occupied: Compositor.isWorkspaceOccupied(wsIndex)
property bool focused: wsIndex === Compositor.focusedWorkspaceId
width: 26
height: 26
// Icon container — only used on Hyprland
ClippingRectangle {
id: iconContainer
anchors.centerIn: parent
width: 20
height: 20
color: "transparent"
radius: Appearance.rounding.small
clip: true
IconImage {
id: appIcon
anchors.fill: parent
visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && occupied
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
source: {
const win = Compositor.focusedWindowForWorkspace(wsIndex);
return win ? AppRegistry.iconForClass(win.class) : "";
}
layer.enabled: true
layer.effect: MultiEffect {
saturation: (Config.runtime.appearance.tintIcons || (Config.runtime.appearance.colors.matugenScheme === "scheme-monochrome" && Config.runtime.appearance.colors.autogenerated) || Config.runtime.appearance.colors.scheme.toLowerCase() === "monochrome") ? -1.0 : 1.0
}
}
}
// Kanji mode — only if not Hyprland
StyledText {
anchors.centerIn: parent
visible: ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons
text: japaneseNumber(index + 1)
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
}
// Numbers mode — only if not Hyprland
StyledText {
anchors.centerIn: parent
visible: !ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons
text: index + 1
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
}
// Symbols for unoccupied workspaces — only for Hyprland icons
MaterialSymbol {
property string displayText: Config.runtime.appearance.rounding.factor === 0 ? "crop_square" : "fiber_manual_record"
anchors.centerIn: parent
visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && !occupied
text: displayText
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
font.pixelSize: Metrics.iconSize(10)
fill: 1
}
MouseArea {
anchors.fill: parent
onClicked: Compositor.changeWorkspace(wsIndex)
}
}
}
}
}
}

View File

@@ -0,0 +1,576 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Quickshell
import Quickshell.Io
import qs.config
import qs.modules.functions
import qs.modules.components
import qs.services
FloatingWindow {
id: appWin
color: Appearance.m3colors.m3background
property bool initialChatSelected: false
property bool chatsInitialized: false
function appendMessage(sender, message) {
messageModel.append({
"sender": sender,
"message": message
});
scrollToBottom();
}
function updateChatsList(files) {
let existing = {
};
for (let i = 0; i < chatListModel.count; i++) existing[chatListModel.get(i).name] = true
for (let file of files) {
let name = file.trim();
if (!name.length)
continue;
if (name.endsWith(".txt"))
name = name.slice(0, -4);
if (!existing[name])
chatListModel.append({
"name": name
});
delete existing[name];
}
// remove chats that no longer exist
for (let name in existing) {
for (let i = 0; i < chatListModel.count; i++) {
if (chatListModel.get(i).name === name) {
chatListModel.remove(i);
break;
}
}
}
// ensure default exists
let hasDefault = false;
for (let i = 0; i < chatListModel.count; i++) if (chatListModel.get(i).name === "default") {
hasDefault = true;
}
if (!hasDefault) {
chatListModel.insert(0, {
"name": "default"
});
FileUtils.createFile(FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/default.txt");
}
}
function scrollToBottom() {
// Always scroll to end after appending
chatView.forceLayout();
chatView.positionViewAtEnd();
}
function sendMessage() {
if (userInput.text === "" || Zenith.loading)
return ;
Zenith.pendingInput = userInput.text;
appendMessage("You", userInput.text);
userInput.text = "";
Zenith.loading = true;
Zenith.send();
}
function loadChatHistory(chatName) {
messageModel.clear();
Zenith.loadChat(chatName);
}
function selectDefaultChat() {
let defaultIndex = -1;
for (let i = 0; i < chatListModel.count; i++) {
if (chatListModel.get(i).name === "default") {
defaultIndex = i;
break;
}
}
if (defaultIndex !== -1) {
chatSelector.currentIndex = defaultIndex;
Zenith.currentChat = "default";
loadChatHistory("default");
} else if (chatListModel.count > 0) {
chatSelector.currentIndex = 0;
Zenith.currentChat = chatListModel.get(0).name;
loadChatHistory(Zenith.currentChat);
}
}
visible: Globals.states.intelligenceWindowOpen
onVisibleChanged: {
if (!visible)
return ;
chatsInitialized = false;
messageModel.clear();
}
IpcHandler {
function openWindow() {
Globals.states.intelligenceWindowOpen = true;
}
function closeWindow() {
Globals.states.intelligenceWindowOpen = false;
}
target: "intelligence"
}
ListModel {
// { sender: "You" | "AI", message: string }
id: messageModel
}
ListModel {
id: chatListModel
}
ColumnLayout {
spacing: Metrics.spacing(8)
anchors.centerIn: parent
StyledText {
visible: !Config.runtime.misc.intelligence.enabled
text: "Intelligence is disabled!"
Layout.leftMargin: Metrics.margin(24)
font.pixelSize: Metrics.fontSize("huge")
}
StyledText {
visible: !Config.runtime.misc.intelligence.enabled
text: "Go to the settings to enable intelligence"
}
}
StyledRect {
anchors.fill: parent
color: "transparent"
visible: Config.runtime.misc.intelligence.enabled
ColumnLayout {
anchors.fill: parent
anchors.margins: Metrics.margin(16)
spacing: Metrics.spacing(10)
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(10)
StyledDropDown {
id: chatSelector
Layout.fillWidth: true
model: chatListModel
textRole: "name"
Layout.preferredHeight: 40
onCurrentIndexChanged: {
if (currentIndex < 0)
return ;
let name = chatListModel.get(currentIndex).name;
if (name === Zenith.currentChat)
return ;
Zenith.currentChat = name;
loadChatHistory(name);
}
}
StyledButton {
icon: "add"
Layout.preferredWidth: 40
onClicked: {
let name = "new-chat-" + chatListModel.count;
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
FileUtils.createFile(path, function(success) {
if (success) {
chatListModel.append({
"name": name
});
chatSelector.currentIndex = chatListModel.count - 1;
Zenith.currentChat = name;
messageModel.clear();
}
});
}
}
StyledButton {
icon: "edit"
Layout.preferredWidth: 40
enabled: chatSelector.currentIndex >= 0
onClicked: renameDialog.open()
}
StyledButton {
icon: "delete"
Layout.preferredWidth: 40
enabled: chatSelector.currentIndex >= 0 && chatSelector.currentText !== "default"
onClicked: {
let name = chatSelector.currentText;
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
FileUtils.removeFile(path, function(success) {
if (success) {
chatListModel.remove(chatSelector.currentIndex);
selectDefaultChat();
}
});
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(10)
StyledDropDown {
id: modelSelector
Layout.fillWidth: true
model: ["openai/gpt-4o","openai/gpt-4","openai/gpt-3.5-turbo","openai/gpt-4o-mini","anthropic/claude-3.5-sonnet","anthropic/claude-3-haiku","meta-llama/llama-3.3-70b-instruct:free","deepseek/deepseek-r1-0528:free","qwen/qwen3-coder:free"]
currentIndex: 0
Layout.preferredHeight: 40
onCurrentTextChanged: Zenith.currentModel = currentText
}
StyledButton {
icon: "close_fullscreen"
Layout.preferredWidth: 40
onClicked: {
Quickshell.execDetached(["nucleus", "ipc", "call", "intelligence", "closeWindow"]);
Globals.visiblility.sidebarLeft = false;
}
}
}
StyledRect {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Metrics.radius("normal")
color: Appearance.m3colors.m3surfaceContainerLow
ScrollView {
anchors.fill: parent
clip: true
ListView {
id: chatView
model: messageModel
spacing: Metrics.spacing(8)
anchors.fill: parent
anchors.margins: Metrics.margin(12)
clip: true
delegate: Item {
property bool isCodeBlock: message.split("\n").length > 2 && message.includes("import ") // simple heuristic
width: chatView.width
height: bubble.implicitHeight + 6
Component.onCompleted: {
chatView.forceLayout();
}
Row {
width: parent.width
spacing: Metrics.spacing(8)
Item {
width: sender === "AI" ? 0 : parent.width * 0.2
}
StyledRect {
id: bubble
radius: Metrics.radius("normal")
color: sender === "You" ? Appearance.m3colors.m3primaryContainer : Appearance.m3colors.m3surfaceContainerHigh
implicitWidth: Math.min(textItem.implicitWidth + 20, chatView.width * 0.8)
implicitHeight: textItem.implicitHeight
anchors.right: sender === "You" ? parent.right : undefined
anchors.left: sender === "AI" ? parent.left : undefined
anchors.topMargin: Metrics.margin(2)
TextEdit {
id: textItem
text: StringUtils.markdownToHtml(message)
wrapMode: TextEdit.Wrap
textFormat: TextEdit.RichText
readOnly: true // make it selectable but not editable
font.pixelSize: Metrics.fontSize(16)
color: Appearance.syntaxHighlightingTheme
padding: Metrics.padding(8)
anchors.fill: parent
}
MouseArea {
id: ma
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
let p = Qt.createQmlObject('import Quickshell; import Quickshell.Io; Process { command: ["wl-copy", "' + message + '"] }', parent);
p.running = true;
}
}
}
Item {
width: sender === "You" ? 0 : parent.width * 0.2
}
}
}
}
}
}
StyledRect {
Layout.fillWidth: true
height: 50
radius: Metrics.radius("normal")
color: Appearance.m3colors.m3surfaceContainer
RowLayout {
anchors.fill: parent
anchors.margins: Metrics.margin(6)
spacing: Metrics.spacing(10)
StyledTextField {
// Shift+Enter → insert newline
// Enter → send message
id: userInput
Layout.fillWidth: true
placeholderText: "Type your message..."
font.pixelSize: Metrics.iconSize(14)
padding: Metrics.spacing(8)
Keys.onPressed: {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (event.modifiers & Qt.ShiftModifier)
insert("\n");
else
sendMessage();
event.accepted = true;
}
}
}
StyledButton {
text: "Send"
enabled: userInput.text.trim().length > 0 && !Zenith.loading
opacity: enabled ? 1 : 0.5
onClicked: sendMessage()
}
}
}
}
Dialog {
id: renameDialog
title: "Rename Chat"
modal: true
visible: false
standardButtons: Dialog.NoButton
x: (appWin.width - 360) / 2 // center horizontally
y: (appWin.height - 160) / 2 // center vertically
width: 360
height: 200
ColumnLayout {
anchors.fill: parent
anchors.margins: Metrics.margin(16)
spacing: Metrics.spacing(12)
StyledText {
text: "Enter a new name for the chat"
font.pixelSize: Metrics.fontSize(18)
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
StyledTextField {
id: renameInput
Layout.fillWidth: true
placeholderText: "New name"
filled: false
highlight: false
text: chatSelector.currentText
font.pixelSize: Metrics.fontSize(16)
Layout.preferredHeight: 45
padding: Metrics.padding(8)
}
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(12)
Layout.alignment: Qt.AlignRight
StyledButton {
text: "Cancel"
Layout.preferredWidth: 80
onClicked: renameDialog.close()
}
StyledButton {
text: "Rename"
Layout.preferredWidth: 100
enabled: renameInput.text.trim().length > 0 && renameInput.text !== chatSelector.currentText
onClicked: {
let oldName = chatSelector.currentText;
let newName = renameInput.text.trim();
let oldPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + oldName + ".txt";
let newPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + newName + ".txt";
FileUtils.renameFile(oldPath, newPath, function(success) {
if (success) {
chatListModel.set(chatSelector.currentIndex, {
"name": newName
});
Zenith.currentChat = newName;
renameDialog.close();
}
});
}
}
}
}
background: StyledRect {
color: Appearance.m3colors.m3surfaceContainer
radius: Metrics.radius("normal")
border.color: Appearance.colors.colOutline
border.width: 1
}
header: StyledRect {
color: Appearance.m3colors.m3surfaceContainer
radius: Metrics.radius("normal")
border.color: Appearance.colors.colOutline
border.width: 1
}
}
StyledText {
text: "Thinking…"
visible: Zenith.loading
color: Appearance.colors.colSubtext
font.pixelSize: Metrics.fontSize(14)
anchors {
left: parent.left
bottom: parent.bottom
leftMargin: Metrics.margin(22)
bottomMargin: Metrics.margin(76)
}
}
}
Connections {
// only auto-select once
function onChatsListed(text) {
let lines = text.split(/\r?\n/);
let previousChat = Zenith.currentChat;
updateChatsList(lines);
// select & load once
if (!chatsInitialized) {
chatsInitialized = true;
let index = -1;
for (let i = 0; i < chatListModel.count; i++) {
if (chatListModel.get(i).name === previousChat) {
index = i;
break;
}
}
if (index === -1 && chatListModel.count > 0)
index = 0;
if (index !== -1) {
chatSelector.currentIndex = index;
Zenith.currentChat = chatListModel.get(index).name;
loadChatHistory(Zenith.currentChat);
}
return ;
}
// AFTER init: only react if current chat vanished
let stillExists = false;
for (let i = 0; i < chatListModel.count; i++) {
if (chatListModel.get(i).name === Zenith.currentChat) {
stillExists = true;
break;
}
}
if (!stillExists && chatListModel.count > 0) {
chatSelector.currentIndex = 0;
Zenith.currentChat = chatListModel.get(0).name;
loadChatHistory(Zenith.currentChat);
}
}
function onAiReply(text) {
appendMessage("AI", text.slice(5));
Zenith.loading = false;
}
function onChatLoaded(text) {
let lines = text.split(/\r?\n/);
let batch = [];
for (let l of lines) {
let line = l.trim();
if (!line.length)
continue;
let u = line.match(/^\[\d{4}-.*\] User: (.*)$/);
let a = line.match(/^\[\d{4}-.*\] AI: (.*)$/);
if (u)
batch.push({
"sender": "You",
"message": u[1]
});
else if (a)
batch.push({
"sender": "AI",
"message": a[1]
});
else if (batch.length)
batch[batch.length - 1].message += "\n" + line;
}
messageModel.clear();
for (let m of batch) messageModel.append(m)
scrollToBottom();
}
target: Zenith
}
}

View File

@@ -0,0 +1,122 @@
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Controls
import qs.config
import qs.modules.components
import qs.modules.functions
StyledRect {
id: root
property bool hovered: false
property bool selected: false
required property int parentWidth
width: parentWidth
height: 50
color: {
if (selected || hovered)
return Appearance.m3colors.m3surfaceContainerHigh
else
return Appearance.m3colors.m3surface
}
radius: Metrics.radius(15)
Behavior on color {
PropertyAnimation {
duration: Metrics.chronoDuration(200)
easing.type: Easing.InSine
}
}
ClippingWrapperRectangle {
id: entryIcon
anchors.left: parent.left
anchors.leftMargin: Metrics.margin(10)
anchors.top: parent.top
anchors.topMargin: (parent.height / 2) - (size / 2)
property int size: 25
height: size
width: size
radius: Metrics.radius(1000)
color: "transparent"
child: Image {
source: Quickshell.iconPath(modelData.icon, "application-x-executable")
layer.enabled: true
layer.effect: MultiEffect { // Tint if needed, ngl this looks fucking cool when you use monochrome
saturation: (Config.runtime.appearance.tintIcons || (Config.runtime.appearance.colors.matugenScheme === "scheme-monochrome" && Config.runtime.appearance.colors.autogenerated) || Config.runtime.appearance.colors.scheme.toLowerCase() === "monochrome") ? -1.0 : 1.0
}
}
}
ColumnLayout {
anchors.left: entryIcon.right
anchors.leftMargin: Metrics.margin(10)
anchors.top: parent.top
anchors.topMargin: (parent.height / 2) - (height / 2)
height: 40
spacing: Metrics.spacing(-5)
StyledText {
font.weight: 400
text: modelData.name
font.pixelSize: Metrics.fontSize(14)
color: {
if (root.hovered || root.selected)
return Appearance.m3colors.m3onSurface
else
return Appearance.colors.colOutline
}
Behavior on color {
PropertyAnimation {
duration: Metrics.chronoDuration(200)
easing.type: Easing.InSine
}
}
}
StyledText {
font.weight: 400
text: StringUtils.shortText(modelData.comment, 65) // Limit maximum chars to 65
font.pixelSize: Metrics.fontSize(12)
color: {
if (root.hovered || root.selected)
return Qt.alpha(Appearance.m3colors.m3onSurface, 0.7)
else
return Qt.alpha(Appearance.colors.colOutline, 0.7)
}
Behavior on color {
PropertyAnimation {
duration: Metrics.chronoDuration(200)
easing.type: Easing.InSine
}
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.hovered = true
onExited: root.hovered = false
onClicked: {
modelData.execute()
IPCLoader.toggleLauncher()
}
}
}

View File

@@ -0,0 +1,170 @@
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Controls
import qs.modules.components
import qs.modules.functions
import qs.config
import qs.services
PanelWindow {
id: launcherWindow
readonly property bool launcherOpen: Globals.visiblility.launcher
visible: launcherOpen
focusable: true
aboveWindows: true // btw I never knew this was a property (read docs)
color: "transparent"
anchors {
top: true
bottom: true
left: true
right: true
}
exclusionMode: ExclusionMode.Ignore // why this? idk but it works atleast
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
ScrollView {
id: maskId
implicitHeight: DisplayMetrics.scaledHeight(0.623)
implicitWidth: DisplayMetrics.scaledWidth(0.3)
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: (parent.width / 2) - (implicitWidth / 2)
anchors.topMargin: (parent.height / 2) - (implicitHeight / 2)
clip: true
focus: true
Rectangle {
id: launcher
property string currentSearch: ""
property int entryIndex: 0
property list<DesktopEntry> appList: Apps.list
Connections {
target: launcherWindow
function onLauncherOpenChanged() {
if (!launcherWindow.launcherOpen) {
launcher.currentSearch = ""
launcher.entryIndex = 0
launcher.appList = Apps.list
}
}
}
anchors.fill: parent
color: Appearance.m3colors.m3surface
radius: Metrics.radius(21)
StyledRect {
id: searchBox
anchors.top: parent.top
anchors.topMargin: Metrics.margin(10)
color: Appearance.m3colors.m3surfaceContainerLow
width: parent.width - 20
anchors.left: parent.left
anchors.leftMargin: (parent.width / 2) - (width / 2)
height: 45
radius: Metrics.radius(15)
z: 2
focus: true
Keys.onDownPressed: launcher.entryIndex += 1
Keys.onUpPressed: {
if (launcher.entryIndex != 0)
launcher.entryIndex -= 1
}
Keys.onEscapePressed: Globals.visiblility.launcher = false
Keys.onPressed: event => {
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
launcher.appList[launcher.entryIndex].execute()
Globals.visiblility.launcher = false
} else if (event.key === Qt.Key_Backspace) {
launcher.currentSearch = launcher.currentSearch.slice(0, -1)
} else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) {
launcher.currentSearch += event.text
}
launcher.appList = Apps.fuzzyQuery(launcher.currentSearch)
launcher.entryIndex = 0
}
MaterialSymbol {
id: iconText
anchors.left: parent.left
anchors.leftMargin: Metrics.margin(10)
icon: "search"
font.pixelSize: Metrics.fontSize(14)
font.weight: 600
anchors.top: parent.top
anchors.topMargin: (parent.height / 2) - ((font.pixelSize + 5) / 2)
opacity: 0.8
}
StyledText {
id: placeHolderText
anchors.left: iconText.right
anchors.leftMargin: Metrics.margin(10)
color: (launcher.currentSearch != "") ? Appearance.m3colors.m3onSurface : Appearance.colors.colOutline
text: (launcher.currentSearch != "") ? launcher.currentSearch : "Start typing to search ..."
font.pixelSize: Metrics.fontSize(13)
anchors.top: parent.top
anchors.topMargin: (parent.height / 2) - ((font.pixelSize + 5) / 2)
animate: false
opacity: 0.8
}
}
ScrollView {
anchors.top: searchBox.bottom
anchors.topMargin: Metrics.margin(10)
anchors.left: parent.left
anchors.leftMargin: (parent.width / 2) - (width / 2)
width: parent.width - 20
height: parent.height - searchBox.height - 20
ListView {
id: appList
anchors.fill: parent
spacing: Metrics.spacing(10)
anchors.bottomMargin: Metrics.margin(4)
model: launcher.appList
currentIndex: launcher.entryIndex
delegate: AppItem {
required property int index
required property DesktopEntry modelData
selected: index === launcher.entryIndex
parentWidth: appList.width
}
}
}
}
}
IpcHandler {
function toggle() {
Globals.visiblility.launcher = !Globals.visiblility.launcher;
}
target: "launcher"
}
}

View File

@@ -0,0 +1,313 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.config
import qs.modules.functions
import qs.modules.components
import qs.services
/*
This LauncherContent has been depricated.
And yet not used. (4/3/26)
*/
Item {
id: content
property int selectedIndex: -1
property string searchQuery: ""
property var calcVars: ({})
property alias listView: listView
property alias filteredModel: filteredModel
function launchCurrent() {
launchApp(listView.currentIndex)
}
function webSearchUrl(query) {
const engine = (Config.runtime.launcher.webSearchEngine || "").toLowerCase()
if (engine.startsWith("http"))
return engine.replace("%s", encodeURIComponent(query))
const engines = {
"google": "https://www.google.com/search?q=%s",
"duckduckgo": "https://duckduckgo.com/?q=%s",
"brave": "https://search.brave.com/search?q=%s",
"bing": "https://www.bing.com/search?q=%s",
"startpage": "https://www.startpage.com/search?q=%s"
}
const template = engines[engine] || engines["duckduckgo"]
return template.replace("%s", encodeURIComponent(query))
}
function moveSelection(delta) {
if (filteredModel.count === 0) return
selectedIndex = Math.max(0, Math.min(selectedIndex + delta, filteredModel.count - 1))
listView.currentIndex = selectedIndex
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
}
function fuzzyMatch(text, pattern) {
text = text.toLowerCase()
pattern = pattern.toLowerCase()
let ti = 0, pi = 0
while (ti < text.length && pi < pattern.length) {
if (text[ti] === pattern[pi]) pi++
ti++
}
return pi === pattern.length
}
function evalExpression(expr) {
try {
const fn = new Function("vars", `
with (vars) { with (Math) { return (${expr}); } }
`)
const res = fn(calcVars)
if (res === undefined || Number.isNaN(res)) return null
return res
} catch (e) {
return null
}
}
function updateFilter() {
filteredModel.clear()
const query = searchQuery.toLowerCase().trim()
const calcVal = evalExpression(query)
if (calcVal !== null && query !== "") {
filteredModel.append({
name: String(calcVal),
displayName: String(calcVal),
comment: "Calculation",
icon: "",
exec: "",
isCalc: true,
isWeb: false
})
}
const sourceApps = AppRegistry.apps
if (query === "") {
for (let app of sourceApps) {
filteredModel.append({
name: app.name,
displayName: app.name,
comment: app.comment,
icon: AppRegistry.iconForDesktopIcon(app.icon),
exec: app.exec,
isCalc: false,
isWeb: false
})
}
selectedIndex = filteredModel.count > 0 ? 0 : -1
listView.currentIndex = selectedIndex
return
}
let exactMatches = []
let startsWithMatches = []
let containsMatches = []
let fuzzyMatches = []
for (let app of sourceApps) {
const name = app.name ? app.name.toLowerCase() : ""
const comment = app.comment ? app.comment.toLowerCase() : ""
if (name === query) exactMatches.push(app)
else if (name.startsWith(query)) startsWithMatches.push(app)
else if (name.includes(query) || comment.includes(query)) containsMatches.push(app)
else if (Config.runtime.launcher.fuzzySearchEnabled && fuzzyMatch(name, query)) fuzzyMatches.push(app)
}
const sortedResults = [
...exactMatches,
...startsWithMatches,
...containsMatches,
...fuzzyMatches
]
for (let app of sortedResults) {
filteredModel.append({
name: app.name,
displayName: app.name,
comment: app.comment,
icon: AppRegistry.iconForDesktopIcon(app.icon),
exec: app.exec,
isCalc: false,
isWeb: false
})
}
if (filteredModel.count === 0 && query !== "") {
filteredModel.append({
name: query,
displayName: "Search the web for \"" + query + "\"",
comment: "Web search",
icon: "public",
exec: webSearchUrl(query),
isCalc: false,
isWeb: true
})
}
selectedIndex = filteredModel.count > 0 ? 0 : -1
listView.currentIndex = selectedIndex
listView.positionViewAtBeginning()
}
function launchApp(idx) {
if (idx < 0 || idx >= filteredModel.count) return
const app = filteredModel.get(idx)
if (app.isCalc) return
if (app.isWeb)
Quickshell.execDetached(["xdg-open", app.exec])
else
Quickshell.execDetached(["bash", "-c", app.exec + " &"])
closeLauncher()
}
function closeLauncher() {
Globals.visiblility.launcher = false
}
function resetSearch() {
searchQuery = ""
updateFilter()
selectedIndex = -1
listView.currentIndex = -1
}
Connections {
target: AppRegistry
function onReady() {
updateFilter()
}
}
anchors.fill: parent
opacity: Globals.visiblility.launcher ? 1 : 0
anchors.margins: Metrics.margin(10)
ListModel { id: filteredModel }
ColumnLayout {
anchors.fill: parent
anchors.margins: Metrics.margin(16)
spacing: Metrics.spacing(12)
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ListView {
id: listView
model: filteredModel
spacing: Metrics.spacing(8)
clip: true
boundsBehavior: Flickable.StopAtBounds
highlightRangeMode: ListView.StrictlyEnforceRange
preferredHighlightBegin: 0
preferredHighlightEnd: height
highlightMoveDuration: 120
currentIndex: selectedIndex
delegate: Rectangle {
property bool isSelected: listView.currentIndex === index
width: listView.width
height: 60
radius: Appearance.rounding.normal
color: isSelected ? Appearance.m3colors.m3surfaceContainerHighest : "transparent"
Row {
anchors.fill: parent
anchors.margins: Metrics.margin(10)
spacing: Metrics.spacing(12)
Item {
width: 32
height: 32
Image {
anchors.fill: parent
visible: !model.isCalc && !model.isWeb
smooth: true
mipmap: true
antialiasing: true
fillMode: Image.PreserveAspectFit
sourceSize.width: 128
sourceSize.height: 128
source: model.icon
}
MaterialSymbol {
anchors.centerIn: parent
visible: model.isCalc
icon: "calculate"
iconSize: Metrics.iconSize(28)
color: Appearance.m3colors.m3onSurfaceVariant
}
MaterialSymbol {
anchors.centerIn: parent
visible: model.isWeb
icon: "public"
iconSize: Metrics.iconSize(28)
color: Appearance.m3colors.m3onSurfaceVariant
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: listView.width - 120
spacing: Metrics.spacing(4)
Text {
text: model.displayName
font.pixelSize: Metrics.fontSize(14)
font.bold: true
elide: Text.ElideRight
color: Appearance.m3colors.m3onSurface
}
Text {
text: model.comment
font.pixelSize: Metrics.fontSize(11)
elide: Text.ElideRight
color: Appearance.m3colors.m3onSurfaceVariant
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: launchApp(index)
onEntered: listView.currentIndex = index
}
}
}
}
}
Behavior on opacity {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration(400)
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.curves.standard
}
}
}

View File

@@ -0,0 +1,56 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pam
// I just copied the default example and modified it. lol
Scope {
id: root
signal unlocked()
signal failed()
// These properties are in the context and not individual lock surfaces
// so all surfaces can share the same state.
property string currentText: ""
property bool unlockInProgress: false
property bool showFailure: false
// Clear the failure text once the user starts typing.
onCurrentTextChanged: showFailure = false;
function tryUnlock() {
if (currentText === "") return;
root.unlockInProgress = true;
pam.start();
}
PamContext {
id: pam
// Its best to have a custom pam config for quickshell, as the system one
// might not be what your interface expects, and break in some way.
// This particular example only supports passwords.
configDirectory: "pam"
config: "password.conf"
// pam_unix will ask for a response for the password prompt
onPamMessage: {
if (this.responseRequired) {
this.respond(root.currentText);
}
}
// pam_unix won't send any important messages so all we need is the completion status.
onCompleted: result => {
if (result == PamResult.Success) {
root.unlocked();
} else {
root.currentText = "";
root.showFailure = true;
}
root.unlockInProgress = false;
}
}
}

View File

@@ -0,0 +1,41 @@
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Scope {
// This stores all the information shared between the lock surfaces on each screen.
LockContext {
id: lockContext
onUnlocked: {
// Unlock the screen before exiting, or the compositor will display a
// fallback lock you can't interact with.
lock.locked = false;
}
}
WlSessionLock {
id: lock
// Lock the session immediately when quickshell starts.
locked: false
WlSessionLockSurface {
LockSurface {
anchors.fill: parent
context: lockContext
}
}
}
IpcHandler {
target: "lockscreen"
function lock() {
lock.locked = true;
}
function unlock() {
lock.locked = false;
}
}
}

View File

@@ -0,0 +1,267 @@
import "../../components/morphedPolygons/geometry/offset.js" as Offset
import "../../components/morphedPolygons/material-shapes.js" as MaterialShapes // For polygons
import "../../components/morphedPolygons/shapes/corner-rounding.js" as CornerRounding
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import Quickshell.Wayland
import qs.config
import qs.modules.functions
import qs.modules.interface.background
import qs.modules.components
import qs.modules.components.morphedPolygons
import qs.services
Rectangle {
id: root
required property LockContext context
color: "transparent"
Image {
anchors.fill: parent
z: -1
source: Config.runtime.appearance.background.path
}
RowLayout {
spacing: Metrics.spacing(20)
anchors {
top: parent.top
right: parent.right
topMargin: Metrics.spacing(20)
rightMargin: Metrics.spacing(30)
}
MaterialSymbol {
id: themeIcon
fill: 1
icon: Config.runtime.appearance.theme === "light" ? "light_mode" : "dark_mode"
iconSize: Metrics.fontSize("hugeass")
}
MaterialSymbol {
id: wifi
icon: Network.icon
iconSize: Metrics.fontSize("hugeass")
}
MaterialSymbol {
id: btIcon
icon: Bluetooth.icon
iconSize: Metrics.fontSize("hugeass")
}
StyledText {
id: keyboardLayoutIcon
text: SystemDetails.keyboardLayout
font.pixelSize: Metrics.fontSize(Appearance.font.size.huge - 4)
}
}
ColumnLayout {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: Metrics.margin(150)
}
StyledText {
id: clock
visible: !Config.runtime.appearance.background.clock.isAnalog
Layout.alignment: Qt.AlignBottom
animate: false
renderType: Text.NativeRendering
font.pixelSize: Metrics.fontSize(180)
text: Time.format("hh:mm")
}
StyledText {
id: date
visible: !Config.runtime.appearance.background.clock.isAnalog
Layout.alignment: Qt.AlignCenter
animate: false
renderType: Text.NativeRendering
font.pixelSize: Metrics.fontSize(50)
text: Time.format("dddd, dd/MM")
}
Item {
id: analogClockContainer
property int hours: parseInt(Time.format("hh"))
property int minutes: parseInt(Time.format("mm"))
property int seconds: parseInt(Time.format("ss"))
readonly property real cx: width / 2
readonly property real cy: height / 2
property var shapes: [MaterialShapes.getCookie7Sided, MaterialShapes.getCookie9Sided, MaterialShapes.getCookie12Sided, MaterialShapes.getPixelCircle, MaterialShapes.getCircle, MaterialShapes.getGhostish]
visible: Config.runtime.appearance.background.clock.isAnalog
width: 350
height: 350
// Polygon
MorphedPolygon {
id: shapeCanvas
anchors.fill: parent
color: Appearance.m3colors.m3secondaryContainer
roundedPolygon: analogClockContainer.shapes[Config.runtime.appearance.background.clock.shape]()
}
ClockDial {
anchors.fill: parent
anchors.margins: parent.width * 0.12
color: Appearance.colors.colOnSecondaryContainer
z: 0
}
// Hour hand
StyledRect {
z: 2
width: 10
height: parent.height * 0.3
radius: Metrics.radius("full")
color: Qt.darker(Appearance.m3colors.m3secondary, 0.8)
x: analogClockContainer.cx - width / 2
y: analogClockContainer.cy - height
transformOrigin: Item.Bottom
rotation: (analogClockContainer.hours % 12 + analogClockContainer.minutes / 60) * 30
}
StyledRect {
anchors.centerIn: parent
width: 16
height: 16
radius: width / 2
color: Appearance.m3colors.m3secondary
z: 99 // Ensures its on top of everthing
// Inner dot
StyledRect {
width: parent.width / 2
height: parent.height / 2
radius: width / 2
anchors.centerIn: parent
z: 100
color: Appearance.m3colors.m3primaryContainer
}
}
// Minute hand
StyledRect {
width: 14
height: parent.height * 0.35
radius: Metrics.radius("full")
color: Appearance.m3colors.m3secondary
x: analogClockContainer.cx - width / 2
y: analogClockContainer.cy - height
transformOrigin: Item.Bottom
rotation: analogClockContainer.minutes * 6
z: 10 // On top of all hands
}
// Second hand
StyledRect {
visible: true
width: 4
height: parent.height * 0.28
radius: Metrics.radius("full")
color: Appearance.m3colors.m3error
x: analogClockContainer.cx - width / 2
y: analogClockContainer.cy - height
transformOrigin: Item.Bottom
rotation: analogClockContainer.seconds * 6
z: 2
}
StyledText {
text: Time.format("hh")
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Metrics.margin(60)
font.pixelSize: Metrics.fontSize(100)
font.bold: true
opacity: 0.3
animate: false
}
StyledText {
text: Time.format("mm")
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Metrics.margin(150)
font.pixelSize: Metrics.fontSize(100)
font.bold: true
opacity: 0.3
animate: false
}
}
}
ColumnLayout {
// Commenting this will make the password entry visible on all monitors.
visible: Window.active
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: Metrics.margin(20)
}
RowLayout {
StyledTextField {
id: passwordBox
implicitWidth: 300
padding: Metrics.padding(10)
placeholder: root.context.showFailure ? "Incorrect Password" : "Enter Password"
focus: true
enabled: !root.context.unlockInProgress
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
// Update the text in the context when the text in the box changes.
onTextChanged: root.context.currentText = this.text
// Try to unlock when enter is pressed.
onAccepted: root.context.tryUnlock()
// Update the text in the box to match the text in the context.
// This makes sure multiple monitors have the same text.
Connections {
function onCurrentTextChanged() {
passwordBox.text = root.context.currentText;
}
target: root.context
}
}
StyledButton {
icon: "chevron_right"
padding: Metrics.padding(10)
radius: Metrics.radius("unsharpenmore")
// don't steal focus from the text box
focusPolicy: Qt.NoFocus
enabled: !root.context.unlockInProgress && root.context.currentText !== ""
onClicked: root.context.tryUnlock()
}
}
}
}

View File

@@ -0,0 +1 @@
auth required pam_unix.so

View File

@@ -0,0 +1,144 @@
import qs.config
import qs.modules.components
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
Rectangle {
id: root
property bool startAnim: false
property string title: "No Title"
property string body: "No content"
property var rawNotif: null
property bool tracked: false
property string image: ""
property var buttons: [
{ label: "Okay!", onClick: () => console.log("Okay") }
]
opacity: tracked ? 1 : (startAnim ? 1 : 0)
Behavior on opacity {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration("small")
easing.type: Easing.InOutExpo
}
}
Layout.fillWidth: true
radius: Metrics.radius("large")
property bool hovered: mouseHandler.containsMouse
property bool clicked: mouseHandler.containsPress
color: hovered ? (clicked ? Appearance.m3colors.m3surfaceContainerHigh : Appearance.m3colors.m3surfaceContainerLow) : Appearance.m3colors.m3surface
Behavior on color {
enabled: Config.runtime.appearance.animations.enabled
ColorAnimation {
duration: Metrics.chronoDuration("small")
easing.type: Easing.InOutExpo
}
}
implicitHeight: Math.max(content.implicitHeight + 30, 80)
RowLayout {
id: content
anchors.fill: parent
anchors.margins: Metrics.margin(10)
spacing: Metrics.spacing(10)
ClippingRectangle {
width: 50
height: 50
radius: Metrics.radius("large")
clip: true
color: root.image === "" ? Appearance.m3colors.m3surfaceContainer : "transparent"
Image {
anchors.fill: parent
source: root.image
fillMode: Image.PreserveAspectCrop
smooth: true
}
MaterialSymbol {
icon: "chat"
color: Appearance.m3colors.m3onSurfaceVariant
anchors.centerIn: parent
visible: root.image === ""
iconSize: Metrics.iconSize(22)
}
}
ColumnLayout {
StyledText {
text: root.title
font.bold: true
font.pixelSize: Metrics.fontSize(18)
wrapMode: Text.Wrap
color: Appearance.m3colors.m3onSurface
Layout.fillWidth: true
}
StyledText {
text: root.body.length > 123 ? root.body.substr(0, 120) + "..." : root.body
visible: root.body.length > 0
font.pixelSize: Metrics.fontSize(12)
color: Appearance.m3colors.m3onSurfaceVariant
wrapMode: Text.Wrap
Layout.fillWidth: true
}
RowLayout {
visible: root.buttons.length > 1
Layout.preferredHeight: 40
Layout.fillWidth: true
spacing: Metrics.spacing(10)
Repeater {
model: buttons
StyledButton {
Layout.fillWidth: true
implicitHeight: 30
implicitWidth: 0
text: modelData.label
base_bg: index !== 0
? Appearance.m3colors.m3secondaryContainer
: Appearance.m3colors.m3primary
base_fg: index !== 0
? Appearance.m3colors.m3onSecondaryContainer
: Appearance.m3colors.m3onPrimary
onClicked: modelData.onClick()
}
}
}
}
}
MouseArea {
id: mouseHandler
anchors.fill: parent
hoverEnabled: true
visible: root.buttons.length === 0 || root.buttons.length === 1
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.buttons.length === 1 && root.buttons[0].onClick) {
root.buttons[0].onClick()
root.rawNotif?.notification.dismiss()
} else if (root.buttons.length === 0) {
console.log("[Notification] Dismissed a notification with no action.")
root.rawNotif.notification.tracked = false
root.rawNotif.popup = false
root.rawNotif?.notification.dismiss()
} else {
console.log("[Notification] Dismissed a notification with multiple actions.")
root.rawNotif?.notification.dismiss()
}
}
}
Component.onCompleted: {
startAnim = true
}
}

View File

@@ -0,0 +1,154 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Notifications
import Quickshell.Wayland
import Quickshell.Widgets
import qs.services
import qs.config
import qs.modules.components
Scope {
id: root
property int innerSpacing: Metrics.spacing(10)
PanelWindow {
id: window
implicitWidth: 520
visible: true
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusionMode: ExclusionMode.Normal
WlrLayershell.namespace: "nucleus:notification"
anchors {
top: true
left: Config.runtime.notifications.position.endsWith("left")
bottom: true
right: Config.runtime.notifications.position.endsWith("right")
}
Item {
id: notificationList
anchors.leftMargin: 0
anchors.topMargin: Metrics.margin(10)
anchors.rightMargin: 0
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
Rectangle {
id: bgRectangle
layer.enabled: true
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: Metrics.margin(20)
anchors.rightMargin: Metrics.margin(20)
anchors.right: parent.right
height: window.mask.height > 0 ? window.mask.height + 40 : 0
color: Appearance.m3colors.m3background
radius: Metrics.radius("large")
layer.effect: MultiEffect {
shadowEnabled: true
shadowOpacity: 1
shadowColor: Appearance.m3colors.m3shadow
shadowBlur: 1
shadowScale: 1
}
Behavior on height {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration("small")
easing.type: Easing.InOutExpo
}
}
}
Item {
id: notificationColumn
anchors.left: parent.left
anchors.right: parent.right
Repeater {
id: rep
model: (!Config.runtime.notifications.doNotDisturb && Config.runtime.notifications.enabled) ? NotifServer.popups : []
NotificationChild {
id: child
width: notificationColumn.width - 80
anchors.horizontalCenter: notificationColumn.horizontalCenter
y: {
var pos = 0;
for (let i = 0; i < index; i++) {
var prev = rep.itemAt(i);
if (prev)
pos += prev.height + root.innerSpacing;
}
return pos + 20;
}
Component.onCompleted: {
if (!modelData.shown)
modelData.shown = true;
}
title: modelData.summary
body: modelData.body
image: modelData.image || modelData.appIcon
rawNotif: modelData
tracked: modelData.shown
buttons: modelData.actions.map((action) => {
return ({
"label": action.text,
"onClick": () => {
return action.invoke();
}
});
})
Behavior on y {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration("normal")
easing.type: Easing.InOutExpo
}
}
}
}
}
}
mask: Region {
width: window.width
height: {
var total = 0;
for (let i = 0; i < rep.count; i++) {
var child = rep.itemAt(i);
if (child)
total += child.height + (i < rep.count - 1 ? root.innerSpacing : 0);
}
return total;
}
}
}
}

View File

@@ -0,0 +1,99 @@
import qs.config
import qs.modules.components
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell.Wayland
import Quickshell
import Quickshell.Widgets
Scope {
id: root
Connections {
target: Brightness
function onBrightnessChanged() {
root.shouldShowOsd = true;
hideTimer.restart();
}
}
property var monitor: Brightness.monitors.length > 0 ? Brightness.monitors[0] : null
property bool shouldShowOsd: false
Timer {
id: hideTimer
interval: 3000
onTriggered: root.shouldShowOsd = false
}
LazyLoader {
active: root.shouldShowOsd
PanelWindow {
visible: Config.runtime.overlays.brightnessOverlayEnabled && Config.runtime.overlays.enabled
WlrLayershell.namespace: "nucleus:brightnessOsd"
exclusiveZone: 0
anchors.top: Config.runtime.overlays.brightnessOverlayPosition.startsWith("top")
anchors.bottom: Config.runtime.overlays.brightnessOverlayPosition.startsWith("bottom")
anchors.right: Config.runtime.overlays.brightnessOverlayPosition.endsWith("right")
anchors.left: Config.runtime.overlays.brightnessOverlayPosition.endsWith("left")
margins {
top: Metrics.margin(10)
bottom: Metrics.margin(10)
left: Metrics.margin(10)
right: Metrics.margin(10)
}
implicitWidth: 460
implicitHeight: 105
color: "transparent"
mask: Region {}
Rectangle {
anchors.fill: parent
radius: Appearance.rounding.childish
color: Appearance.m3colors.m3background
RowLayout {
spacing: Metrics.spacing(10)
anchors {
fill: parent
leftMargin: Metrics.margin(15)
rightMargin: Metrics.margin(25)
}
MaterialSymbol {
property real brightnessLevel: Math.floor(Brightness.getMonitorForScreen(Hyprland.focusedMonitor)?.multipliedBrightness*100)
icon: {
if (brightnessLevel > 66) return "brightness_high"
else if (brightnessLevel > 33) return "brightness_medium"
else return "brightness_low"
}
iconSize: Metrics.iconSize(30)
}
ColumnLayout {
implicitHeight: 40
spacing: Metrics.spacing(5)
StyledText {
animate: false
text: "Brightness - " + Math.round(monitor.brightness * 100) + '%'
font.pixelSize: Metrics.fontSize(18)
}
StyledSlider {
implicitHeight: 35
from: 0
to: 100
value: Math.round(monitor.brightness * 100)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
import QtQuick
import Quickshell
Scope {
id: root
VolumeOverlay{}
BrightnessOverlay{}
}

View File

@@ -0,0 +1,109 @@
import qs.config
import qs.modules.components
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Pipewire
import Quickshell.Widgets
Scope {
id: root
PwObjectTracker {
objects: [ Pipewire.defaultAudioSink ]
}
Connections {
target: Pipewire.defaultAudioSink?.audio ?? null
function onVolumeChanged() {
root.shouldShowOsd = true;
hideTimer.restart();
}
function onMutedChanged() {
root.shouldShowOsd = true;
hideTimer.restart();
}
}
property bool shouldShowOsd: false
Timer {
id: hideTimer
interval: 3000
onTriggered: root.shouldShowOsd = false
}
LazyLoader {
active: root.shouldShowOsd
PanelWindow {
visible: Config.runtime.overlays.volumeOverlayEnabled && Config.runtime.overlays.enabled
WlrLayershell.namespace: "nucleus:brightnessOsd"
exclusiveZone: 0
anchors.top: Config.runtime.overlays.volumeOverlayPosition.startsWith("top")
anchors.bottom: Config.runtime.overlays.volumeOverlayPosition.startsWith("bottom")
anchors.right: Config.runtime.overlays.volumeOverlayPosition.endsWith("right")
anchors.left: Config.runtime.overlays.volumeOverlayPosition.endsWith("left")
margins {
top: Metrics.margin(10)
bottom: Metrics.margin(10)
left: Metrics.margin(10)
right: Metrics.margin(10)
}
implicitWidth: 460
implicitHeight: 105
color: "transparent"
mask: Region {}
Rectangle {
anchors.fill: parent
radius: Appearance.rounding.childish
color: Appearance.m3colors.m3background
RowLayout {
spacing: Metrics.spacing(10)
anchors {
fill: parent
leftMargin: Metrics.margin(15)
rightMargin: Metrics.margin(25)
}
MaterialSymbol {
property real volume: Pipewire.defaultAudioSink?.audio.muted ? 0 : Pipewire.defaultAudioSink?.audio.volume * 100
icon: volume > 50 ? "volume_up" : volume > 0 ? "volume_down" : 'volume_off'
iconSize: Metrics.iconSize(34);
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Metrics.spacing(2)
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
animate: false
text: (Pipewire.defaultAudioSink?.description ?? "Unknown") + " - " +
(Pipewire.defaultAudioSink?.audio.muted ? 'Muted' : Math.floor(Pipewire.defaultAudioSink?.audio.volume * 100) + '%')
font.pixelSize: Metrics.fontSize(18)
}
StyledSlider {
Layout.fillWidth: true
implicitHeight: 35
value: (Pipewire.defaultAudioSink?.audio.muted ? 0 : Pipewire.defaultAudioSink?.audio.volume) * 100
}
}
}
}
}
}
}

View File

@@ -0,0 +1,207 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.config
import qs.services
import qs.modules.components
Scope {
id: root
property bool active: false
property var window: null
Connections {
target: Polkit
function onIsActiveChanged() {
if (Polkit.isActive) {
root.active = true;
} else if (root.active && window) {
window.closeWithAnimation();
}
}
}
LazyLoader {
active: root.active
component: Prompt {
id: window
Component.onCompleted: root.window = window
Component.onDestruction: root.window = null
onFadeOutFinished: root.active = false
Item {
id: promptContainer
property bool showPassword: false
property bool authenticating: false
anchors.centerIn: parent
width: promptBg.width
height: promptBg.height
Item {
Component.onCompleted: {
parent.layer.enabled = true;
parent.layer.effect = effectComponent;
}
Component {
id: effectComponent
MultiEffect {
shadowEnabled: true
shadowOpacity: 1
shadowColor: Appearance.colors.m3shadow
shadowBlur: 1
shadowScale: 1
}
}
}
Rectangle {
id: promptBg
width: promptLayout.width + 40
height: promptLayout.height + 40
color: Appearance.m3colors.m3surface
radius: Metrics.radius(20)
Behavior on height {
NumberAnimation {
duration: Metrics.chronoDuration("small")
easing.type: Appearance.animation.easing
}
}
}
ColumnLayout {
id: promptLayout
spacing: Metrics.spacing(10)
anchors {
left: promptBg.left
leftMargin: Metrics.margin(20)
top: promptBg.top
topMargin: Metrics.margin(20)
}
ColumnLayout {
spacing: Metrics.spacing(5)
MaterialSymbol {
icon: "security"
color: Appearance.m3colors.m3primary
font.pixelSize: Metrics.fontSize(22)
Layout.alignment: Qt.AlignHCenter
}
StyledText {
text: "Authentication required"
font.family: "Outfit SemiBold"
font.pixelSize: Metrics.fontSize(20)
Layout.alignment: Qt.AlignHCenter
}
StyledText {
text: Polkit.flow.message
Layout.alignment: Qt.AlignHCenter
}
}
RowLayout {
spacing: Metrics.spacing(5)
StyledTextField {
id: textfield
Layout.fillWidth: true
leftPadding: undefined
padding: Metrics.padding(10)
filled: false
enabled: !promptContainer.authenticating
placeholder: Polkit.flow.inputPrompt.substring(0, Polkit.flow.inputPrompt.length - 2)
echoMode: promptContainer.showPassword ? TextInput.Normal : TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
focus: true
Keys.onReturnPressed: okButton.clicked()
}
StyledButton {
Layout.fillHeight: true
width: height
radius: Metrics.radius(10)
topLeftRadius: Metrics.radius(5)
bottomLeftRadius: Metrics.radius(5)
enabled: !promptContainer.authenticating
checkable: true
checked: promptContainer.showPassword
icon: promptContainer.showPassword ? 'visibility' : 'visibility_off'
onToggled: promptContainer.showPassword = !promptContainer.showPassword
}
}
RowLayout {
RowLayout {
visible: Polkit.flow.failed && !Polkit.flow.isSuccessful && !promptContainer.authenticating
MaterialSymbol {
icon: "warning"
color: Appearance.m3colors.m3error
font.pixelSize: Metrics.fontSize(15)
}
StyledText {
text: "Failed to authenticate, incorrect password."
color: Appearance.m3colors.m3error
font.pixelSize: Metrics.fontSize(15)
}
}
LoadingIcon {
visible: promptContainer.authenticating
Layout.alignment: Qt.AlignLeft
}
Item {
Layout.fillWidth: true
}
StyledButton {
radius: Metrics.radius(10)
topRightRadius: Metrics.radius(5)
bottomRightRadius: Metrics.radius(5)
secondary: true
text: "Cancel"
// enabled: !promptContainer.authenticating (Allows to cancel if stuck in loop)
onClicked: Polkit.flow.cancelAuthenticationRequest()
}
StyledButton {
id: okButton
radius: Metrics.radius(10)
topLeftRadius: Metrics.radius(5)
bottomLeftRadius: Metrics.radius(5)
text: promptContainer.authenticating ? "Authenticating..." : "OK"
enabled: !promptContainer.authenticating
onClicked: {
promptContainer.authenticating = true;
Polkit.flow.submit(textfield.text);
}
}
}
}
Connections {
target: Polkit.flow
function onIsCompletedChanged() {
if (Polkit.flow.isCompleted) {
promptContainer.authenticating = false;
}
}
function onFailedChanged() {
if (Polkit.flow.failed) {
promptContainer.authenticating = false;
}
}
function onIsCancelledChanged() {
if (Polkit.flow.isCancelled) {
promptContainer.authenticating = false;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,151 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.config
import qs.services
import qs.modules.components
PanelWindow {
id: window
property bool isClosing: false
default property alias content: contentContainer.data
signal fadeOutFinished()
anchors {
top: true
left: true
right: true
bottom: true
}
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
WlrLayershell.namespace: "nucleus:prompt"
function closeWithAnimation() {
if (isClosing) return
isClosing = true
fadeOutAnim.start()
}
Item {
anchors.fill: parent
Keys.onPressed: {
if (event.key === Qt.Key_Escape) {
window.closeWithAnimation()
}
}
ScreencopyView {
id: screencopy
visible: hasContent
captureSource: window.screen
anchors.fill: parent
opacity: 0
scale: 1
layer.enabled: true
layer.effect: MultiEffect {
blurEnabled: true
blur: 1
blurMax: 32
brightness: -0.05
layer.enabled: true
layer.effect: MultiEffect {
autoPaddingEnabled: false
blurEnabled: true
blur: 1
blurMax: 32
}
}
}
NumberAnimation {
id: fadeInAnim
target: screencopy
property: "opacity"
from: 0
to: 1
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
running: screencopy.visible && !window.isClosing
}
ParallelAnimation {
id: scaleInAnim
running: screencopy.visible && !window.isClosing
NumberAnimation {
target: contentContainer
property: "scale"
from: 0.9
to: 1
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
ColorAnimation {
target: window
property: "color"
from: "transparent"
to: Appearance.m3colors.m3surface
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
NumberAnimation {
target: contentContainer
property: "opacity"
from: 0
to: 1
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
}
ParallelAnimation {
id: fadeOutAnim
NumberAnimation {
target: screencopy
property: "opacity"
to: 0
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
ColorAnimation {
target: window
property: "color"
to: "transparent"
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
NumberAnimation {
target: contentContainer
property: "opacity"
to: 0
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
NumberAnimation {
target: contentContainer
property: "scale"
to: 0.9
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
onFinished: {
window.visible = false
window.fadeOutFinished()
}
}
Item {
id: contentContainer
anchors.fill: parent
opacity: 0
scale: 0.9
}
}
}

View File

@@ -0,0 +1,153 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Wayland
import qs.config
import qs.modules.functions
import qs.services
import qs.modules.interface.lockscreen
import qs.modules.components
PanelWindow {
id: powermenu
WlrLayershell.keyboardFocus: Compositor.require("hyprland") && Globals.visiblility.powermenu
function togglepowermenu() {
Globals.visiblility.powermenu = !Globals.visiblility.powermenu; // Simple toggle logic kept in a function as it might have more things to it later on.
}
WlrLayershell.namespace: "nucleus:powermenu"
WlrLayershell.layer: WlrLayer.Top
visible: Config.initialized && Globals.visiblility.powermenu
color: "transparent"
exclusiveZone: 0
implicitWidth: DisplayMetrics.scaledWidth(0.25)
implicitHeight: DisplayMetrics.scaledWidth(0.168)
HyprlandFocusGrab {
id: grab
active: Compositor.require("hyprland")
windows: [powermenu]
}
StyledRect {
id: container
color: Appearance.m3colors.m3background
radius: Metrics.radius("verylarge")
implicitWidth: powermenu.implicitWidth
anchors.fill: parent
FocusScope {
focus: true
anchors.fill: parent
Keys.onPressed: {
if (event.key === Qt.Key_Escape)
Globals.visiblility.powermenu = false;
}
Item {
id: content
anchors.margins: Metrics.radius(12)
anchors.topMargin: Metrics.radius(16)
anchors.leftMargin: Metrics.radius(18)
anchors.fill: parent
Grid {
columns: 3
rows: 3
rowSpacing: Metrics.spacing(10)
columnSpacing: Metrics.spacing(10)
anchors.fill: parent
PowerMenuButton {
buttonIcon: "power_settings_new"
onClicked: {
Quickshell.execDetached(["poweroff"]);
Globals.visiblility.powermenu = false;
}
}
PowerMenuButton {
buttonIcon: "logout"
onClicked: {
Quickshell.execDetached(["hyprctl", "dispatch", "exit"]);
Globals.visiblility.powermenu = false;
}
}
PowerMenuButton {
buttonIcon: "sleep"
onClicked: {
Quickshell.execDetached(["systemctl", "suspend"]);
Globals.visiblility.powermenu = false;
}
}
PowerMenuButton {
buttonIcon: "lock"
onClicked: {
Quickshell.execDetached(["nucleus", "ipc", "call", "lockscreen", "lock"]);
Globals.visiblility.powermenu = false;
}
}
PowerMenuButton {
buttonIcon: "restart_alt"
onClicked: {
Quickshell.execDetached(["reboot"]);
Globals.visiblility.powermenu = false;
}
}
PowerMenuButton {
buttonIcon: "light_off"
onClicked: {
Quickshell.execDetached(["systemctl", "hibernate"]);
Globals.visiblility.powermenu = false;
}
}
}
component Anim: NumberAnimation {
running: Config.runtime.appearance.animations.enabled
duration: Metrics.chronoDuration(400)
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.curves.standard
}
}
}
}
IpcHandler {
function toggle() {
togglepowermenu();
}
target: "powermenu"
}
component PowerMenuButton: StyledButton {
property string buttonIcon
icon: buttonIcon
iconSize: Metrics.iconSize(50)
width: powermenu.implicitWidth / 3.4
height: powermenu.implicitHeight / 2.3
radius: beingHovered ? Metrics.radius("verylarge") * 2 : Metrics.radius("large")
}
}

View File

@@ -0,0 +1,606 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell.Wayland
import qs.config
import qs.modules.components
Scope {
id: root
property bool active: false
property rect selectedRegion: Qt.rect(0, 0, 0, 0)
property string tempScreenshot: ""
IpcHandler {
target: "screen"
function capture() {
if (root.active) {
console.info("screencap", "already active");
return;
}
console.info("screencap", "starting capture");
root.active = true;
}
}
LazyLoader {
active: root.active
component: PanelWindow {
id: win
property bool closing: false
property bool ready: false
property bool processing: false
property bool windowMode: false
property string savedPath: ""
property bool savedSuccess: false
color: Appearance.m3colors.m3surface
anchors { top: true; left: true; right: true; bottom: true }
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "nucleus:screencapture"
Component.onCompleted: {
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
root.tempScreenshot = "/tmp/screenshot_" + ts + ".png";
}
function close() {
if (closing) return;
closing = true;
closeAnim.start();
}
function saveFullscreen() {
console.info("screencap", "saveFullscreen started");
win.processing = true;
screencopy.grabToImage(function(result) {
console.info("screencap", "fullscreen grabbed");
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
win.savedPath = Quickshell.env("HOME") + "/Pictures/Screenshots/screenshot_" + ts + ".png";
console.info("screencap", "saving to: " + win.savedPath);
if (result.saveToFile(win.savedPath)) {
console.info("screencap", "saved, copying");
Quickshell.execDetached({
command: ["sh", "-c", "cat '" + win.savedPath + "' | wl-copy --type image/png"]
});
win.savedSuccess = true;
} else {
console.info("screencap", "save failed");
win.savedSuccess = false;
}
win.processing = false;
console.info("screencap", "closing window");
win.close();
});
}
Component {
id: ffmpegProc
Process {
property string outputPath
property bool success: false
onExited: (code) => {
console.info("screencap", "ffmpeg exited: " + code);
success = code === 0;
if (success) {
console.info("screencap", "copying to clipboard");
Quickshell.execDetached({
command: ["sh", "-c", "cat '" + outputPath + "' | wl-copy --type image/png"]
});
}
Quickshell.execDetached({ command: ["rm", root.tempScreenshot] });
win.savedSuccess = success;
win.processing = false;
console.info("screencap", "done, closing");
win.close();
destroy();
}
}
}
function saveRegion(rect, suffix) {
console.info("screencap", "saveRegion started: " + rect.x + "," + rect.y + " " + rect.width + "x" + rect.height);
screencopy.grabToImage(function(result) {
console.info("screencap", "full screenshot grabbed for cropping");
if (!result.saveToFile(root.tempScreenshot)) {
console.info("screencap", "ERROR: failed to save temp screenshot");
win.savedSuccess = false;
win.processing = false;
win.close();
return;
}
console.info("screencap", "temp saved, cropping with ffmpeg");
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
win.savedPath = Quickshell.env("HOME") + "/Pictures/Screenshots/screenshot_" + ts + suffix + ".png";
ffmpegProc.createObject(win, {
command: ["ffmpeg", "-i", root.tempScreenshot, "-vf", "crop=" + Math.floor(rect.width) + ":" + Math.floor(rect.height) + ":" + Math.floor(rect.x) + ":" + Math.floor(rect.y), "-y", win.savedPath],
outputPath: win.savedPath,
running: true
});
});
}
function captureFullscreen() {
win.processing = true;
saveFullscreen();
}
function captureWindow(rect) {
win.processing = true;
saveRegion(rect, "_window");
}
function captureRegion() {
if (!ready || !selection.hasSelection) return;
win.processing = true;
saveRegion(root.selectedRegion, "_region");
}
ScreencopyView {
id: screencopy
anchors.fill: parent
captureSource: win.screen
z: -999
live: false
onHasContentChanged: {
console.info("screencap", "hasContent: " + hasContent);
if (hasContent) {
console.info("screencap", "grabbing for preview");
grabToImage(function(result) {
console.info("screencap", "preview grabbed: " + result.url);
frozen.source = result.url;
readyTimer.start();
});
}
}
}
Timer {
id: readyTimer
interval: Metrics.chronoDuration("normal") + 50
onTriggered: {
console.info("screencap", "UI ready");
win.ready = true;
}
}
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: win.close()
Keys.onPressed: event => {
if (event.key === Qt.Key_F) {
win.captureFullscreen();
event.accepted = true;
} else if (event.key === Qt.Key_W) {
win.windowMode = !win.windowMode;
event.accepted = true;
}
}
Image {
id: bg
anchors.fill: parent
source: Config.runtime.appearance.background.path
fillMode: Image.PreserveAspectCrop
opacity: 0
scale: 1
layer.enabled: true
layer.effect: MultiEffect {
blurEnabled: true
blur: 1.0
blurMax: 64
brightness: -0.1
}
onStatusChanged: {
if (status === Image.Ready) fadeIn.start();
}
NumberAnimation on opacity {
id: fadeIn
to: 1
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
}
Item {
id: container
anchors.centerIn: parent
width: win.width
height: win.height
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowOpacity: 1
shadowColor: Appearance.m3colors.m3shadow
}
Image {
id: frozen
anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
cache: false
}
Item {
id: darkOverlay
anchors.fill: parent
visible: (selection.hasSelection || selection.selecting) && !win.windowMode
Rectangle {
y: 0
width: parent.width
height: selection.sy
color: "black"
opacity: 0.5
}
Rectangle {
y: selection.sy + selection.h
width: parent.width
height: parent.height - (selection.sy + selection.h)
color: "black"
opacity: 0.5
}
Rectangle {
x: 0
y: selection.sy
width: selection.sx
height: selection.h
color: "black"
opacity: 0.5
}
Rectangle {
x: selection.sx + selection.w
y: selection.sy
width: parent.width - (selection.sx + selection.w)
height: selection.h
color: "black"
opacity: 0.5
}
Rectangle {
x: selection.sx
y: selection.sy
width: selection.w
height: selection.h
color: "black"
opacity: win.processing ? 0.6 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
LoadingIcon {
anchors.centerIn: parent
visible: win.processing
}
}
}
Rectangle {
id: outline
x: selection.sx
y: selection.sy
width: selection.w
height: selection.h
color: "transparent"
border.color: Appearance.m3colors.m3primary
border.width: 2
visible: (selection.selecting || selection.hasSelection) && !win.windowMode
}
Rectangle {
visible: selection.selecting
anchors.top: outline.bottom
anchors.topMargin: Metrics.margin(10)
anchors.horizontalCenter: outline.horizontalCenter
width: coords.width + 10
height: coords.height + 10
color: Appearance.m3colors.m3surface
radius: Metrics.radius(20)
StyledText {
id: coords
anchors.centerIn: parent
font.pixelSize: Metrics.fontSize(14)
animate: false
color: Appearance.m3colors.m3onSurface
property real scaleX: container.width / win.width
property real scaleY: container.height / win.height
text: Math.floor(selection.sx/scaleX) + "," + Math.floor(selection.sy/scaleY) + " " + Math.floor(selection.w/scaleX) + "x" + Math.floor(selection.h/scaleY)
}
}
MouseArea {
id: selection
anchors.fill: parent
enabled: win.ready && !win.windowMode
property real x1: 0
property real y1: 0
property real x2: 0
property real y2: 0
property bool selecting: false
property bool hasSelection: false
property real xp: 0
property real yp: 0
property real wp: 0
property real hp: 0
property real sx: xp * parent.width
property real sy: yp * parent.height
property real w: wp * parent.width
property real h: hp * parent.height
onPressed: mouse => {
if (!win.ready) return;
x1 = Math.max(0, Math.min(mouse.x, width));
y1 = Math.max(0, Math.min(mouse.y, height));
x2 = x1;
y2 = y1;
selecting = true;
hasSelection = false;
}
onPositionChanged: mouse => {
if (selecting) {
x2 = Math.max(0, Math.min(mouse.x, width));
y2 = Math.max(0, Math.min(mouse.y, height));
xp = Math.min(x1, x2) / width;
yp = Math.min(y1, y2) / height;
wp = Math.abs(x2 - x1) / width;
hp = Math.abs(y2 - y1) / height;
}
}
onReleased: mouse => {
if (!selecting) return;
x2 = Math.max(0, Math.min(mouse.x, width));
y2 = Math.max(0, Math.min(mouse.y, height));
selecting = false;
hasSelection = Math.abs(x2 - x1) > 5 && Math.abs(y2 - y1) > 5;
if (hasSelection) {
xp = Math.min(x1, x2) / width;
yp = Math.min(y1, y2) / height;
wp = Math.abs(x2 - x1) / width;
hp = Math.abs(y2 - y1) / height;
root.selectedRegion = Qt.rect(
Math.min(x1, x2) * win.screen.width / width,
Math.min(y1, y2) * win.screen.height / height,
Math.abs(x2 - x1) * win.screen.width / width,
Math.abs(y2 - y1) * win.screen.height / height
);
win.captureRegion();
} else {
win.close();
}
}
}
Repeater {
model: {
if (!win.windowMode || !win.ready) return [];
var ws = Hyprland.focusedMonitor?.activeWorkspace;
return ws?.toplevels ? ws.toplevels.values : [];
}
delegate: Item {
required property var modelData
property var w: modelData?.lastIpcObject
visible: w?.at && w?.size
property real barX: 0
property real barY: 0
property real sx: container.width / (win.screen.width - barX)
property real sy: container.height / (win.screen.height - barY)
x: visible ? (w.at[0] - barX) * sx : 0
y: visible ? (w.at[1] - barY) * sy : 0
width: visible ? w.size[0] * sx : 0
height: visible ? w.size[1] * sy : 0
z: w?.floating ? (hover.containsMouse ? 1000 : 100) : (hover.containsMouse ? 50 : 0)
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Appearance.m3colors.m3primary
border.width: hover.containsMouse ? 3 : 0
radius: Metrics.radius(8)
Behavior on border.width {
NumberAnimation { duration: Metrics.chronoDuration(150) }
}
}
Rectangle {
anchors.fill: parent
color: Appearance.m3colors.m3primary
opacity: hover.containsMouse ? 0.15 : 0
radius: Metrics.radius(8)
Behavior on opacity {
NumberAnimation { duration: Metrics.chronoDuration(150) }
}
}
MouseArea {
id: hover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
win.captureWindow(Qt.rect(w.at[0], w.at[1], w.size[0], w.size[1]));
}
}
}
}
ParallelAnimation {
running: win.visible && !win.closing && frozen.source != ""
NumberAnimation {
target: bg
property: "scale"
to: bg.scale + 0.05
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
NumberAnimation {
target: container
property: "width"
to: win.width * 0.8
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
NumberAnimation {
target: container
property: "height"
to: win.height * 0.8
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
}
ParallelAnimation {
id: closeAnim
NumberAnimation {
target: bg
property: "scale"
to: bg.scale - 0.05
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
NumberAnimation {
target: container
property: "width"
to: win.width
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
NumberAnimation {
target: container
property: "height"
to: win.height
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
NumberAnimation {
target: darkOverlay
property: "opacity"
to: 0
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
NumberAnimation {
target: outline
property: "opacity"
to: 0
duration: Metrics.chronoDuration("normal")
easing.type: Appearance.animation.easing
}
onFinished: {
root.active = false;
if (win.savedSuccess) {
Quickshell.execDetached({
command: ["notify-send", "Screenshot saved", win.savedPath.split("/").pop() + " (copied)"]
});
} else if (win.savedPath !== "") {
Quickshell.execDetached({
command: ["notify-send", "Screenshot failed", "Could not save"]
});
}
}
}
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Metrics.margin(30)
width: row.width + 20
height: row.height + 20
visible: true
Rectangle {
anchors.fill: parent
color: Appearance.m3colors.m3surface
radius: Metrics.radius("large")
}
RowLayout {
id: row
anchors.centerIn: parent
StyledButton {
icon: "fullscreen"
text: "Full screen"
tooltipText: "Capture the whole screen [F]"
onClicked: win.captureFullscreen()
}
Rectangle {
Layout.fillHeight: true
width: 2
color: Appearance.m3colors.m3onSurfaceVariant
opacity: 0.2
}
StyledButton {
icon: "window"
checkable: true
checked: win.windowMode
text: "Window"
tooltipText: "Hover and click a window [W]"
onClicked: win.windowMode = !win.windowMode
}
StyledButton {
secondary: true
icon: "close"
tooltipText: "Exit [Escape]"
onClicked: win.close()
}
}
}
}
HyprlandFocusGrab {
id: grab
windows: [win]
}
onVisibleChanged: {
if (visible) grab.active = true
}
Connections {
target: grab
function onActiveChanged() {
if (!grab.active && !win.closing) win.close();
}
}
}
}
}

View File

@@ -0,0 +1,115 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.config
import qs.services
Item {
id: root
Layout.fillWidth: true
Layout.fillHeight: true
property int logoOffset: -30
Column {
anchors.centerIn: parent
width: 460
spacing: Metrics.spacing(12)
Item {
width: parent.width
height: Metrics.fontSize(200)
StyledText {
text: SystemDetails.osIcon
anchors.centerIn: parent
x: root.logoOffset
font.pixelSize: Metrics.fontSize(200)
}
}
StyledText {
text: "Nucleus Shell"
width: parent.width
horizontalAlignment: Text.AlignHCenter
font.family: "Outfit ExtraBold"
font.pixelSize: Metrics.fontSize(26)
}
StyledText {
text: "A shell built to get things done."
width: parent.width
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Metrics.fontSize(14)
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Metrics.spacing(10)
StyledButton {
text: "View on GitHub"
icon: "code"
secondary: true
onClicked: Qt.openUrlExternally("https://github.com/xZepyx/nucleus-shell")
}
StyledButton {
text: "Report Issue"
icon: "bug_report"
secondary: true
onClicked: Qt.openUrlExternally("https://github.com/xZepyx/nucleus-shell/issues")
}
}
}
StyledText {
text: "Nucleus-Shell v" + Config.runtime.shell.version
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Metrics.margin(24)
font.pixelSize: Metrics.fontSize(12)
}
StyledRect {
width: 52
height: 52
radius: Appearance.rounding.small
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Metrics.margin(24)
StyledText {
text: "↻"
anchors.centerIn: parent
font.pixelSize: Metrics.fontSize(22)
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
Globals.states.settingsOpen = false
Quickshell.execDetached(["notify-send", "Updating Nucleus Shell"])
Quickshell.execDetached([
"kitty",
"--hold",
"bash",
"-c",
Directories.scriptsPath + "/system/update.sh"
])
}
}
}
}

View File

@@ -0,0 +1,364 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.services
import qs.plugins
ContentMenu {
title: "Appearance"
description: "Adjust how the desktop looks like."
ContentCard {
ColumnLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(16)
ColumnLayout {
spacing: Metrics.spacing(4)
StyledText {
text: "Select Theme"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Choose between dark or light mode."
font.pixelSize: Metrics.fontSize(12)
color: "#888888"
}
}
RowLayout {
Layout.leftMargin: Metrics.margin(15)
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
spacing: Metrics.spacing(16)
StyledButton {
Layout.preferredHeight: 300
Layout.preferredWidth: 460
Layout.maximumHeight: 400
Layout.maximumWidth: 500
icon: "dark_mode"
iconSize: Metrics.iconSize(64)
checked: Config.runtime.appearance.theme === "dark"
hoverEnabled: true
onClicked: {
if (!Config.runtime.appearance.colors.autogenerated) {
const scheme = Config.runtime.appearance.colors.scheme
const file = Theme.map[scheme]?.dark
if (!file) {
Theme.notifyMissingVariant(scheme, "dark")
return
}
Config.updateKey("appearance.theme", "dark")
Quickshell.execDetached([
"nucleus", "theme", "switch", file
])
} else {
Config.updateKey("appearance.theme", "dark")
Quickshell.execDetached([
"nucleus", "ipc", "call", "global", "regenColors"
])
}
}
}
StyledButton {
Layout.preferredHeight: 300
Layout.preferredWidth: 460
Layout.maximumHeight: 400
Layout.maximumWidth: 500
icon: "light_mode"
iconSize: Metrics.iconSize(64)
checked: Config.runtime.appearance.theme === "light"
hoverEnabled: true
onClicked: {
if (!Config.runtime.appearance.colors.autogenerated) {
const scheme = Config.runtime.appearance.colors.scheme
const file = Theme.map[scheme]?.light
if (!file) {
Theme.notifyMissingVariant(scheme, "light")
return
}
Config.updateKey("appearance.theme", "light")
Quickshell.execDetached([
"nucleus", "theme", "switch", file
])
} else {
Config.updateKey("appearance.theme", "light")
Quickshell.execDetached([
"nucleus", "ipc", "call", "global", "regenColors"
])
}
}
}
}
Item {
width: Metrics.spacing(30)
}
}
}
ContentCard {
RowLayout {
opacity: autogeneratedColorsSelector.enabled ? 1 : 0.8
ColumnLayout {
StyledText {
text: "Color Generation Schemes:"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Choose the scheme for autogenerated color generation."
font.pixelSize: Metrics.fontSize(12)
}
}
Item { Layout.fillWidth: true }
StyledDropDown {
id: autogeneratedColorsSelector
label: "Color Scheme"
model: [
"scheme-content",
"scheme-expressive",
"scheme-fidelity",
"scheme-fruit-salad",
"scheme-monochrome",
"scheme-neutral",
"scheme-rainbow",
"scheme-tonal-spot"
]
currentIndex: model.indexOf(Config.runtime.appearance.colors.matugenScheme)
onSelectedIndexChanged: (index) => {
if (!Config.runtime.appearance.colors.autogenerated)
return
const selectedScheme = model[index]
Config.updateKey("appearance.colors.matugenScheme", selectedScheme)
Quickshell.execDetached([
"nucleus", "ipc", "call", "global", "regenColors"
])
}
enabled: Config.runtime.appearance.colors.autogenerated
}
}
RowLayout {
opacity: predefinedThemeSelector.enabled ? 1 : 0.8
ColumnLayout {
StyledText {
font.pixelSize: Metrics.fontSize(16)
text: "Predefined/Custom Themes:"
}
StyledText {
font.pixelSize: Metrics.fontSize(12)
text: "Choose a pre-defined theme for your interface."
}
}
Item { Layout.fillWidth: true }
StyledDropDown {
id: predefinedThemeSelector
label: "Theme"
model: Object.keys(Theme.map)
currentIndex: model.indexOf(Config.runtime.appearance.colors.scheme)
onSelectedIndexChanged: (index) => {
if (Config.runtime.appearance.colors.autogenerated)
return
const selectedTheme = model[index]
const variant = Config.runtime.appearance.theme
const file = Theme.map[selectedTheme][variant]
if (!file) return
Config.updateKey("appearance.colors.scheme", selectedTheme)
Quickshell.execDetached([
"nucleus", "theme", "switch", file
])
}
enabled: !Config.runtime.appearance.colors.autogenerated
}
}
}
ContentCard {
StyledSwitchOption {
title: "Tint Icons"
description: "Either tint icons across the shell or keep them colorized."
prefField: "appearance.tintIcons"
}
StyledSwitchOption {
title: "Use Autogenerated Themes"
description: "Use autogenerated themes."
prefField: "appearance.colors.autogenerated"
}
StyledSwitchOption {
title: "Use User Defined Themes"
description: "Enabling this will also run the default `config.toml` in `~/.config/matugen` dir."
prefField: "appearance.colors.runMatugenUserWide"
}
}
ContentCard {
StyledText {
text: "Clock"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledSwitchOption {
title: "Show Clock"
description: "Whether to show or disable the clock on the background."
prefField: "appearance.background.clock.enabled"
}
StyledSwitchOption {
title: "Analog Variant"
description: "Whether to use analog clock or not."
prefField: "appearance.background.clock.isAnalog"
}
StyledSwitchOption {
title: "Rotate Clock Polygon"
description: "Rotate the shape polygon of the analog clock."
prefField: "appearance.background.clock.rotatePolygonBg"
enabled: Config.runtime.appearance.background.clock.isAnalog
opacity: enabled ? 1 : 0.8
}
NumberStepper {
label: "Rotation Duration"
description: "Adjust the duration in which the clock rotates 360* (Seconds)."
prefField: "appearance.background.clock.rotationDuration"
minimum: 1
maximum: 40
step: 1
}
RowLayout {
id: shapeSelector
ColumnLayout {
StyledText {
text: "Analog Clock Shape"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Choose the analog clock's shape."
font.pixelSize: Metrics.fontSize(12)
}
}
Item { Layout.fillWidth: true }
StyledDropDown {
label: "Shape Type"
model: ["Cookie 7 Sided", "Cookie 9 Sided", "Cookie 12 Sided", "Pixelated Circle", "Circle"]
currentIndex: Config.runtime.appearance.background.clock.shape
onSelectedIndexChanged: (index) => {
Config.updateKey(
"appearance.background.clock.shape",
index
)
}
}
}
}
ContentCard {
StyledText {
text: "Rounding"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
NumberStepper {
label: "Factor"
description: "Adjust the rounding factor."
prefField: "appearance.rounding.factor"
minimum: 0
maximum: 1
step: 0.1
}
}
ContentCard {
StyledText {
text: "Font"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
NumberStepper {
label: "Scale"
description: "Adjust the font scale."
prefField: "appearance.font.scale"
minimum: 0.1
maximum: 2
step: 0.1
}
}
ContentCard {
StyledText {
text: "Transparency"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledSwitchOption {
title: "Enabled"
description: "Whether to enable or disable transparency."
prefField: "appearance.transparency.enabled"
}
NumberStepper {
label: "Factor"
description: "Adjust the alpha value for transparency."
prefField: "appearance.transparency.alpha"
minimum: 0.1
maximum: 1
step: 0.1
}
}
ContentCard {
StyledText {
text: "Animations"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledSwitchOption {
title: "Enabled"
description: "Whether to enable or disable animations (applies everywhere in the shell)."
prefField: "appearance.animations.enabled"
}
NumberStepper {
label: "Duration Scale"
description: "Adjust the duration scale of the animations."
prefField: "appearance.animations.durationScale"
minimum: 0.1
maximum: 1
step: 0.1
}
}
}

View File

@@ -0,0 +1,355 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import qs.modules.functions
import qs.config
import qs.modules.components
import qs.services
ContentMenu {
title: "Sound"
description: "Volume and audio devices"
ContentCard {
ColumnLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(20)
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(16)
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: Metrics.radius("large")
color: Appearance.m3colors.m3primaryContainer
MaterialSymbol {
anchors.centerIn: parent
icon: "volume_up"
color: Appearance.m3colors.m3onPrimaryContainer
iconSize: Metrics.iconSize(24)
}
}
ColumnLayout {
Layout.fillWidth: true
StyledText {
text: "Output"
font.pixelSize: Metrics.fontSize(16)
font.family: Metrics.fontFamily("Outfit Medium")
color: Appearance.m3colors.m3onSurface
}
StyledText {
text: Volume.defaultSink.description
font.pixelSize: Metrics.fontSize(13)
color: Appearance.m3colors.m3onSurfaceVariant
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Appearance.m3colors.m3outlineVariant
opacity: 0.4
}
ColumnLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(12)
RowLayout {
Layout.fillWidth: true
StyledText {
text: "Volume"
font.pixelSize: Metrics.fontSize(14)
font.family: Metrics.fontFamily("Outfit Medium")
color: Appearance.m3colors.m3onSurface
}
Item { Layout.fillWidth: true }
StyledText {
animate: false
text: Math.round(Volume.defaultSink.audio.volume * 100) + "%"
font.pixelSize: Metrics.fontSize(14)
font.family: Metrics.fontFamily("Outfit SemiBold")
color: Appearance.m3colors.m3primary
}
}
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(16)
MaterialSymbol {
icon: Volume.defaultSink.audio.muted ? "volume_off"
: Volume.defaultSink.audio.volume < 0.33 ? "volume_mute"
: Volume.defaultSink.audio.volume < 0.66 ? "volume_down"
: "volume_up"
color: Appearance.m3colors.m3onSurfaceVariant
iconSize: Metrics.iconSize(24)
}
StyledSlider {
id: outputVolumeSlider
Layout.fillWidth: true
value: Volume.defaultSink.audio.volume * 100
onValueChanged: Volume.setVolume(value / 100)
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(8)
StyledText {
text: "Device"
font.pixelSize: Metrics.fontSize(14)
font.family: Metrics.fontFamily("Outfit Medium")
color: Appearance.m3colors.m3onSurface
}
StyledDropDown {
Layout.fillWidth: true
label: "Output device"
model: Volume.sinks.map(d => d.description)
currentIndex: {
for (let i = 0; i < Volume.sinks.length; i++)
if (Volume.sinks[i].name === Volume.defaultSink.name) return i
return -1
}
onSelectedIndexChanged: index => {
if (index >= 0 && index < Volume.sinks.length)
Volume.setDefaultSink(Volume.sinks[index])
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 56
radius: Metrics.radius("small")
color: Appearance.m3colors.m3surfaceContainerHigh
RowLayout {
anchors.fill: parent
anchors.leftMargin: Metrics.margin(16)
anchors.rightMargin: Metrics.margin(16)
spacing: Metrics.spacing(12)
MaterialSymbol {
icon: Volume.defaultSink.audio.muted ? "volume_off" : "volume_up"
color: Volume.defaultSink.audio.muted ? Appearance.m3colors.m3error : Appearance.m3colors.m3onSurfaceVariant
iconSize: Metrics.iconSize(24)
}
StyledText {
Layout.fillWidth: true
text: "Mute output"
font.pixelSize: Metrics.fontSize(14)
color: Appearance.m3colors.m3onSurface
}
StyledSwitch {
checked: Volume.defaultSink.audio.muted
onToggled: Volume.toggleMuted(Volume.defaultSink)
}
}
}
}
}
ContentCard {
ColumnLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(20)
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(16)
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: Metrics.radius("large")
color: Appearance.m3colors.m3secondaryContainer
MaterialSymbol {
anchors.centerIn: parent
icon: "mic"
color: Appearance.m3colors.m3onSecondaryContainer
iconSize: Metrics.iconSize(24)
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(2)
StyledText {
text: "Input"
font.pixelSize: Metrics.fontSize(16)
font.family: Metrics.fontFamily("Outfit Medium")
color: Appearance.m3colors.m3onSurface
}
StyledText {
visible: Volume.sources.length > 0
text: Volume.defaultSource.description
font.pixelSize: Metrics.fontSize(13)
color: Appearance.m3colors.m3onSurfaceVariant
}
}
}
Rectangle {
visible: Volume.sources.length === 0
Layout.fillWidth: true
Layout.preferredHeight: 120
radius: Metrics.radius("small")
color: Appearance.m3colors.m3surfaceContainerHigh
ColumnLayout {
anchors.centerIn: parent
spacing: Metrics.spacing(8)
MaterialSymbol {
icon: "mic_off"
iconSize: Metrics.iconSize(48)
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.3)
Layout.alignment: Qt.AlignHCenter
}
StyledText {
text: "No input devices"
font.pixelSize: Metrics.fontSize(14)
font.family: Metrics.fontFamily("Outfit Medium")
color: Appearance.m3colors.m3onSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
Rectangle {
visible: Volume.sources.length > 0
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Appearance.m3colors.m3outlineVariant
opacity: 0.4
}
ColumnLayout {
visible: Volume.sources.length > 0
Layout.fillWidth: true
spacing: Metrics.spacing(12)
RowLayout {
Layout.fillWidth: true
StyledText {
text: "Volume"
font.pixelSize: Metrics.fontSize(14)
font.family: Metrics.fontFamily("Outfit Medium")
color: Appearance.m3colors.m3onSurface
}
Item { Layout.fillWidth: true }
StyledText {
animate: false
text: Math.round(Volume.defaultSource.audio.volume * 100) + "%"
font.pixelSize: Metrics.fontSize(14)
font.family: Metrics.fontFamily("Outfit SemiBold")
color: Appearance.m3colors.m3primary
}
}
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(16)
MaterialSymbol {
icon: Volume.defaultSource.audio.muted ? "mic_off" : "mic"
color: Appearance.m3colors.m3onSurfaceVariant
iconSize: Metrics.iconSize(24)
}
StyledSlider {
id: inputVolumeSlider
Layout.fillWidth: true
value: Volume.defaultSource.audio.volume * 100
onValueChanged: Volume.setSourceVolume(value / 100)
}
}
}
ColumnLayout {
visible: Volume.sources.length > 0
Layout.fillWidth: true
spacing: Metrics.spacing(8)
StyledText {
text: "Device"
font.pixelSize: Metrics.fontSize(14)
font.family: Metrics.fontFamily("Outfit Medium")
color: Appearance.m3colors.m3onSurface
}
StyledDropDown {
Layout.fillWidth: true
label: "Input device"
model: Volume.sources.map(d => d.description)
currentIndex: {
for (let i = 0; i < Volume.sources.length; i++)
if (Volume.sources[i].name === Volume.defaultSource.name) return i
return -1
}
onSelectedIndexChanged: index => {
if (index >= 0 && index < Volume.sources.length)
Volume.setDefaultSource(Volume.sources[index])
}
}
}
Rectangle {
visible: Volume.sources.length > 0
Layout.fillWidth: true
Layout.preferredHeight: 56
radius: Metrics.radius("small")
color: Appearance.m3colors.m3surfaceContainerHigh
RowLayout {
anchors.fill: parent
anchors.leftMargin: Metrics.margin(16)
anchors.rightMargin: Metrics.margin(16)
spacing: Metrics.spacing(12)
MaterialSymbol {
icon: Volume.defaultSource.audio.muted ? "mic_off" : "mic"
color: Volume.defaultSource.audio.muted ? Appearance.m3colors.m3error : Appearance.m3colors.m3onSurfaceVariant
iconSize: Metrics.iconSize(24)
}
StyledText {
Layout.fillWidth: true
text: "Mute input"
font.pixelSize: Metrics.fontSize(14)
color: Appearance.m3colors.m3onSurface
}
StyledSwitch {
checked: Volume.defaultSource.audio.muted
onToggled: Volume.toggleMuted(Volume.defaultSource)
}
}
}
}
}
}

View File

@@ -0,0 +1,300 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import QtQuick.Controls
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.services
ContentMenu {
property string barKey: "bar"
title: "Bar"
description: "Adjust the bar's look."
ContentCard {
id: monitorSelectorCard
StyledText {
text: "Monitor Bar Configuration"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledText {
text: (Config.runtime.monitors?.[monitorSelector.model[monitorSelector.currentIndex]]?.bar)
? "This monitor has its own bar configuration."
: "This monitor currently uses the global bar."
wrapMode: Text.WordWrap
}
RowLayout {
spacing: Metrics.spacing("normal")
StyledDropDown {
id: monitorSelector
Layout.preferredWidth: 220
model: Xrandr.monitors.map(m => m.name)
currentIndex: 0
onCurrentIndexChanged: monitorSelectorCard.updateMonitorProperties()
}
Item { Layout.fillWidth: true }
StyledButton {
id: createButton
icon: "add"
text: "Override Bar: (" + monitorSelector.model[monitorSelector.currentIndex] + ")"
Layout.preferredWidth: 280
onClicked: {
const monitorName = monitorSelector.model[monitorSelector.currentIndex]
if (!monitorName) return
if (!Config.runtime.monitors) Config.runtime.monitors = {}
if (!Config.runtime.monitors[monitorName])
Config.runtime.monitors[monitorName] = {}
const defaultBar = {
density: 50,
enabled: true,
floating: false,
gothCorners: true,
margins: 16,
merged: false,
modules: {
height: 34,
paddingColor: "#1f1f1f",
radius: 17,
statusIcons: {
bluetoothStatusEnabled: true,
enabled: true,
networkStatusEnabled: true
},
systemUsage: {
cpuStatsEnabled: true,
enabled: true,
memoryStatsEnabled: true,
tempStatsEnabled: true
},
workspaces: {
enabled: true,
showAppIcons: true,
showJapaneseNumbers: false,
workspaceIndicators: 8
}
},
position: "top",
radius: 23
}
Config.updateKey("monitors." + monitorName + ".bar", defaultBar)
monitorSelectorCard.updateMonitorProperties()
}
}
StyledButton {
id: deleteButton
icon: "delete"
text: "Use Global Bar: (" + monitorSelector.model[monitorSelector.currentIndex] + ")"
secondary: true
Layout.preferredWidth: 280
onClicked: {
const monitorName = monitorSelector.model[monitorSelector.currentIndex]
if (!monitorName) return
Config.updateKey("monitors." + monitorName + ".bar", undefined)
monitorSelectorCard.updateMonitorProperties()
}
}
}
function updateMonitorProperties() {
const monitorName = monitorSelector.model[monitorSelector.currentIndex]
const monitorBar = Config.runtime.monitors?.[monitorName]?.bar
barKey = monitorBar ? "monitors." + monitorName + ".bar" : "bar"
createButton.enabled = !monitorBar
deleteButton.enabled = !!monitorBar
monitorSelector.model = Xrandr.monitors.map(m => m.name)
monitorSelector.currentIndex = Xrandr.monitors.findIndex(m => m.name === monitorName)
}
}
ContentCard {
StyledText {
text: "Bar"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
ColumnLayout {
StyledText {
text: "Position"
font.pixelSize: Metrics.fontSize(16)
}
RowLayout {
spacing: Metrics.spacing(8)
Repeater {
model: ["Top", "Bottom", "Left", "Right"]
delegate: StyledButton {
property string pos: modelData.toLowerCase()
text: modelData
Layout.fillWidth: true
checked: ConfigResolver.bar(monitorSelector.model[monitorSelector.currentIndex]).position === pos
topLeftRadius: Metrics.radius("normal")
topRightRadius: Metrics.radius("normal")
bottomLeftRadius: Metrics.radius("normal")
bottomRightRadius: Metrics.radius("normal")
onClicked: Config.updateKey(barKey + ".position", pos)
}
}
}
}
StyledSwitchOption {
title: "Enabled"
description: "Toggle the bar visibility on/off"
prefField: barKey + ".enabled"
}
StyledSwitchOption {
title: "Floating Bar"
description: "Make the bar float above other windows instead of being part of the desktop"
prefField: barKey + ".floating"
}
StyledSwitchOption {
title: "Goth Corners"
description: "Apply gothic-style corner cutouts to the bar"
prefField: barKey + ".gothCorners"
}
StyledSwitchOption {
title: "Merged Layout"
description: "Merge all modules into a single continuous layout"
prefField: barKey + ".merged"
}
}
ContentCard {
StyledText {
text: "Bar Rounding & Size"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
NumberStepper {
label: "Bar Density"
prefField: barKey + ".density"
description: "Modify the bar's density"
minimum: 40
maximum: 128
}
NumberStepper {
label: "Bar Radius"
prefField: barKey + ".radius"
description: "Modify the bar's radius"
minimum: 10
maximum: 128
}
NumberStepper {
label: "Module Container Radius"
prefField: barKey + ".modules.radius"
description: "Modify the bar's module.radius"
minimum: 10
maximum: 128
}
NumberStepper {
label: "Module Height"
prefField: barKey + ".modules.height"
description: "Modify the bar's module.height"
minimum: 10
maximum: 128
}
NumberStepper {
label: "Workspace Indicators"
prefField: barKey + ".modules.workspaces.workspaceIndicators"
description: "Adjust how many workspace indicators to show."
minimum: 1
maximum: 10
}
}
ContentCard {
StyledText {
text: "Bar Modules"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledText {
text: "Workspaces"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
StyledSwitchOption {
title: "Enabled"
description: "Show workspace indicator module"
prefField: barKey + ".modules.workspaces.enabled"
}
StyledSwitchOption {
title: "Show App Icons"
description: "Display application icons in workspace indicators"
prefField: barKey + ".modules.workspaces.showAppIcons"
enabled: !barKey.modules.workspaces.showJapaneseNumbers && Compositor.require("hyprland")
opacity: !barKey.modules.workspaces.showJapaneseNumbers && Compositor.require("hyprland") ? 1 : 0.8
}
StyledSwitchOption {
title: "Show Japanese Numbers"
description: "Use Japanese-style numbers instead of standard numerals"
prefField: barKey + ".modules.workspaces.showJapaneseNumbers"
enabled: !barKey.modules.workspaces.showAppIcons
opacity: !barKey.modules.workspaces.showAppIcons ? 1 : 0.8
}
StyledText {
text: "Status Icons"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
StyledSwitchOption {
title: "Enabled"
description: "Show status icons module (wifi, bluetooth)"
prefField: barKey + ".modules.statusIcons.enabled"
}
StyledSwitchOption {
title: "Show Wifi Status"
description: "Display wifi connection status and signal strength"
prefField: barKey + ".modules.statusIcons.networkStatusEnabled"
}
StyledSwitchOption {
title: "Show Bluetooth Status"
description: "Display bluetooth connection status"
prefField: barKey + ".modules.statusIcons.bluetoothStatusEnabled"
}
StyledText {
text: "System Stats"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
StyledSwitchOption {
title: "Enabled"
description: "Show system resource monitoring module"
prefField: barKey + ".modules.systemUsage.enabled"
}
StyledSwitchOption {
title: "Show Cpu Usage Stats"
description: "Display CPU usage percentage and load"
prefField: barKey + ".modules.systemUsage.cpuStatsEnabled"
}
StyledSwitchOption {
title: "Show Memory Usage Stats"
description: "Display RAM usage and available memory"
prefField: barKey + ".modules.systemUsage.memoryStatsEnabled"
}
StyledSwitchOption {
title: "Show Cpu Temperature Stats"
description: "Display CPU temperature readings"
prefField: barKey + ".modules.systemUsage.tempStatsEnabled"
}
}
}

View File

@@ -0,0 +1,187 @@
import QtQuick
import QtQuick.Layouts
import qs.config
import qs.modules.components
import qs.modules.functions
import qs.services
import Quickshell.Bluetooth as QsBluetooth
ContentMenu {
title: "Bluetooth"
description: "Manage Bluetooth devices and connections."
ContentCard {
ContentRowCard {
cardSpacing: Metrics.spacing(0)
verticalPadding: Bluetooth.defaultAdapter.enabled ? Metrics.padding(10) : Metrics.padding(0)
cardMargin: Metrics.margin(0)
StyledText {
text: powerSwitch.checked ? "Power: On" : "Power: Off"
font.pixelSize: Metrics.fontSize(16)
font.bold: true
}
Item { Layout.fillWidth: true }
StyledSwitch {
id: powerSwitch
checked: Bluetooth.defaultAdapter?.enabled
onToggled: Bluetooth.defaultAdapter.enabled = checked
}
}
ContentRowCard {
visible: Bluetooth.defaultAdapter.enabled
cardSpacing: Metrics.spacing(0)
verticalPadding: Metrics.padding(10)
cardMargin: Metrics.margin(0)
ColumnLayout {
spacing: Metrics.spacing(2)
StyledText {
text: "Discoverable"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Allow other devices to find this computer."
font.pixelSize: Metrics.fontSize(12)
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
}
}
Item { Layout.fillWidth: true }
StyledSwitch {
checked: Bluetooth.defaultAdapter?.discoverable
onToggled: Bluetooth.defaultAdapter.discoverable = checked
}
}
ContentRowCard {
visible: Bluetooth.defaultAdapter.enabled
cardSpacing: Metrics.spacing(0)
verticalPadding: Metrics.padding(0)
cardMargin: Metrics.margin(0)
ColumnLayout {
spacing: Metrics.spacing(2)
StyledText {
text: "Scanning"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Search for nearby Bluetooth devices."
font.pixelSize: Metrics.fontSize(12)
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
}
}
Item { Layout.fillWidth: true }
StyledSwitch {
checked: Bluetooth.defaultAdapter?.discovering
onToggled: Bluetooth.defaultAdapter.discovering = checked
}
}
}
ContentCard {
visible: connectedDevices.count > 0
StyledText {
text: "Connected Devices"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
Repeater {
id: connectedDevices
model: Bluetooth.devices.filter(d => d.connected)
delegate: BluetoothDeviceCard {
device: modelData
statusText: modelData.batteryAvailable
? "Connected, " + Math.floor(modelData.battery * 100) + "% left"
: "Connected"
showDisconnect: true
showRemove: true
usePrimary: true
}
}
}
ContentCard {
visible: Bluetooth.defaultAdapter?.enabled
StyledText {
text: "Paired Devices"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
Item {
visible: pairedDevices.count === 0
width: parent.width
height: Metrics.spacing(40)
StyledText {
anchors.left: parent.left
text: "No paired devices"
font.pixelSize: Metrics.fontSize(14)
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
}
}
Repeater {
id: pairedDevices
model: Bluetooth.devices.filter(d => !d.connected && d.paired)
delegate: BluetoothDeviceCard {
device: modelData
statusText: "Not connected"
showConnect: true
showRemove: true
}
}
}
ContentCard {
visible: Bluetooth.defaultAdapter?.enabled
StyledText {
text: "Available Devices"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
Item {
visible: discoveredDevices.count === 0 && !Bluetooth.defaultAdapter.discovering
width: parent.width
height: Metrics.spacing(40)
StyledText {
Layout.alignment: Qt.AlignHCenter
text: "No new devices found"
font.pixelSize: Metrics.fontSize(14)
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
}
}
Repeater {
id: discoveredDevices
model: Bluetooth.devices.filter(d => !d.paired && !d.connected)
delegate: BluetoothDeviceCard {
device: modelData
statusText: "Discovered"
showConnect: true
showPair: true
}
}
}
}

View File

@@ -0,0 +1,90 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.components
import qs.config
import qs.modules.functions
import Quickshell.Bluetooth as QsBluetooth
ContentRowCard {
id: deviceRow
property var device
property string statusText: ""
property bool usePrimary: false
property bool showConnect: false
property bool showDisconnect: false
property bool showPair: false
property bool showRemove: false
cardMargin: Metrics.margin(0)
cardSpacing: Metrics.spacing(10)
verticalPadding: Metrics.padding(0)
opacity: device.state === QsBluetooth.BluetoothDeviceState.Connecting ||
device.state === QsBluetooth.BluetoothDeviceState.Disconnecting ? 0.6 : 1
function mapBluetoothIcon(dbusIcon, name) {
console.log(dbusIcon, " / ", name)
const iconMap = {
"audio-headset": "headset",
"audio-headphones": "headphones",
"input-keyboard": "keyboard",
"input-mouse": "mouse",
"input-gaming": "sports_esports",
"phone": "phone_android",
"computer": "computer",
"printer": "print",
"camera": "photo_camera",
"unknown": "bluetooth"
}
return iconMap[dbusIcon] || "bluetooth"
}
MaterialSymbol {
icon: mapBluetoothIcon(device.icon, device.name)
font.pixelSize: Metrics.fontSize(32)
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
spacing: Metrics.spacing(0)
StyledText {
text: device.name || device.address
font.pixelSize: Metrics.fontSize(16)
font.bold: true
}
StyledText {
text: statusText
font.pixelSize: Metrics.fontSize(12)
color: usePrimary
? Appearance.m3colors.m3primary
: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.6)
}
}
Item { Layout.fillWidth: true }
StyledButton {
visible: showConnect
icon: "link"
onClicked: device.connect()
}
StyledButton {
visible: showDisconnect
icon: "link_off"
onClicked: device.disconnect()
}
StyledButton {
visible: showPair
icon: "add"
onClicked: device.pair()
}
StyledButton {
visible: showRemove
icon: "delete"
onClicked: Bluetooth.removeDevice(device)
}
}

View File

@@ -0,0 +1,79 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.services
ContentMenu {
title: "Launcher"
description: "Adjust launcher's settings."
ContentCard {
StyledText {
text: "Filters & Search"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledSwitchOption {
title: "Fuzzy Search"
description: "Enable or disable fuzzy search."
prefField: "launcher.fuzzySearchEnabled"
}
RowLayout {
id: webEngineSelector
property string title: "Web Search Engine"
property string description: "Choose the web search engine for web searches."
property string prefField: ''
ColumnLayout {
StyledText {
text: webEngineSelector.title
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: webEngineSelector.description
font.pixelSize: Metrics.fontSize(12)
}
}
Item {
Layout.fillWidth: true
}
StyledDropDown {
label: "Engine"
model: ["Google", "Brave", "DuckDuckGo", "Bing"]
// Set the initial index based on the lowercase value in Config
currentIndex: {
switch (Config.runtime.launcher.webSearchEngine.toLowerCase()) {
case "google":
return 0;
case "brave":
return 1;
case "duckduckgo":
return 2;
case "bing":
return 3;
default:
return 0;
}
}
onSelectedIndexChanged: (index) => {
// Update Config with lowercase version of selected model
Config.updateKey("launcher.webSearchEngine", model[index].toLowerCase());
}
}
}
}
}

View File

@@ -0,0 +1,109 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.services
ContentMenu {
title: "Miscellaneous"
description: "Configure misc settings."
ContentCard {
StyledText {
text: "Versions"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
RowLayout {
id: releaseChannelSelector
property string title: "Release Channel"
property string description: "Choose the release channel for updates."
property string prefField: ''
ColumnLayout {
StyledText {
text: releaseChannelSelector.title
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: releaseChannelSelector.description
font.pixelSize: Metrics.fontSize(12)
}
}
Item {
Layout.fillWidth: true
}
StyledDropDown {
label: "Type"
model: ["Stable", "Edge (indev)"]
currentIndex: Config.runtime.shell.releaseChannel === "edge" ? 1 : 0
onSelectedIndexChanged: (index) => {
Config.updateKey("shell.releaseChannel", index === 1 ? "edge" : "stable");
UpdateNotifier.notified = false;
}
}
}
}
ContentCard {
StyledText {
text: "Intelligence"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledSwitchOption {
title: "Enabled"
description: "Enable or disable intelligence."
prefField: "misc.intelligence.enabled"
}
}
ContentCard {
StyledText {
text: "Intelligence Bearer/API"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledTextField {
id: apiKeyTextField
clip: true
horizontalAlignment: Text.AlignLeft
placeholderText: Config.runtime.misc.intelligence.apiKey !== "" ? Config.runtime.misc.intelligence.apiKey : "Bearer Key"
Layout.fillWidth: true
Keys.onPressed: (event) => {
if (event.key === Qt.Key_S && (event.modifiers & Qt.ControlModifier)) {
event.accepted = true;
Config.updateKey("misc.intelligence.apiKey", apiKeyTextField.text);
Quickshell.execDetached(["notify-send", "Saved Bearer/API Key"])
}
}
font.pixelSize: Metrics.fontSize(16)
}
Item {
width: 20
}
InfoCard {
title: "How to save the api key"
description: "In order to save the api key press Ctrl+S and it will save the api key to the config."
}
}
}

View File

@@ -0,0 +1,136 @@
import QtQuick
import QtQuick.Layouts
import qs.config
import qs.modules.components
import qs.services
import qs.modules.functions
Item {
id: networkRow
property var connection
property bool isActive: false
property bool showConnect: false
property bool showDisconnect: false
property bool showPasswordField: false
property string password: ""
width: parent.width
implicitHeight: mainLayout.implicitHeight
function signalIcon(strength, secure) {
if (!connection) return "network_wifi";
if (connection.type === "ethernet") return "settings_ethernet";
if (strength >= 75) return "network_wifi";
if (strength >= 50) return "network_wifi_3_bar";
if (strength >= 25) return "network_wifi_2_bar";
if (strength > 0) return "network_wifi_1_bar";
return "network_wifi_1_bar";
}
ColumnLayout {
id: mainLayout
anchors.fill: parent
spacing: Metrics.spacing(10)
RowLayout {
spacing: Metrics.spacing(10)
// Signal icon with lock overlay
Item {
width: Metrics.spacing(32)
height: Metrics.spacing(32)
MaterialSymbol {
anchors.fill: parent
icon: connection ? signalIcon(connection.strength, connection.isSecure) : "network_wifi"
font.pixelSize: Metrics.fontSize(32)
}
// Lock overlay (anchors are safe because Item is not layout-managed)
MaterialSymbol {
icon: "lock"
visible: connection && connection.type === "wifi" && connection.isSecure
font.pixelSize: Metrics.fontSize(12)
anchors.right: parent.right
anchors.bottom: parent.bottom
}
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
spacing: Metrics.spacing(0)
StyledText {
text: connection ? connection.name : ""
font.pixelSize: Metrics.fontSize(16)
font.bold: true
}
StyledText {
text: connection ? (
isActive ? "Connected" :
connection.type === "ethernet" ? connection.device || "Ethernet" :
connection.isSecure ? "Secured" : "Open"
) : ""
font.pixelSize: Metrics.fontSize(12)
color: isActive
? Appearance.m3colors.m3primary
: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.4)
}
}
Item { Layout.fillWidth: true }
StyledButton {
visible: showConnect && !showPasswordField
icon: "link"
onClicked: {
if (!connection) return;
if (connection.type === "ethernet") Network.connect(connection, "")
else if (connection.isSecure) showPasswordField = true
else Network.connect(connection, "")
}
}
StyledButton {
visible: showDisconnect && !showPasswordField
icon: "link_off"
onClicked: Network.disconnect()
}
}
// Password row
RowLayout {
visible: showPasswordField && connection && connection.type === "wifi"
spacing: Metrics.spacing(10)
StyledTextField {
padding: Metrics.padding(10)
Layout.fillWidth: true
placeholderText: "Enter password"
echoMode: parent.showPassword ? TextInput.Normal : TextInput.Password
onTextChanged: networkRow.password = text
onAccepted: {
if (!connection) return;
Network.connect(connection, networkRow.password)
showPasswordField = false
}
}
StyledButton {
property bool showPassword: false
icon: parent.showPassword ? "visibility" : "visibility_off"
onClicked: parent.showPassword = !parent.showPassword
}
StyledButton {
icon: "link"
onClicked: {
if (!connection) return;
Network.connect(connection, networkRow.password)
showPasswordField = false
}
}
}
}
}

View File

@@ -0,0 +1,172 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.functions
import qs.config
import qs.modules.components
import qs.services
ContentMenu {
title: "Network"
description: "Manage network connections."
ContentCard {
ContentRowCard {
cardSpacing: Metrics.spacing(0)
verticalPadding: Network.wifiEnabled ? Metrics.padding(10) : Metrics.padding(0)
cardMargin: Metrics.margin(0)
StyledText {
text: powerSwitch.checked ? "Wi-Fi: On" : "Wi-Fi: Off"
font.pixelSize: Metrics.fontSize(16)
font.bold: true
}
Item { Layout.fillWidth: true }
StyledSwitch {
id: powerSwitch
checked: Network.wifiEnabled
onToggled: Network.enableWifi(checked)
}
}
ContentRowCard {
visible: Network.wifiEnabled
cardSpacing: Metrics.spacing(0)
verticalPadding: Metrics.padding(10)
cardMargin: Metrics.margin(0)
ColumnLayout {
spacing: Metrics.spacing(2)
StyledText {
text: "Scanning"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Search for nearby Wi-Fi networks."
font.pixelSize: Metrics.fontSize(12)
color: ColorUtils.transparentize(
Appearance.m3colors.m3onSurface, 0.4
)
}
}
Item { Layout.fillWidth: true }
StyledSwitch {
checked: Network.scanning
onToggled: {
if (checked)
Network.rescan()
}
}
}
}
InfoCard {
visible: Network.message !== "" && Network.message !== "ok"
icon: "error"
backgroundColor: Appearance.m3colors.m3error
contentColor: Appearance.m3colors.m3onError
title: "Failed to connect to " + Network.lastNetworkAttempt
description: Network.message
}
ContentCard {
visible: Network.active !== null
StyledText {
text: "Active Connection"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
NetworkCard {
connection: Network.active
isActive: true
showDisconnect: Network.active?.type === "wifi"
}
}
ContentCard {
visible: Network.connections.filter(c => c.type === "ethernet").length > 0
StyledText {
text: "Ethernet"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
Repeater {
model: Network.connections.filter(c => c.type === "ethernet" && !c.active)
delegate: NetworkCard {
connection: modelData
showConnect: true
}
}
}
ContentCard {
visible: Network.wifiEnabled
StyledText {
text: "Available Wi-Fi Networks"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
Item {
visible: Network.connections.filter(c => c.type === "wifi").length === 0 && !Network.scanning
width: parent.width
height: Metrics.spacing(40)
StyledText {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
text: "No networks found"
font.pixelSize: Metrics.fontSize(14)
color: ColorUtils.transparentize(Appearance.m3colors.m3onSurface, 0.4)
}
}
Repeater {
model: Network.connections.filter(c => c.type === "wifi" && !c.active)
delegate: NetworkCard {
connection: modelData
showConnect: true
}
}
}
ContentCard {
visible: Network.savedNetworks.length > 0
StyledText {
text: "Remembered Networks"
font.pixelSize: Metrics.fontSize(18)
font.bold: true
}
Item {
visible: Network.savedNetworks.length === 0
width: parent.width
height: Metrics.spacing(40)
StyledText {
anchors.left: parent.left
text: "No remembered networks"
font.pixelSize: Metrics.fontSize(14)
color: Appearance.colors.colSubtext
}
}
Repeater {
model: Network.connections.filter(c => c.type === "wifi" && c.saved && !c.active)
delegate: NetworkCard {
connection: modelData
showConnect: false
showDisconnect: false
}
}
}
}

View File

@@ -0,0 +1,215 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.services
import qs.modules.functions
ContentMenu {
title: "Notifications & Overlays"
description: "Adjust notification and overlay settings."
function indexFromPosition(pos, model) {
pos = pos.toLowerCase()
for (let i = 0; i < model.length; i++) {
if (model[i].toLowerCase().replace(" ", "-") === pos)
return i
}
return 0
}
ContentCard {
StyledText {
text: "Notifications"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledSwitchOption {
title: "Enabled"
description: "Enable or disable built-in notification daemon."
prefField: "notifications.enabled"
}
StyledSwitchOption {
title: "Do not disturb enabled"
description: "Enable or disable dnd."
prefField: "notifications.doNotDisturb"
}
RowLayout {
ColumnLayout {
StyledText {
text: "Notification Position"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Select where notification will be shown."
font.pixelSize: Metrics.fontSize(12)
}
}
Item { Layout.fillWidth: true }
StyledDropDown {
id: notificationDropdown
label: "Position"
property var positions: ["Top Left", "Top Right", "Top"]
model: positions
currentIndex:
indexFromPosition(
Config.runtime.notifications.position,
positions
)
onSelectedIndexChanged: function(index) {
Config.updateKey(
"notifications.position",
positions[index].toLowerCase().replace(" ", "-")
)
}
}
}
RowLayout {
ColumnLayout {
StyledText {
text: "Test Notifications"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Run a test notification."
font.pixelSize: Metrics.fontSize(12)
}
}
Item { Layout.fillWidth: true }
StyledButton {
text: "Test"
icon: "chat"
onClicked:
Quickshell.execDetached([
"notify-send",
"Quickshell",
"This is a test notification"
])
}
}
}
ContentCard {
StyledText {
text: "Overlays / OSDs"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
}
StyledSwitchOption {
title: "Enabled"
description: "Enable or disable built-in osd daemon."
prefField: "overlays.enabled"
}
StyledSwitchOption {
title: "Volume OSD enabled"
description: "Enable or disable volume osd."
prefField: "overlays.volumeOverlayEnabled"
}
StyledSwitchOption {
title: "Brightness OSD enabled"
description: "Enable or disable brightness osd."
prefField: "overlays.brightnessOverlayEnabled"
}
RowLayout {
ColumnLayout {
StyledText {
text: "Brightness OSD Position"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Choose where brightness OSD is shown."
font.pixelSize: Metrics.fontSize(12)
}
}
Item { Layout.fillWidth: true }
StyledDropDown {
property var positions:
["Top Left","Top Right","Bottom Left","Bottom Right","Top","Bottom"]
model: positions
currentIndex:
indexFromPosition(
Config.runtime.overlays.brightnessOverlayPosition,
positions
)
onSelectedIndexChanged: function(index) {
Config.updateKey(
"overlays.brightnessOverlayPosition",
positions[index].toLowerCase().replace(" ", "-")
)
}
}
}
RowLayout {
ColumnLayout {
StyledText {
text: "Volume OSD Position"
font.pixelSize: Metrics.fontSize(16)
}
StyledText {
text: "Choose where volume OSD is shown."
font.pixelSize: Metrics.fontSize(12)
}
}
Item { Layout.fillWidth: true }
StyledDropDown {
property var positions:
["Top Left","Top Right","Bottom Left","Bottom Right","Top","Bottom"]
model: positions
currentIndex:
indexFromPosition(
Config.runtime.overlays.volumeOverlayPosition,
positions
)
onSelectedIndexChanged: function(index) {
Config.updateKey(
"overlays.volumeOverlayPosition",
positions[index].toLowerCase().replace(" ", "-")
)
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.plugins
ContentMenu {
title: "Plugins"
description: "Modify and Customize Installed Plugins."
ContentCard {
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
color: "transparent"
GridLayout {
id: grid
columns: 1
Layout.fillWidth: true
columnSpacing: Metrics.spacing(16)
rowSpacing: Metrics.spacing(16)
StyledText {
text: "Plugins not found!"
font.pixelSize: Metrics.fontSize(20)
font.bold: true
visible: PluginLoader.plugins.length === 0
Layout.alignment: Qt.AlignHCenter
}
Repeater {
model: PluginLoader.plugins
delegate: ContentCard {
Layout.fillWidth: true
Loader {
Layout.fillWidth: true
asynchronous: true
source: Qt.resolvedUrl(
Directories.shellConfig + "/plugins/" + modelData + "/Settings.qml"
)
}
}
}
}
}
}

View File

@@ -0,0 +1,201 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.config
import qs.modules.functions
import qs.modules.components
import qs.services
Scope {
property var settingsWindow: null
IpcHandler {
function open(menu: string) {
Globals.states.settingsOpen = true;
if (menu !== "" && settingsWindow !== null) {
for (var i = 0; i < settingsWindow.menuModel.length; i++) {
var item = settingsWindow.menuModel[i];
if (!item.header && item.label.toLowerCase() === menu.toLowerCase()) {
settingsWindow.selectedIndex = item.page;
break;
}
}
}
}
target: "settings"
}
LazyLoader {
active: Globals.states.settingsOpen
Window {
id: root
width: 1280
height: 720
visible: true
title: "Nucleus - Settings"
color: Appearance.m3colors.m3background
onClosing: Globals.states.settingsOpen = false
Component.onCompleted: settingsWindow = root
property int selectedIndex: 0
property bool sidebarCollapsed: false
property var menuModel: [
{ "header": true, "label": "System" },
{ "icon": "bluetooth", "label": "Bluetooth", "page": 0 },
{ "icon": "network_wifi", "label": "Network", "page": 1 },
{ "icon": "volume_up", "label": "Audio", "page": 2 },
{ "icon": "instant_mix", "label": "Appearance", "page": 3 },
{ "header": true, "label": "Customization" },
{ "icon": "toolbar", "label": "Bar", "page": 4 },
{ "icon": "wallpaper", "label": "Wallpapers", "page": 5 },
{ "icon": "apps", "label": "Launcher", "page": 6 },
{ "icon": "chat", "label": "Notifications", "page": 7 },
{ "icon": "extension", "label": "Plugins", "page": 8 },
{ "icon": "apps", "label": "Store", "page": 9 },
{ "icon": "build", "label": "Miscellaneous", "page": 10 },
{ "header": true, "label": "About" },
{ "icon": "info", "label": "About", "page": 11 }
]
Item {
anchors.fill: parent
Rectangle {
id: sidebarBG
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.sidebarCollapsed ? 80 : 350
color: Appearance.m3colors.m3surfaceContainerLow
ColumnLayout {
anchors.fill: parent
anchors.margins: Metrics.margin(40)
spacing: Metrics.spacing(5)
RowLayout {
Layout.fillWidth: true
StyledText {
Layout.fillWidth: true
text: "Settings"
font.family: "Outfit ExtraBold"
font.pixelSize: Metrics.fontSize(28)
visible: !root.sidebarCollapsed
}
StyledButton {
Layout.preferredHeight: 40
icon: root.sidebarCollapsed ? "chevron_right" : "chevron_left"
secondary: true
onClicked: root.sidebarCollapsed = !root.sidebarCollapsed
}
}
ListView {
id: sidebarList
Layout.fillWidth: true
Layout.fillHeight: true
model: root.menuModel
spacing: Metrics.spacing(5)
clip: true
delegate: Item {
width: sidebarList.width
height: modelData.header ? (root.sidebarCollapsed ? 0 : 30) : 42
visible: !modelData.header || !root.sidebarCollapsed
// header
Item {
width: parent.width
height: parent.height
StyledText {
y: (parent.height - height) * 0.5
x: 10
text: modelData.label
font.pixelSize: Metrics.fontSize(14)
font.bold: true
opacity: modelData.header ? 1 : 0
}
}
Rectangle {
anchors.fill: parent
visible: !modelData.header
radius: Appearance.rounding.large
color: root.selectedIndex === modelData.page
? Appearance.m3colors.m3primary
: "transparent"
RowLayout {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 10
spacing: 10
MaterialSymbol {
visible: !modelData.header
icon: modelData.icon ? modelData.icon : ""
iconSize: Metrics.iconSize(24)
}
StyledText {
text: modelData.label
visible: !root.sidebarCollapsed
}
}
}
MouseArea {
anchors.fill: parent
enabled: modelData.page !== undefined
onClicked: {
root.selectedIndex = modelData.page
settingsStack.currentIndex = modelData.page
}
}
}
}
}
Behavior on width {
NumberAnimation { duration: 180; easing.type: Easing.InOutCubic }
}
}
StackLayout {
id: settingsStack
anchors.left: sidebarBG.right
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
currentIndex: root.selectedIndex
BluetoothConfig { Layout.fillWidth: true; Layout.fillHeight: true }
NetworkConfig { Layout.fillWidth: true; Layout.fillHeight: true }
AudioConfig { Layout.fillWidth: true; Layout.fillHeight: true }
AppearanceConfig { Layout.fillWidth: true; Layout.fillHeight: true }
BarConfig { Layout.fillWidth: true; Layout.fillHeight: true }
WallpaperConfig { Layout.fillWidth: true; Layout.fillHeight: true }
LauncherConfig { Layout.fillWidth: true; Layout.fillHeight: true }
NotificationConfig { Layout.fillWidth: true; Layout.fillHeight: true }
Plugins { Layout.fillWidth: true; Layout.fillHeight: true }
Store { Layout.fillWidth: true; Layout.fillHeight: true }
MiscConfig { Layout.fillWidth: true; Layout.fillHeight: true }
About { Layout.fillWidth: true; Layout.fillHeight: true }
}
}
}
}
}

View File

@@ -0,0 +1,104 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.plugins
ContentMenu {
title: "Store"
description: "Manage plugins and other stuff for the shell."
ContentCard {
Layout.fillWidth: true
GridLayout {
columns: 1
Layout.fillWidth: true
columnSpacing: Metrics.spacing(16)
rowSpacing: Metrics.spacing(16)
Repeater {
model: PluginParser.model
delegate: StyledRect {
Layout.preferredHeight: 90
Layout.fillWidth: true
radius: Metrics.radius("small")
color: Appearance.m3colors.m3surfaceContainer
RowLayout {
anchors.fill: parent
anchors.margins: Metrics.margin("normal")
spacing: Metrics.spacing(12)
Column {
Layout.fillWidth: true
spacing: Metrics.spacing(2)
StyledText {
font.pixelSize: Metrics.fontSize("large")
text: name
}
RowLayout {
spacing: Metrics.spacing(6)
StyledText {
font.pixelSize: Metrics.fontSize("small")
text: author
color: Appearance.colors.colSubtext
}
StyledText {
font.pixelSize: Metrics.fontSize("small")
text: "| Requires Nucleus " + requires_nucleus
color: Appearance.colors.colSubtext
}
}
StyledText {
font.pixelSize: Metrics.fontSize("normal")
text: description
color: Appearance.colors.colSubtext
}
}
RowLayout {
spacing: Metrics.spacing(8)
StyledButton {
icon: "download"
text: "Install"
visible: !installed
secondary: true
Layout.preferredWidth: 140
onClicked: PluginParser.install(id)
}
StyledButton {
icon: "update"
text: "Update"
visible: installed
secondary: true
Layout.preferredWidth: 140
onClicked: PluginParser.update(id)
}
StyledButton {
icon: "delete"
text: "Remove"
visible: installed
secondary: true
Layout.preferredWidth: 140
onClicked: PluginParser.uninstall(id)
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,294 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.services
ContentMenu {
property string displayName: root.screen?.name ?? ""
property var intervalOptions: [{
"value": 5,
"label": "5 minutes"
}, {
"value": 15,
"label": "15 minutes"
}, {
"value": 30,
"label": "30 minutes"
}, {
"value": 60,
"label": "1 hour"
}, {
"value": 120,
"label": "2 hours"
}, {
"value": 360,
"label": "6 hours"
}]
function getIntervalIndex(minutes) {
for (let i = 0; i < intervalOptions.length; i++) {
if (intervalOptions[i].value === minutes)
return i;
}
return 0;
}
title: "Wallpaper"
description: "Manage your wallpapers"
ContentCard {
ClippingRectangle {
id: wpContainer
Layout.alignment: Qt.AlignHCenter
width: root.screen.width / 2
height: width * root.screen.height / root.screen.width
radius: Metrics.radius("unsharpenmore")
color: Appearance.m3colors.m3surfaceContainer
StyledText {
text: "Current Wallpaper:"
font.pixelSize: Metrics.fontSize("big")
font.bold: true
}
ClippingRectangle {
id: wpPreview
Layout.alignment: Qt.AlignHCenter | Qt.AlignCenter
anchors.fill: parent
radius: Metrics.radius("unsharpenmore")
color: Appearance.m3colors.m3paddingContainer
layer.enabled: true
StyledText {
opacity: !Config.runtime.appearance.background.enabled ? 1 : 0
font.pixelSize: Metrics.fontSize("title")
text: "Wallpaper Manager Disabled"
anchors.centerIn: parent
Behavior on opacity {
enabled: Config.runtime.appearance.animations.enabled
Anim { }
}
}
Image {
opacity: Config.runtime.appearance.background.enabled ? 1 : 0
anchors.fill: parent
source: previewImg + "?t=" + Date.now()
property string previewImg: {
const displays = Config.runtime.monitors
const fallback = Config.runtime.appearance.background.defaultPath
if (!displays)
return fallback
const monitor = displays?.[displayName]
return monitor?.wallpaper ?? fallback
}
fillMode: Image.PreserveAspectCrop
cache: true
Behavior on opacity {
enabled: Config.runtime.appearance.animations.enabled
Anim { }
}
}
}
}
StyledButton {
icon: "wallpaper"
text: "Change Wallpaper"
Layout.fillWidth: true
onClicked: {
Quickshell.execDetached(["nucleus", "ipc", "call", "background", "change"]);
}
}
StyledSwitchOption {
title: "Enabled"
description: "Enabled or disable built-in wallpaper daemon."
prefField: "appearance.background.enabled"
}
}
ContentCard {
StyledText {
text: "Parallax Effect"
font.pixelSize: Metrics.fontSize("big")
font.bold: true
}
StyledSwitchOption {
title: "Enabled"
description: "Enabled or disable wallpaper parallax effect."
prefField: "appearance.background.parallax.enabled"
}
StyledSwitchOption {
title: "Enabled for Sidebar Left"
description: "Show parralax effect when sidebarLeft is opened."
prefField: "appearance.background.parallax.enableSidebarLeft"
}
StyledSwitchOption {
title: "Enabled for Sidebar Right"
description: "Show parralax effect when sidebarRight is opened."
prefField: "appearance.background.parallax.enableSidebarRight"
}
NumberStepper {
label: "Zoom Amount"
description: "Adjust the zoom of the parallax effect."
prefField: "appearance.background.parallax.zoom"
step: 0.1
minimum: 1.10
maximum: 2
}
}
ContentCard {
StyledText {
text: "Wallpaper Slideshow"
font.pixelSize: Metrics.fontSize("big")
font.bold: true
}
StyledSwitchOption {
title: "Enable Slideshow"
description: "Automatically rotate wallpapers from a folder."
prefField: "appearance.background.slideshow.enabled"
}
StyledSwitchOption {
title: "Include Subfolders"
description: "Also search for wallpapers in subfolders."
prefField: "appearance.background.slideshow.includeSubfolders"
}
ColumnLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(8)
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(12)
ColumnLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(4)
StyledText {
text: "Wallpaper Folder"
font.pixelSize: Metrics.fontSize("normal")
}
StyledText {
text: Config.runtime.appearance.background.slideshow.folder || "No folder selected"
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurfaceVariant
elide: Text.ElideMiddle
Layout.fillWidth: true
}
}
StyledButton {
icon: "folder_open"
text: "Browse"
onClicked: folderPickerProc.running = true
}
}
}
RowLayout {
id: skipWallpaper
property string title: "Skip To Next Wallpaper"
property string description: "Skip to the next wallpaper in the wallpaper directory."
property string prefField: ''
ColumnLayout {
StyledText {
text: skipWallpaper.title
font.pixelSize: Metrics.fontSize("normal")
}
StyledText {
text: skipWallpaper.description
font.pixelSize: Metrics.fontSize("small")
}
}
Item { Layout.fillWidth: true }
StyledButton {
icon: "skip_next"
text: "Skip Next"
enabled: WallpaperSlideshow.wallpapers.length > 0
onClicked: {
Quickshell.execDetached(["nucleus", "ipc", "call", "background", "next"]);
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(12)
ColumnLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(4)
StyledText {
text: "Change Interval"
font.pixelSize: Metrics.fontSize("normal")
}
StyledText {
text: "How often to change the wallpaper."
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurfaceVariant
}
}
Item { Layout.fillWidth: true }
StyledDropDown {
label: "Interval"
model: intervalOptions.map((opt) => {
return opt.label;
})
currentIndex: getIntervalIndex(Config.runtime.appearance.background.slideshow.interval)
onSelectedIndexChanged: (index) => {
Config.updateKey("appearance.background.slideshow.interval", intervalOptions[index].value);
}
}
}
}
Process {
id: folderPickerProc
command: ["bash", Directories.scriptsPath + "/interface/selectfolder.sh", Config.runtime.appearance.background.slideshow.folder || Directories.pictures]
stdout: StdioCollector {
onStreamFinished: {
const out = text.trim();
if (out !== "null" && out.length > 0)
Config.updateKey("appearance.background.slideshow.folder", out);
}
}
}
component Anim: NumberAnimation {
duration: Metrics.chronoDuration(400)
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animation.curves.standard
}
}

View File

@@ -0,0 +1,525 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.config
import qs.modules.functions
import qs.modules.components
import qs.services
Item {
id: root
property bool initialChatSelected: false
function appendMessage(sender, message) {
messageModel.append({
"sender": sender,
"message": message
});
scrollToBottom();
}
function updateChatsList(files) {
let existing = {
};
for (let i = 0; i < chatListModel.count; i++) existing[chatListModel.get(i).name] = true
for (let file of files) {
let name = file.trim();
if (!name.length)
continue;
if (name.endsWith(".txt"))
name = name.slice(0, -4);
if (!existing[name])
chatListModel.append({
"name": name
});
delete existing[name];
}
// remove chats that no longer exist
for (let name in existing) {
for (let i = 0; i < chatListModel.count; i++) {
if (chatListModel.get(i).name === name) {
chatListModel.remove(i);
break;
}
}
}
// ensure default exists
let hasDefault = false;
for (let i = 0; i < chatListModel.count; i++) if (chatListModel.get(i).name === "default") {
hasDefault = true;
}
if (!hasDefault) {
chatListModel.insert(0, {
"name": "default"
});
FileUtils.createFile(FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/default.txt");
}
}
function scrollToBottom() {
chatView.positionViewAtEnd();
}
function sendMessage() {
if (userInput.text === "" || Zenith.loading)
return ;
Zenith.pendingInput = userInput.text;
appendMessage("You", userInput.text);
userInput.text = "";
Zenith.loading = true;
Zenith.send();
}
function loadChatHistory(chatName) {
messageModel.clear();
Zenith.loadChat(chatName);
}
function selectDefaultChat() {
let defaultIndex = -1;
for (let i = 0; i < chatListModel.count; i++) {
if (chatListModel.get(i).name === "default") {
defaultIndex = i;
break;
}
}
if (defaultIndex !== -1) {
chatSelector.currentIndex = defaultIndex;
Zenith.currentChat = "default";
loadChatHistory("default");
} else if (chatListModel.count > 0) {
chatSelector.currentIndex = 0;
Zenith.currentChat = chatListModel.get(0).name;
loadChatHistory(Zenith.currentChat);
}
}
ListModel {
// { sender: "You" | "AI", message: string }
id: messageModel
}
ListModel {
id: chatListModel
}
ColumnLayout {
spacing: Metrics.spacing(8)
anchors.centerIn: parent
StyledText {
visible: !Config.runtime.misc.intelligence.enabled
text: "Intelligence is disabled!"
Layout.leftMargin: Metrics.margin(24)
font.pixelSize: Appearance.font.size.huge
}
StyledText {
visible: !Config.runtime.misc.intelligence.enabled
text: "Go to the settings to enable intelligence"
}
}
StyledRect {
anchors.topMargin: Metrics.margin(74)
radius: Metrics.radius("normal")
anchors.fill: parent
color: "transparent"
visible: Config.runtime.misc.intelligence.enabled
ColumnLayout {
anchors.fill: parent
anchors.margins: Metrics.margin(16)
spacing: Metrics.spacing(10)
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(10)
StyledDropDown {
id: chatSelector
Layout.fillWidth: true
model: chatListModel
textRole: "name"
Layout.preferredHeight: 40
onCurrentIndexChanged: {
if (!initialChatSelected)
return ;
if (currentIndex < 0 || currentIndex >= chatListModel.count)
return ;
let chatName = chatListModel.get(currentIndex).name;
Zenith.currentChat = chatName;
loadChatHistory(chatName);
}
}
StyledButton {
icon: "add"
Layout.preferredWidth: 40
onClicked: {
let name = "new-chat-" + chatListModel.count;
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
FileUtils.createFile(path, function(success) {
if (success) {
chatListModel.append({
"name": name
});
chatSelector.currentIndex = chatListModel.count - 1;
Zenith.currentChat = name;
messageModel.clear();
}
});
}
}
StyledButton {
icon: "edit"
Layout.preferredWidth: 40
enabled: chatSelector.currentIndex >= 0
onClicked: renameDialog.open()
}
StyledButton {
icon: "delete"
Layout.preferredWidth: 40
enabled: chatSelector.currentIndex >= 0 && chatSelector.currentText !== "default"
onClicked: {
let name = chatSelector.currentText;
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
FileUtils.removeFile(path, function(success) {
if (success) {
chatListModel.remove(chatSelector.currentIndex);
selectDefaultChat();
}
});
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(10)
StyledDropDown {
id: modelSelector
Layout.fillWidth: true
model: ["openai/gpt-4o","openai/gpt-4","openai/gpt-3.5-turbo","openai/gpt-4o-mini","anthropic/claude-3.5-sonnet","anthropic/claude-3-haiku","meta-llama/llama-3.3-70b-instruct:free","deepseek/deepseek-r1-0528:free","qwen/qwen3-coder:free"]
currentIndex: 0
Layout.preferredHeight: 40
onCurrentTextChanged: Zenith.currentModel = currentText
}
StyledButton {
icon: "fullscreen"
Layout.preferredWidth: 40
onClicked: {
Quickshell.execDetached(["nucleus", "ipc", "call", "intelligence", "openWindow"]);
Globals.visiblility.sidebarLeft = false;
}
}
}
StyledRect {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Metrics.radius("normal")
color: Appearance.m3colors.m3surfaceContainerLow
ScrollView {
anchors.fill: parent
clip: true
ListView {
id: chatView
model: messageModel
spacing: Metrics.spacing(8)
anchors.fill: parent
anchors.margins: Metrics.margin(12)
clip: true
delegate: Item {
property bool isCodeBlock: message.split("\n").length > 2 && message.includes("import ") // simple heuristic
width: chatView.width
height: bubble.implicitHeight + 6
Component.onCompleted: {
chatView.forceLayout();
}
Row {
width: parent.width
spacing: Metrics.spacing(8)
Item {
width: sender === "AI" ? 0 : parent.width * 0.2
}
StyledRect {
id: bubble
radius: Metrics.radius("normal")
color: sender === "You" ? Appearance.m3colors.m3primaryContainer : Appearance.m3colors.m3surfaceContainerHigh
implicitWidth: Math.min(textItem.implicitWidth + 20, chatView.width * 0.8)
implicitHeight: textItem.implicitHeight
anchors.right: sender === "You" ? parent.right : undefined
anchors.left: sender === "AI" ? parent.left : undefined
anchors.topMargin: Metrics.margin(2)
TextEdit {
id: textItem
text: StringUtils.markdownToHtml(message)
wrapMode: TextEdit.Wrap
textFormat: TextEdit.RichText
readOnly: true // make it selectable but not editable
font.pixelSize: Metrics.fontSize(16)
anchors.leftMargin: Metrics.margin(12)
color: Appearance.syntaxHighlightingTheme
padding: Metrics.padding(8)
anchors.fill: parent
}
MouseArea {
id: ma
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
let p = Qt.createQmlObject('import Quickshell; import Quickshell.Io; Process { command: ["wl-copy", "' + message + '"] }', parent);
p.running = true;
}
}
}
Item {
width: sender === "You" ? 0 : parent.width * 0.2
}
}
}
}
}
}
StyledRect {
Layout.fillWidth: true
height: 50
radius: Metrics.radius("normal")
color: Appearance.m3colors.m3surfaceContainer
RowLayout {
anchors.fill: parent
anchors.margins: 6
spacing: 10
StyledTextField {
// Shift+Enter → insert newline
// Enter → send message
id: userInput
Layout.fillWidth: true
placeholderText: "Type your message..."
font.pixelSize: Metrics.fontSize(14)
padding: Metrics.padding(8)
Keys.onPressed: {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (event.modifiers & Qt.ShiftModifier)
insert("\n");
else
sendMessage();
event.accepted = true;
}
}
}
StyledButton {
text: "Send"
enabled: userInput.text.trim().length > 0 && !Zenith.loading
opacity: enabled ? 1 : 0.5
onClicked: sendMessage()
}
}
}
}
Dialog {
id: renameDialog
title: "Rename Chat"
modal: true
visible: false
standardButtons: Dialog.NoButton
x: (root.width - 360) / 2 // center horizontally
y: (root.height - 160) / 2 // center vertically
width: 360
height: 200
ColumnLayout {
anchors.fill: parent
anchors.margins: Metrics.margin(16)
spacing: Metrics.spacing(12)
StyledText {
text: "Enter a new name for the chat"
font.pixelSize: Metrics.fontSize(18)
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
StyledTextField {
id: renameInput
Layout.fillWidth: true
placeholderText: "New name"
filled: false
highlight: false
text: chatSelector.currentText
font.pixelSize: Metrics.iconSize(16)
Layout.preferredHeight: 45
padding: Metrics.padding(8)
}
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(12)
Layout.alignment: Qt.AlignRight
StyledButton {
text: "Cancel"
Layout.preferredWidth: 80
onClicked: renameDialog.close()
}
StyledButton {
text: "Rename"
Layout.preferredWidth: 100
enabled: renameInput.text.trim().length > 0 && renameInput.text !== chatSelector.currentText
onClicked: {
let oldName = chatSelector.currentText;
let newName = renameInput.text.trim();
let oldPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + oldName + ".txt";
let newPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + newName + ".txt";
FileUtils.renameFile(oldPath, newPath, function(success) {
if (success) {
chatListModel.set(chatSelector.currentIndex, {
"name": newName
});
Zenith.currentChat = newName;
renameDialog.close();
}
});
}
}
}
}
background: StyledRect {
color: Appearance.m3colors.m3surfaceContainer
radius: Metrics.radius("normal")
border.color: Appearance.colors.colOutline
border.width: 1
}
header: StyledRect {
color: Appearance.m3colors.m3surfaceContainer
radius: Metrics.radius("normal")
border.color: Appearance.colors.colOutline
border.width: 1
}
}
StyledText {
text: "Thinking…"
visible: Zenith.loading
color: Appearance.colors.colSubtext
font.pixelSize: Metrics.fontSize(14)
anchors {
left: parent.left
bottom: parent.bottom
leftMargin: Metrics.margin(22)
bottomMargin: Metrics.margin(76)
}
}
}
Connections {
function onChatsListed(text) {
let lines = text.split(/\r?\n/);
updateChatsList(lines);
// only auto-select once
if (!initialChatSelected) {
selectDefaultChat();
initialChatSelected = true;
}
}
function onAiReply(text) {
appendMessage("AI", text.slice(5));
Zenith.loading = false;
}
function onChatLoaded(text) {
let lines = text.split(/\r?\n/);
let batch = [];
for (let l of lines) {
let line = l.trim();
if (!line.length)
continue;
let u = line.match(/^\[\d{4}-.*\] User: (.*)$/);
let a = line.match(/^\[\d{4}-.*\] AI: (.*)$/);
if (u)
batch.push({
"sender": "You",
"message": u[1]
});
else if (a)
batch.push({
"sender": "AI",
"message": a[1]
});
else if (batch.length)
batch[batch.length - 1].message += "\n" + line;
}
messageModel.clear();
for (let m of batch) messageModel.append(m)
scrollToBottom();
}
target: Zenith
}
}

View File

@@ -0,0 +1,90 @@
import qs.config
import qs.modules.components
import qs.modules.functions
import qs.services
import QtQuick
import Quickshell
import QtQuick.Layouts
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Hyprland
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
PanelWindow {
id: sidebarLeft
WlrLayershell.namespace: "nucleus:sidebarLeft"
WlrLayershell.layer: WlrLayer.Top
visible: Config.initialized && Globals.visiblility.sidebarLeft && !Globals.visiblility.sidebarRight
color: "transparent"
exclusiveZone: 0
WlrLayershell.keyboardFocus: Compositor.require("hyprland") && Globals.visiblility.sidebarLeft
property real sidebarLeftWidth: 500
implicitWidth: Compositor.screenW
HyprlandFocusGrab {
id: grab
active: Compositor.require("hyprland")
windows: [sidebarLeft]
}
anchors {
top: true
left: (Config.runtime.bar.position === "top" || Config.runtime.bar.position === "bottom" || Config.runtime.bar.position === "left")
bottom: true
right: (Config.runtime.bar.position === "right")
}
margins {
top: Config.runtime.bar.margins
bottom: Config.runtime.bar.margins
left: Metrics.margin("small")
right: Metrics.margin("small")
}
MouseArea {
anchors.fill: parent
z: 0
onPressed: Globals.visiblility.sidebarLeft = false
}
StyledRect {
id: container
z: 1
color: Appearance.m3colors.m3background
radius: Metrics.radius("large")
width: sidebarLeft.sidebarLeftWidth
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
FocusScope {
focus: true
anchors.fill: parent
Keys.onPressed: {
if (event.key === Qt.Key_Escape) {
Globals.visiblility.sidebarLeft = false;
}
}
SidebarLeftContent {}
}
}
function togglesidebarLeft() {
Globals.visiblility.sidebarLeft = !Globals.visiblility.sidebarLeft;
}
IpcHandler {
target: "sidebarLeft"
function toggle() {
togglesidebarLeft();
}
}
}

View File

@@ -0,0 +1,142 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.config
import qs.modules.functions
import qs.modules.components
import qs.services
Item {
anchors.fill: parent
SwipeView {
id: view
anchors.fill: parent
// IntelligencePanel {}
SystemOverview {}
// WallpapersPage {}
}
Rectangle {
height: 2
width: parent.width - Metrics.margin("verylarge")
color: Appearance.m3colors.m3outlineVariant
opacity: 0.6
anchors {
top: view.top
topMargin: segmentedIndicator.height + Metrics.margin("verysmall")
horizontalCenter: view.horizontalCenter
}
}
Rectangle {
id: activeTabIndicator
height: 2
width: 96
radius: Metrics.radius(1)
color: Appearance.m3colors.m3primary
x: (segmentedIndicator.width / view.count) * view.currentIndex + (segmentedIndicator.width / view.count - width) / 2
anchors {
top: segmentedIndicator.bottom
topMargin: Metrics.margin(8)
}
Behavior on x {
NumberAnimation {
duration: Metrics.chronoDuration(220)
easing.type: Easing.OutCubic
}
}
}
Item {
id: segmentedIndicator
height: 56
width: parent.width
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
}
Row {
anchors.fill: parent
spacing: 0
Repeater {
model: [
// {
// "icon": "neurology",
// "text": "Intelligence"
// },
{
"icon": "overview",
"text": "Overview"
},
// {
// "icon": "wallpaper",
// "text": "Wallpapers"
// }
]
Item {
width: segmentedIndicator.width / view.count
height: parent.height
MouseArea {
anchors.fill: parent
onClicked: view.currentIndex = index
}
// Icon (true center)
MaterialSymbol {
icon: modelData.icon
iconSize: Metrics.iconSize("huge")
color: view.currentIndex === index ? Appearance.m3colors.m3primary : Appearance.m3colors.m3onSurfaceVariant
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: Metrics.margin(12)
}
}
// Label (independent centering)
StyledText {
text: modelData.text
font.pixelSize: Metrics.fontSize("large")
font.weight: Font.Medium
color: view.currentIndex === index ? Appearance.m3colors.m3primary : Appearance.m3colors.m3onSurfaceVariant
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 0
}
}
}
}
}
}
}

View File

@@ -0,0 +1,486 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.modules.functions
import qs.services
import qs.config
import qs.modules.components
Item {
id: root
implicitWidth: 300
implicitHeight: parent ? parent.height : 500
ColumnLayout {
anchors.topMargin: Metrics.margin(90)
anchors.fill: parent
anchors.margins: Metrics.margin("normal")
spacing: Metrics.margin("small")
// Header
RowLayout {
Layout.fillWidth: true
RowLayout {
spacing: Metrics.margin("normal")
StyledText {
text: SystemDetails.osIcon
font.family: Metrics.fontFamily("nerdIcons")
font.pixelSize: Metrics.fontSize(48)
color: Appearance.colors.colPrimary
}
ColumnLayout {
spacing: Metrics.spacing(2)
StyledText {
text: SystemDetails.osName
font.pixelSize: Metrics.fontSize("large")
color: Appearance.m3colors.m3onSurface
}
StyledText {
text: `${SystemDetails.username}@${SystemDetails.hostname}`
font.pixelSize: Metrics.fontSize("small")
color: Appearance.colors.colSubtext
}
}
}
Item {
Layout.fillWidth: true
}
ColumnLayout {
spacing: Metrics.spacing(2)
Layout.alignment: Qt.AlignRight
StyledText {
text: `qs ${SystemDetails.qsVersion}`
font.pixelSize: Metrics.fontSize("small")
color: Appearance.colors.colSubtext
}
StyledText {
text: `nucleus-shell v${Config.runtime.shell.version}`
font.pixelSize: Metrics.fontSize("smaller")
color: Appearance.colors.colSubtext
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 56
radius: Metrics.radius("normal")
color: Appearance.colors.colLayer2
RowLayout {
anchors.fill: parent
anchors.margins: Metrics.margin("small")
StyledText {
text: "Uptime"
font.pixelSize: Metrics.fontSize("normal")
color: Appearance.colors.colPrimary
}
Item {
Layout.fillWidth: true
}
StyledText {
text: SystemDetails.uptime
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurface
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 56
radius: Metrics.radius("normal")
color: Appearance.colors.colLayer2
RowLayout {
anchors.fill: parent
anchors.margins: Metrics.margin("small")
StyledText {
text: "Operating System"
font.pixelSize: Metrics.fontSize("normal")
color: Appearance.colors.colPrimary
}
Item {
Layout.fillWidth: true
}
StyledText {
text: SystemDetails.osName
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurface
elide: Text.ElideRight
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 320
radius: Metrics.radius("large")
color: Appearance.colors.colLayer2
ColumnLayout {
anchors.fill: parent
anchors.margins: Metrics.margin("large")
spacing: Metrics.margin("normal")
ColumnLayout {
spacing: Metrics.spacing(6)
RowLayout {
StyledText {
text: "CPU Usage"
color: Appearance.colors.colSubtext
}
Item {
Layout.fillWidth: true
}
StyledText {
animate: false
text: SystemDetails.cpuLoad
color: Appearance.colors.colSubtext
}
}
Rectangle {
Layout.fillWidth: true
height: 10
radius: Metrics.radius(5)
color: Appearance.colors.colLayer1
Rectangle {
width: parent.width * SystemDetails.cpuPercent
height: parent.height
radius: Metrics.radius(5)
color: Appearance.colors.colPrimary
}
}
}
ColumnLayout {
spacing: Metrics.spacing(6)
RowLayout {
StyledText {
text: "Ram Usage"
color: Appearance.colors.colSubtext
}
Item {
Layout.fillWidth: true
}
StyledText {
animate: false
text: SystemDetails.ramUsage
color: Appearance.colors.colSubtext
}
}
Rectangle {
Layout.fillWidth: true
height: 10
radius: Metrics.radius(5)
color: Appearance.colors.colLayer1
Rectangle {
width: parent.width * SystemDetails.ramPercent
height: parent.height
radius: Metrics.radius(5)
color: Appearance.colors.colPrimary
}
}
}
ColumnLayout {
spacing: Metrics.spacing(6)
RowLayout {
StyledText {
text: "Disk Usage"
color: Appearance.colors.colSubtext
}
Item {
Layout.fillWidth: true
}
StyledText {
animate: false
text: SystemDetails.diskUsage
color: Appearance.colors.colSubtext
}
}
Rectangle {
Layout.fillWidth: true
height: 10
radius: Metrics.radius(5)
color: Appearance.colors.colLayer1
Rectangle {
width: parent.width * SystemDetails.diskPercent
height: parent.height
radius: Metrics.radius(5)
color: Appearance.colors.colPrimary
}
}
}
ColumnLayout {
spacing: Metrics.spacing(6)
RowLayout {
StyledText {
text: "Swap Usage"
color: Appearance.colors.colSubtext
}
Item {
Layout.fillWidth: true
}
StyledText {
text: SystemDetails.swapUsage
color: Appearance.colors.colSubtext
}
}
Rectangle {
Layout.fillWidth: true
height: 10
radius: Metrics.radius(5)
color: Appearance.colors.colLayer1
Rectangle {
width: parent.width * SystemDetails.swapPercent
height: parent.height
radius: Metrics.radius(5)
color: Appearance.colors.colPrimary
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 72
radius: Metrics.radius("normal")
color: Appearance.colors.colLayer2
RowLayout {
anchors.fill: parent
anchors.margins: Metrics.margin("small")
spacing: Metrics.margin("large")
ColumnLayout {
spacing: Metrics.spacing(2)
StyledText {
text: "Kernel"
font.pixelSize: Metrics.fontSize("small")
color: Appearance.colors.colSubtext
}
StyledText {
text: SystemDetails.kernelVersion
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurface
}
}
Item {
Layout.fillWidth: true
}
ColumnLayout {
spacing: Metrics.spacing(2)
Layout.alignment: Qt.AlignRight
StyledText {
text: "Architecture"
font.pixelSize: Metrics.fontSize("small")
color: Appearance.colors.colSubtext
horizontalAlignment: Text.AlignRight
}
StyledText {
text: SystemDetails.architecture
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurface
horizontalAlignment: Text.AlignRight
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 72
radius: Metrics.radius("normal")
color: Appearance.colors.colLayer2
visible: UPower.batteryPresent
RowLayout {
anchors.fill: parent
anchors.margins: Metrics.margin("small")
spacing: Metrics.margin("large")
ColumnLayout {
spacing: Metrics.spacing(2)
StyledText {
text: "Battery"
font.pixelSize: Metrics.fontSize("small")
color: Appearance.colors.colSubtext
}
StyledText {
text: `${Math.round(UPower.percentage)}%`
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurface
}
}
Item {
Layout.fillWidth: true
}
ColumnLayout {
spacing: Metrics.spacing(2)
Layout.alignment: Qt.AlignRight
StyledText {
text: "AC"
font.pixelSize: Metrics.fontSize("small")
color: Appearance.colors.colSubtext
horizontalAlignment: Text.AlignRight
}
StyledText {
text: UPower.acOnline ? "online" : "battery"
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurface
horizontalAlignment: Text.AlignRight
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 56
radius: Metrics.radius("normal")
color: Appearance.colors.colLayer2
RowLayout {
anchors.fill: parent
anchors.margins: Metrics.margin("small")
StyledText {
text: "Running Processes"
font.pixelSize: Metrics.fontSize("normal")
color: Appearance.colors.colPrimary
}
Item {
Layout.fillWidth: true
}
StyledText {
text: SystemDetails.runningProcesses
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurface
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 56
radius: Metrics.radius("normal")
color: Appearance.colors.colLayer2
RowLayout {
anchors.fill: parent
anchors.margins: Metrics.margin("small")
StyledText {
text: "Logged-in Users"
font.pixelSize: Metrics.fontSize("normal")
color: Appearance.colors.colPrimary
}
Item {
Layout.fillWidth: true
}
StyledText {
text: SystemDetails.loggedInUsers
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurface
}
}
}
Item {
Layout.fillHeight: true
}
}
}

View File

@@ -0,0 +1,114 @@
import Qt.labs.folderlistmodel
import Qt5Compat.GraphicalEffects
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.modules.functions
import qs.services
import qs.config
import qs.modules.components
Item {
id: wallpapersPage
property string displayName: screen?.name ?? ""
FolderListModel {
id: wallpaperModel
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation) + "/Wallpapers"
nameFilters: ["*.png", "*.jpg", "*.jpeg", "*.webp", "mp4", "mkv", "webm", "avi", "mov", "flv", "wmv", "m4v"]
showDirs: false
showDotAndDotDot: false
}
// EMPTY STATE
StyledText {
visible: wallpaperModel.count === 0
text: "Put some wallpapers in\n~/Pictures/Wallpapers"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Appearance.m3colors.m3onSurfaceVariant
font.pixelSize: Appearance.font.size.large
anchors.centerIn: parent
}
// WALLPAPER LIST
ListView {
anchors.topMargin: 90
visible: wallpaperModel.count > 0
anchors.fill: parent
model: wallpaperModel
spacing: Appearance.margin.normal
clip: true
delegate: Item {
width: ListView.view.width
height: 240
StyledRect {
id: imgContainer
property bool activeWallpaper:
Config.runtime.monitors?.[wallpapersPage.displayName]?.wallpaper === fileUrl
anchors.fill: parent
anchors.leftMargin: 20
anchors.rightMargin: 20
radius: Appearance.rounding.normal
color: activeWallpaper
? Appearance.m3colors.m3secondaryContainer
: Appearance.m3colors.m3surfaceContainerLow
layer.enabled: true
Image {
id: wallImg
anchors.fill: parent
source: fileUrl
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
clip: true
}
StyledText {
anchors.centerIn: parent
text: "Unsupported / Corrupted Image"
visible: wallImg.status === Image.Error
}
MouseArea {
anchors.fill: parent
onClicked: {
Config.updateKey(
"monitors." + wallpapersPage.displayName + ".wallpaper",
fileUrl
);
if (Config.runtime.appearance.colors.autogenerated) {
Quickshell.execDetached([
"nucleus", "ipc", "call", "global", "regenColors"
]);
}
}
cursorShape: Qt.PointingHandCursor
}
layer.effect: OpacityMask {
maskSource: Rectangle {
width: imgContainer.width
height: imgContainer.height
radius: imgContainer.radius
}
}
}
}
}
}

View File

@@ -0,0 +1,97 @@
import qs.config
import qs.modules.components
import qs.modules.functions
import qs.services
import QtQuick
import Quickshell
import QtQuick.Layouts
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Hyprland
import QtQuick.Controls
import Quickshell.Services.Pipewire
import Qt5Compat.GraphicalEffects
PanelWindow {
id: sidebarRight
WlrLayershell.namespace: "nucleus:sidebarRight"
WlrLayershell.layer: WlrLayer.Top
visible: Config.initialized && Globals.visiblility.sidebarRight && !Globals.visiblility.sidebarLeft
color: "transparent"
exclusiveZone: 0
WlrLayershell.keyboardFocus: Compositor.require("hyprland") && Globals.visiblility.sidebarRight
property real sidebarRightWidth: 500
implicitWidth: Compositor.screenW
HyprlandFocusGrab {
id: grab
active: Compositor.require("hyprland")
windows: [sidebarRight]
}
anchors {
top: true
right: (Config.runtime.bar.position === "top" || Config.runtime.bar.position === "bottom" || Config.runtime.bar.position === "right")
bottom: true
left: (Config.runtime.bar.position === "left")
}
margins {
top: Config.runtime.bar.margins
bottom: Config.runtime.bar.margins
left: Metrics.margin("small")
right: Metrics.margin("small")
}
PwObjectTracker {
objects: [Pipewire.defaultAudioSink]
}
property var sink: Pipewire.defaultAudioSink?.audio
MouseArea {
anchors.fill: parent
z: 0
onPressed: Globals.visiblility.sidebarRight = false
}
StyledRect {
id: container
z: 1
color: Appearance.m3colors.m3background
radius: Metrics.radius("large")
width: sidebarRight.sidebarRightWidth
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
}
FocusScope {
focus: true
anchors.fill: parent
Keys.onPressed: {
if (event.key === Qt.Key_Escape) {
Globals.visiblility.sidebarRight = false;
}
}
SidebarRightContent {}
}
}
function togglesidebarRight() {
Globals.visiblility.sidebarRight = !Globals.visiblility.sidebarRight;
}
IpcHandler {
target: "sidebarRight"
function toggle() {
togglesidebarRight();
}
}
}

View File

@@ -0,0 +1,247 @@
import qs.config
import qs.modules.components
import qs.services
import qs.modules.functions
import QtQuick
import Quickshell
import QtQuick.Layouts
import Quickshell.Wayland
import Quickshell.Io
import QtQuick.Controls
import Quickshell.Services.Pipewire
import Qt5Compat.GraphicalEffects
import "content/"
Item {
anchors.fill: parent
anchors.leftMargin: Metrics.margin("normal")
anchors.rightMargin: Metrics.margin("normal")
anchors.topMargin: Metrics.margin("large")
anchors.bottomMargin: Metrics.margin("large")
ColumnLayout {
id: mainLayout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Metrics.margin("tiny")
anchors.rightMargin: Metrics.margin("tiny")
anchors.margins: Metrics.margin("large")
spacing: Metrics.margin("large")
RowLayout {
id: topSection
Layout.fillWidth: true
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Metrics.margin(10)
Layout.alignment: Qt.AlignVCenter
spacing: Metrics.spacing(2)
RowLayout {
spacing: Metrics.spacing(8)
StyledText {
text: SystemDetails.osIcon
font.pixelSize: Metrics.fontSize("hugeass") + 6
}
StyledText {
text: SystemDetails.uptime
font.pixelSize: Metrics.fontSize("large")
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: Metrics.margin(5)
}
}
}
Item { Layout.fillWidth: true }
Row {
spacing: Metrics.spacing(6)
Layout.leftMargin: Metrics.margin(25)
Layout.alignment: Qt.AlignVCenter
StyledRect {
id: screenshotbtncontainer
color: "transparent"
radius: Metrics.radius("large")
implicitHeight: screenshotButton.height + Metrics.margin("tiny")
implicitWidth: screenshotButton.width + Metrics.margin("small")
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.topMargin: Metrics.margin(10)
Layout.leftMargin: Metrics.margin(15)
MaterialSymbolButton {
id: screenshotButton
icon: "edit"
anchors.centerIn: parent
iconSize: Metrics.iconSize("hugeass") + 2
tooltipText: "Take a screenshot"
onButtonClicked: {
Quickshell.execDetached(["nucleus", "ipc", "call", "screen", "capture"])
Globals.visiblility.sidebarRight = false;
}
}
}
StyledRect {
id: reloadbtncontainer
color: "transparent"
radius: Metrics.radius("large")
implicitHeight: reloadButton.height + Metrics.margin("tiny")
implicitWidth: reloadButton.width + Metrics.margin("small")
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.topMargin: Metrics.margin(10)
Layout.leftMargin: Metrics.margin(15)
MaterialSymbolButton {
id: reloadButton
icon: "refresh"
anchors.centerIn: parent
iconSize: Metrics.iconSize("hugeass") + 4
tooltipText: "Reload Nucleus Shell"
onButtonClicked: {
Quickshell.execDetached(["nucleus", "run", "--reload"])
}
}
}
StyledRect {
id: settingsbtncontainer
color: "transparent"
radius: Metrics.radius("large")
implicitHeight: settingsButton.height + Metrics.margin("tiny")
implicitWidth: settingsButton.width + Metrics.margin("small")
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.topMargin: Metrics.margin(10)
Layout.leftMargin: Metrics.margin(15)
MaterialSymbolButton {
id: settingsButton
icon: "settings"
anchors.centerIn: parent
iconSize: Metrics.iconSize("hugeass") + 2
tooltipText: "Open Settings"
onButtonClicked: {
Globals.visiblility.sidebarRight = false
Globals.states.settingsOpen = true
}
}
}
StyledRect {
id: powerbtncontainer
color: "transparent"
radius: Metrics.radius("large")
implicitHeight: settingsButton.height + Metrics.margin("tiny")
implicitWidth: settingsButton.width + Metrics.margin("small")
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.topMargin: Metrics.margin(10)
Layout.leftMargin: Metrics.margin(15)
MaterialSymbolButton {
id: powerButton
icon: "power_settings_new"
anchors.centerIn: parent
iconSize: Metrics.iconSize("hugeass") + 2
tooltipText: "Open PowerMenu"
onButtonClicked: {
Globals.visiblility.sidebarRight = false
Globals.visiblility.powermenu = true
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Appearance.m3colors.m3outlineVariant
radius: Metrics.radius(1)
}
ColumnLayout {
id: sliderColumn
Layout.fillWidth: true
VolumeSlider {
Layout.fillWidth: true
Layout.preferredHeight: 50
icon: "volume_up"
iconSize: Metrics.iconSize("large") + 3
}
BrightnessSlider {
Layout.fillWidth: true
Layout.preferredHeight: 50
icon: "brightness_high"
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Appearance.m3colors.m3outlineVariant
radius: Metrics.radius(1)
}
GridLayout {
id: middleGrid
Layout.fillWidth: true
columns: 1
columnSpacing: Metrics.spacing(8)
rowSpacing: Metrics.spacing(8)
Layout.preferredWidth: parent.width
RowLayout {
NetworkToggle {
Layout.fillWidth: true
Layout.preferredHeight: 80
}
FlightModeToggle {
Layout.fillWidth: true
Layout.preferredHeight: 80
}
}
RowLayout {
BluetoothToggle {
Layout.preferredWidth: 220
Layout.preferredHeight: 80
}
ThemeToggle {
Layout.preferredHeight: 80
Layout.fillWidth: true
}
NightModeToggle {
Layout.preferredHeight: 80
Layout.fillWidth: true
}
}
}
ColumnLayout {
spacing: Metrics.margin("small")
Layout.fillWidth: true
Rectangle {
Layout.fillWidth: true
height: 1
color: Appearance.m3colors.m3outlineVariant
radius: Metrics.radius(1)
Layout.topMargin: Metrics.margin(5)
Layout.bottomMargin: Metrics.margin(5)
}
NotifModal {
Layout.preferredHeight: (Config.runtime.bar.position === "left" || Config.runtime.bar.position === "right") ? 480 : 470
}
}
}
}

View File

@@ -0,0 +1,91 @@
import qs.config
import qs.modules.components
import qs.services
import QtQuick
import Quickshell
import QtQuick.Layouts
StyledRect {
id: root
width: 200
height: 80
radius: Metrics.radius("verylarge")
color: Appearance.m3colors.m3surfaceContainerHigh
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
readonly property bool adapterPresent: Bluetooth.defaultAdapter !== null
readonly property bool enabled: Bluetooth.defaultAdapter?.enabled ?? false
readonly property var activeDevice: Bluetooth.activeDevice
readonly property string iconName: Bluetooth.icon
readonly property string statusText: {
if (!adapterPresent)
return "No adapter";
if (!enabled)
return "Disabled";
if (activeDevice)
return activeDevice.name;
return Bluetooth.defaultAdapter.discovering
? "Scanning…"
: "Enabled";
}
StyledRect {
id: iconBg
width: 50
height: 50
radius: Metrics.radius("verylarge")
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Metrics.margin("small")
color: {
if (!enabled)
return Appearance.m3colors.m3surfaceContainerHigh;
if (activeDevice)
return Appearance.m3colors.m3primaryContainer;
return Appearance.m3colors.m3secondaryContainer;
}
MaterialSymbol {
anchors.centerIn: parent
iconSize: Metrics.iconSize(35)
icon: iconName
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
anchors.left: iconBg.right
anchors.leftMargin: Metrics.margin("small")
spacing: Metrics.spacing(2)
StyledText {
text: "Bluetooth"
font.pixelSize: Metrics.fontSize("large")
elide: Text.ElideRight
width: root.width - iconBg.width - 30
}
StyledText {
text: statusText
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurfaceVariant
elide: Text.ElideRight
width: root.width - iconBg.width - 30
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (!adapterPresent)
return;
Bluetooth.defaultAdapter.enabled =
!Bluetooth.defaultAdapter.enabled;
}
}
}

View File

@@ -0,0 +1,28 @@
import qs.config
import qs.modules.components
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import QtQuick.Controls
StyledSlider {
id: brightnessSlider
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
property var monitor: Brightness.monitors.length > 0 ? Brightness.monitors[0] : null
value: monitor ? monitor.brightness : 0.5
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
property real level: brightnessSlider.value * 100
onMoved: if (monitor) {
monitor.setBrightness(value);
}
}

View File

@@ -0,0 +1,73 @@
import qs.config
import qs.modules.components
import qs.modules.functions
import qs.services
import QtQuick
import Quickshell
import Quickshell.Io
import QtQuick.Layouts
StyledRect {
id: root
width: 150
height: 50
radius: Metrics.radius("verylarge")
color: Appearance.m3colors.m3surfaceContainerHigh
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
property bool flightMode
property string flightModeText: flightMode ? "Enabled" : "Disabled"
Process {
id: toggleflightModeProc
running: false
command: []
function toggle() {
flightMode = !flightMode;
const cmd = flightMode ? "off" : "on";
toggleflightModeProc.command = ["bash", "-c", `nmcli radio all ${cmd}`];
toggleflightModeProc.running = true;
}
}
StyledRect {
id: iconBg
width: 50
height: 50
radius: Metrics.radius("large")
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Metrics.margin(10)
color: !flightMode ? Appearance.m3colors.m3surfaceContainerHigh : Appearance.m3colors.m3primaryContainer
MaterialSymbol {
anchors.centerIn: parent
iconSize: Metrics.iconSize(35)
icon: "flight"
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
anchors.left: iconBg.right
anchors.leftMargin: Metrics.margin(10)
StyledText {
text: "Flight Mode"
font.pixelSize: Metrics.fontSize(20)
}
StyledText {
text: flightModeText
font.pixelSize: Metrics.fontSize("small")
}
}
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onClicked: toggleflightModeProc.toggle()
}
}

View File

@@ -0,0 +1,196 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import qs.config
import qs.modules.functions
import qs.modules.interface.notifications
import qs.modules.components
import qs.services
StyledRect {
id: root
Layout.fillWidth: true
radius: Metrics.radius("normal")
color: Appearance.m3colors.m3surfaceContainer
ClippingRectangle {
color: Appearance.colors.colLayer1
radius: Metrics.radius("normal")
implicitHeight: 90
anchors.fill: parent
RowLayout {
anchors.fill: parent
spacing: Metrics.margin("small")
ClippingRectangle {
implicitWidth: 140
implicitHeight: 140
Layout.leftMargin: Metrics.margin("large")
radius: Metrics.radius("normal")
clip: true
color: Appearance.colors.colLayer2
Image {
anchors.fill: parent
source: Mpris.artUrl
fillMode: Image.PreserveAspectCrop
cache: true
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.rightMargin: Metrics.margin("small")
spacing: Metrics.spacing(2)
Text {
text: Mpris.albumTitle
elide: Text.ElideRight
Layout.maximumWidth: 190
font.family: Metrics.fontFamily("title")
font.pixelSize: Metrics.fontSize("hugeass")
font.bold: true
color: Appearance.colors.colOnLayer2
}
Text {
text: Mpris.albumArtist
elide: Text.ElideRight
Layout.maximumWidth: 160
font.family: Metrics.fontFamily("main")
font.pixelSize: Metrics.fontSize("normal")
color: Appearance.colors.colSubtext
}
RowLayout {
Layout.fillWidth: true
spacing: Metrics.spacing(12)
Process {
id: control
}
Button {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
onClicked: Quickshell.execDetached(["playerctl", "previous"])
background: Rectangle {
radius: Metrics.radius("large")
color: Appearance.colors.colLayer2
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
icon: "skip_previous"
font.pixelSize: Metrics.fontSize(24)
color: Appearance.colors.colOnLayer2
fill: 1
}
}
Button {
Layout.preferredWidth: 42
Layout.preferredHeight: 42
onClicked: Quickshell.execDetached(["playerctl", "play-pause"])
background: Rectangle {
radius: Metrics.radius("full")
color: Appearance.colors.colPrimary
}
contentItem: MaterialSymbol {
anchors.bottom: parent.bottom
anchors.top: parent.top
icon: "play_arrow"
font.pixelSize: Metrics.fontSize(36)
color: Appearance.colors.colOnPrimary
fill: 1
}
}
Button {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
onClicked: Quickshell.execDetached(["playerctl", "next"])
background: Rectangle {
radius: Metrics.radius("large")
color: Appearance.colors.colLayer2
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
icon: "skip_next"
font.pixelSize: Metrics.fontSize(24)
color: Appearance.colors.colOnLayer2
fill: 1
}
}
}
RowLayout {
Layout.topMargin: Metrics.margin(15)
Layout.fillWidth: true
spacing: Metrics.spacing(12)
Text {
text: Mpris.formatTime(Mpris.positionSec)
font.pixelSize: Metrics.fontSize("smallest")
color: Appearance.colors.colSubtext
}
Item {
Layout.fillWidth: true
implicitHeight: 20
Rectangle {
anchors.fill: parent
radius: Metrics.radius("full")
color: Appearance.colors.colLayer2
}
Rectangle {
width: parent.width * (Mpris.lengthSec > 0 ? Mpris.positionSec / Mpris.lengthSec : 0)
radius: Metrics.radius("full")
color: Appearance.colors.colPrimary
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
}
}
Text {
text: Mpris.formatTime(Mpris.lengthSec)
font.pixelSize: Metrics.fontSize("smallest")
color: Appearance.colors.colSubtext
}
}
}
}
}
}

View File

@@ -0,0 +1,78 @@
import qs.config
import qs.modules.components
import qs.modules.functions
import qs.services
import QtQuick
import Quickshell
import QtQuick.Layouts
StyledRect {
id: root
width: 150
height: 50
radius: Metrics.radius("verylarge")
color: Appearance.m3colors.m3surfaceContainerHigh
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
// Service bindings
readonly property bool wifiEnabled: Network.wifiEnabled
readonly property bool hasActive: Network.active !== null
readonly property string iconName: Network.icon
readonly property string titleText: Network.label
readonly property string statusText: Network.status
// Icon background
StyledRect {
id: iconBg
width: 50
height: 50
radius: Metrics.radius("large")
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Metrics.margin(10)
color: {
if (!wifiEnabled)
return Appearance.m3colors.m3surfaceContainerHigh;
if (hasActive)
return Appearance.m3colors.m3primaryContainer;
return Appearance.m3colors.m3secondaryContainer;
}
MaterialSymbol {
anchors.centerIn: parent
icon: iconName
iconSize: Metrics.iconSize(35)
}
}
// Labels
Column {
anchors.verticalCenter: parent.verticalCenter
anchors.left: iconBg.right
anchors.leftMargin: Metrics.margin(10)
spacing: Metrics.spacing(2)
StyledText {
text: titleText
font.pixelSize: Metrics.fontSize(20)
elide: Text.ElideRight
width: root.width - iconBg.width - 30
}
StyledText {
text: statusText
font.pixelSize: Metrics.fontSize("small")
color: Appearance.m3colors.m3onSurfaceVariant
elide: Text.ElideRight
width: root.width - iconBg.width - 30
}
}
// Interaction
MouseArea {
anchors.fill: parent
onClicked: Network.toggleWifi()
}
}

View File

@@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.config
import qs.modules.components
Rectangle {
id: root
property bool nightTime
width: 200
height: 80
radius: Metrics.radius("childish")
color: !nightTime ? Appearance.m3colors.m3surfaceContainer : Appearance.m3colors.m3paddingContainer
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.margins: 0
MaterialSymbol {
anchors.centerIn: parent
iconSize: Metrics.iconSize(35)
icon: "coffee"
}
MouseArea {
anchors.fill: parent
onClicked: {
nightTime = !nightTime;
nightTime ? Quickshell.execDetached(["gammastep", "-O", "4000"]) : Quickshell.execDetached(["killall", "gammastep"]);
}
}
}

View File

@@ -0,0 +1,110 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.modules.functions
import qs.modules.interface.notifications
import qs.services
import qs.config
import qs.modules.components
StyledRect {
id: root
Layout.fillWidth: true
radius: Metrics.radius("normal")
color: Appearance.colors.colLayer1
property bool dndActive: Config.runtime.notifications.doNotDisturb
function toggleDnd() {
Config.updateKey("notifications.doNotDisturb", !dndActive);
}
StyledButton {
id: clearButton
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.bottomMargin: Metrics.margin(10)
anchors.rightMargin: Metrics.margin(10)
icon: "clear_all"
text: "Clear"
implicitHeight: 40
implicitWidth: 100
secondary: true
onClicked: {
for (let i = 0; i < NotifServer.history.length; i++) {
let n = NotifServer.history[i];
if (n?.notification) n.notification.dismiss();
}
}
}
StyledButton {
id: silentButton
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.bottomMargin: Metrics.margin(10)
anchors.rightMargin: clearButton.implicitWidth + Metrics.margin(15)
text: "Silent"
icon: "do_not_disturb_on"
implicitHeight: 40
implicitWidth: 100
secondary: true
checkable: true
checked: Config.runtime.notifications.doNotDisturb
onClicked: {
toggleDnd()
}
}
StyledText {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.bottomMargin: Metrics.margin(15)
anchors.leftMargin: Metrics.margin(15)
text: NotifServer.history.length + " Notifications"
}
StyledText {
anchors.centerIn: parent
text: "No notifications"
visible: NotifServer.history.length < 1
font.pixelSize: Metrics.fontSize("huge")
}
ListView {
id: notifList
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: clearButton.top
anchors.margins: Metrics.margin(10)
clip: true
spacing: Metrics.spacing(8)
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar { }
model: Config.runtime.notifications.enabled
? NotifServer.history
: []
delegate: NotificationChild {
width: notifList.width
title: model.summary
body: model.body
image: model.image || model.appIcon
rawNotif: model
buttons: model.actions.map((action) => ({
"label": action.text,
"onClick": () => action.invoke()
}))
}
}
}

View File

@@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.config
import qs.modules.components
Rectangle {
id: root
readonly property bool isDark: Config.runtime.appearance.theme === "dark"
property string themestatusicon: isDark ? "dark_mode" : "light_mode"
width: 200
height: 80
radius: Metrics.radius("childish")
color: Appearance.m3colors.m3paddingContainer
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.margins: 0
MaterialSymbol {
anchors.centerIn: parent
iconSize: Metrics.iconSize(35)
icon: themestatusicon
}
MouseArea {
anchors.fill: parent
onClicked: {
Quickshell.execDetached(["nucleus", "ipc", "call", "global", "toggleTheme"]);
}
}
}

View File

@@ -0,0 +1,34 @@
import qs.config
import qs.modules.components
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import QtQuick.Controls
StyledSlider {
id: volSlider
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: sink ? sink.volume : 0
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
property real level: volSlider.value * 100
PwObjectTracker {
objects: [Pipewire.defaultAudioSink]
}
property var sink: Pipewire.defaultAudioSink?.audio
onMoved: {
if (sink) sink.volume = value
}
}