init: working version

This commit is contained in:
Kiana Sheibani 2025-10-07 19:43:46 -04:00
commit 7d8d7dacae
Signed by: toki
GPG key ID: 6CB106C25E86A9F7
109 changed files with 15066 additions and 0 deletions

137
modules/bar/Bar.qml Normal file
View file

@ -0,0 +1,137 @@
import QtQuick
import Quickshell
import qs.config
import qs.custom
import qs.services
import "modules"
import "popouts" as BarPopouts
Item {
id: root
required property PersistentProperties uiState
required property ShellScreen screen
required property BarPopouts.Wrapper popouts
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
implicitHeight: Config.bar.height
// Modules
NixOS {
id: nixos
objectName: "nixos"
anchors.left: parent.left
}
Workspaces {
id: workspaces
anchors.left: nixos.right
workspaces: root.uiState.workspaces
}
Window {
id: window
objectName: "window"
// Expand window title from center as much as possible
// without intersecting other modules
anchors.centerIn: parent
implicitWidth: 2 * Math.min(
(parent.width / 2) - workspaces.x - workspaces.width,
tray.x - (parent.width / 2)) - 40
}
Tray {
id: tray
objectName: "tray"
anchors.right: clock.left
anchors.rightMargin: 8
}
Clock {
id: clock
objectName: "clock"
anchors.right: statusIcons.left
anchors.rightMargin: 12
}
StatusIcons {
id: statusIcons
objectName: "statusIcons"
anchors.right: power.left
anchors.rightMargin: 6
}
Power {
id: power
uiState: root.uiState
anchors.right: parent.right
anchors.rightMargin: 6
}
// Popout Interactions
function checkPopout(x: real): void {
const ch = childAt(x, height / 2);
if (!ch) {
popouts.hasCurrent = false;
return;
}
const name = ch.objectName;
const left = ch.x;
const chWidth = ch.implicitWidth;
if (name === "nixos") {
popouts.currentName = "nixos";
popouts.currentCenter = 0;
popouts.hasCurrent = true;
} else if (name === "statusIcons") {
const layout = ch.children[0];
const icon = layout.childAt(mapToItem(layout, x, 0).x, layout.height / 2);
if (icon && icon.objectName) {
popouts.currentName = icon.objectName;
popouts.currentCenter = Qt.binding(() =>
icon.mapToItem(root, 0, icon.implicitWidth / 2).x);
popouts.hasCurrent = true;
} else if (icon && !icon.objectName) {
popouts.hasCurrent = false;
}
} else if (name === "tray") {
const index = Math.floor(((x - left) / chWidth) * tray.repeater.count);
const trayItem = tray.repeater.itemAt(index);
if (trayItem) {
popouts.currentName = `traymenu${index}`;
popouts.currentCenter = Qt.binding(() =>
trayItem.mapToItem(root, 0, trayItem.implicitWidth / 2).x);
popouts.hasCurrent = true;
}
} else if (name === "clock") {
popouts.currentName = "calendar";
popouts.currentCenter = ch.mapToItem(root, chWidth / 2, 0).x;
popouts.hasCurrent = true;
} else if (name === "window" && Hypr.activeToplevel) {
const inner = ch.childAt(mapToItem(ch, x, 0).x, height / 2)
if (inner) {
popouts.currentName = "activewindow";
popouts.currentCenter = ch.mapToItem(root, chWidth / 2, 0).x;
popouts.hasCurrent = true;
} else {
popouts.hasCurrent = false;
}
} else {
popouts.hasCurrent = false;
}
}
}

15
modules/bar/Container.qml Normal file
View file

@ -0,0 +1,15 @@
import QtQuick
import Quickshell
import qs.config
import qs.custom
CustomRect {
color: Config.colors.container
implicitWidth: Math.max(childrenRect.width, height)
implicitHeight: Config.bar.containerHeight
anchors.verticalCenter: parent.verticalCenter
radius: 1000
}

View file

@ -0,0 +1,35 @@
import QtQuick
import qs.services
import qs.config
import qs.custom
Row {
id: root
anchors.verticalCenter: parent.verticalCenter
property color color: Config.colors.turqoise
spacing: 4
MaterialIcon {
id: icon
text: "calendar_month"
font.pointSize: Config.font.size.normal + 1
color: root.color
anchors.verticalCenter: parent.verticalCenter
}
CustomText {
id: text
anchors.verticalCenter: parent.verticalCenter
text: Time.format("hh:mm")
font.pointSize: Config.font.size.smaller
font.family: Config.font.family.mono
color: root.color
}
}

View file

@ -0,0 +1,15 @@
import qs.config
import qs.custom
import QtQuick
CustomText {
anchors.verticalCenter: parent.verticalCenter
text: ""
width: implicitWidth + 32
horizontalAlignment: Text.AlignHCenter
font.family: Config.font.family.mono
font.pointSize: Config.font.size.normal
color: Config.colors.nixos
}

View file

@ -0,0 +1,29 @@
import Quickshell
import qs.config
import qs.custom
import qs.services
StateLayer {
required property PersistentProperties uiState
anchors.verticalCenter: parent.verticalCenter
implicitWidth: icon.implicitHeight + 10
implicitHeight: implicitWidth
function onClicked(): void {
uiState.session = !uiState.session;
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: 0.5
text: "power_settings_new"
color: Config.colors.error
font.bold: true
font.pointSize: Config.font.size.smaller
}
}

View file

@ -0,0 +1,166 @@
import Quickshell
import Quickshell.Services.UPower
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.config
import qs.custom
import qs.util
import qs.modules.bar
Container {
id: root
implicitWidth: layout.width + 20
RowLayout {
id: layout
anchors.centerIn: parent
spacing: 10
MaterialIcon {
id: network
objectName: "network"
Layout.alignment: Qt.AlignVCenter
text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off"
color: text !== "wifi_off" ? Config.colors.secondary : Config.colors.tertiary
animate: (from, to) => from === "wifi_off" || to === "wifi_off"
MouseArea {
anchors.fill: parent
onClicked: Network.toggleWifi()
}
}
/* MaterialIcon { */
/* id: bluetooth */
/* objectName: "bluetooth" */
/* anchors.verticalCenter: parent.verticalCenter */
/* animate: true */
/* text: Bluetooth.powered ? "bluetooth" : "bluetooth_disabled" */
/* } */
/* Row { */
/* id: devices */
/* objectName: "devices" */
/* anchors.verticalCenter: parent.verticalCenter */
/* Repeater { */
/* id: repeater */
/* model: ScriptModel { */
/* values: Bluetooth.devices.filter(d => d.connected) */
/* } */
/* MaterialIcon { */
/* required property Bluetooth.Device modelData */
/* animate: true */
/* text: Icons.getBluetoothIcon(modelData.icon) */
/* fill: 1 */
/* } */
/* } */
/* } */
MaterialIcon {
id: idleinhibit
objectName: "idleinhibit"
Layout.alignment: Qt.AlignVCenter
text: Idle.inhibit ? "visibility" : "visibility_off"
color: text === "visibility" ? Config.colors.secondary : Config.colors.tertiary
fill: Idle.inhibit ? 1 : 0
animate: true
MouseArea {
anchors.fill: parent
onClicked: Idle.inhibit = !Idle.inhibit
}
}
MaterialIcon {
id: battery
objectName: "battery"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: hasBattery ? -2 : 0
Layout.topMargin: hasBattery ? 0.5 : 2
readonly property bool hasBattery: UPower.displayDevice.isLaptopBattery
readonly property real percentage: UPower.displayDevice.percentage
readonly property bool charging: !UPower.onBattery && batteryText.text !== "100"
readonly property bool warning: UPower.onBattery && percentage < 0.15
text: {
if (!hasBattery) {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
if (PowerProfiles.profile === PowerProfile.Performance)
return "rocket_launch";
return "balance";
}
return `battery_android_full`;
}
fill: 1
font.pointSize: hasBattery ? 18 : Config.font.size.normal
grade: 50
font.weight: 100
color: !hasBattery ? Config.colors.secondary :
warning ? Config.colors.errorBg :
batteryText.text === "100" ? Config.colors.battery :
Color.mute(Config.colors.battery, 0.6, 1.5)
CustomRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: 9
anchors.bottomMargin: 9
anchors.leftMargin: 3
width: (battery.width - 7) * battery.percentage
radius: 2
visible: battery.hasBattery
color: battery.warning ? Config.colors.batteryWarning : Config.colors.battery
}
Row {
anchors.centerIn: parent
anchors.horizontalCenterOffset: battery.charging ? width / 20 : -width / 15
visible: battery.hasBattery
spacing: -1
CustomText {
id: batteryText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 0.5
text: Math.round(battery.percentage * 100)
color: battery.warning ? Config.colors.batteryWarning : Config.colors.bg
font.family: Config.font.family.mono
font.pointSize: 6
font.weight: 800
}
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
visible: battery.charging
text: "bolt"
fill: 1
color: Config.colors.bg
font.pointSize: 7
font.weight: 300
}
}
}
}
}

View file

@ -0,0 +1,92 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import qs.config
import qs.custom
Item {
id: root
clip: true
anchors.verticalCenter: parent.verticalCenter
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
// To avoid warnings about being visible with no size
visible: width > 0 && height > 0
readonly property Item repeater: repeater
Row {
id: layout
add: Transition {
Anim {
property: "scale"
from: 0
to: 1
easing.bezierCurve: Config.anim.curves.standardDecel
}
}
move: Transition {
Anim {
property: "scale"
to: 1
easing.bezierCurve: Config.anim.curves.standardDecel
}
Anim {
properties: "x,y"
easing.bezierCurve: Config.anim.curves.standard
}
}
Repeater {
id: repeater
model: SystemTray.items
MouseArea {
id: trayItem
required property SystemTrayItem modelData
implicitWidth: icon.implicitWidth + 10
implicitHeight: icon.implicitHeight
onClicked: modelData.activate()
IconImage {
id: icon
anchors.centerIn: parent
source: {
let icon = trayItem.modelData.icon;
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
icon = `file://${path}/${name.slice(name.lastIndexOf("/") + 1)}`;
}
return icon;
}
asynchronous: true
implicitSize: Config.font.size.larger
}
}
}
}
Behavior on implicitWidth {
Anim {
easing.bezierCurve: Config.anim.curves.emphasized
}
}
Behavior on implicitHeight {
Anim {
easing.bezierCurve: Config.anim.curves.emphasized
}
}
}

View file

@ -0,0 +1,96 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.config
import qs.custom
import qs.services
import qs.util
Item {
id: root
property color color: Config.colors.primary
implicitHeight: child.implicitHeight
Item {
id: child
anchors.centerIn: parent
implicitWidth: icon.implicitWidth + current.implicitWidth + current.anchors.leftMargin
implicitHeight: Math.max(icon.implicitHeight, current.implicitHeight)
clip: true
property Item current: text1
MaterialIcon {
id: icon
animate: true
text: {
const cls = Hypr.activeToplevel?.lastIpcObject.class;
if (!cls) return "desktop_windows";
Icons.getAppCategoryIcon(cls, "ad")
}
color: root.color
font.pointSize: Config.font.size.larger
anchors.verticalCenter: parent.verticalCenter
}
Title {
id: text1
}
Title {
id: text2
}
TextMetrics {
id: metrics
text: Hypr.activeToplevel?.title ?? qsTr("Desktop")
font.pointSize: Config.font.size.smaller
font.family: Config.font.family.mono
elide: Qt.ElideRight
elideWidth: root.width - icon.width
onTextChanged: {
const next = child.current === text1 ? text2 : text1;
next.text = elidedText;
child.current = next;
}
onElideWidthChanged: child.current.text = elidedText
}
Behavior on implicitWidth {
Anim {
easing.bezierCurve: Config.anim.curves.emphasized
}
}
Behavior on implicitHeight {
Anim {
easing.bezierCurve: Config.anim.curves.emphasized
}
}
}
component Title: CustomText {
id: text
anchors.verticalCenter: icon.verticalCenter
anchors.left: icon.right
anchors.leftMargin: 8
font.pointSize: metrics.font.pointSize
font.family: metrics.font.family
color: root.color
opacity: child.current === this ? 1 : 0
Behavior on opacity {
Anim {}
}
}
}

View file

@ -0,0 +1,234 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.config
import qs.custom
import qs.util
import qs.modules.bar
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
Container {
id: root
required property ListModel workspaces
readonly property HyprlandMonitor monitor: Hypr.monitorFor(QsWindow.window.screen)
// Workspace Layout
implicitWidth: Math.max(list.width + Config.bar.workspaceMargin * 2, height)
Behavior on implicitWidth {
Anim {
easing.bezierCurve: Config.anim.curves.emphasized
}
}
readonly property real workspaceSize: Config.bar.containerHeight - Config.bar.workspaceMargin * 2
Item {
id: listWrapper
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Config.bar.workspaceMargin
width: list.width
height: list.height
layer.enabled: true
layer.smooth: true
ListView {
id: list
anchors.centerIn: parent
width: contentWidth
height: root.workspaceSize
acceptedButtons: Qt.NoButton
boundsBehavior: Flickable.StopAtBounds
spacing: Config.bar.workspaceMargin
orientation: ListView.Horizontal
model: root.workspaces
delegate: Item {
id: buttonWrapper
required property int index
required property HyprlandWorkspace workspace
width: button.width
height: button.height
readonly property Item button: button
StateLayer {
id: button
anchors.horizontalCenter: buttonWrapper.horizontalCenter
anchors.verticalCenter: buttonWrapper.verticalCenter
width: height
height: root.workspaceSize
drag.target: this
drag.axis: Drag.XAxis
drag.threshold: 2
drag.minimumX: list.x
drag.maximumX: list.x + list.width - root.workspaceSize
states: State {
name: "dragging"
when: button.drag.active
ParentChange {
target: button
parent: listWrapper
}
AnchorChanges {
target: button
anchors.horizontalCenter: undefined
}
}
transitions: Transition {
from: "dragging"
to: ""
ParentAnimation {
AnchorAnimation {
duration: Config.anim.durations.small
easing.type: Easing.BezierSpline
easing.bezierCurve: Config.anim.curves.standardDecel
}
}
}
function onClicked(): void {
if (root.monitor.activeWorkspace !== workspace)
Hypr.dispatch(`workspace ${workspace.id}`);
}
onXChanged: {
if (!drag.active) return;
const wsx = x / (width + list.spacing);
root.workspaces.move(index, Math.round(wsx), 1);
}
MaterialIcon {
anchors.centerIn: parent
text: Icons.getWorkspaceIcon(buttonWrapper.workspace)
color: Config.colors.primary
font.pointSize: Config.font.size.larger
animate: true
animateDuration: Config.anim.durations.small
}
}
}
add: Transition {
Anim {
property: "opacity"
from: 0
to: 1
}
}
remove: Transition {
Anim {
property: "opacity"
from: 1
to: 0
}
}
move: Transition {
Anim {
property: "x"
}
Anim {
properties: "opacity"
to: 1
}
}
displaced: Transition {
Anim {
property: "x"
}
Anim {
properties: "opacity"
to: 1
}
}
}
}
// Active Indicator
CustomRect {
id: activeInd
readonly property Item active: {
list.count;
const activeWorkspace = root.monitor.activeWorkspace;
for (let i = 0; i < (root.workspaces?.count ?? 0); i++) {
if (root.workspaces.get(i).workspace === activeWorkspace)
return list.itemAtIndex(i);
}
return null;
}
x: active ? (active.button.drag.active ? active.button.x : active.x) + Config.bar.workspaceMargin : 0
y: Config.bar.workspaceMargin
width: active?.width ?? workspaceSize
height: active?.height ?? workspaceSize
radius: 1000
color: Config.colors.workspaces
clip: true
property bool transition: false
onActiveChanged: transition = true
Behavior on x {
enabled: activeInd.transition
SequentialAnimation {
Anim {
easing.bezierCurve: Config.anim.curves.emphasized
}
PropertyAction {
target: activeInd
property: "transition"
value: false
}
}
}
CustomRect {
id: base
visible: false
anchors.fill: parent
color: Config.colors.primaryDark
}
MultiEffect {
source: base
maskSource: listWrapper
maskEnabled: true
maskSpreadAtMin: 1
maskThresholdMin: 0.5
x: -parent.x + Config.bar.workspaceMargin
implicitWidth: listWrapper.width
implicitHeight: listWrapper.height
anchors.verticalCenter: parent.verticalCenter
}
}
}

View file

@ -0,0 +1,425 @@
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property PersistentProperties uiState
required property Item wrapper
required property HyprlandToplevel window
property HyprlandToplevel toplevel: Hypr.activeToplevel
property var screen: QsWindow.window.screen
property bool pinned: false
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
Component.onCompleted: {
if (window) {
state = "detail";
pinned = true;
toplevel = window;
}
wrapper.window = null;
}
// States
states: [
State {
name: "detail"
StateChangeScript {
script: root.wrapper.persistent = true
}
ParentChange {
target: header
parent: infobox
}
PropertyChanges {
infobox { visible: true }
preview { visible: true }
pin { visible: true }
visit { visible: true }
del { visible: true }
expand { rotation: -90 }
title { maximumLineCount: 1 }
}
}
]
transitions: Transition {
Anim {
targets: [infobox, preview]
property: "scale"
from: 0; to: 1
}
Anim {
target: buttons
property: "opacity"
from: 0; to: 1
duration: Config.anim.durations.large * 2
}
Anim {
targets: header
property: "scale"
to: 1
}
}
// Reveal on window title change
// (or close if window is invalid)
Anim on opacity {
id: reveal
from: 0
to: 1
}
onToplevelChanged: {
root.opacity = 0;
if (!toplevel) {
root.wrapper.hasCurrent = false;
} else if (!root.pinned) {
reveal.restart();
} else {
root.opacity = 1;
}
}
RowLayout {
id: layout
spacing: 15
RowLayout {
id: header
spacing: 12
Binding {
when: infobox.visible
header {
anchors.left: infobox.left
anchors.right: infobox.right
anchors.top: infobox.top
anchors.margins: 12
}
}
IconImage {
id: icon
Layout.alignment: Qt.AlignVCenter
implicitSize: 36
source: Icons.getAppIcon(toplevel?.lastIpcObject.class ?? "", "")
}
ColumnLayout {
id: names
spacing: 0
Layout.fillWidth: true
Layout.maximumWidth: 400
CustomText {
id: title
Layout.fillWidth: true
text: toplevel?.title ?? ""
color: Config.colors.secondary
elide: Text.ElideRight
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on text {
Anim {
target: names
property: "opacity"
from: 0
to: 1
}
}
}
CustomText {
Layout.fillWidth: true
text: toplevel?.lastIpcObject.class ?? ""
font.pointSize: Config.font.size.small
color: Config.colors.tertiary
elide: Text.ElideRight
Behavior on text {
Anim {
target: names
property: "opacity"
from: 0
to: 1
}
}
}
}
}
CustomRect {
id: infobox
visible: false
transformOrigin: Item.TopRight
implicitWidth: 300
implicitHeight: 240
color: Config.colors.container
radius: 17
ColumnLayout {
id: infolayout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 48
anchors.margins: 12
spacing: 3
CustomRect {
color: Config.colors.inactive
Layout.fillWidth: true
Layout.preferredHeight: 1
Layout.topMargin: 8
Layout.bottomMargin: 6
Layout.leftMargin: 5
Layout.rightMargin: 5
}
Detail {
icon: "workspaces"
text: {
for (let i = 0; i < root.uiState.workspaces.count; i++) {
if (root.uiState.workspaces.get(i).workspace === root.toplevel?.workspace)
return qsTr("Workspace: %1").arg(i + 1)
}
return qsTr("Workspace unknown")
}
}
Detail {
icon: "desktop_windows"
text: {
const mon = root.toplevel?.monitor;
mon ? qsTr("Monitor: %1 (%2)").arg(mon.name).arg(mon.id) : qsTr("Monitor: unknown")
}
}
Detail {
icon: "location_on"
text: qsTr("Address: %1").arg(`0x${root.toplevel?.address}` ?? "unknown")
}
Detail {
icon: "location_searching"
text: qsTr("Position: %1, %2").arg(root.toplevel?.lastIpcObject.at[0] ?? -1).arg(root.toplevel?.lastIpcObject.at[1] ?? -1)
}
Detail {
icon: "resize"
text: qsTr("Size: %1 %2").arg(root.toplevel?.lastIpcObject.size[0] ?? -1).arg(root.toplevel?.lastIpcObject.size[1] ?? -1)
color: Config.colors.tertiary
}
Detail {
icon: "account_tree"
text: qsTr("Process id: %1").arg(Number(root.toplevel?.lastIpcObject.pid ?? -1).toLocaleString(undefined, "f"))
color: Config.colors.primary
}
Detail {
icon: "gradient"
text: qsTr("Xwayland: %1").arg(root.toplevel?.lastIpcObject.xwayland ? "yes" : "no")
}
Detail {
icon: "picture_in_picture_center"
text: qsTr("Floating: %1").arg(root.toplevel?.lastIpcObject.floating ? "yes" : "no")
color: Config.colors.secondary
}
}
}
ClippingWrapperRectangle {
id: preview
visible: false
Layout.alignment: Qt.AlignVCenter
transformOrigin: Item.TopLeft
color: "transparent"
radius: 12
ScreencopyView {
captureSource: root.toplevel?.wayland ?? null
live: true
constraintSize.width: 375
constraintSize.height: 240
}
}
Item {
id: buttons
Layout.fillHeight: true
width: buttonTopLayout.width
property real buttonSize: 37
ColumnLayout {
id: buttonTopLayout
spacing: 2
anchors.top: parent.top
StateLayer {
id: expand
implicitWidth: buttons.buttonSize
implicitHeight: buttons.buttonSize
function onClicked(): void {
if (root.state === "")
root.state = "detail";
else
root.wrapper.hasCurrent = false;
}
MaterialIcon {
anchors.centerIn: parent
anchors.horizontalCenterOffset: font.pointSize * 0.05
text: "chevron_right"
font.pointSize: Config.font.size.large
}
}
StateLayer {
id: pin
visible: false
implicitWidth: buttons.buttonSize
implicitHeight: buttons.buttonSize
function onClicked(): void {
if (root.pinned) {
root.pinned = false;
root.toplevel = Qt.binding(() => Hypr.activeToplevel);
} else {
root.pinned = true;
root.toplevel = root.toplevel;
}
}
MaterialIcon {
anchors.centerIn: parent
text: "keep"
fill: root.pinned
font.pointSize: Config.font.size.large - 3
}
}
}
ColumnLayout {
id: buttonBottomLayout
anchors.bottom: parent.bottom
spacing: 2
StateLayer {
id: visit
visible: false
implicitWidth: buttons.buttonSize
implicitHeight: buttons.buttonSize
disabled: Hypr.activeToplevel === root.toplevel
function onClicked(): void {
if (root.toplevel)
Hypr.dispatch(`focuswindow address:0x${root.toplevel.address}`);
}
MaterialIcon {
anchors.centerIn: parent
text: "flip_to_front"
color: parent.disabled ? Config.colors.inactive : Config.colors.primary
font.pointSize: Config.font.size.large - 3
Behavior on color {
CAnim {}
}
}
}
StateLayer {
id: del
visible: false
Layout.bottomMargin: 8
color: Config.colors.error
implicitWidth: buttons.buttonSize
implicitHeight: buttons.buttonSize
function onClicked(): void {
if (root.toplevel)
Hypr.dispatch(`killwindow address:0x${root.toplevel.address}`);
root.wrapper.hasCurrent = false;
}
MaterialIcon {
anchors.centerIn: parent
text: "delete"
color: Config.colors.error
font.pointSize: Config.font.size.large - 3
}
}
}
}
}
component Detail: RowLayout {
id: detail
required property string icon
required property string text
property alias color: icon.color
Layout.fillWidth: true
spacing: 7
MaterialIcon {
id: icon
Layout.alignment: Qt.AlignVCenter
font.pointSize: Config.font.size.smaller
text: detail.icon
}
CustomText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: detail.text
elide: Text.ElideRight
font.family: Config.font.family.mono
font.pointSize: Config.font.size.smaller
}
}
}

View file

@ -0,0 +1,80 @@
import qs.config
import qs.custom
import qs.services
import QtQuick
import QtQuick.Shapes
Shape {
id: root
required property Item wrapper
readonly property bool invertLeftRounding: wrapper.x <= 2
readonly property bool invertRightRounding: wrapper.x + wrapper.width >= wrapper.parent.width - 2
readonly property real rounding: Config.border.rounding
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
property real ilr: invertLeftRounding ? -1 : 1
property real irr: invertRightRounding ? -1 : 1
property real sideRounding: wrapper.y > 0 ? -1 : 1
ShapePath {
startX: -root.rounding * root.sideRounding + (invertRightRounding ? 1 : 0)
startY: -1
strokeWidth: -1
fillColor: Config.colors.bg
PathArc {
relativeX: root.rounding * root.sideRounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: root.sideRounding < 0 ? PathArc.Counterclockwise : PathArc.Clockwise
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY - root.roundingY * root.ilr
}
PathArc {
relativeX: root.rounding
relativeY: root.roundingY * root.ilr
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: root.ilr < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY * root.irr
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: root.irr < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY - root.roundingY * root.irr)
}
PathArc {
relativeX: root.rounding * root.sideRounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: root.sideRounding < 0 ? PathArc.Counterclockwise : PathArc.Clockwise
}
}
Behavior on ilr {
Anim {}
}
Behavior on irr {
Anim {}
}
Behavior on sideRounding {
Anim {}
}
}

View file

@ -0,0 +1,351 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import Quickshell.Services.UPower
import QtQuick
import QtQuick.Shapes
import QtQuick.Layouts
ColumnLayout {
id: root
spacing: 4
readonly property color color: UPower.onBattery && UPower.displayDevice.percentage < 0.15 ?
Config.colors.batteryWarning :
Config.colors.battery
Loader {
Layout.alignment: Qt.AlignHCenter
active: UPower.displayDevice.isLaptopBattery
asynchronous: true
height: active ? (item?.implicitHeight ?? 0) : 0
sourceComponent: Item {
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: meter.width
implicitHeight: meter.height + estimate.height + 8
Shape {
id: meter
preferredRendererType: Shape.CurveRenderer
visible: false
readonly property real size: 96
readonly property real padding: 8
readonly property real thickness: 8
readonly property real angle: 280
ShapePath {
id: path
fillColor: "transparent"
strokeColor: Qt.alpha(root.color, 0.1)
strokeWidth: meter.thickness
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: detail.x + detail.width / 2
centerY: detail.y + detail.height / 2
radiusX: (meter.size + meter.thickness) / 2 + meter.padding
radiusY: radiusX
startAngle: -90 - meter.angle / 2
sweepAngle: meter.angle
}
Behavior on strokeColor {
CAnim {}
}
}
ShapePath {
fillColor: "transparent"
strokeColor: root.color
strokeWidth: meter.thickness
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: detail.x + detail.width / 2
centerY: detail.y + detail.height / 2
radiusX: (meter.size + meter.thickness) / 2 + meter.padding
radiusY: radiusX
startAngle: -90 - meter.angle / 2
sweepAngle: meter.angle * UPower.displayDevice.percentage
}
Behavior on strokeColor {
CAnim {}
}
}
}
Column {
id: detail
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: (meter.size + meter.thickness - height) / 2 + meter.padding
spacing: -6
// HACK: Prevent load order issues
Component.onCompleted: meter.visible = true;
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
text: Math.round(UPower.displayDevice.percentage * 100) + "%"
font.pointSize: Config.font.size.largest
}
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 10
text: UPowerDeviceState.toString(UPower.displayDevice.state)
animate: true
font.pointSize: Config.font.size.smaller
height: implicitHeight * 1.4
}
}
Column {
id: estimate
anchors.top: meter.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 3
spacing: -3
CustomText {
id: estimateTime
anchors.horizontalCenter: parent.horizontalCenter
text: UPower.onBattery ? Time.formatSeconds(UPower.displayDevice.timeToEmpty) || "--"
: Time.formatSeconds(UPower.displayDevice.timeToFull) || "--"
animate: (from, to) => from === "--" || to === "--"
font.family: Config.font.family.mono
font.pointSize: Config.font.size.normal
}
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
text: UPower.onBattery ? "remaining" : "to full"
animate: true
font.family: Config.font.family.mono
font.pointSize: Config.font.size.small
}
}
}
}
Loader {
Layout.alignment: Qt.AlignHCenter
active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None
asynchronous: true
height: active ? (item?.implicitHeight ?? 0) : 0
sourceComponent: CustomRect {
implicitWidth: child.implicitWidth + 20
implicitHeight: child.implicitHeight + 20
color: Config.colors.errorBg
border.color: Config.colors.error
radius: 12
Column {
id: child
anchors.centerIn: parent
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 7
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -font.pointSize / 10
text: "warning"
color: Config.colors.error
}
CustomText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Performance Degraded")
color: Config.colors.error
font.family: Config.font.family.mono
font.weight: 500
}
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -font.pointSize / 10
text: "warning"
color: Config.colors.error
}
}
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Reason: %1").arg(PerformanceDegradationReason.toString(PowerProfiles.degradationReason))
color: Config.colors.secondary
}
}
}
}
CustomRect {
id: profiles
Layout.topMargin: 4
property string current: {
const p = PowerProfiles.profile;
if (p === PowerProfile.PowerSaver)
return saver.icon;
if (p === PowerProfile.Performance)
return perf.icon;
return balance.icon;
}
Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: 10
Layout.rightMargin: 10
implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + 60
implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + 8
color: Config.colors.container
radius: 1000
CustomRect {
id: indicator
color: root.color
radius: 1000
state: profiles.current
states: [
State {
name: saver.icon
Fill {
item: saver
}
},
State {
name: balance.icon
Fill {
item: balance
}
},
State {
name: perf.icon
Fill {
item: perf
}
}
]
transitions: Transition {
AnchorAnimation {
duration: Config.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Config.anim.curves.emphasized
}
}
}
Profile {
id: saver
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 4
profile: PowerProfile.PowerSaver
icon: "energy_savings_leaf"
}
Profile {
id: balance
anchors.centerIn: parent
profile: PowerProfile.Balanced
icon: "balance"
}
Profile {
id: perf
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 4
profile: PowerProfile.Performance
icon: "rocket_launch"
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -2
text: "Performance: " + PowerProfile.toString(PowerProfiles.profile)
animate: true
color: Config.colors.secondary
font.pointSize: Config.font.size.small
font.weight: 500
}
component Fill: AnchorChanges {
required property Item item
target: indicator
anchors.left: item.left
anchors.right: item.right
anchors.top: item.top
anchors.bottom: item.bottom
}
component Profile: StateLayer {
required property string icon
required property int profile
implicitWidth: icon.implicitHeight + 5
implicitHeight: icon.implicitHeight + 5
function onClicked(): void {
PowerProfiles.profile = profile;
}
MaterialIcon {
id: icon
anchors.centerIn: parent
text: parent.icon
font.pointSize: Config.font.size.larger
color: profiles.current === text ? Config.colors.primaryDark : Config.colors.primary
fill: profiles.current === text ? 1 : 0
Behavior on fill {
Anim {}
}
}
}
}

View file

@ -0,0 +1,289 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ColumnLayout {
id: root
spacing: 4
property date currentDate: new Date()
property int currentYear: currentDate.getFullYear()
property int currentMonth: currentDate.getMonth()
RowLayout {
Layout.alignment: Qt.AlignHCenter
MaterialIcon {
Layout.bottomMargin: 1
text: "calendar_month"
color: Config.colors.primary
font.pointSize: Config.font.size.large
}
CustomText {
text: Time.format("hh:mm:ss")
color: Config.colors.primary
font.weight: 600
font.pointSize: Config.font.size.large
font.family: Config.font.family.mono
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 4
spacing: 0
CustomText {
text: Time.format("dddd, MMMM d")
font.weight: 600
font.pointSize: Config.font.size.normal
}
CustomText {
text: Time.format("yyyy-MM-dd")
color: Config.colors.tertiary
font.pointSize: Config.font.size.smaller
}
}
}
// Calendar grid
GridLayout {
id: calendarGrid
Layout.fillWidth: true
Layout.margins: 10
rowSpacing: 7
columnSpacing: 2
// Month navigation
RowLayout {
Layout.fillWidth: true
Layout.bottomMargin: 7
Layout.columnSpan: 2
CustomRect {
implicitWidth: implicitHeight
implicitHeight: prevIcon.implicitHeight + 8
radius: 1000
color: Config.colors.container
StateLayer {
anchors.fill: parent
function onClicked(): void {
if (root.currentMonth !== 0) {
root.currentMonth = root.currentMonth - 1;
} else {
root.currentMonth = 11;
root.currentYear = root.currentYear - 1;
}
}
}
MaterialIcon {
id: prevIcon
anchors.centerIn: parent
text: "chevron_left"
color: Config.colors.secondary
}
}
CustomText {
Layout.fillWidth: true
readonly property list<string> monthNames:
Array.from({ length: 12 }, (_, i) => Qt.locale().monthName(i, Qt.locale().LongFormat))
text: monthNames[root.currentMonth] + " " + root.currentYear
horizontalAlignment: Text.AlignHCenter
font.weight: 600
font.pointSize: Config.font.size.normal
}
CustomRect {
implicitWidth: implicitHeight
implicitHeight: nextIcon.implicitHeight + 8
radius: 1000
color: Config.colors.container
StateLayer {
anchors.fill: parent
function onClicked(): void {
if (root.currentMonth !== 11) {
root.currentMonth = root.currentMonth + 1;
} else {
root.currentMonth = 0;
root.currentYear = root.currentYear + 1;
}
}
}
MaterialIcon {
id: nextIcon
anchors.centerIn: parent
text: "chevron_right"
color: Config.colors.primary
}
}
}
// Day headers
DayOfWeekRow {
Layout.row: 1
Layout.column: 1
Layout.fillWidth: true
Layout.preferredHeight: Config.font.size.largest
delegate: CustomText {
required property var model
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: model.shortName
color: Config.colors.tertiary
font.pointSize: Config.font.size.small
font.weight: 500
}
}
CustomText {
Layout.row: 1
Layout.leftMargin: -2
text: "Week"
color: Config.colors.tertiary
font.pointSize: Config.font.size.small
font.weight: 500
font.italic: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
// ISO week markers
WeekNumberColumn {
Layout.row: 2
Layout.fillHeight: true
Layout.rightMargin: 15
height: 240
month: root.currentMonth
year: root.currentYear
delegate: CustomText {
required property var model
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: model.weekNumber
color: Config.colors.tertiary
font.pointSize: Config.font.size.small
font.weight: 600
font.italic: true
}
}
// Calendar days grid
MonthGrid {
Layout.row: 2
Layout.column: 1
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
Layout.margins: 6
month: root.currentMonth
year: root.currentYear
spacing: 20
delegate: Item {
id: dayItem
required property var model
implicitWidth: implicitHeight
implicitHeight: dayText.implicitHeight + 10
CustomRect {
anchors.centerIn: parent
implicitWidth: parent.implicitHeight
implicitHeight: parent.implicitHeight
radius: 1000
color: dayItem.model.today ? Config.colors.calendar : "transparent"
StateLayer {
anchors.fill: parent
visible: dayItem.model.month === root.currentMonth
function onClicked(): void {}
}
CustomText {
id: dayText
anchors.centerIn: parent
anchors.verticalCenterOffset: 0.5
horizontalAlignment: Text.AlignHCenter
text: Qt.formatDate(dayItem.model.date, "d")
color: dayItem.model.today ? Config.colors.primaryDark :
dayItem.model.month === root.currentMonth ? Config.colors.primary : Config.colors.inactive
font.pointSize: Config.font.size.small
font.weight: dayItem.model.today ? 600 : 400
}
}
}
}
}
// Today button
CustomRect {
Layout.fillWidth: true
implicitHeight: todayBtn.implicitHeight + 10
radius: 25
color: Config.colors.calendar
StateLayer {
anchors.fill: parent
color: Config.colors.secondary
function onClicked(): void {
const today = new Date();
root.currentYear = today.getFullYear();
root.currentMonth = today.getMonth();
}
}
Row {
id: todayBtn
anchors.centerIn: parent
spacing: 7
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
text: "today"
color: Config.colors.primaryDark
font.pointSize: Config.font.size.normal
}
CustomText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Today")
color: Config.colors.primaryDark
}
}
}
}

View file

@ -0,0 +1,157 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.SystemTray
import Quickshell.Services.UPower
import QtQuick
Item {
id: root
required property PersistentProperties uiState
required property Item wrapper
required property HyprlandToplevel window
anchors.centerIn: parent
implicitWidth: (content.children.find(c => c.shouldBeActive)?.implicitWidth ?? 0) + 30
implicitHeight: (content.children.find(c => c.shouldBeActive)?.implicitHeight ?? 0) + 20
readonly property color color: content.children.find(c => c.active)?.color ?? "transparent"
clip: true
Item {
id: content
anchors.fill: parent
anchors.margins: 15
Popout {
name: "nixos"
source: "NixOS.qml"
color: Config.colors.nixos
}
Popout {
name: "activewindow"
sourceComponent: ActiveWindow {
uiState: root.uiState
wrapper: root.wrapper
window: root.window
}
color: Config.colors.activeWindow
}
Popout {
name: "calendar"
source: "Calendar.qml"
color: Config.colors.calendar
}
Popout {
name: "network"
source: "Network.qml"
color: Config.colors.network
}
Popout {
name: "idleinhibit"
source: "IdleInhibit.qml"
color: Config.colors.idle
}
Popout {
name: "battery"
source: "Battery.qml"
color: UPower.displayDevice.isLaptopBattery &&
UPower.onBattery && UPower.displayDevice.percentage < 0.15 ?
Config.colors.batteryWarning :
Config.colors.battery
}
Repeater {
model: SystemTray.items
Popout {
id: trayMenu
required property SystemTrayItem modelData
required property int index
anchors.verticalCenterOffset: -5
name: `traymenu${index}`
sourceComponent: trayMenuComp
color: Qt.tint(Config.colors.brown, Qt.alpha(Config.colors.yellow, Math.min(index / 8, 1)))
Connections {
target: root.wrapper
function onHasCurrentChanged(): void {
if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) {
trayMenu.sourceComponent = null;
trayMenu.sourceComponent = trayMenuComp;
}
}
}
Component {
id: trayMenuComp
TrayMenu {
popouts: root.wrapper
trayItem: trayMenu.modelData.menu
}
}
}
}
}
component Popout: Loader {
id: popout
required property string name
property bool shouldBeActive: root.wrapper.currentName === name
property color color
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -3
anchors.right: parent.right
opacity: 0
scale: 0.8
active: false
asynchronous: true
states: State {
name: "active"
when: popout.shouldBeActive
PropertyChanges {
popout.active: true
popout.opacity: 1
popout.scale: 1
}
}
transitions: [
Transition {
from: ""
to: "active"
SequentialAnimation {
PropertyAction {
target: popout
property: "active"
}
Anim {
properties: "opacity,scale"
}
}
}
]
}
}

View file

@ -0,0 +1,48 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
spacing: 15
Toggle {
label.text: qsTr("Idle Inhibitor")
label.font.weight: 500
checked: Idle.inhibit
toggle.onToggled: Idle.inhibit = !Idle.inhibit
}
Toggle {
label.text: qsTr("Inhibit While Playing Audio")
checked: Idle.inhibitPipewire
toggle.onToggled: Idle.toggleInhibitPipewire()
}
component Toggle: RowLayout {
property alias checked: toggle.checked
property alias label: label
property alias toggle: toggle
Layout.fillWidth: true
Layout.rightMargin: 5
spacing: 15
CustomText {
id: label
Layout.fillWidth: true
}
CustomSwitch {
id: toggle
accent: Color.mute(Config.colors.idle, 1.1)
}
}
}

View file

@ -0,0 +1,259 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
property string connectingToSsid: ""
spacing: 5
width: 320
Toggle {
label.text: qsTr("Wifi %1".arg(Network.wifiEnabled ? "Enabled" : "Disabled"))
label.font.weight: 500
checked: Network.wifiEnabled
toggle.onToggled: Network.enableWifi(checked)
}
CustomText {
Layout.topMargin: 7
text: qsTr("%1 networks available").arg(Network.networks.length)
color: Config.colors.primary
font.pointSize: Config.font.size.small
}
CustomListView {
id: list
Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, 240)
spacing: 5
clip: true
CustomScrollBar.vertical: CustomScrollBar {
flickable: list
}
model: ScriptModel {
values: [...Network.networks].sort((a, b) => {
if (a.active !== b.active)
return b.active - a.active;
return b.strength - a.strength;
})
}
delegate: RowLayout {
id: networkItem
required property Network.AccessPoint modelData
readonly property bool isConnecting: root.connectingToSsid === modelData.ssid
readonly property bool loading: networkItem.isConnecting
readonly property color iconColor: networkItem.modelData.active ? Config.colors.primary : Config.colors.inactive
width: list.width - 8
spacing: 5
MaterialIcon {
text: Icons.getNetworkIcon(networkItem.modelData.strength)
color: iconColor
MouseArea {
width: networkItem.width
height: networkItem.height
onClicked: {
if (Network.wifiEnabled && !networkItem.loading) {
if (networkItem.modelData.active) {
Network.disconnectFromNetwork();
} else {
root.connectingToSsid = networkItem.modelData.ssid;
Network.connectToNetwork(networkItem.modelData.ssid, "");
}
}
}
}
}
MaterialIcon {
opacity: networkItem.modelData.isSecure ? 1 : 0
text: "lock"
font.pointSize: Config.font.size.smaller
color: iconColor
}
CustomText {
Layout.fillWidth: true
Layout.rightMargin: 10
text: networkItem.modelData.ssid
elide: Text.ElideRight
font.weight: networkItem.modelData.active ? 500 : 400
color: networkItem.modelData.active ? Config.colors.secondary : Config.colors.tertiary
}
CustomBusyIndicator {
implicitWidth: implicitHeight
implicitHeight: Config.font.size.normal
running: opacity > 0
opacity: networkItem.loading ? 1 : 0
Behavior on opacity {
Anim {}
}
}
}
add: Transition {
Anim {
property: "opacity"
from: 0
to: 1
}
Anim {
property: "scale"
from: 0.7
to: 1
}
}
remove: Transition {
Anim {
property: "opacity"
from: 1
to: 0
}
Anim {
property: "scale"
from: 1
to: 0.7
}
}
addDisplaced: Transition {
Anim {
property: "y"
duration: Config.anim.durations.small
}
Anim {
properties: "opacity,scale"
to: 1
}
}
displaced: Transition {
Anim {
property: "y"
}
Anim {
properties: "opacity,scale"
to: 1
}
}
}
// Rescan button
CustomRect {
Layout.topMargin: 8
Layout.fillWidth: true
implicitHeight: rescanBtn.implicitHeight + 10
radius: 17
color: !Network.wifiEnabled ? Config.colors.inactive : Qt.alpha(Config.colors.network, Network.scanning ? 0.8 : 1)
Behavior on color {
CAnim { duration: Config.anim.durations.small }
}
StateLayer {
id: layer
anchors.fill: parent
color: Config.colors.primaryDark
disabled: Network.scanning || !Network.wifiEnabled
function onClicked(): void {
Network.rescanWifi();
}
}
Row {
id: rescanBtn
anchors.centerIn: parent
spacing: 7
property color color: layer.disabled ? Config.colors.bg : Config.colors.primaryDark
Behavior on color {
CAnim { duration: Config.anim.durations.small }
}
MaterialIcon {
id: scanIcon
anchors.verticalCenter: parent.verticalCenter
animate: true
text: Network.scanning ? "refresh" : "wifi_find"
color: parent.color
RotationAnimation on rotation {
running: Network.scanning
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
}
}
CustomText {
anchors.verticalCenter: parent.verticalCenter
text: Network.scanning ? qsTr("Scanning...") : qsTr("Rescan networks")
color: parent.color
}
}
}
// Reset connecting state when network changes
Connections {
target: Network
function onActiveChanged(): void {
if (Network.active && root.connectingToSsid === Network.active.ssid) {
root.connectingToSsid = "";
}
}
function onScanningChanged(): void {
if (!Network.scanning)
scanIcon.rotation = 0;
}
}
component Toggle: RowLayout {
property alias checked: toggle.checked
property alias label: label
property alias toggle: toggle
Layout.fillWidth: true
Layout.rightMargin: 5
spacing: 15
CustomText {
id: label
Layout.fillWidth: true
}
CustomSwitch {
id: toggle
accent: Color.mute(Config.colors.network)
}
}
}

View file

@ -0,0 +1,161 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
spacing: 7
width: 340
function nixosVersionShort(version: string): string {
const parts = version.split('.');
return `${parts[0]}.${parts[1]}`;
}
Row {
spacing: 12
Image {
anchors.verticalCenter: parent.verticalCenter
readonly property real size: 72
source: "root:/assets/nixos-logo.svg"
width: size
height: size
sourceSize.width: size
sourceSize.height: size
}
CustomText {
anchors.verticalCenter: parent.verticalCenter
text: "NixOS"
color: Config.colors.secondary
font.pointSize: Config.font.size.largest * 1.3
}
Column {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -2
CustomText {
text: "v" + root.nixosVersionShort(NixOS.currentGen?.nixosVersion)
font.pointSize: Config.font.size.larger
}
CustomText {
text: "Nix " + NixOS.nixVersion
}
}
}
CustomRect {
Layout.topMargin: 5
Layout.fillWidth: true
height: 180
radius: 17
color: Config.colors.container
CustomText {
id: genText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 7
text: "Generations"
color: Config.colors.secondary
font.pointSize: Config.font.size.normal
font.weight: 500
}
CustomListView {
id: list
anchors.top: genText.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 6
anchors.topMargin: 8
spacing: 6
clip: true
model: ScriptModel {
values: [...NixOS.generations]
objectProp: "id"
}
CustomScrollBar.vertical: CustomScrollBar {
flickable: list
}
delegate: CustomRect {
required property NixOS.Generation modelData
width: list.width
height: 42
radius: 12
color: modelData.current ?
Qt.tint(Config.colors.container, Qt.alpha(Config.colors.nixos, 0.2)) :
Config.colors.containerAlt
Item {
anchors.fill: parent
anchors.margins: 7
anchors.topMargin: 2
anchors.bottomMargin: 1
CustomText {
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: 2
text: `Generation ${modelData.id}`
color: modelData.current ? Config.colors.nixos : Config.colors.secondary
}
CustomText {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 6
text: modelData.revision !== "Unknown" ? modelData.revision : ""
elide: Text.ElideRight
font.family: Config.font.family.mono
color: modelData.current ? Config.colors.secondary : Config.colors.primary
}
CustomText {
anchors.top: parent.top
anchors.right: parent.right
text: `NixOS ${root.nixosVersionShort(modelData.nixosVersion)} 🞄 Linux ${modelData.kernelVersion}`
color: modelData.current ? Config.colors.secondary : Config.colors.primary
}
CustomText {
anchors.bottom: parent.bottom
anchors.right: parent.right
text: Qt.formatDateTime(modelData.date, "yyyy-MM-dd ddd hh:mm")
font.family: Config.font.family.mono
color: modelData.current ? Config.colors.secondary : Config.colors.primary
}
}
}
}
}
}

View file

@ -0,0 +1,231 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
import QtQuick
import QtQuick.Controls
StackView {
id: root
required property Item popouts
required property QsMenuHandle trayItem
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
initialItem: SubMenu {
handle: root.trayItem
}
pushEnter: NoAnim {}
pushExit: NoAnim {}
popEnter: NoAnim {}
popExit: NoAnim {}
component NoAnim: Transition {
NumberAnimation {
duration: 0
}
}
component SubMenu: Column {
id: menu
required property QsMenuHandle handle
property bool isSubMenu
property bool shown
spacing: 7
opacity: shown ? 1 : 0
scale: shown ? 1 : 0.8
Component.onCompleted: shown = true
StackView.onActivating: shown = true
StackView.onDeactivating: shown = false
StackView.onRemoved: destroy()
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
QsMenuOpener {
id: menuOpener
menu: menu.handle
}
Repeater {
model: menuOpener.children
CustomRect {
id: item
required property QsMenuEntry modelData
implicitWidth: 200
implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight
radius: 100
color: modelData.isSeparator ? Config.colors.inactive : "transparent"
Loader {
id: children
anchors.left: parent.left
anchors.right: parent.right
active: !item.modelData.isSeparator
asynchronous: true
sourceComponent: Item {
implicitHeight: label.implicitHeight
StateLayer {
anchors.fill: parent
anchors.margins: -2
anchors.leftMargin: -7
anchors.rightMargin: -7
radius: item.radius
disabled: !item.modelData.enabled
function onClicked(): void {
const entry = item.modelData;
if (entry.hasChildren)
root.push(subMenuComp.createObject(null, {
handle: entry,
isSubMenu: true
}));
else {
item.modelData.triggered();
root.popouts.hasCurrent = false;
}
}
}
Loader {
id: icon
anchors.left: parent.left
active: item.modelData.icon !== ""
asynchronous: true
sourceComponent: IconImage {
implicitSize: label.implicitHeight
source: item.modelData.icon
}
}
CustomText {
id: label
anchors.left: icon.right
anchors.leftMargin: icon.active ? 10 : 0
text: labelMetrics.elidedText
color: item.modelData.enabled ? Config.colors.primary : Config.colors.tertiary
}
TextMetrics {
id: labelMetrics
text: item.modelData.text
font.pointSize: label.font.pointSize
font.family: label.font.family
elide: Text.ElideRight
elideWidth: 200 - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + 12 : 0)
}
Loader {
id: expand
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
active: item.modelData.hasChildren
asynchronous: true
sourceComponent: MaterialIcon {
text: "chevron_right"
color: item.modelData.enabled ? Config.colors.primary : Config.colors.tertiary
}
}
}
}
}
}
Loader {
active: menu.isSubMenu
asynchronous: true
sourceComponent: Item {
implicitWidth: back.implicitWidth
implicitHeight: back.implicitHeight + Appearance.spacing.small / 2
Item {
anchors.bottom: parent.bottom
implicitWidth: back.implicitWidth
implicitHeight: back.implicitHeight
CustomRect {
anchors.fill: parent
anchors.margins: -7
anchors.leftMargin: -7
anchors.rightMargin: -14
radius: 1000
color: Config.colors.container
StateLayer {
anchors.fill: parent
radius: parent.radius
color: Config.colors.primary
function onClicked(): void {
root.pop();
}
}
}
Row {
id: back
anchors.verticalCenter: parent.verticalCenter
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
text: "chevron_left"
color: Config.colors.primary
}
CustomText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Back")
color: Config.colors.primary
}
}
}
}
}
}
Component {
id: subMenuComp
SubMenu {}
}
}

View file

@ -0,0 +1,156 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import QtQuick
import QtQuick.Effects
Item {
id: root
required property PersistentProperties uiState
required property ShellScreen screen
readonly property real nonAnimWidth: content.implicitWidth
readonly property real nonAnimHeight: y > 0 || hasCurrent ? content.implicitHeight : 0
property string currentName
property real currentCenter
property bool hasCurrent
property real currentCenterBounded: Math.min(Math.max(currentCenter, nonAnimWidth / 2),
parent.width - nonAnimWidth / 2)
x: currentCenterBounded - implicitWidth / 2
property HyprlandToplevel window
property bool persistent
visible: width > 0 && height > 0
implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
Background {
id: background
visible: false
wrapper: root
}
MultiEffect {
anchors.fill: background
source: background
shadowEnabled: true
blurMultiplier: 0.3
blurMax: 30
shadowColor: content.active ? content.item.color : "transparent"
Behavior on shadowColor {
CAnim {}
}
}
Item {
anchors.fill: parent
clip: true
Comp {
id: content
shouldBeActive: root.hasCurrent
asynchronous: true
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: Content {
id: content
wrapper: root
uiState: root.uiState
window: root.window
}
}
}
Behavior on currentCenterBounded {
enabled: root.implicitHeight > 0
Anim {
easing.bezierCurve: Config.anim.curves.emphasized
}
}
Behavior on implicitWidth {
enabled: root.implicitHeight > 0
Anim {
easing.bezierCurve: Config.anim.curves.emphasized
}
}
Behavior on implicitHeight {
Anim {
easing.bezierCurve: Config.anim.curves.emphasized
}
}
component Comp: Loader {
id: comp
property bool shouldBeActive
asynchronous: true
active: false
opacity: 0
states: State {
name: "active"
when: comp.shouldBeActive
PropertyChanges {
comp.opacity: 1
comp.active: true
}
}
transitions: [
Transition {
from: ""
to: "active"
SequentialAnimation {
PropertyAction {
property: "active"
}
Anim {
property: "opacity"
easing.bezierCurve: Config.anim.curves.standard
}
}
},
Transition {
from: "active"
to: ""
SequentialAnimation {
Anim {
property: "opacity"
easing.bezierCurve: Config.anim.curves.standard
}
PropertyAction {
property: "active"
}
PropertyAction {
target: root
property: "persistent"
value: false
}
}
}
]
}
}