Merge pull request #1510 from caelestia-dots/feat/nexus

Nexus v2
This commit is contained in:
2 * r + 2 * t 2026-06-07 22:25:24 +10:00 committed by GitHub
commit dd3402eb14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
83 changed files with 5746 additions and 93 deletions

View file

@ -245,7 +245,7 @@ For example, to disable the bar on DP-1:
> - `utilities` (`toasts`, `vpn`)
> - `services` (`weatherLocation`, `useFahrenheit`, `useFahrenheitPerformance`, `useTwelveHourClock`,
> `gpuType`, `visualiserBars`, `audioIncrement`, `brightnessIncrement`, `maxVolume`, `smartScheme`,
> `defaultPlayer`, `playerAliases`, `showLyrics`, `lyricsBackend`)
> `defaultPlayer`, `playerAliases`, `lyricsBackend`)
> - `paths` (`wallpaperDir`, `lyricsDir`)
>
> </details>

BIN
assets/wallpaper.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

42
components/AnimLoader.qml Normal file
View file

@ -0,0 +1,42 @@
import QtQuick
Loader {
id: root
property Component sourceComp
property bool isComplete
property int outAnimType: Anim.FastEffects
property int inAnimType: Anim.DefaultEffects
asynchronous: true
Component.onCompleted: {
isComplete = true;
sourceComponent = sourceComp;
}
onSourceCompChanged: {
if (isComplete)
anim.restart();
}
SequentialAnimation {
id: anim
running: false
Anim {
target: root
property: "opacity"
to: 0
type: root.outAnimType
}
ScriptAction {
script: root.sourceComponent = root.sourceComp
}
Anim {
target: root
property: "opacity"
to: 1
type: root.inAnimType
}
}
}

View file

@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Shapes
import Caelestia
import Caelestia.Config
import qs.services
@ -8,10 +9,11 @@ MouseArea {
property bool disabled
property bool showHoverBackground: true
property bool manualPressOverride
readonly property alias rect: base
property bool shapeMorph
property real stateOpacity: pressed ? 0.1 : containsMouse ? 0.08 : 0
property real stateOpacity: containsMouse ? 0.08 : 0
property real pressX: width / 2
property real pressY: height / 2
@ -29,7 +31,7 @@ MouseArea {
const d2 = distSq(width, 0);
const d3 = distSq(0, height);
const d4 = distSq(width, height);
return Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0);
return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3;
}
property real endRadiusAtPress
@ -59,12 +61,17 @@ MouseArea {
onPressed: e => press(e.x, e.y)
onPressedChanged: {
if (!pressed && !rippleAnim.running && circle.opacity > 0)
if (!(pressed || manualPressOverride) && !rippleAnim.running && circle.opacity > 0)
fadeAnim.start();
}
onManualPressOverrideChanged: {
if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running)
fadeAnim.start();
}
onCircleRadiusChanged: {
if (!pressed && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running)
if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running)
fadeAnim.start();
}
@ -75,7 +82,7 @@ MouseArea {
target: root
property: "circleRadius"
to: root.endRadius
easing: Tokens.anim.expressiveSlowEffects
easing: Tokens.anim.standard
duration: Tokens.anim.durations.expressiveSlowEffects * 2
}
@ -94,8 +101,14 @@ MouseArea {
anchors.fill: parent
opacity: root.stateOpacity
color: Colours.palette.m3onSurface
// Pick up radius from parent if it has one (parent can be anything with a radius property)
radius: root.parent?.radius ?? 0 // qmllint disable missing-property
// Pick up radius from parent if it has one (parent can be anything with radius props)
// qmllint disable missing-property
radius: root.parent?.radius ?? 0
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
// qmllint enable missing-property
}
Shape {
@ -121,12 +134,12 @@ MouseArea {
color: Qt.alpha(base.color, 1)
}
GradientStop {
position: 0.99
position: CUtils.clamp(1 - 0.2 * root.endRadius / root.circleRadius, 0.01, 0.99)
color: Qt.alpha(base.color, 1)
}
GradientStop {
position: 1
color: Qt.alpha(base.color, 0)
color: Qt.alpha(base.color, CUtils.clamp((root.circleRadius / root.endRadius - 0.9) / 0.1, 0, 1))
}
}

View file

@ -4,11 +4,28 @@ import qs.components
Flickable {
id: root
property bool doneFakeFlick
maximumFlickVelocity: 3000
rebound: Transition {
onRunningChanged: {
if (!running && !root.doneFakeFlick) {
root.doneFakeFlick = true;
root.flick(1, 1);
root.flick(-1, -1);
Qt.callLater(() => root.cancelFlick());
}
}
Anim {
properties: "x,y"
}
}
Timer {
running: root.doneFakeFlick
interval: 10
onTriggered: root.doneFakeFlick = false
}
}

View file

@ -4,11 +4,28 @@ import qs.components
ListView {
id: root
property bool doneFakeFlick
maximumFlickVelocity: 3000
rebound: Transition {
onRunningChanged: {
if (!running && !root.doneFakeFlick) {
root.doneFakeFlick = true;
root.flick(1, 1);
root.flick(-1, -1);
Qt.callLater(() => root.cancelFlick());
}
}
Anim {
properties: "x,y"
}
}
Timer {
running: root.doneFakeFlick
interval: 10
onTriggered: root.doneFakeFlick = false
}
}

View file

@ -0,0 +1,72 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.components
import qs.components.effects
StyledFlickable {
id: root
property real fadeAmount: 0.2
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
function fadeShouldBeActive(isStart: bool): bool {
// When content is smaller than flickable size, hide fade when rebound starts
if (contentHeight + topMargin + bottomMargin < height && rebound.running && ((isStart ? verticalOvershoot > 0 : verticalOvershoot < 0)))
return false;
if (isStart)
return visibleArea.yPosition > 0;
return visibleArea.yPosition + visibleArea.heightRatio < 1;
}
flickableDirection: Flickable.VerticalFlick
layer.enabled: true
layer.effect: Mask {
maskSource: mask
Rectangle {
id: mask
anchors.fill: parent
visible: false
layer.enabled: true
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Qt.rgba(0, 0, 0, root.topFadeOpacity)
}
GradientStop {
position: root.fadeAmount
color: Qt.rgba(0, 0, 0, 1)
}
GradientStop {
position: 1 - root.fadeAmount
color: Qt.rgba(0, 0, 0, 1)
}
GradientStop {
position: 1
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity)
}
}
}
}
Behavior on topFadeOpacity {
Anim {
type: Anim.SlowEffects
}
}
Behavior on bottomFadeOpacity {
Anim {
type: Anim.SlowEffects
}
}
}

View file

@ -1,5 +1,3 @@
//@ pragma Internal
import QtQuick
import Caelestia.Config
import qs.components
@ -35,10 +33,10 @@ StyledRect {
readonly property alias stateLayer: stateLayer
readonly property alias radiusAnim: radiusAnim
required property color activeColour
required property color inactiveColour
required property color activeOnColour
required property color inactiveOnColour
property color activeColour
property color inactiveColour
property color activeOnColour
property color inactiveOnColour
property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1)
property color disabledOnColour: Qt.alpha(Colours.palette.m3onSurface, 0.38)
@ -60,7 +58,7 @@ StyledRect {
if (internalChecked)
return checkedRadius;
if (isRound)
return implicitHeight / 2 * Math.min(1, Tokens.rounding.scale);
return (height || implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale);
return defaultRadius;
}
color: type === ButtonBase.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour

View file

@ -13,10 +13,27 @@ ButtonBase {
readonly property alias iconLabel: iconLabel
readonly property alias label: label
activeColour: type === IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary
inactiveColour: type === IconTextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer
activeOnColour: type === IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary
inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
horizontalPadding: Tokens.padding.medium
verticalPadding: Tokens.padding.small
activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary
inactiveColour: {
if (!isToggle && type === TextButton.Filled)
return Colours.palette.m3primary;
return type === TextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer;
}
activeOnColour: {
if (type === TextButton.Text)
return Colours.palette.m3primary;
return type === TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary;
}
inactiveOnColour: {
if (!isToggle && type === TextButton.Filled)
return Colours.palette.m3onPrimary;
if (type === TextButton.Text)
return Colours.palette.m3primary;
return type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer;
}
implicitWidth: row.implicitWidth + horizontalPadding * 2
implicitHeight: row.implicitHeight + verticalPadding * 2
@ -34,6 +51,11 @@ ButtonBase {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)
color: root.onColour
fill: root.internalChecked ? 1 : 0
fontStyle: {
const f = Qt.font(root.font);
f.pointSize = Math.round(root.font.pointSize * 1.1);
return f;
}
Behavior on fill {
Anim {
@ -48,6 +70,7 @@ ButtonBase {
Layout.alignment: Qt.AlignVCenter
Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575)
color: root.onColour
font: root.font
}
}
}

View file

@ -116,7 +116,7 @@ MouseArea {
required property int index
required property MenuItem modelData
readonly property bool active: modelData === root.active
readonly property bool active: modelData === root?.active
Layout.fillWidth: true
implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.medium * 2
@ -125,8 +125,8 @@ MouseArea {
radius: active ? Tokens.rounding.medium : Tokens.rounding.extraSmall
topLeftRadius: index === 0 ? Tokens.rounding.medium : radius
topRightRadius: index === 0 ? Tokens.rounding.medium : radius
bottomLeftRadius: index === repeater.count - 1 ? Tokens.rounding.medium : radius
bottomRightRadius: index === repeater.count - 1 ? Tokens.rounding.medium : radius
bottomLeftRadius: index === repeater?.count - 1 ? Tokens.rounding.medium : radius
bottomRightRadius: index === repeater?.count - 1 ? Tokens.rounding.medium : radius
color: Qt.alpha(Colours.palette.m3tertiaryContainer, active ? 1 : 0)
@ -159,21 +159,21 @@ MouseArea {
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
text: item.modelData.icon
text: item.modelData?.icon ?? ""
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant
}
StyledText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: item.modelData.text
text: item.modelData?.text ?? ""
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface
}
Loader {
asynchronous: true
Layout.alignment: Qt.AlignVCenter
active: item.modelData.trailingIcon.length > 0
active: item.modelData?.trailingIcon.length > 0
visible: active
sourceComponent: MaterialIcon {

View file

@ -15,6 +15,7 @@ Slider {
property bool animateWave
property real waveFrequency: 6
property int waveDuration: 1000
property int radius: Tokens.rounding.medium
property color fgColour: enabled ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3onSurface, 0.38)
property color bgColour: enabled ? Colours.palette.m3secondaryContainer : Qt.alpha(Colours.palette.m3onSurface, 0.1)
@ -43,7 +44,7 @@ Slider {
implicitHeight: parent.height * (parent.height <= 12 ? opacity : Math.min(opacity * 2, 1))
opacity: Math.min(width, 12) / 12
radius: Tokens.rounding.medium
radius: root.radius
topLeftRadius: Tokens.rounding.extraSmall / 2
bottomLeftRadius: Tokens.rounding.extraSmall / 2
color: root.bgColour
@ -71,9 +72,9 @@ Slider {
implicitWidth: 4
implicitHeight: {
const mult = parent.height <= 12 ? 3 : 1.2;
const pressMult = parent.height <= 12 ? 4 : 1.5;
return parent.height * (mouse.pressed ? pressMult : mult);
const t = CUtils.clamp((parent.height - 12) / 16, 0, 1);
const lerp = (a, b) => a + (b - a) * t;
return parent.height * (mouse.pressed ? lerp(3.5, 1.5) : lerp(3, 1.2));
}
radius: Tokens.rounding.full
@ -103,7 +104,7 @@ Slider {
implicitWidth: root.filledWidth
implicitHeight: root.height
radius: Tokens.rounding.medium
radius: root.radius
topRightRadius: Tokens.rounding.extraSmall / 2
bottomRightRadius: Tokens.rounding.extraSmall / 2
color: root.fgColour

View file

@ -50,7 +50,10 @@ LazyLoader {
implicitWidth: 1000
implicitHeight: 600
minimumSize.width: 400
minimumSize.height: 300
color: Colours.tPalette.m3surface
surfaceFormat.opaque: false
title: loader.title
onVisibleChanged: {

View file

@ -90,7 +90,7 @@ StyledRect {
return "folder";
}
color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
fontStyle: Tokens.font.icon.large
fontStyle: Tokens.font.icon.medium
fill: place.selected ? 1 : 0
Behavior on fill {
@ -104,7 +104,7 @@ StyledRect {
Layout.fillWidth: true
text: place.modelData
color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
font: Tokens.font.body.medium
font: Tokens.font.body.small
elide: Text.ElideRight
}
}

View file

@ -4,5 +4,5 @@ import Quickshell
Singleton {
property int itemWidth: 103
property int sidebarWidth: 200
property int sidebarWidth: 230
}

View file

@ -6,10 +6,15 @@ Image {
id: root
property bool hadPrevious
property bool fadingOut
property bool preventInit
property int fadeOutAnim: Anim.FastEffects
property int fadeInAnim: Anim.DefaultEffects
property int fadeInLargeAnim: Anim.StandardLarge
function maybeStartInAnim(): void {
if (!opacityInAnim.running && status === Image.Ready) {
opacityInAnim.type = hadPrevious ? Anim.DefaultEffects : Anim.StandardLarge;
if (!preventInit && !opacityInAnim.running && status === Image.Ready) {
opacityInAnim.type = hadPrevious ? fadeInAnim : fadeInLargeAnim;
opacityInAnim.start();
}
}
@ -26,6 +31,7 @@ Image {
opacity: 0
onStatusChanged: maybeStartInAnim()
onPreventInitChanged: maybeStartInAnim()
Anim on opacity {
id: opacityInAnim
@ -36,11 +42,24 @@ Image {
Behavior on source {
SequentialAnimation {
ScriptAction {
script: opacityInAnim.stop()
}
PropertyAction {
target: root
property: "fadingOut"
value: true
}
Anim {
target: root
property: "opacity"
to: 0
type: Anim.FastEffects
type: root.fadeOutAnim
}
PropertyAction {
target: root
property: "fadingOut"
value: false
}
PropertyAction {
target: root

View file

@ -4,7 +4,7 @@ import Quickshell.Io
import Caelestia
import qs.components.misc
import qs.services
import qs.modules.controlcenter
import qs.modules.nexus
Scope {
id: root
@ -15,8 +15,8 @@ Scope {
// qmllint disable unresolved-type
CustomShortcut {
// qmllint enable unresolved-type
name: "controlCenter"
description: "Open control center"
name: "nexus"
description: "Open nexus"
onPressed: WindowFactory.create()
}
@ -133,7 +133,7 @@ Scope {
WindowFactory.create();
}
target: "controlCenter"
target: "nexus"
}
IpcHandler {

View file

@ -7,7 +7,7 @@ import Quickshell.Wayland
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.controlcenter
import qs.modules.nexus
import qs.modules.windowinfo
Item {
@ -18,7 +18,7 @@ Item {
readonly property alias content: content
readonly property alias winfo: winfo
readonly property alias controlCenter: controlCenter
readonly property alias nexus: nexus
readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth
readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight
@ -128,15 +128,24 @@ Item {
}
Comp {
id: controlCenter
id: nexus
shouldBeActive: root.detachedMode === "any"
anchors.centerIn: parent
sourceComponent: ControlCenter {
screen: root.screen
active: root.queuedMode
onClose: root.close()
sourceComponent: StyledClippingRect {
radius: Tokens.rounding.extraLarge
implicitWidth: nexusInner.implicitWidth
implicitHeight: nexusInner.implicitHeight
Nexus {
id: nexusInner
anchors.fill: parent
nState.screen: root.screen
nState.currentPageIdx: ["appearance", "network", "bluetooth", "audio"].indexOf(root.queuedMode)
onClose: root.close()
}
}
}

View file

@ -202,7 +202,6 @@ Item {
anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.1)
text: "up " + SysInfo.uptime.split(",").slice(0, 2).join(",") // Max 2 components
font: Tokens.font.label.medium
width: Tokens.sizes.dashboard.userWidth - x - Tokens.padding.extraLarge
elide: Text.ElideRight
}

View file

@ -19,7 +19,7 @@ Item {
id: blobGroup
color: Colours.palette.m3surfaceContainerHighest
smoothing: Tokens.rounding.medium
smoothing: root.Tokens.rounding.medium
cornerFill: false
Behavior on color {

View file

@ -39,6 +39,8 @@ StyledWindow {
readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg)
readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness
property color surfaceColour: Colours.tPalette.m3surface
readonly property int dragMaskPadding: {
if (focusGrab.active || panels.popouts.isDetached)
return 0;
@ -76,6 +78,10 @@ StyledWindow {
Anim {}
}
Behavior on surfaceColour {
CAnim {}
}
Region {
id: emptyRegion
@ -129,7 +135,7 @@ StyledWindow {
Item {
anchors.fill: parent
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
opacity: root.surfaceColour.a
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
@ -140,12 +146,8 @@ StyledWindow {
BlobGroup {
id: blobGroup
color: Colours.palette.m3surface
color: root.surfaceColour
smoothing: root.contentItem.Config.border.smoothing
Behavior on color {
CAnim {}
}
}
BlobInvertedRect {

View file

@ -34,8 +34,8 @@ ColumnLayout {
ProfilePic {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.bottomMargin: Tokens.spacing.medium
Layout.topMargin: Tokens.spacing.extraExtraLarge * root.centerScale
Layout.bottomMargin: Tokens.spacing.extraLarge * root.centerScale
centerWidth: root.centerWidth
}

26
modules/nexus/NavPane.qml Normal file
View file

@ -0,0 +1,26 @@
import "navpane"
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.modules.nexus
ColumnLayout {
id: root
required property NexusState nState
spacing: Tokens.spacing.large
SearchBar {
Layout.fillWidth: true
nState: root.nState
}
NavLocations {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: -topMargin
Layout.bottomMargin: -bottomMargin
nState: root.nState
}
}

109
modules/nexus/Nexus.qml Normal file
View file

@ -0,0 +1,109 @@
pragma ComponentBehavior: Bound
import QtQuick
import Caelestia.Blobs
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.modules.nexus
Item {
id: root
readonly property NexusState nState: NexusState {
id: nState
onClose: root.close()
}
property color blobColour: Colours.tPalette.m3surfaceContainerLow
signal close
implicitWidth: implicitHeight * Tokens.sizes.nexus.ratio
implicitHeight: nState.screen.height * Tokens.sizes.nexus.heightMult
Behavior on blobColour {
CAnim {}
}
BlobGroup {
id: blobGroup
smoothing: root.Tokens.rounding.medium
color: root.blobColour
}
BlobInvertedRect {
anchors.fill: parent
group: blobGroup
opacity: root.blobColour.a
radius: Tokens.rounding.large
borderLeft: navPane.width + navPane.anchors.margins * 2
borderRight: Tokens.padding.medium
borderTop: Tokens.padding.medium
borderBottom: Tokens.padding.medium
}
BlobRect {
id: windowBtnRect
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: root.nState.isWindow ? 0 : Tokens.padding.extraSmall
group: blobGroup
opacity: root.blobColour.a
radius: Tokens.rounding.medium
implicitWidth: windowBtn.implicitWidth + (root.nState.isWindow ? Tokens.padding.extraSmall : Tokens.padding.small) * 2
implicitHeight: windowBtn.implicitHeight + (root.nState.isWindow ? Tokens.padding.extraSmall : Tokens.padding.small)
}
IconButton {
id: windowBtn
anchors.centerIn: windowBtnRect
icon: nState.isWindow ? "close" : "pip"
type: IconButton.Text
label.fill: 0
inactiveOnColour: hovered ? nState.isWindow ? Colours.palette.m3error : Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
stateLayer.opacity: 0
onClicked: {
if (!nState.isWindow)
WindowFactory.create();
root.close();
}
label.scale: pressed ? 0.8 : 1
label.renderType: Text.QtRendering
Behavior on label.scale {
Anim {}
}
}
NavPane {
id: navPane
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Tokens.padding.large
nState: nState
width: Math.min(Tokens.sizes.nexus.maxNavWidth, Math.round(root.width / 3))
}
Pages {
anchors.left: navPane.right
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.leftMargin: navPane.anchors.margins + anchors.margins
anchors.margins: Tokens.padding.extraLarge
nState: nState
}
}

View file

@ -0,0 +1,30 @@
import QtQuick
import Quickshell
import Quickshell.Bluetooth
QtObject {
property ShellScreen screen
property bool isWindow
property int currentPageIdx
property list<int> subPageIdxStack
property bool searchOpen
property string selectedWallpaperCategory
property BluetoothDevice selectedBtDevice
signal close
signal subPageOpened(idx: int)
signal subPageClosed
function openSubPage(idx: int): void {
subPageIdxStack.push(idx);
subPageOpened(idx);
}
function closeSubPage(): void {
subPageClosed();
subPageIdxStack.pop();
}
onCurrentPageIdxChanged: subPageIdxStack.length = 0
}

View file

@ -0,0 +1,180 @@
pragma Singleton
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus.common
import qs.modules.nexus.pages
import qs.modules.nexus.pages.audio
import qs.modules.nexus.pages.bluetooth
import qs.modules.nexus.pages.panels
import qs.modules.nexus.pages.wallandstyle
import qs.modules.nexus.pages.panels.taskbar
QtObject {
id: root
readonly property list<Component> pageComps: [
// Appearance
Component {
// Wallpaper & style
StackPage {
Component {
WallpaperAndStyle {}
}
Component {
WallpaperSelect {}
}
Component {
WallpaperCategory {}
}
Component {
ColourSelect {}
}
}
},
// Connectivity
Component {
// Network
StackPage {
Component {
NetworkPage {}
}
}
},
Component {
// Bluetooth
StackPage {
Component {
BluetoothPage {}
}
Component {
BtDeviceInfo {}
}
Component {
BluetoothPairing {}
}
}
},
Component {
// Audio
StackPage {
Component {
AudioPage {}
}
Component {
AppVolumes {}
}
}
},
// System
Component {
PlaceholderComp {}
},
Component {
PlaceholderComp {}
},
// Shell
Component {
// Panels
StackPage {
Component {
PanelsPage {}
}
Component {
DashboardPanel {}
}
Component {
TaskbarPanel {}
}
Component {
LauncherPanel {}
}
Component {
SidebarPanel {}
}
// Taskbar component sub-pages
Component {
BarWorkspaces {}
}
Component {
BarActiveWindow {}
}
Component {
BarTray {}
}
Component {
BarStatusIcons {}
}
Component {
BarClock {}
}
}
},
Component {
// Services
StackPage {
Component {
ServicesPage {}
}
}
},
Component {
// Language & region
StackPage {
Component {
LanguageAndRegion {}
}
}
},
// About
Component {
StackPage {
Component {
AboutPage {}
}
}
}
]
readonly property Component placeholderComp: Component {
PlaceholderComp {}
}
component PlaceholderComp: Item {
property NexusState nState // To avoid the warning from non-existent property
ColumnLayout {
anchors.centerIn: parent
spacing: Tokens.padding.extraSmall
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "handyman"
color: Colours.palette.m3outlineVariant
font: Tokens.font.icon.extraLarge
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Page under construction")
color: Colours.palette.m3outlineVariant
font: Tokens.font.title.large
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("This page will be available in a future update.")
color: Colours.palette.m3outlineVariant
font: Tokens.font.body.large
}
}
}
}

View file

@ -0,0 +1,87 @@
pragma Singleton
import QtQuick
QtObject {
id: root
readonly property list<var> pages: [
// Appearance
{
label: qsTr("Wallpaper & style"),
icon: "palette",
description: qsTr("Wallpaper, fonts, colours"),
category: "appearance"
},
// Connectivity
// TODO
// {
// label: qsTr("Display"),
// icon: "monitor",
// description: qsTr("Output configuration"),
// category: "connectivity"
// },
{
label: qsTr("Network"),
icon: "wifi",
description: qsTr("Wi-Fi, ethernet"),
category: "connectivity"
},
{
label: qsTr("Connected devices"),
icon: "devices_other",
description: qsTr("Bluetooth, pairing"),
category: "connectivity",
noFill: true
},
{
label: qsTr("Audio"),
icon: "volume_up",
description: qsTr("App volumes, sound devices"),
category: "connectivity"
},
// System
{
label: qsTr("Updates"),
icon: "update",
description: qsTr("System updates"),
category: "system"
},
{
label: qsTr("Plugins"),
icon: "extension",
description: qsTr("Manage plugins"),
category: "system"
},
// Shell
{
label: qsTr("Panels"),
icon: "dock_to_bottom",
description: qsTr("Dashboard, taskbar, launcher, sidebar"),
category: "shell"
},
{
label: qsTr("Services"),
icon: "build",
description: qsTr("Poll intervals, lyrics backend"),
category: "shell"
},
{
label: qsTr("Language & region"),
icon: "globe",
description: qsTr("UI language, weather location, display units"),
category: "shell"
},
// About
{
label: qsTr("About"),
icon: "info",
description: qsTr("System information, credits"),
category: "about"
},
]
}

95
modules/nexus/Pages.qml Normal file
View file

@ -0,0 +1,95 @@
import QtQuick
import Caelestia.Config
import qs.components
import qs.modules.nexus
Item {
id: root
required property NexusState nState
property int lastPageIdx
property int animOff
property Item currentItem
function loadPage(idx: int): void {
if (currentItem)
currentItem.destroy();
const comp = PageCompRegistry.pageComps[idx] ?? PageCompRegistry.placeholderComp;
const incubator = comp.incubateObject(container, {
nState
});
const attach = () => {
incubator.object.anchors.fill = container;
currentItem = incubator.object;
};
if (incubator.status === Component.Ready)
attach();
else
incubator.onStatusChanged = status => {
if (status === Component.Ready)
attach();
};
}
Item {
id: container
anchors.fill: parent
layer.enabled: opacity < 1
Component.onCompleted: root.loadPage(root.nState.currentPageIdx)
}
Connections {
function onCurrentPageIdxChanged(): void {
switchAnim.complete();
root.animOff = root.Tokens.padding.extraLarge * (root.nState.currentPageIdx > root.lastPageIdx ? 1 : -1);
switchAnim.start();
root.lastPageIdx = root.nState.currentPageIdx;
}
target: root.nState
}
SequentialAnimation {
id: switchAnim
Anim {
target: container
property: "opacity"
to: 0
type: Anim.DefaultEffects
}
ScriptAction {
script: root.loadPage(root.nState.currentPageIdx)
}
PropertyAction {
target: container.anchors
property: "topMargin"
value: root.animOff
}
PropertyAction {
target: container.anchors
property: "bottomMargin"
value: -root.animOff
}
ParallelAnimation {
Anim {
target: container
property: "opacity"
from: 0
to: 1
type: Anim.SlowEffects
}
Anim {
target: container.anchors
properties: "topMargin,bottomMargin"
to: 0
type: Anim.SlowEffects
}
}
}
}

View file

@ -0,0 +1,60 @@
pragma Singleton
import QtQuick
import Quickshell
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus
Singleton {
id: root
function create(parent: Item, props: var): void {
nexusComp.createObject(parent ?? dummy, props);
}
QtObject {
id: dummy
}
Component {
id: nexusComp
FloatingWindow {
id: win
color: Colours.tPalette.m3surface
surfaceFormat.opaque: false
onVisibleChanged: {
if (!visible)
destroy();
}
implicitWidth: nexus.implicitWidth
implicitHeight: nexus.implicitHeight
minimumSize.width: contentItem.Tokens.sizes.nexus.minWidth
minimumSize.height: contentItem.Tokens.sizes.nexus.minHeight
contentItem.Config.screen: screen.name
contentItem.Tokens.screen: screen.name
title: qsTr("Nexus — %1").arg(PageRegistry.pages[nexus.nState.currentPageIdx].label)
Nexus {
id: nexus
anchors.fill: parent
nState.screen: win.screen
nState.isWindow: true
onClose: win.destroy()
}
Behavior on color {
CAnim {}
}
}
}
}

View file

@ -0,0 +1,513 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import qs.components
import qs.services
Item {
id: root
property real blurAmount: skipIntroAnimation ? 0.0 : 1.0
property bool skipIntroAnimation: false
property real star1Opacity: skipIntroAnimation ? 1.0 : 0.0
property real star2Opacity: skipIntroAnimation ? 1.0 : 0.0
property real star3Opacity: skipIntroAnimation ? 1.0 : 0.0
property real star1Scale: skipIntroAnimation ? 1.0 : 0.0
property real star2Scale: skipIntroAnimation ? 1.0 : 0.0
property real star3Scale: skipIntroAnimation ? 1.0 : 0.0
readonly property alias topShape: topShape
readonly property alias bottomShape: bottomShape
readonly property alias star1: star1
readonly property alias star2: star2
readonly property alias star3: star3
signal animationCompleted
implicitWidth: 128
implicitHeight: 90.38
Item {
id: logo
readonly property real designWidth: 128
readonly property real designHeight: 90.38
property color topColour: Colours.palette.m3primary
property color bottomColour: Colours.palette.m3onSurface
implicitWidth: designWidth
implicitHeight: designHeight
transformOrigin: Item.Center
scale: root.skipIntroAnimation ? 1.0 : 0.0
opacity: root.skipIntroAnimation ? 1.0 : 0.0
rotation: 0.0
layer.enabled: root.blurAmount > 0
layer.effect: MultiEffect {
blurEnabled: true
blur: root.blurAmount
blurMax: 60
}
Component.onCompleted: {
root.star1.opacity = Qt.binding(() => root.star1Opacity);
root.star1.scale = Qt.binding(() => root.star1Scale);
root.star2.opacity = Qt.binding(() => root.star2Opacity);
root.star2.scale = Qt.binding(() => root.star2Scale);
root.star3.opacity = Qt.binding(() => root.star3Opacity);
root.star3.scale = Qt.binding(() => root.star3Scale);
}
Behavior on topColour {
CAnim {}
}
Behavior on bottomColour {
CAnim {}
}
Shape {
id: topShape
anchors.centerIn: parent
width: logo.designWidth
height: logo.designHeight
scale: Math.min(logo.width / width, logo.height / height)
transformOrigin: Item.Center
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: logo.topColour
strokeColor: "transparent"
PathSvg {
path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z"
}
}
}
Shape {
id: bottomShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: logo.bottomColour
strokeColor: "transparent"
PathSvg {
path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z"
}
}
}
Shape {
id: star1
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
opacity: 0.0
ShapePath {
fillColor: logo.topColour
strokeColor: "transparent"
PathSvg {
path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z"
}
}
}
Shape {
id: star2
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
opacity: 0.0
ShapePath {
fillColor: logo.topColour
strokeColor: "transparent"
PathSvg {
path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z"
}
}
}
Shape {
id: star3
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
opacity: 0.0
ShapePath {
fillColor: logo.topColour
strokeColor: "transparent"
PathSvg {
path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z"
}
}
}
}
SequentialAnimation {
running: !root.skipIntroAnimation
onFinished: root.animationCompleted()
ParallelAnimation {
SequentialAnimation {
NumberAnimation {
target: logo
property: "rotation"
from: 0
to: 750
duration: 1000
easing.type: Easing.OutCubic
}
NumberAnimation {
target: logo
property: "rotation"
from: 750
to: 710
duration: 300
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: logo
property: "rotation"
from: 710
to: 725
duration: 350
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: logo
property: "rotation"
from: 725
to: 720
duration: 250
easing.type: Easing.OutQuad
}
ScriptAction {
script: logo.rotation = 0
}
}
SequentialAnimation {
NumberAnimation {
target: logo
property: "scale"
from: 0.0
to: 1.08
duration: 1000
easing.type: Easing.OutCubic
}
NumberAnimation {
target: logo
property: "scale"
from: 1.08
to: 0.96
duration: 200
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: logo
property: "scale"
from: 0.96
to: 1.0
duration: 250
easing.type: Easing.OutBack
easing.overshoot: 1.05
}
}
NumberAnimation {
target: logo
property: "opacity"
from: 0.0
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root
property: "blurAmount"
from: 1.0
to: 0.0
duration: 900
easing.type: Easing.OutCubic
}
SequentialAnimation {
PauseAnimation {
duration: 1100
}
ParallelAnimation {
NumberAnimation {
target: root
property: "star1Opacity"
from: 0.0
to: 1.0
duration: 700
easing.type: Easing.InOutQuad
}
SequentialAnimation {
NumberAnimation {
target: root
property: "star1Scale"
from: 0.0
to: 1.08
duration: 500
easing.type: Easing.OutQuad
}
NumberAnimation {
target: root
property: "star1Scale"
from: 1.08
to: 1.0
duration: 400
easing.type: Easing.InOutQuad
}
}
}
}
SequentialAnimation {
PauseAnimation {
duration: 1250
}
ParallelAnimation {
NumberAnimation {
target: root
property: "star2Opacity"
from: 0.0
to: 1.0
duration: 700
easing.type: Easing.InOutQuad
}
SequentialAnimation {
NumberAnimation {
target: root
property: "star2Scale"
from: 0.0
to: 1.08
duration: 500
easing.type: Easing.OutQuad
}
NumberAnimation {
target: root
property: "star2Scale"
from: 1.08
to: 1.0
duration: 400
easing.type: Easing.InOutQuad
}
}
}
}
SequentialAnimation {
PauseAnimation {
duration: 1400
}
ParallelAnimation {
NumberAnimation {
target: root
property: "star3Opacity"
from: 0.0
to: 1.0
duration: 700
easing.type: Easing.InOutQuad
}
SequentialAnimation {
NumberAnimation {
target: root
property: "star3Scale"
from: 0.0
to: 1.08
duration: 500
easing.type: Easing.OutQuad
}
NumberAnimation {
target: root
property: "star3Scale"
from: 1.08
to: 1.0
duration: 400
easing.type: Easing.InOutQuad
}
}
}
}
}
}
SequentialAnimation {
running: true
loops: Animation.Infinite
PauseAnimation {
duration: 2500
}
ParallelAnimation {
SequentialAnimation {
loops: Animation.Infinite
NumberAnimation {
target: root.star1
property: "y"
from: root.star1.y
to: root.star1.y - 5
duration: 2500
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.star1
property: "y"
from: root.star1.y - 5
to: root.star1.y
duration: 2500
easing.type: Easing.InOutQuad
}
}
SequentialAnimation {
loops: Animation.Infinite
NumberAnimation {
target: root.star2
property: "y"
from: root.star2.y
to: root.star2.y + 5
duration: 3000
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.star2
property: "y"
from: root.star2.y + 5
to: root.star2.y
duration: 3000
easing.type: Easing.InOutQuad
}
}
SequentialAnimation {
loops: Animation.Infinite
NumberAnimation {
target: root.star3
property: "y"
from: root.star3.y
to: root.star3.y - 5
duration: 2800
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.star3
property: "y"
from: root.star3.y - 5
to: root.star3.y
duration: 2800
easing.type: Easing.InOutQuad
}
}
SequentialAnimation {
loops: Animation.Infinite
NumberAnimation {
target: root.star1
property: "scale"
from: 1.0
to: 1.08
duration: 2500
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.star1
property: "scale"
from: 1.08
to: 1.0
duration: 2500
easing.type: Easing.InOutQuad
}
}
SequentialAnimation {
loops: Animation.Infinite
NumberAnimation {
target: root.star2
property: "scale"
from: 1.0
to: 1.12
duration: 3000
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.star2
property: "scale"
from: 1.12
to: 1.0
duration: 3000
easing.type: Easing.InOutQuad
}
}
SequentialAnimation {
loops: Animation.Infinite
NumberAnimation {
target: root.star3
property: "scale"
from: 1.0
to: 1.08
duration: 2800
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.star3
property: "scale"
from: 1.08
to: 1.0
duration: 2800
easing.type: Easing.InOutQuad
}
}
}
}
}

View file

@ -0,0 +1,97 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus.common
ItemList {
id: root
property var nodes: []
property int currentId: -1
property string iconName: "speaker"
signal selected(node: PwNode)
last: true
showList: true
model: ScriptModel {
values: [...root.nodes].sort((a, b) => (a.description || a.name || "").localeCompare(b.description || b.name || ""))
}
delegate: Item {
id: device
required property PwNode modelData
required property int index
readonly property bool active: device.modelData?.id === root.currentId
anchors.left: root.list.contentItem.left
anchors.right: root.list.contentItem.right
implicitHeight: deviceLayout.implicitHeight + deviceLayout.anchors.margins * 2
StateLayer {
radius: Tokens.rounding.extraSmall
bottomLeftRadius: device.index === root?.list.count - 1 ? Tokens.rounding.extraLarge : radius
bottomRightRadius: device.index === root?.list.count - 1 ? Tokens.rounding.extraLarge : radius
onClicked: root.selected(device.modelData)
}
RowLayout {
id: deviceLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
StyledRect {
implicitWidth: implicitHeight
implicitHeight: devIcon.implicitHeight + Tokens.padding.small * 2
radius: Tokens.rounding.full
color: device.active ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon {
id: devIcon
anchors.centerIn: parent
text: root.iconName
color: device.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
font: Tokens.font.icon.medium
fill: device.active ? 1 : 0
Behavior on fill {
Anim {}
}
}
}
StyledText {
Layout.fillWidth: true
text: device.modelData?.description || device.modelData?.name || qsTr("Unknown")
font: Tokens.font.body.small
elide: Text.ElideRight
}
MaterialIcon {
text: "check"
color: Colours.palette.m3primary
font: Tokens.font.icon.medium
opacity: device.active ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}
}

View file

@ -0,0 +1,15 @@
import QtQuick
import Caelestia.Config
import qs.components
import qs.services
StyledRect {
property bool first
property bool last
color: Colours.tPalette.m3surfaceContainer
topLeftRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
topRightRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
bottomLeftRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
bottomRightRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
}

View file

@ -0,0 +1,59 @@
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus.common
ConnectedRect {
id: root
property alias label: label.text
property string subtext
property alias value: value.text
Layout.fillWidth: true
implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
id: label
Layout.fillWidth: true
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
visible: root.subtext
text: root.subtext
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
}
}
StyledText {
id: value
Layout.maximumWidth: root.width / 2
horizontalAlignment: Text.AlignRight
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.small
elide: Text.ElideRight
}
}
}

View file

@ -0,0 +1,121 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.services
ConnectedRect {
id: root
property bool showList
property string placeholderIcon
property string placeholderText
property int extraHeight
property alias model: list.model
property alias delegate: list.delegate
readonly property alias list: list
Layout.fillWidth: true
implicitHeight: (showList && list.count > 0 ? list.contentHeight : placeholder.implicitHeight + Tokens.padding.extraLarge * 2) + extraHeight
color: Colours.tPalette.m3surfaceContainer
clip: true
Behavior on implicitHeight {
Anim {}
}
Loader {
id: placeholder
anchors.centerIn: parent
active: opacity > 0
opacity: root.showList && list.count > 0 ? 0 : 1
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.extraSmall
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: root.placeholderIcon
color: Colours.palette.m3outline
font: Tokens.font.icon.large
animate: true
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: root.placeholderText
color: Colours.palette.m3outline
font: Tokens.font.body.large
animate: true
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
ListView {
id: list
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
spacing: 0
interactive: false
opacity: root.showList ? 1 : 0
add: Transition {
Anim {
property: "opacity"
from: 0
to: 1
type: Anim.DefaultEffects
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
type: Anim.DefaultEffects
}
}
move: Transition {
Anim {
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
property: "y"
}
}
displaced: Transition {
Anim {
property: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
property: "y"
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}

View file

@ -0,0 +1,72 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus.common
ConnectedRect {
id: root
property alias icon: icon.text
property alias label: label.text
property alias status: status.text
signal clicked
Layout.fillWidth: true
implicitHeight: navLayout.implicitHeight + navLayout.anchors.margins * 2
StateLayer {
onClicked: root.clicked()
}
RowLayout {
id: navLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
MaterialIcon {
id: icon
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
id: label
Layout.fillWidth: true
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
id: status
Layout.fillWidth: true
visible: text
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
animate: true
}
}
MaterialIcon {
text: "chevron_right"
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
}
}
}

View file

@ -0,0 +1,61 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.components.containers
import qs.components.controls
import qs.services
import qs.modules.nexus
ColumnLayout {
id: root
required property string title
required property NexusState nState
property bool isSubPage
readonly property int cappedWidth: Math.min(800, width)
default property Item contentChild
spacing: Tokens.spacing.extraLargeIncreased
RowLayout {
spacing: Tokens.spacing.largeIncreased
Loader {
visible: active
active: root.isSubPage
asynchronous: true
sourceComponent: IconButton {
icon: "arrow_back"
font: Tokens.font.icon.medium
type: IconButton.Tonal
isRound: true
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurfaceVariant
onClicked: root.nState.closeSubPage()
}
}
StyledText {
text: root.title
font: Tokens.font.title.large
}
}
VerticalFadeFlickable {
id: flickable
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: -topMargin
topMargin: Tokens.padding.large
bottomMargin: Tokens.padding.extraLarge
contentHeight: root.contentChild?.implicitHeight ?? 0
contentItem.children: [root.contentChild]
}
}

View file

@ -0,0 +1,18 @@
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.services
StyledText {
property bool first
Layout.fillWidth: true
Layout.topMargin: first ? 0 : Tokens.spacing.largeIncreased - ((parent as ColumnLayout).spacing ?? 0)
Layout.bottomMargin: Tokens.spacing.extraSmall
Layout.leftMargin: Tokens.padding.small
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.label.medium
elide: Text.ElideRight
}

View file

@ -0,0 +1,65 @@
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.modules.nexus.common
ConnectedRect {
id: root
property alias label: label.text
property string subtext
property alias menuItems: splitButton.menuItems
property alias active: splitButton.active
property alias fallbackText: splitButton.fallbackText
property alias fallbackIcon: splitButton.fallbackIcon
property alias menuOnTop: splitButton.menuOnTop
signal selected(item: MenuItem)
implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2
clip: false
z: splitButton.expanded ? 1 : 0
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
id: label
Layout.fillWidth: true
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
visible: root.subtext
text: root.subtext
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
}
}
SplitButton {
id: splitButton
type: SplitButton.Tonal
stateLayer.onClicked: splitButton.expanded = !splitButton.expanded
menu.onItemSelected: item => root.selected(item)
}
}
}

View file

@ -0,0 +1,88 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.modules.nexus.common
ConnectedRect {
id: root
property alias icon: icon.text
property alias label: label.text
property alias valueLabel: valueLabel.text
property real value
signal moved(value: real)
implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins + rowLayout.anchors.topMargin
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.margins: Tokens.padding.largeIncreased
anchors.topMargin: Tokens.padding.large
spacing: Tokens.spacing.medium
MaterialIcon {
id: icon
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
}
ColumnLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.medium
RowLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.small
StyledText {
id: label
Layout.fillWidth: true
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
id: valueLabel
color: Colours.palette.m3outline
font: Tokens.font.body.small
}
}
CustomMouseArea {
function onWheel(event: WheelEvent): void {
const step = GlobalConfig.services.audioIncrement;
if (event.angleDelta.y > 0)
root.moved(Math.min(1, root.value + step));
else if (event.angleDelta.y < 0)
root.moved(Math.max(0, root.value - step));
}
Layout.fillWidth: true
implicitHeight: Tokens.padding.medium * 2
StyledSlider {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.implicitHeight
radius: Tokens.rounding.small
value: root.value
enabled: root.enabled
onInteraction: v => root.moved(v)
}
}
}
}
}

View file

@ -0,0 +1,122 @@
import QtQuick
import QtQuick.Controls
import Caelestia.Config
import qs.components
import qs.modules.nexus
StackView {
id: root
required property NexusState nState
default property list<Component> pages
readonly property int animMovement: Tokens.padding.extraExtraLarge * 2
function openSubPage(idx: int, immediate: bool): void {
const page = pages[idx];
if (page) {
push(page, {
nState
}, immediate ? StackView.Immediate : StackView.PushTransition);
} else {
console.warn(logCat, "Attempted to open invalid sub-page index", idx);
nState.closeSubPage();
}
}
clip: busy
Component.onCompleted: {
openSubPage(0, true);
for (const page of nState.subPageIdxStack)
openSubPage(page, true);
}
pushEnter: Transition {
SequentialAnimation {
PropertyAction {
property: "opacity"
value: 0
}
PauseAnimation {
duration: Tokens.anim.durations.expressiveDefaultEffects
}
ParallelAnimation {
Anim {
property: "opacity"
to: 1
type: Anim.SlowEffects
}
Anim {
property: "x"
from: root.animMovement
to: 0
type: Anim.SlowEffects
}
}
}
}
pushExit: Transition {
Anim {
property: "opacity"
to: 0
type: Anim.DefaultEffects
}
}
popEnter: Transition {
SequentialAnimation {
PropertyAction {
property: "opacity"
value: 0
}
PauseAnimation {
duration: Tokens.anim.durations.expressiveDefaultEffects
}
ParallelAnimation {
Anim {
property: "opacity"
to: 1
type: Anim.SlowEffects
}
Anim {
property: "x"
from: -root.animMovement
to: 0
type: Anim.SlowEffects
}
}
}
}
popExit: Transition {
Anim {
property: "opacity"
to: 0
type: Anim.DefaultEffects
}
}
LoggingCategory {
id: logCat
name: "caelestia.nexus"
defaultLogLevel: LoggingCategory.Info
}
Connections {
function onSubPageOpened(idx: int): void {
root.openSubPage(idx, false);
}
function onSubPageClosed(): void {
if (root.depth < root.nState.subPageIdxStack.length) {
console.log(logCat, "Attempted to close page while depth < stack depth. Ignoring.");
return;
}
root.pop();
}
target: root.nState
}
}

View file

@ -0,0 +1,64 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.modules.nexus.common
ConnectedRect {
id: root
property alias label: label.text
property string subtext
property real value
property real from: 0
property real to: 99
property real stepSize: 1
signal moved(value: real)
implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
id: label
Layout.fillWidth: true
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
visible: root.subtext
text: root.subtext
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
}
}
CustomSpinBox {
min: root.from
max: root.to
step: root.stepSize
value: root.value
onValueModified: v => root.moved(v)
}
}
}

View file

@ -0,0 +1,79 @@
import QtQuick
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
StyledSwitch {
id: root
property string subtext
property alias first: bg.first
property alias last: bg.last
readonly property alias bg: bg
horizontalPadding: Tokens.padding.largeIncreased
verticalPadding: Tokens.padding.medium
font: Tokens.font.body.small
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
cLayer: 2
indicator.anchors.verticalCenter: verticalCenter
indicator.anchors.right: right
indicator.anchors.rightMargin: root.horizontalPadding
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
background: ConnectedRect {
id: bg
StateLayer {
id: stateLayer
manualPressOverride: root.pressed
}
}
contentItem: Item {
anchors.left: parent.left
anchors.right: root.indicator.left
anchors.leftMargin: root.horizontalPadding
anchors.rightMargin: Tokens.spacing.medium
implicitWidth: column.implicitWidth
implicitHeight: column.implicitHeight
Column {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
StyledText {
id: label
anchors.left: parent.left
anchors.right: parent.right
text: root.text
font: root.font
elide: Text.ElideRight
}
StyledText {
anchors.left: parent.left
anchors.right: parent.right
visible: root.subtext
text: root.subtext
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
}
}
}
}

View file

@ -0,0 +1,105 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
Item {
id: root
property alias source: img.source
property alias text: label.text
property alias radius: imgWrapper.radius
property alias imgHeight: imgWrapper.implicitHeight
property bool fillLabel: true
signal clicked
Layout.fillWidth: true
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
anchors.fill: parent
spacing: Tokens.spacing.small
StyledClippingRect {
id: imgWrapper
Layout.fillWidth: true
implicitHeight: width
radius: Tokens.rounding.largeIncreased
color: Colours.tPalette.m3surfaceContainer
Loader {
anchors.centerIn: parent
opacity: img.status === Image.Ready ? 0 : 1
active: opacity > 0
sourceComponent: StyledRect {
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
color: Colours.palette.m3primaryContainer
radius: Tokens.rounding.full
LoadingIndicator {
id: loadingIndicator
anchors.centerIn: parent
containsIcon: true
implicitSize: Math.min(imgWrapper.width, imgWrapper.height) * 0.3
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Image {
id: img
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
retainWhileLoading: true
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
}
StyledText {
id: label
Layout.bottomMargin: Tokens.padding.small
Layout.fillWidth: true
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.label.builders.small.weight(Font.Medium).build()
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
StateLayer {
anchors.bottomMargin: root.fillLabel ? 0 : layout.implicitHeight - imgWrapper.implicitHeight
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,130 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.components.containers
import qs.services
import qs.modules.nexus
VerticalFadeFlickable {
id: root
required property NexusState nState
topMargin: Tokens.padding.large
bottomMargin: Tokens.padding.large
contentHeight: content.implicitHeight
ColumnLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
spacing: Tokens.spacing.extraSmall
Repeater {
id: list
model: PageRegistry.pages
StyledRect {
id: item
required property var modelData
required property int index
readonly property bool isCurrentPage: index === root.nState.currentPageIdx
readonly property bool isCategoryStart: index === 0 || PageRegistry.pages[index - 1].category !== modelData.category
readonly property bool isCategoryEnd: index === list.model.length - 1 || PageRegistry.pages[index + 1].category !== modelData.category
Layout.fillWidth: true
Layout.topMargin: index !== 0 && isCategoryStart ? Tokens.spacing.medium : 0
implicitHeight: {
const h = layout.implicitHeight + layout.anchors.margins * 2;
return h % 2 === 0 ? h : h + 1;
}
color: isCurrentPage ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
topLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLargeIncreased : isCategoryStart ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
topRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLargeIncreased : isCategoryStart ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
bottomLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLargeIncreased : isCategoryEnd ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
bottomRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLargeIncreased : isCategoryEnd ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
RadiusBehavior on topLeftRadius {}
RadiusBehavior on topRightRadius {}
RadiusBehavior on bottomLeftRadius {}
RadiusBehavior on bottomRightRadius {}
StateLayer {
id: stateLayer
anchors.fill: parent
topLeftRadius: parent.topLeftRadius
topRightRadius: parent.topRightRadius
bottomLeftRadius: parent.bottomLeftRadius
bottomRightRadius: parent.bottomRightRadius
onClicked: root.nState.currentPageIdx = item.index
}
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.medium
StyledRect {
Layout.fillHeight: true
Layout.topMargin: -1
Layout.bottomMargin: -1
implicitWidth: height
radius: Tokens.rounding.full
color: item.isCurrentPage ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon {
anchors.centerIn: parent
anchors.verticalCenterOffset: 1
text: item.modelData.icon
color: item.isCurrentPage ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
grade: 25
fill: item.modelData.noFill ? 0 : 1
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
Layout.fillWidth: true
text: item.modelData.label
font: Tokens.font.body.medium
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: item.modelData.description
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.label.small
elide: Text.ElideRight
}
}
}
}
}
}
component RadiusBehavior: Behavior {
Anim {
type: Anim.DefaultEffects
}
}
}

View file

@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.modules.nexus
StyledRect {
id: root
required property NexusState nState
implicitHeight: searchLayout.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.full
color: Colours.tPalette.m3surfaceContainerLowest
border.color: Colours.palette.m3outlineVariant
Behavior on border.color {
CAnim {}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: searchField.focus = true
}
RowLayout {
id: searchLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.small
MaterialIcon {
text: "search"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.medium
}
StyledTextField {
id: searchField
Layout.fillWidth: true
Layout.fillHeight: true
placeholderText: qsTr("Search settings")
placeholderTextColor: Colours.palette.m3onSurfaceVariant
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.large
Binding {
target: root.nState
property: "searchOpen"
value: searchField.text.length > 0
}
}
IconButton {
icon: "close"
font: Tokens.font.icon.medium
type: IconButton.Text
padding: Tokens.padding.extraSmall
isRound: true
onClicked: searchField.clear()
opacity: searchField.text.length > 0 ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}

View file

@ -0,0 +1,157 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import Caelestia
import Caelestia.Config
import qs.components
import qs.services
import qs.utils
import qs.modules.nexus.common
PageBase {
id: root
// Plugin support is not wired up yet; always 0 for now
readonly property int pluginCount: 0
property string quickshellVersion
property string cliVersion
title: qsTr("About")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// e.g. "Quickshell 0.3.0 (revision ...)"
Process {
running: true
command: ["quickshell", "--version"]
stdout: StdioCollector {
onStreamFinished: root.quickshellVersion = text.trim().split(" ")[1] ?? ""
}
}
// Parsed from the caelestia CLI's package listing; the sh wrapper avoids a
// warning when the (optional) CLI isn't installed
Process {
running: true
command: ["sh", "-c", "caelestia --version 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
const m = text.match(/caelestia-cli\S*\s+(\d+(?:\.\d+)*)/);
root.cliVersion = m ? m[1] : "";
}
}
}
// Hero
ConnectedRect {
Layout.fillWidth: true
first: true
last: true
implicitHeight: hero.implicitHeight + Tokens.padding.extraLarge * 2
ColumnLayout {
id: hero
anchors.centerIn: parent
width: parent.width - Tokens.padding.largeIncreased * 2
spacing: Tokens.spacing.small
AnimatedLogo {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: implicitWidth
Layout.preferredHeight: implicitHeight
}
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.small
text: "Caelestia"
font: Tokens.font.headline.builders.large.width(110).build()
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: CUtils.version ? `v${CUtils.version}` : "…"
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.medium
}
}
}
// System
SectionHeader {
text: qsTr("System")
}
InfoRow {
first: true
label: qsTr("Hostname")
value: SysInfo.hostname
}
InfoRow {
label: qsTr("Device")
value: SysInfo.device
}
InfoRow {
label: qsTr("Distro")
value: SysInfo.osPrettyName || SysInfo.osName
}
InfoRow {
label: qsTr("Kernel")
value: SysInfo.kernel
}
InfoRow {
last: true
label: qsTr("Firmware")
value: SysInfo.firmware
}
// Software
SectionHeader {
text: qsTr("Software")
}
InfoRow {
first: true
label: qsTr("Shell")
value: CUtils.version || "…"
}
InfoRow {
label: qsTr("CLI")
value: root.cliVersion || "…"
}
InfoRow {
label: qsTr("Quickshell")
value: root.quickshellVersion || "…"
}
InfoRow {
last: true
label: qsTr("Qt")
value: CUtils.qtVersion || "…"
}
// Plugins
SectionHeader {
text: qsTr("Plugins")
}
InfoRow {
first: true
last: true
label: qsTr("Loaded plugins")
value: root.pluginCount.toString()
}
}
}

View file

@ -0,0 +1,134 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.services
import qs.utils
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Audio")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// Output
SliderRow {
Layout.fillWidth: true
first: true
icon: Icons.getVolumeIcon(Audio.volume, Audio.muted)
label: qsTr("Output")
valueLabel: Math.round(value * 100) + "%"
value: Audio.volume
enabled: !Audio.muted
onMoved: v => Audio.setVolume(v)
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Muted")
checked: Audio.muted
onToggled: Audio.setStreamMuted(Audio.sink, checked)
}
AudioDeviceList {
nodes: Audio.sinks
currentId: Audio.sink?.id ?? -1
iconName: "speaker"
placeholderIcon: "speaker"
placeholderText: qsTr("No output devices")
onSelected: node => Audio.setAudioSink(node)
}
// Input
SliderRow {
Layout.fillWidth: true
Layout.topMargin: Tokens.spacing.large - parent.spacing
first: true
icon: Icons.getMicVolumeIcon(Audio.sourceVolume, Audio.sourceMuted)
label: qsTr("Input")
valueLabel: Math.round(value * 100) + "%"
value: Audio.sourceVolume
enabled: !Audio.sourceMuted
onMoved: v => Audio.setSourceVolume(v)
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Muted")
checked: Audio.sourceMuted
onToggled: Audio.setStreamMuted(Audio.source, checked)
}
AudioDeviceList {
nodes: Audio.sources
currentId: Audio.source?.id ?? -1
iconName: "mic"
placeholderIcon: "mic_off"
placeholderText: qsTr("No input devices")
onSelected: node => Audio.setAudioSource(node)
}
// Per-app volumes
ConnectedRect {
Layout.fillWidth: true
Layout.topMargin: Tokens.spacing.large - parent.spacing
implicitHeight: appLayout.implicitHeight + appLayout.anchors.margins * 2
first: true
last: true
StateLayer {
onClicked: root.nState.openSubPage(1)
}
RowLayout {
id: appLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
MaterialIcon {
text: "tune"
font: Tokens.font.icon.medium
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
Layout.fillWidth: true
text: qsTr("App volumes")
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: Audio.streams.length === 0 ? qsTr("No apps playing audio") : Audio.streams.length === 1 ? qsTr("1 app playing audio") : qsTr("%1 apps playing audio").arg(Audio.streams.length)
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
animate: true
}
}
MaterialIcon {
text: "chevron_right"
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
}
}
}
}
}

View file

@ -0,0 +1,248 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.utils
import qs.modules.nexus.common
PageBase {
id: root
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter // qmllint disable unresolved-type
readonly property bool btEnabled: adapter?.enabled ?? false
title: qsTr("Connected devices")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Bluetooth")
font: Tokens.font.body.medium
horizontalPadding: Tokens.padding.largeIncreased
checked: root.btEnabled
onToggled: {
if (root.adapter)
root.adapter.enabled = checked;
}
}
ItemList {
id: savedList
showList: root.btEnabled
placeholderIcon: root.btEnabled ? "devices_other" : "bluetooth_disabled"
placeholderText: root.btEnabled ? qsTr("No saved devices") : qsTr("Bluetooth disabled")
model: ScriptModel {
values: Bluetooth.devices.values.filter(d => d.bonded).sort((a, b) => (b.connected - a.connected) || a.name.localeCompare(b.name)) // qmllint disable unresolved-type
}
delegate: StyledRect {
id: device
required property BluetoothDevice modelData
readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type
readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) // qmllint disable unresolved-type
property real textOpacity: loading ? 0.5 : 1
anchors.left: savedList.list.contentItem.left
anchors.right: savedList.list.contentItem.right
implicitHeight: deviceLayout.implicitHeight + deviceLayout.anchors.margins * 2
radius: Tokens.rounding.extraSmall
color: "transparent"
Behavior on textOpacity {
Anim {
type: Anim.DefaultEffects
}
}
StateLayer {
disabled: device.loading
onClicked: {
if (!device.modelData || device.loading)
return;
device.modelData.connected = !device.connected;
}
}
RowLayout {
id: deviceLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
StyledRect {
implicitWidth: implicitHeight
implicitHeight: deviceIcon.implicitHeight + Tokens.padding.small * 2
radius: Tokens.rounding.full
color: device.connected ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon {
id: deviceIcon
anchors.centerIn: parent
text: Icons.getBluetoothIcon(device.modelData?.icon ?? "")
color: device.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
font: Tokens.font.icon.medium
fill: device.connected ? 1 : 0
opacity: device.textOpacity
Behavior on fill {
Anim {}
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
opacity: device.textOpacity
StyledText {
Layout.fillWidth: true
text: device.modelData?.name ?? qsTr("Unknown")
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: device.connected ? qsTr("Connected%1").arg(device.modelData?.batteryAvailable ? " • " + Math.round(device.modelData.battery * 100) + "%" : "") : qsTr("Saved")
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
animate: true
}
}
Item {
Layout.fillHeight: true
implicitWidth: height
AnimLoader {
anchors.centerIn: parent
sourceComp: device.loading ? loadingComp : btnComp
Component {
id: btnComp
IconButton {
icon: "settings"
type: IconButton.Text
padding: Tokens.padding.small
inactiveOnColour: device.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
label.fill: 0
onClicked: {
root.nState.selectedBtDevice = device.modelData;
root.nState.openSubPage(1); // Per device info page
}
}
}
Component {
id: loadingComp
LoadingIndicator {
implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
}
}
}
}
}
}
}
ConnectedRect {
Layout.fillWidth: true
implicitHeight: pairLayout.implicitHeight + pairLayout.anchors.margins * 2
last: true
StateLayer {
disabled: !root.btEnabled
onClicked: root.nState.openSubPage(2)
}
RowLayout {
id: pairLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
opacity: root.btEnabled ? 1 : 0.5
Behavior on opacity {
Anim {}
}
MaterialIcon {
text: "add"
font: Tokens.font.icon.medium
}
StyledText {
Layout.fillWidth: true
text: qsTr("Pair new device")
font: Tokens.font.body.small
elide: Text.ElideRight
}
}
}
ToggleRow {
Layout.fillWidth: true
Layout.topMargin: Tokens.spacing.large - parent.spacing
first: true
text: qsTr("Discoverable")
subtext: qsTr("Allow nearby devices to find this one")
enabled: root.btEnabled
opacity: root.btEnabled ? 1 : 0.5
checked: root.adapter?.discoverable ?? false
onToggled: {
if (root.adapter)
root.adapter.discoverable = checked;
}
Behavior on opacity {
Anim {}
}
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Pairable")
subtext: qsTr("Allow nearby devices to pair with this one")
enabled: root.btEnabled
opacity: root.btEnabled ? 1 : 0.5
checked: root.adapter?.pairable ?? false
onToggled: {
if (root.adapter)
root.adapter.pairable = checked;
}
Behavior on opacity {
Anim {}
}
}
}
}

View file

@ -0,0 +1,175 @@
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.modules.nexus.common
PageBase {
id: root
// Temperature units (index 0 = Celsius, 1 = Fahrenheit matches Weather.formatTemp)
readonly property list<MenuItem> tempItems: [
MenuItem {
text: "°C"
},
MenuItem {
text: "°F"
}
]
// Clock format (index 0 = 24-hour, 1 = 12-hour matches Time.useTwelveHourClock)
readonly property list<MenuItem> clockItems: [
MenuItem {
text: qsTr("24-hour")
},
MenuItem {
text: qsTr("12-hour")
}
]
title: qsTr("Language & region")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// Language
SectionHeader {
first: true
text: qsTr("Language")
}
// Read-only: the shell follows the system locale (no in-shell translations yet)
ConnectedRect {
Layout.fillWidth: true
first: true
last: true
implicitHeight: localeLayout.implicitHeight + localeLayout.anchors.margins * 2
RowLayout {
id: localeLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
Layout.fillWidth: true
text: qsTr("System language")
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: qsTr("Follows your system locale (%1)").arg(Qt.locale().name)
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
}
}
StyledText {
text: Qt.locale().nativeLanguageName || Qt.locale().name
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.small
}
}
}
// Weather
SectionHeader {
text: qsTr("Weather")
}
// Placeholder until the map-based location picker lands
ConnectedRect {
Layout.fillWidth: true
first: true
last: true
implicitHeight: comingSoon.implicitHeight + Tokens.padding.extraLarge * 2
ColumnLayout {
id: comingSoon
anchors.centerIn: parent
width: parent.width - Tokens.padding.largeIncreased * 2
spacing: Tokens.padding.extraSmall
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "map"
color: Colours.palette.m3outlineVariant
font: Tokens.font.icon.extraLarge
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Location picker coming soon")
color: Colours.palette.m3outlineVariant
font: Tokens.font.title.small
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
text: qsTr("Choose your weather location on a map in a future update")
color: Colours.palette.m3outlineVariant
font: Tokens.font.body.small
}
}
}
// Units
SectionHeader {
text: qsTr("Units")
}
SelectRow {
Layout.fillWidth: true
first: true
label: qsTr("Temperature")
subtext: qsTr("Units for weather temperatures")
menuItems: root.tempItems
active: root.tempItems[GlobalConfig.services.useFahrenheit ? 1 : 0]
onSelected: item => GlobalConfig.services.useFahrenheit = root.tempItems.indexOf(item) === 1
}
SelectRow {
Layout.fillWidth: true
last: true
label: qsTr("System temperatures")
subtext: qsTr("Units for CPU and GPU temperatures")
menuItems: root.tempItems
active: root.tempItems[GlobalConfig.services.useFahrenheitPerformance ? 1 : 0]
onSelected: item => GlobalConfig.services.useFahrenheitPerformance = root.tempItems.indexOf(item) === 1
}
// Time & date
SectionHeader {
text: qsTr("Time & date")
}
SelectRow {
Layout.fillWidth: true
first: true
last: true
label: qsTr("Clock format")
subtext: qsTr("How times are shown across the shell")
menuItems: root.clockItems
active: root.clockItems[GlobalConfig.services.useTwelveHourClock ? 1 : 0]
onSelected: item => GlobalConfig.services.useTwelveHourClock = root.clockItems.indexOf(item) === 1
}
}
}

View file

@ -0,0 +1,235 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.utils
import qs.modules.nexus.common
PageBase {
id: root
signal networkSelected(ap: Nmcli.AccessPoint)
title: qsTr("Network")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
Timer {
running: root.visible && Nmcli.wifiEnabled
repeat: true
triggeredOnStart: true
interval: GlobalConfig.nexus.networkRescanInterval
onTriggered: Nmcli.rescanWifi()
}
Timer {
id: wifiScanDelay
interval: 100
onTriggered: Nmcli.rescanWifi()
}
Connections {
function onWifiEnabledChanged(): void {
if (Nmcli.wifiEnabled)
wifiScanDelay.start();
}
target: Nmcli
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Wi-Fi")
font: Tokens.font.body.medium
horizontalPadding: Tokens.padding.largeIncreased
checked: Nmcli.wifiEnabled
onToggled: Nmcli.enableWifi(checked)
}
ItemList {
id: networkList
showList: Nmcli.wifiEnabled
placeholderIcon: Nmcli.wifiEnabled ? "wifi_find" : "signal_wifi_off"
placeholderText: Nmcli.wifiEnabled ? qsTr("No networks found") : qsTr("Wi-Fi disabled")
extraHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0 // Inline so it isn't affected by anim
list.anchors.top: scanningIndicator.bottom
model: ScriptModel {
values: {
const connecting = Nmcli.connectingSsid();
// Lower rank sorts higher in the list
const rank = n => n.active ? 0 : n.ssid === connecting ? 1 : Nmcli.hasSavedProfile(n.ssid) ? 2 : 3;
return [...Nmcli.networks].sort((a, b) => rank(a) - rank(b) || b.strength - a.strength);
}
}
delegate: StateLayer {
id: network
required property Nmcli.AccessPoint modelData
property bool currentSelected
property real textOpacity: disabled ? 0.5 : 1
disabled: currentSelected || Nmcli.connectingSsid() === modelData.ssid
anchors.left: networkList.list.contentItem.left
anchors.right: networkList.list.contentItem.right
implicitHeight: networkLayout.implicitHeight + networkLayout.anchors.margins * 2
radius: Tokens.rounding.extraSmall
anchors.fill: undefined
onClicked: {
if (!modelData.active) {
NetworkConnection.handleConnect(modelData);
currentSelected = true;
root.networkSelected(modelData);
}
}
Behavior on textOpacity {
Anim {
type: Anim.DefaultEffects
}
}
Connections {
function onActiveChanged(): void {
if (network.modelData.active)
network.currentSelected = false;
}
target: network.modelData
}
Connections {
function onNetworkSelected(ap: Nmcli.AccessPoint): void {
if (ap !== network.modelData)
network.currentSelected = false;
}
target: root
}
RowLayout {
id: networkLayout
anchors.fill: parent
anchors.margins: Tokens.padding.large
anchors.leftMargin: Tokens.padding.extraLarge
anchors.rightMargin: Tokens.padding.extraLarge
spacing: Tokens.spacing.medium
MaterialIcon {
text: Icons.getNetworkIcon(network.modelData.strength)
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
opacity: network.textOpacity
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
opacity: network.textOpacity
StyledText {
Layout.fillWidth: true
text: network.modelData.ssid
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: qsTr("Security: %1%2").arg(network.modelData.security).arg(Nmcli.hasSavedProfile(network.modelData.ssid) ? qsTr(" • Saved") : "")
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
}
}
AnimLoader {
sourceComp: Nmcli.connectingSsid() === network.modelData.ssid ? loadingComp : iconComp
Component {
id: iconComp
MaterialIcon {
text: network.modelData.active ? "settings" : "lock"
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
opacity: network.textOpacity
}
}
Component {
id: loadingComp
LoadingIndicator {
implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
}
}
}
}
}
StyledProgressBar {
id: scanningIndicator
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 1
implicitHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0
indeterminate: true
Behavior on implicitHeight {
Anim {
type: Anim.DefaultEffects
}
}
}
}
ConnectedRect {
Layout.fillWidth: true
implicitHeight: addNetworkLayout.implicitHeight + addNetworkLayout.anchors.margins * 2
last: true
StateLayer {}
RowLayout {
id: addNetworkLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
MaterialIcon {
text: "add"
font: Tokens.font.icon.medium
}
StyledText {
Layout.fillWidth: true
text: qsTr("Add network")
font: Tokens.font.body.small
elide: Text.ElideRight
}
}
}
}
}

View file

@ -0,0 +1,46 @@
import QtQuick.Layouts
import Caelestia.Config
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Panels")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
NavRow {
first: true
icon: "dashboard"
label: qsTr("Dashboard")
status: Config.dashboard.enabled ? qsTr("Enabled") : qsTr("Disabled")
onClicked: root.nState.openSubPage(1)
}
NavRow {
icon: "dock_to_bottom"
label: qsTr("Taskbar")
status: Config.bar.persistent ? qsTr("Always visible") : Config.bar.showOnHover ? qsTr("Reveal on hover") : qsTr("Reveal on drag")
onClicked: root.nState.openSubPage(2)
}
NavRow {
icon: "apps"
label: qsTr("Launcher")
status: Config.launcher.enabled ? qsTr("Enabled") : qsTr("Disabled")
onClicked: root.nState.openSubPage(3)
}
NavRow {
last: true
icon: "dock_to_right"
label: qsTr("Sidebar")
status: Config.sidebar.enabled ? qsTr("Enabled") : qsTr("Disabled")
onClicked: root.nState.openSubPage(4)
}
}
}

View file

@ -0,0 +1,225 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Caelestia.Config
import Caelestia.Services
import qs.components
import qs.components.controls
import qs.services
import qs.modules.nexus.common
PageBase {
id: root
// Lyrics backends, ordered to match LyricsBackend::Backend (Auto, Local, LRCLIB, NetEase)
readonly property list<MenuItem> lyricsItems: [
MenuItem {
text: qsTr("Auto")
},
MenuItem {
text: "Local"
},
MenuItem {
text: "LRCLIB"
},
MenuItem {
text: "NetEase"
}
]
// GPU options + the config string each maps to (see Gpu::parseType)
readonly property list<MenuItem> gpuItems: [
MenuItem {
text: qsTr("Auto")
},
MenuItem {
text: "NVIDIA"
},
MenuItem {
text: qsTr("Generic")
},
MenuItem {
text: qsTr("None")
}
]
readonly property list<string> gpuValues: ["", "NVIDIA", "GENERIC", "None"]
function gpuKeyToIndex(key: string): int {
const u = (key ?? "").trim().toUpperCase();
if (u === "")
return 0; // Auto
if (u === "NVIDIA")
return 1;
if (u === "GENERIC")
return 2;
return 3; // None
}
title: qsTr("Services")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// Detected running players, used as default-player options
Variants {
id: playerVariants
model: [...new Set(Players.list.map(p => Players.getIdentity(p)).filter(id => id))]
MenuItem {
required property string modelData
text: modelData
icon: modelData === GlobalConfig.services.defaultPlayer ? "check" : ""
activeIcon: "music_note"
}
}
// Polling
SectionHeader {
first: true
text: qsTr("Polling")
}
StepperRow {
Layout.fillWidth: true
first: true
label: qsTr("Media refresh")
subtext: qsTr("How often the media position updates (ms)")
value: GlobalConfig.dashboard.mediaUpdateInterval
from: 100
to: 2000
stepSize: 50
onMoved: v => GlobalConfig.dashboard.mediaUpdateInterval = v
}
StepperRow {
Layout.fillWidth: true
label: qsTr("System stats refresh")
subtext: qsTr("CPU, memory and GPU update interval (seconds)")
value: GlobalConfig.dashboard.resourceUpdateInterval / 1000
from: 0.5
to: 10
stepSize: 0.5
onMoved: v => GlobalConfig.dashboard.resourceUpdateInterval = Math.round(v * 1000)
}
StepperRow {
Layout.fillWidth: true
last: true
label: qsTr("Wi-Fi rescan")
subtext: qsTr("How often available networks are rescanned (seconds)")
value: GlobalConfig.nexus.networkRescanInterval / 1000
from: 5
to: 120
stepSize: 5
onMoved: v => GlobalConfig.nexus.networkRescanInterval = Math.round(v * 1000)
}
// Media & lyrics
SectionHeader {
text: qsTr("Media & lyrics")
}
SelectRow {
Layout.fillWidth: true
first: true
label: qsTr("Lyrics backend")
subtext: qsTr("Source used to fetch synced lyrics")
menuItems: root.lyricsItems
active: root.lyricsItems[Lyrics.preferredBackend] ?? root.lyricsItems[0]
onSelected: item => Lyrics.preferredBackend = root.lyricsItems.indexOf(item)
}
SelectRow {
Layout.fillWidth: true
last: true
label: qsTr("Default player")
subtext: qsTr("Preferred media player when several are open")
menuItems: playerVariants.instances
active: menuItems.find(i => i.text === GlobalConfig.services.defaultPlayer) ?? null
fallbackIcon: "music_note"
fallbackText: GlobalConfig.services.defaultPlayer || qsTr("Auto")
onSelected: item => GlobalConfig.services.defaultPlayer = item.text
}
// Input increments
SectionHeader {
text: qsTr("Input increments")
}
StepperRow {
Layout.fillWidth: true
first: true
label: qsTr("Volume step")
subtext: qsTr("Amount the volume changes per scroll (%)")
value: Math.round(GlobalConfig.services.audioIncrement * 100)
from: 1
to: 50
stepSize: 1
onMoved: v => GlobalConfig.services.audioIncrement = v / 100
}
StepperRow {
Layout.fillWidth: true
label: qsTr("Brightness step")
subtext: qsTr("Amount the brightness changes per scroll (%)")
value: Math.round(GlobalConfig.services.brightnessIncrement * 100)
from: 1
to: 50
stepSize: 1
onMoved: v => GlobalConfig.services.brightnessIncrement = v / 100
}
StepperRow {
Layout.fillWidth: true
last: true
label: qsTr("Max volume")
subtext: qsTr("Upper limit for output volume (%)")
value: Math.round(GlobalConfig.services.maxVolume * 100)
from: 50
to: 200
stepSize: 5
onMoved: v => GlobalConfig.services.maxVolume = v / 100
}
// Service tuning
SectionHeader {
text: qsTr("Service tuning")
}
StepperRow {
Layout.fillWidth: true
first: true
label: qsTr("Visualiser bars")
subtext: qsTr("Number of bars in the audio visualisers")
value: GlobalConfig.services.visualiserBars
from: 10
to: 120
stepSize: 2
onMoved: v => GlobalConfig.services.visualiserBars = v
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Smart colour scheme")
subtext: qsTr("Derive theme mode and variant from the wallpaper")
checked: GlobalConfig.services.smartScheme
onToggled: GlobalConfig.services.smartScheme = checked
}
SelectRow {
Layout.fillWidth: true
last: true
label: qsTr("GPU")
subtext: Gpu.name ? qsTr("Monitoring: %1").arg(Gpu.name) : qsTr("Override for GPU type")
menuOnTop: true
menuItems: root.gpuItems
active: root.gpuItems[root.gpuKeyToIndex(GlobalConfig.services.gpuType)]
onSelected: item => GlobalConfig.services.gpuType = root.gpuValues[root.gpuItems.indexOf(item)]
}
}
}

View file

@ -0,0 +1,202 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Components
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.components.images
import qs.services
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Wallpaper & style")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.large
StyledClippingRect {
id: wallWrapper
Layout.alignment: Qt.AlignHCenter
implicitWidth: {
const screen = root.nState.screen;
return implicitHeight / screen.height * screen.width;
}
implicitHeight: {
const screen = root.nState.screen;
const cWidth = root.cappedWidth;
return Math.min(Math.round(cWidth * 0.4), cWidth / screen.width * screen.height);
}
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.large
Loader {
anchors.centerIn: parent
opacity: Config.background.wallpaperEnabled ? 0 : 1
active: opacity > 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.extraSmall
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "hide_image"
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.extraLarge
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Wallpaper disabled")
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.large
}
}
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
Item {
anchors.fill: parent
opacity: Config.background.wallpaperEnabled ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
Loader {
id: wallIndicatorLoader
anchors.centerIn: parent
opacity: 0
active: opacity > 0
sourceComponent: StyledRect {
implicitWidth: wallLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
implicitHeight: wallLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
color: Colours.palette.m3primaryContainer
radius: Tokens.rounding.full
LoadingIndicator {
id: wallLoadingIndicator
anchors.centerIn: parent
containsIcon: true
implicitSize: Math.min(wallWrapper.implicitWidth, wallWrapper.implicitHeight) * 0.4
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Timer {
id: wallLoadDebounceTimer
interval: 100
onTriggered: {
if (wallImg.status !== Image.Ready)
wallIndicatorLoader.opacity = 1;
}
}
FadeImage {
id: wallImg
anchors.fill: parent
source: Wallpapers.current
preventInit: wallIndicatorLoader.opacity > 0
fadeOutAnim: Anim.DefaultEffects
fadeInAnim: Anim.SlowEffects
onSourceChanged: wallLoadDebounceTimer.restart()
onStatusChanged: {
if (status === Image.Ready) {
wallLoadDebounceTimer.stop();
wallIndicatorLoader.opacity = 0;
}
}
}
}
}
ButtonRow {
Layout.alignment: Qt.AlignHCenter
spacing: Tokens.spacing.small
IconTextButton {
icon: "wallpaper"
text: qsTr("Wallpapers")
font: Tokens.font.body.large
isRound: true
shapeMorph: true
type: IconTextButton.Tonal
horizontalPadding: Tokens.padding.extraLarge
verticalPadding: Tokens.padding.medium
disabled: !Config.background.wallpaperEnabled
onClicked: root.nState.openSubPage(1) // Wallpaper page
}
IconTextButton {
icon: "palette"
text: qsTr("Colours")
font: Tokens.font.body.large
isRound: true
shapeMorph: true
type: IconTextButton.Tonal
horizontalPadding: Tokens.padding.extraLarge
verticalPadding: Tokens.padding.medium
onClicked: root.nState.openSubPage(3) // Colours page
}
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Display wallpaper")
checked: Config.background.wallpaperEnabled
onToggled: GlobalConfig.background.wallpaperEnabled = checked
}
ToggleRow {
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
Layout.fillWidth: true
text: qsTr("Transparency")
subtext: qsTr("Base %1, layers %2").arg(Colours.transparency.base).arg(Colours.transparency.layers)
checked: Colours.transparency.enabled
onToggled: GlobalConfig.appearance.transparency.enabled = checked
}
ToggleRow {
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
Layout.fillWidth: true
last: true
text: qsTr("Dark theme")
checked: !Colours.light
onToggled: Colours.setMode(checked ? "dark" : "light")
}
}
}

View file

@ -0,0 +1,70 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import Caelestia.Config
import qs.components
import qs.services
import qs.utils
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("App volumes")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
StyledText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.padding.small
Layout.bottomMargin: Tokens.spacing.medium
text: qsTr("Adjust the volume of individual apps currently playing audio.")
color: Colours.palette.m3outline
font: Tokens.font.body.small
wrapMode: Text.WordWrap
}
ItemList {
id: streamList
first: true
last: true
showList: true
placeholderIcon: "music_off"
placeholderText: qsTr("No apps playing audio")
color: list.count === 0 ? Colours.tPalette.m3surfaceContainer : "transparent"
list.spacing: Tokens.spacing.extraSmall / 2
model: ScriptModel {
values: [...Audio.streams]
}
delegate: SliderRow {
id: stream
required property PwNode modelData
required property int index
anchors.left: streamList.list.contentItem.left
anchors.right: streamList.list.contentItem.right
first: index === 0
last: index === streamList.list.count - 1
icon: Icons.getVolumeIcon(stream.modelData?.audio?.volume ?? 0, stream.modelData?.audio?.muted ?? false)
label: Audio.getStreamName(stream.modelData)
valueLabel: Math.round(value * 100) + "%"
value: stream.modelData?.audio?.volume ?? 0
enabled: !stream.modelData?.audio?.muted
onMoved: v => Audio.setStreamVolume(stream.modelData, v)
}
}
}
}

View file

@ -0,0 +1,193 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.utils
import qs.modules.nexus.common
PageBase {
id: root
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter // qmllint disable unresolved-type
function setScan(on: bool): void {
if (adapter?.enabled)
adapter.discovering = on;
}
title: qsTr("Pair new device")
isSubPage: true
Component.onCompleted: setScan(true)
Component.onDestruction: setScan(false)
onVisibleChanged: setScan(visible)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
Connections {
function onEnabledChanged(): void {
if (root.adapter && !root.adapter.enabled)
root.nState.closeSubPage();
}
target: root.adapter
}
ConnectedRect {
Layout.fillWidth: true
implicitHeight: headerText.implicitHeight + Tokens.padding.medium * 2
first: true
StyledText {
id: headerText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Tokens.padding.large
anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.2)
text: qsTr("Available devices")
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.small
}
}
ItemList {
id: deviceList
Layout.fillWidth: true
showList: true
extraHeight: scanIndicator.implicitHeight
last: true
placeholderIcon: "bluetooth_searching"
placeholderText: qsTr("Searching for devices…")
list.anchors.top: scanIndicator.bottom
model: ScriptModel {
values: Bluetooth.devices.values.filter(d => !d.bonded).sort((a, b) => (b.pairing - a.pairing) || a.name.localeCompare(b.name)) // qmllint disable unresolved-type
}
delegate: Item {
id: newDevice
required property BluetoothDevice modelData
required property int index
property real textOpacity: modelData?.pairing ? 0.5 : 1
property bool wasPairing
anchors.left: deviceList.list.contentItem.left
anchors.right: deviceList.list.contentItem.right
implicitHeight: newLayout.implicitHeight + newLayout.anchors.margins * 2
Behavior on textOpacity {
Anim {
type: Anim.DefaultEffects
}
}
Connections {
function onPairedChanged(): void {
if (newDevice.wasPairing && newDevice.modelData?.paired)
root.nState.closeSubPage();
}
target: newDevice.modelData
}
StateLayer {
radius: Tokens.rounding.extraSmall
bottomLeftRadius: newDevice.index === deviceList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
bottomRightRadius: newDevice.index === deviceList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
disabled: newDevice.modelData?.pairing ?? false
onClicked: {
newDevice.modelData?.pair();
newDevice.wasPairing = true;
}
}
RowLayout {
id: newLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
MaterialIcon {
text: Icons.getBluetoothIcon(newDevice.modelData?.icon ?? "")
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
opacity: newDevice.textOpacity
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
opacity: newDevice.textOpacity
StyledText {
Layout.fillWidth: true
text: newDevice.modelData?.name || qsTr("Unknown device")
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: newDevice.modelData?.pairing ? qsTr("Pairing...") : (newDevice.modelData?.address ?? "")
color: Colours.palette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
animate: true
}
}
Loader {
asynchronous: true
active: opacity > 0
opacity: newDevice.modelData?.pairing ? 1 : 0
sourceComponent: LoadingIndicator {
implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}
StyledProgressBar {
id: scanIndicator
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 1
implicitHeight: Tokens.rounding.extraSmall
indeterminate: true
Behavior on implicitHeight {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}
}

View file

@ -0,0 +1,260 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Bluetooth
import Caelestia.Components
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.modules.nexus.common
PageBase {
id: root
readonly property BluetoothDevice device: nState.selectedBtDevice
readonly property bool connected: device?.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type
readonly property bool loading: device?.state === BluetoothDeviceState.Connecting || device?.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type
readonly property string statusText: {
if (!device)
return "";
let s = connected ? qsTr("Connected") : (device.bonded ? qsTr("Paired") : qsTr("Not paired"));
if (connected && device.batteryAvailable)
s += " • " + Math.round(device.battery * 100) + "%";
return s;
}
onDeviceChanged: {
// Auto close when device lost
if (!device)
nState.closeSubPage();
}
title: device?.name ?? qsTr("Device")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// Big buttons
ButtonRow {
Layout.bottomMargin: Tokens.spacing.large - parent.spacing
Layout.alignment: Qt.AlignHCenter
Layout.minimumWidth: Math.round(root.cappedWidth * 0.7)
spacing: Tokens.spacing.small
ButtonBase {
id: forgetBtn
fillWidth: true
shapeMorph: true
isRound: true
inactiveColour: Colours.palette.m3errorContainer
inactiveOnColour: Colours.palette.m3onErrorContainer
implicitWidth: forgetBtnLayout.implicitWidth + Tokens.padding.extraLarge * 2
implicitHeight: forgetBtnLayout.implicitHeight + Tokens.padding.medium * 2
onClicked: {
root.device?.forget();
root.nState.closeSubPage();
}
ColumnLayout {
id: forgetBtnLayout
anchors.centerIn: parent
spacing: 0
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "delete"
color: forgetBtn.onColour
font: Tokens.font.icon.medium
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Forget")
color: forgetBtn.onColour
}
}
}
ButtonBase {
id: connectBtn
fillWidth: true
shapeMorph: true
isRound: true
inactiveColour: Colours.palette.m3primaryContainer
inactiveOnColour: Colours.palette.m3onPrimaryContainer
stateLayer.disabled: root.loading
implicitWidth: connectBtnContent.implicitWidth + Tokens.padding.extraLarge * 2
implicitHeight: connectBtnContent.implicitHeight + Tokens.padding.medium * 2
onClicked: root.device.connected = !root.connected
AnimLoader {
id: connectBtnContent
anchors.centerIn: parent
sourceComp: root.loading ? loadingComp : textComp
outAnimType: Anim.SlowEffects
inAnimType: Anim.SlowEffects
}
Component {
id: loadingComp
LoadingIndicator {
implicitSize: connectBtn.height - Tokens.padding.large * 2
}
}
Component {
id: textComp
ColumnLayout {
spacing: 0
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: root.connected ? "close" : "add"
color: connectBtn.inactiveOnColour
font: Tokens.font.icon.medium
animate: true
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: root.connected ? qsTr("Disconnect") : qsTr("Connect")
color: connectBtn.inactiveOnColour
animate: true
}
}
}
}
}
// Connection group
ToggleRow {
Layout.fillWidth: true
verticalPadding: Tokens.padding.large
first: true
text: qsTr("Trusted")
subtext: qsTr("Allow this device to connect automatically")
checked: root.device?.trusted ?? false
onToggled: {
if (root.device)
root.device.trusted = checked;
}
}
ToggleRow {
Layout.fillWidth: true
verticalPadding: Tokens.padding.large
text: qsTr("Blocked")
subtext: qsTr("Prevent this device from connecting")
checked: root.device?.blocked ?? false
onToggled: {
if (root.device)
root.device.blocked = checked;
}
}
ToggleRow {
Layout.fillWidth: true
verticalPadding: Tokens.padding.large
last: true
text: qsTr("Wake allowed")
subtext: qsTr("Allow this device to wake the system")
checked: root.device?.wakeAllowed ?? false
onToggled: {
if (root.device)
root.device.wakeAllowed = checked;
}
}
// Information
ConnectedRect {
Layout.topMargin: Tokens.spacing.large - parent.spacing
Layout.fillWidth: true
implicitHeight: batteryLayout.implicitHeight + Tokens.padding.large * 2
first: true
ColumnLayout {
id: batteryLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.medium
StyledText {
Layout.fillWidth: true
text: qsTr("Battery")
}
StyledText {
text: root.device?.batteryAvailable ? Math.round(root.device.battery * 100) + "%" : qsTr("Unavailable")
color: Colours.palette.m3outline
font: Tokens.font.body.small
}
}
Loader {
Layout.fillWidth: true
active: root.device?.batteryAvailable ?? false
visible: active
asynchronous: true
sourceComponent: StyledProgressBar {
implicitHeight: Tokens.padding.medium
value: root.device.battery
}
}
}
}
ConnectedRect {
Layout.fillWidth: true
implicitHeight: addrLayout.implicitHeight + Tokens.padding.large * 2
last: true
RowLayout {
id: addrLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.medium
StyledText {
Layout.fillWidth: true
text: qsTr("Address")
}
StyledText {
text: root.device?.address ?? ""
color: Colours.palette.m3outline
font: Tokens.font.body.small
}
}
}
}
}

View file

@ -0,0 +1,146 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Dashboard")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// General
SectionHeader {
first: true
text: qsTr("General")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Enabled")
checked: Config.dashboard.enabled
onToggled: GlobalConfig.dashboard.enabled = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Show on hover")
subtext: qsTr("Reveal when the cursor reaches the screen edge")
checked: Config.dashboard.showOnHover
onToggled: GlobalConfig.dashboard.showOnHover = checked
}
// Tabs
SectionHeader {
text: qsTr("Tabs")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Dashboard")
checked: Config.dashboard.showDashboard
onToggled: GlobalConfig.dashboard.showDashboard = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Media")
checked: Config.dashboard.showMedia
onToggled: GlobalConfig.dashboard.showMedia = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Performance")
checked: Config.dashboard.showPerformance
onToggled: GlobalConfig.dashboard.showPerformance = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Weather")
checked: Config.dashboard.showWeather
onToggled: GlobalConfig.dashboard.showWeather = checked
}
// Performance widgets
SectionHeader {
text: qsTr("Performance widgets")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Battery")
checked: Config.dashboard.performance.showBattery
onToggled: GlobalConfig.dashboard.performance.showBattery = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("GPU")
checked: Config.dashboard.performance.showGpu
onToggled: GlobalConfig.dashboard.performance.showGpu = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("CPU")
checked: Config.dashboard.performance.showCpu
onToggled: GlobalConfig.dashboard.performance.showCpu = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Memory")
checked: Config.dashboard.performance.showMemory
onToggled: GlobalConfig.dashboard.performance.showMemory = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Storage")
checked: Config.dashboard.performance.showStorage
onToggled: GlobalConfig.dashboard.performance.showStorage = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Network")
checked: Config.dashboard.performance.showNetwork
onToggled: GlobalConfig.dashboard.performance.showNetwork = checked
}
// Behaviour
SectionHeader {
text: qsTr("Behaviour")
}
StepperRow {
Layout.fillWidth: true
first: true
last: true
label: qsTr("Drag threshold")
subtext: qsTr("Pixels dragged before the dashboard opens")
value: Config.dashboard.dragThreshold
from: 0
to: 200
stepSize: 5
onMoved: v => GlobalConfig.dashboard.dragThreshold = v
}
}
}

View file

@ -0,0 +1,147 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Launcher")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// General
SectionHeader {
first: true
text: qsTr("General")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Enabled")
checked: Config.launcher.enabled
onToggled: GlobalConfig.launcher.enabled = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Show on hover")
subtext: qsTr("Reveal when the cursor reaches the screen edge")
checked: Config.launcher.showOnHover
onToggled: GlobalConfig.launcher.showOnHover = checked
}
// Display
SectionHeader {
text: qsTr("Display")
}
StepperRow {
Layout.fillWidth: true
first: true
label: qsTr("Max items shown")
value: Config.launcher.maxShown
from: 1
to: 20
stepSize: 1
onMoved: v => GlobalConfig.launcher.maxShown = v
}
StepperRow {
Layout.fillWidth: true
label: qsTr("Max wallpapers")
value: Config.launcher.maxWallpapers
from: 1
to: 30
stepSize: 1
onMoved: v => GlobalConfig.launcher.maxWallpapers = v
}
StepperRow {
Layout.fillWidth: true
last: true
label: qsTr("Drag threshold")
subtext: qsTr("Pixels dragged before the launcher opens")
value: Config.launcher.dragThreshold
from: 0
to: 200
stepSize: 5
onMoved: v => GlobalConfig.launcher.dragThreshold = v
}
// Behaviour
SectionHeader {
text: qsTr("Behaviour")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Vim keybinds")
subtext: qsTr("Navigate results with Ctrl+hjkl")
checked: GlobalConfig.launcher.vimKeybinds
onToggled: GlobalConfig.launcher.vimKeybinds = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Enable dangerous actions")
subtext: qsTr("Allow actions that shut down or log out")
checked: GlobalConfig.launcher.enableDangerousActions
onToggled: GlobalConfig.launcher.enableDangerousActions = checked
}
// Fuzzy search
SectionHeader {
text: qsTr("Fuzzy search")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Apps")
checked: GlobalConfig.launcher.useFuzzy.apps
onToggled: GlobalConfig.launcher.useFuzzy.apps = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Actions")
checked: GlobalConfig.launcher.useFuzzy.actions
onToggled: GlobalConfig.launcher.useFuzzy.actions = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Schemes")
checked: GlobalConfig.launcher.useFuzzy.schemes
onToggled: GlobalConfig.launcher.useFuzzy.schemes = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Variants")
checked: GlobalConfig.launcher.useFuzzy.variants
onToggled: GlobalConfig.launcher.useFuzzy.variants = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Wallpapers")
checked: GlobalConfig.launcher.useFuzzy.wallpapers
onToggled: GlobalConfig.launcher.useFuzzy.wallpapers = checked
}
}
}

View file

@ -0,0 +1,46 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Sidebar")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
SectionHeader {
first: true
text: qsTr("General")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Enabled")
checked: Config.sidebar.enabled
onToggled: GlobalConfig.sidebar.enabled = checked
}
StepperRow {
Layout.fillWidth: true
last: true
label: qsTr("Drag threshold")
subtext: qsTr("Pixels dragged before the sidebar opens")
value: Config.sidebar.dragThreshold
from: 0
to: 200
stepSize: 5
onMoved: v => GlobalConfig.sidebar.dragThreshold = v
}
}
}

View file

@ -0,0 +1,127 @@
pragma ComponentBehavior: Bound
import QtQuick.Layouts
import Caelestia.Config
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Taskbar")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// Behaviour
SectionHeader {
first: true
text: qsTr("Behaviour")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Persistent")
subtext: qsTr("Keep the bar visible at all times")
checked: Config.bar.persistent
onToggled: GlobalConfig.bar.persistent = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Show on hover")
subtext: qsTr("Reveal the bar when the cursor reaches the screen edge")
checked: Config.bar.showOnHover
onToggled: GlobalConfig.bar.showOnHover = checked
}
StepperRow {
Layout.fillWidth: true
last: true
label: qsTr("Drag threshold")
subtext: qsTr("Pixels dragged before the bar reveals")
value: Config.bar.dragThreshold
from: 0
to: 200
stepSize: 5
onMoved: v => GlobalConfig.bar.dragThreshold = v
}
// Components
SectionHeader {
text: qsTr("Components")
}
NavRow {
first: true
icon: "workspaces"
label: qsTr("Workspaces")
status: qsTr("Indicators, window icons")
onClicked: root.nState.openSubPage(5)
}
NavRow {
icon: "web_asset"
label: qsTr("Active window")
status: qsTr("Title display, popout")
onClicked: root.nState.openSubPage(6)
}
NavRow {
icon: "widgets"
label: qsTr("Tray")
status: qsTr("System tray icons")
onClicked: root.nState.openSubPage(7)
}
NavRow {
icon: "signal_cellular_alt"
label: qsTr("Status icons")
status: qsTr("Visible indicators")
onClicked: root.nState.openSubPage(8)
}
NavRow {
last: true
icon: "schedule"
label: qsTr("Clock")
status: qsTr("Date, icon, background")
onClicked: root.nState.openSubPage(9)
}
// Scroll actions
SectionHeader {
text: qsTr("Scroll actions")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Workspaces")
subtext: qsTr("Scroll over the workspace indicator to switch workspaces")
checked: Config.bar.scrollActions.workspaces
onToggled: GlobalConfig.bar.scrollActions.workspaces = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Volume")
subtext: qsTr("Scroll on the top half of the bar to adjust volume")
checked: Config.bar.scrollActions.volume
onToggled: GlobalConfig.bar.scrollActions.volume = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Brightness")
subtext: qsTr("Scroll on the bottom half of the bar to adjust brightness")
checked: Config.bar.scrollActions.brightness
onToggled: GlobalConfig.bar.scrollActions.brightness = checked
}
}
}

View file

@ -0,0 +1,51 @@
pragma ComponentBehavior: Bound
import QtQuick.Layouts
import Caelestia.Config
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Active window")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Compact")
checked: Config.bar.activeWindow.compact
onToggled: GlobalConfig.bar.activeWindow.compact = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Inverted")
checked: Config.bar.activeWindow.inverted
onToggled: GlobalConfig.bar.activeWindow.inverted = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Show on hover")
subtext: qsTr("Only show the active window title while hovering")
checked: Config.bar.activeWindow.showOnHover
onToggled: GlobalConfig.bar.activeWindow.showOnHover = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Popout on hover")
subtext: qsTr("Show a window details popout when hovering")
checked: Config.bar.popouts.activeWindow
onToggled: GlobalConfig.bar.popouts.activeWindow = checked
}
}
}

View file

@ -0,0 +1,42 @@
pragma ComponentBehavior: Bound
import QtQuick.Layouts
import Caelestia.Config
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Clock")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Background")
checked: Config.bar.clock.background
onToggled: GlobalConfig.bar.clock.background = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Show date")
checked: Config.bar.clock.showDate
onToggled: GlobalConfig.bar.clock.showDate = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Show icon")
checked: Config.bar.clock.showIcon
onToggled: GlobalConfig.bar.clock.showIcon = checked
}
}
}

View file

@ -0,0 +1,98 @@
pragma ComponentBehavior: Bound
import QtQuick.Layouts
import Caelestia.Config
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Status icons")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// Visible icons
SectionHeader {
first: true
text: qsTr("Visible icons")
}
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Speakers")
checked: Config.bar.status.showAudio
onToggled: GlobalConfig.bar.status.showAudio = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Microphone")
checked: Config.bar.status.showMicrophone
onToggled: GlobalConfig.bar.status.showMicrophone = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Keyboard layout")
checked: Config.bar.status.showKbLayout
onToggled: GlobalConfig.bar.status.showKbLayout = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Network")
checked: Config.bar.status.showNetwork
onToggled: GlobalConfig.bar.status.showNetwork = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Wi-Fi")
checked: Config.bar.status.showWifi
onToggled: GlobalConfig.bar.status.showWifi = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Bluetooth")
checked: Config.bar.status.showBluetooth
onToggled: GlobalConfig.bar.status.showBluetooth = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Battery")
checked: Config.bar.status.showBattery
onToggled: GlobalConfig.bar.status.showBattery = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Caps lock")
checked: Config.bar.status.showLockStatus
onToggled: GlobalConfig.bar.status.showLockStatus = checked
}
// Behaviour
SectionHeader {
text: qsTr("Behaviour")
}
ToggleRow {
Layout.fillWidth: true
first: true
last: true
text: qsTr("Popout on hover")
subtext: qsTr("Show a details popout when hovering the status icons")
checked: Config.bar.popouts.statusIcons
onToggled: GlobalConfig.bar.popouts.statusIcons = checked
}
}
}

View file

@ -0,0 +1,50 @@
pragma ComponentBehavior: Bound
import QtQuick.Layouts
import Caelestia.Config
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Tray")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Background")
checked: Config.bar.tray.background
onToggled: GlobalConfig.bar.tray.background = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Recolour icons")
checked: Config.bar.tray.recolour
onToggled: GlobalConfig.bar.tray.recolour = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Compact")
checked: Config.bar.tray.compact
onToggled: GlobalConfig.bar.tray.compact = checked
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Popout on hover")
subtext: qsTr("Show the tray menu popout when hovering")
checked: Config.bar.popouts.tray
onToggled: GlobalConfig.bar.popouts.tray = checked
}
}
}

View file

@ -0,0 +1,86 @@
pragma ComponentBehavior: Bound
import QtQuick.Layouts
import Caelestia.Config
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Workspaces")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
StepperRow {
Layout.fillWidth: true
first: true
label: qsTr("Shown")
subtext: qsTr("Number of workspaces displayed")
value: Config.bar.workspaces.shown
from: 1
to: 20
stepSize: 1
onMoved: v => GlobalConfig.bar.workspaces.shown = v
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Active indicator")
checked: Config.bar.workspaces.activeIndicator
onToggled: GlobalConfig.bar.workspaces.activeIndicator = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Active trail")
checked: Config.bar.workspaces.activeTrail
onToggled: GlobalConfig.bar.workspaces.activeTrail = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Occupied background")
checked: Config.bar.workspaces.occupiedBg
onToggled: GlobalConfig.bar.workspaces.occupiedBg = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Show windows")
subtext: qsTr("Show icons of open windows on each workspace")
checked: Config.bar.workspaces.showWindows
onToggled: GlobalConfig.bar.workspaces.showWindows = checked
}
ToggleRow {
Layout.fillWidth: true
text: qsTr("Windows on special workspaces")
checked: Config.bar.workspaces.showWindowsOnSpecialWorkspaces
onToggled: GlobalConfig.bar.workspaces.showWindowsOnSpecialWorkspaces = checked
}
StepperRow {
Layout.fillWidth: true
label: qsTr("Max window icons")
value: Config.bar.workspaces.maxWindowIcons
from: 0
to: 20
stepSize: 1
onMoved: v => GlobalConfig.bar.workspaces.maxWindowIcons = v
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Per-monitor workspaces")
subtext: qsTr("Show each monitor's workspaces independently")
checked: GlobalConfig.bar.workspaces.perMonitorWorkspaces
onToggled: GlobalConfig.bar.workspaces.perMonitorWorkspaces = checked
}
}
}

View file

@ -0,0 +1,47 @@
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Colours")
isSubPage: true
Item {
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: {
const f = parent.parent as Flickable;
return f.height - f.topMargin - f.bottomMargin;
}
ColumnLayout {
anchors.centerIn: parent
spacing: Tokens.padding.extraSmall
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "handyman"
color: Colours.palette.m3outlineVariant
font: Tokens.font.icon.extraLarge
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Page under construction")
color: Colours.palette.m3outlineVariant
font: Tokens.font.title.large
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("This page will be available in a future update.")
color: Colours.palette.m3outlineVariant
font: Tokens.font.body.large
}
}
}
}

View file

@ -0,0 +1,53 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Caelestia.Config
import Caelestia.Models
import qs.services
import qs.modules.nexus.common
PageBase {
id: root
title: {
const c = nState.selectedWallpaperCategory;
return c.slice(0, 1).toUpperCase() + c.slice(1);
}
isSubPage: true
GridLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
columns: Config.nexus.wallpapersPerRow
rowSpacing: Tokens.spacing.medium
columnSpacing: Tokens.spacing.large
Repeater {
model: {
const walls = Wallpapers.list.filter(w => Wallpapers.getCategoryFor(w) === root.nState.selectedWallpaperCategory).sort((a, b) => a.name.localeCompare(b.name));
while (walls.length < Config.nexus.wallpapersPerRow)
walls.push(null);
return walls;
}
WallItem {
required property FileSystemEntry modelData
// Empty placeholders for sizing
opacity: modelData ? 1 : 0
enabled: modelData
source: String(modelData?.path ?? "")
text: modelData?.name ?? ""
onClicked: {
Wallpapers.setWallpaper(modelData.path);
root.nState.closeSubPage();
root.nState.closeSubPage();
}
}
}
}
}

View file

@ -0,0 +1,184 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Caelestia.Components
import Caelestia.Config
import Caelestia.Models
import qs.components
import qs.components.controls
import qs.components.filedialog
import qs.services
import qs.utils
import qs.modules.nexus.common
PageBase {
id: root
title: qsTr("Wallpapers")
isSubPage: true
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.small
ButtonRow {
Layout.bottomMargin: Tokens.spacing.medium
Layout.alignment: Qt.AlignHCenter
spacing: Tokens.spacing.small
IconTextButton {
icon: "photo_library"
text: qsTr("Browse")
font: Tokens.font.body.large
isRound: true
shapeMorph: true
horizontalPadding: Tokens.padding.extraLarge
verticalPadding: Tokens.padding.medium
onClicked: browseDialog.open()
FileDialog {
id: browseDialog
title: qsTr("Select an image")
filterLabel: qsTr("Image files")
filters: Images.validImageExtensions
onAccepted: path => {
Wallpapers.setWallpaper(path);
root.nState.closeSubPage();
}
}
}
IconTextButton {
icon: "shuffle"
text: qsTr("Random")
font: Tokens.font.body.large
isRound: true
shapeMorph: true
horizontalPadding: Tokens.padding.extraLarge
verticalPadding: Tokens.padding.medium
type: IconTextButton.Tonal
onClicked: {
Wallpapers.setRandom();
root.nState.closeSubPage();
}
}
}
WallItem {
imgHeight: Math.round(width * 0.3)
radius: Tokens.rounding.extraLarge
source: Quickshell.shellPath("assets/wallpaper.webp")
text: qsTr("Featured wallpaper")
fillLabel: false
onClicked: {
Wallpapers.setWallpaper(Quickshell.shellPath("assets/wallpaper.webp"));
root.nState.closeSubPage();
}
}
StyledText {
Layout.topMargin: Tokens.spacing.large
text: qsTr("Local wallpapers")
font: Tokens.font.title.small
}
GridLayout {
Layout.fillWidth: true
visible: localWalls.count > 0
columns: Config.nexus.wallpapersPerRow
rowSpacing: Tokens.spacing.medium
columnSpacing: Tokens.spacing.large
Repeater {
id: localWalls
model: {
const walls = Wallpapers.list;
const baseDir = Paths.wallsdir;
const categories = {};
for (const w of walls) {
if (w.parentDir !== baseDir) {
const category = Wallpapers.getCategoryFor(w);
if (category && (!(category in categories) || categories[category].name.localeCompare(w.name) > 0))
categories[category] = w;
}
}
const cats = Object.values(categories);
while (cats.length < Config.nexus.wallpapersPerRow)
cats.push(null);
return cats;
}
WallItem {
required property FileSystemEntry modelData
// Empty placeholders for sizing
opacity: modelData ? 1 : 0
enabled: modelData
source: String(modelData?.path ?? "")
text: {
if (!modelData)
return "";
if (modelData.parentDir !== Paths.wallsdir) {
const category = Wallpapers.getCategoryFor(modelData);
return category.slice(0, 1).toUpperCase() + category.slice(1);
}
return modelData.name;
}
onClicked: {
if (modelData.parentDir !== Paths.wallsdir) {
root.nState.selectedWallpaperCategory = Wallpapers.getCategoryFor(modelData);
root.nState.openSubPage(2); // Category page
} else {
Wallpapers.setWallpaper(modelData.path);
root.nState.closeSubPage();
}
}
}
}
}
Loader {
Layout.fillWidth: true
asynchronous: true
active: localWalls.count === 0
visible: active
sourceComponent: StyledRect {
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraLarge
implicitHeight: noWallsLayout.implicitHeight + Tokens.padding.extraExtraLarge * 2
ColumnLayout {
id: noWallsLayout
anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "hide_image"
color: Colours.palette.m3outline
fontStyle: Tokens.font.icon.extraLarge
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("No local wallpapers found")
color: Colours.palette.m3outline
font: Tokens.font.title.small
}
}
}
}
}
}

View file

@ -8,6 +8,7 @@ import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.modules.nexus
import qs.modules.bar.popouts as BarPopouts
StyledRect {
@ -118,7 +119,7 @@ StyledRect {
isToggle: false
onClicked: {
root.visibilities.utilities = false;
root.popouts.detach("network");
WindowFactory.create();
}
}
}

View file

@ -15,6 +15,9 @@ qml_module(caelestia
PkgConfig::Qalculate
)
# Bake the shell version in so CUtils can expose it without spawning a process
target_compile_definitions(caelestia PRIVATE CAELESTIA_VERSION="${VERSION}")
add_subdirectory(Components)
add_subdirectory(Config)
add_subdirectory(Internal)

View file

@ -20,6 +20,7 @@ qml_module(caelestia-config
generalconfig.hpp
launcherconfig.hpp
lockconfig.hpp
nexusconfig.hpp
notifsconfig.hpp
osdconfig.hpp
serviceconfig.hpp

View file

@ -221,7 +221,7 @@ public:
m_label->setDefaultFamily(sans);
m_label->large()->setDefaults(14, QFont::Medium, vaxes);
m_label->medium()->setDefaults(12, QFont::Medium, vaxes);
m_label->small()->setDefaults(11, QFont::Medium, vaxes);
m_label->small()->setDefaults(11, QFont::Normal, vaxes);
m_mono->setDefaultFamily(mono);
m_mono->large()->setDefaults(16, QFont::Normal);

View file

@ -9,6 +9,7 @@
#include "launcherconfig.hpp"
#include "lockconfig.hpp"
#include "monitorconfigmanager.hpp"
#include "nexusconfig.hpp"
#include "notifsconfig.hpp"
#include "osdconfig.hpp"
#include "serviceconfig.hpp"
@ -42,14 +43,15 @@ GlobalConfig::GlobalConfig(QObject* parent)
, m_dashboard(new DashboardConfig(this))
, m_controlCenter(new ControlCenterConfig(this))
, m_launcher(new LauncherConfig(this))
, m_lock(new LockConfig(this))
, m_nexus(new NexusConfig(this))
, m_notifs(new NotifsConfig(this))
, m_osd(new OsdConfig(this))
, m_session(new SessionConfig(this))
, m_winfo(new WInfoConfig(this))
, m_lock(new LockConfig(this))
, m_utilities(new UtilitiesConfig(this))
, m_sidebar(new SidebarConfig(this))
, m_services(new ServiceConfig(this))
, m_session(new SessionConfig(this))
, m_sidebar(new SidebarConfig(this))
, m_utilities(new UtilitiesConfig(this))
, m_winfo(new WInfoConfig(this))
, m_paths(new UserPaths(this)) {
setupFileBackend(configDir() + QStringLiteral("shell.json"));
}
@ -64,14 +66,15 @@ GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, cons
, m_dashboard(new DashboardConfig(this))
, m_controlCenter(new ControlCenterConfig(this))
, m_launcher(new LauncherConfig(this))
, m_lock(new LockConfig(this))
, m_nexus(new NexusConfig(this))
, m_notifs(new NotifsConfig(this))
, m_osd(new OsdConfig(this))
, m_session(new SessionConfig(this))
, m_winfo(new WInfoConfig(this))
, m_lock(new LockConfig(this))
, m_utilities(new UtilitiesConfig(this))
, m_sidebar(new SidebarConfig(this))
, m_services(new ServiceConfig(this))
, m_session(new SessionConfig(this))
, m_sidebar(new SidebarConfig(this))
, m_utilities(new UtilitiesConfig(this))
, m_winfo(new WInfoConfig(this))
, m_paths(new UserPaths(this)) {
if (!filePath.isEmpty())
setupFileBackend(filePath, screen);

View file

@ -15,6 +15,7 @@ class DashboardConfig;
class GeneralConfig;
class LauncherConfig;
class LockConfig;
class NexusConfig;
class NotifsConfig;
class OsdConfig;
class ServiceConfig;
@ -37,6 +38,7 @@ class GlobalConfig : public RootConfig {
Q_MOC_INCLUDE("generalconfig.hpp")
Q_MOC_INCLUDE("launcherconfig.hpp")
Q_MOC_INCLUDE("lockconfig.hpp")
Q_MOC_INCLUDE("nexusconfig.hpp")
Q_MOC_INCLUDE("notifsconfig.hpp")
Q_MOC_INCLUDE("osdconfig.hpp")
Q_MOC_INCLUDE("serviceconfig.hpp")
@ -55,14 +57,15 @@ class GlobalConfig : public RootConfig {
CONFIG_SUBOBJECT(DashboardConfig, dashboard)
CONFIG_SUBOBJECT(ControlCenterConfig, controlCenter)
CONFIG_SUBOBJECT(LauncherConfig, launcher)
CONFIG_SUBOBJECT(LockConfig, lock)
CONFIG_SUBOBJECT(NexusConfig, nexus)
CONFIG_SUBOBJECT(NotifsConfig, notifs)
CONFIG_SUBOBJECT(OsdConfig, osd)
CONFIG_SUBOBJECT(SessionConfig, session)
CONFIG_SUBOBJECT(WInfoConfig, winfo)
CONFIG_SUBOBJECT(LockConfig, lock)
CONFIG_SUBOBJECT(UtilitiesConfig, utilities)
CONFIG_SUBOBJECT(SidebarConfig, sidebar)
CONFIG_SUBOBJECT(ServiceConfig, services)
CONFIG_SUBOBJECT(SessionConfig, session)
CONFIG_SUBOBJECT(SidebarConfig, sidebar)
CONFIG_SUBOBJECT(UtilitiesConfig, utilities)
CONFIG_SUBOBJECT(WInfoConfig, winfo)
CONFIG_SUBOBJECT(UserPaths, paths)
public:

View file

@ -73,14 +73,15 @@ CONFIG_ATTACHED_GETTER(BorderConfig, border)
CONFIG_ATTACHED_GETTER(DashboardConfig, dashboard)
CONFIG_ATTACHED_GETTER(ControlCenterConfig, controlCenter)
CONFIG_ATTACHED_GETTER(LauncherConfig, launcher)
CONFIG_ATTACHED_GETTER(LockConfig, lock)
CONFIG_ATTACHED_GETTER(NexusConfig, nexus)
CONFIG_ATTACHED_GETTER(NotifsConfig, notifs)
CONFIG_ATTACHED_GETTER(OsdConfig, osd)
CONFIG_ATTACHED_GETTER(SessionConfig, session)
CONFIG_ATTACHED_GETTER(WInfoConfig, winfo)
CONFIG_ATTACHED_GETTER(LockConfig, lock)
CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities)
CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar)
CONFIG_ATTACHED_GETTER(ServiceConfig, services)
CONFIG_ATTACHED_GETTER(SessionConfig, session)
CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar)
CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities)
CONFIG_ATTACHED_GETTER(WInfoConfig, winfo)
CONFIG_ATTACHED_GETTER(UserPaths, paths)
#undef CONFIG_ATTACHED_GETTER

View file

@ -21,6 +21,7 @@ class Config : public QQuickAttachedPropertyPropagator, public QQmlParserStatus
Q_MOC_INCLUDE("generalconfig.hpp")
Q_MOC_INCLUDE("launcherconfig.hpp")
Q_MOC_INCLUDE("lockconfig.hpp")
Q_MOC_INCLUDE("nexusconfig.hpp")
Q_MOC_INCLUDE("notifsconfig.hpp")
Q_MOC_INCLUDE("osdconfig.hpp")
Q_MOC_INCLUDE("serviceconfig.hpp")
@ -39,14 +40,15 @@ class Config : public QQuickAttachedPropertyPropagator, public QQmlParserStatus
Q_PROPERTY(const caelestia::config::DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::LauncherConfig* launcher READ launcher NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::NexusConfig* nexus READ nexus NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::NotifsConfig* notifs READ notifs NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::OsdConfig* osd READ osd NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::ServiceConfig* services READ services NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged)
Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged)
public:
@ -63,14 +65,15 @@ public:
[[nodiscard]] const DashboardConfig* dashboard() const;
[[nodiscard]] const ControlCenterConfig* controlCenter() const;
[[nodiscard]] const LauncherConfig* launcher() const;
[[nodiscard]] const LockConfig* lock() const;
[[nodiscard]] const NexusConfig* nexus() const;
[[nodiscard]] const NotifsConfig* notifs() const;
[[nodiscard]] const OsdConfig* osd() const;
[[nodiscard]] const SessionConfig* session() const;
[[nodiscard]] const WInfoConfig* winfo() const;
[[nodiscard]] const LockConfig* lock() const;
[[nodiscard]] const UtilitiesConfig* utilities() const;
[[nodiscard]] const SidebarConfig* sidebar() const;
[[nodiscard]] const ServiceConfig* services() const;
[[nodiscard]] const SessionConfig* session() const;
[[nodiscard]] const SidebarConfig* sidebar() const;
[[nodiscard]] const UtilitiesConfig* utilities() const;
[[nodiscard]] const WInfoConfig* winfo() const;
[[nodiscard]] const UserPaths* paths() const;
[[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen);

View file

@ -0,0 +1,19 @@
#pragma once
#include "configobject.hpp"
namespace caelestia::config {
class NexusConfig : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CONFIG_PROPERTY(int, wallpapersPerRow, 4)
CONFIG_GLOBAL_PROPERTY(int, networkRescanInterval, 15000)
public:
explicit NexusConfig(QObject* parent = nullptr)
: ConfigObject(parent) {}
};
} // namespace caelestia::config

View file

@ -32,7 +32,6 @@ class ServiceConfig : public ConfigObject {
CONFIG_GLOBAL_PROPERTY(QString, defaultPlayer, u"Spotify"_s)
CONFIG_GLOBAL_PROPERTY(QVariantList, playerAliases,
{ vmap({ { u"from"_s, u"com.github.th_ch.youtube_music"_s }, { u"to"_s, u"YT Music"_s } }) })
CONFIG_GLOBAL_PROPERTY(bool, showLyrics, false)
CONFIG_GLOBAL_PROPERTY(QString, lyricsBackend, u"Auto"_s)
public:

View file

@ -323,6 +323,21 @@ public:
: ConfigObject(parent) {}
};
class NexusTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CONFIG_PROPERTY(qreal, heightMult, 0.7)
CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0)
CONFIG_PROPERTY(int, minWidth, 800)
CONFIG_PROPERTY(int, minHeight, 500)
CONFIG_PROPERTY(int, maxNavWidth, 600)
public:
explicit NexusTokens(QObject* parent = nullptr)
: ConfigObject(parent) {}
};
class SizeTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
@ -338,6 +353,7 @@ class SizeTokens : public ConfigObject {
CONFIG_SUBOBJECT(LockTokens, lock)
CONFIG_SUBOBJECT(WInfoTokens, winfo)
CONFIG_SUBOBJECT(ControlCenterTokens, controlCenter)
CONFIG_SUBOBJECT(NexusTokens, nexus)
public:
explicit SizeTokens(QObject* parent = nullptr)
@ -352,7 +368,8 @@ public:
, m_utilities(new UtilitiesTokens(this))
, m_lock(new LockTokens(this))
, m_winfo(new WInfoTokens(this))
, m_controlCenter(new ControlCenterTokens(this)) {}
, m_controlCenter(new ControlCenterTokens(this))
, m_nexus(new NexusTokens(this)) {}
};
class TokenConfig : public RootConfig {

View file

@ -142,4 +142,16 @@ qreal CUtils::clamp(qreal value, qreal min, qreal max) {
return qBound(min, value, max);
}
#ifndef CAELESTIA_VERSION
#define CAELESTIA_VERSION ""
#endif
QString CUtils::version() const {
return QStringLiteral(CAELESTIA_VERSION);
}
QString CUtils::qtVersion() const {
return QStringLiteral(QT_VERSION_STR);
}
} // namespace caelestia

View file

@ -11,6 +11,9 @@ class CUtils : public QObject {
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QString version READ version CONSTANT)
Q_PROPERTY(QString qtVersion READ qtVersion CONSTANT)
public:
// clang-format off
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path);
@ -26,6 +29,9 @@ public:
Q_INVOKABLE static QString toLocalFile(const QUrl& url);
Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max);
[[nodiscard]] QString version() const;
[[nodiscard]] QString qtVersion() const;
};
} // namespace caelestia

View file

@ -12,6 +12,7 @@ Singleton {
property var wirelessInterfaces: []
property var ethernetInterfaces: []
property bool isConnected: false
readonly property bool connecting: wirelessInterfaces.some(i => isConnectingState(i.state))
property string activeInterface: ""
property string activeConnection: ""
property bool wifiEnabled: true
@ -166,6 +167,15 @@ Singleton {
return state === "100 (connected)" || state === "connected" || state.startsWith("connected");
}
function isConnectingState(state: string): bool {
return !!state && state.startsWith("connecting");
}
function connectingSsid(): string {
const iface = root.wirelessInterfaces.find(i => isConnectingState(i.state));
return iface ? iface.connection : "";
}
function executeCommand(args: list<string>, callback: var): void {
const proc = commandProc.createObject(root);
proc.cmdArgs = ["nmcli", ...args];

View file

@ -20,6 +20,17 @@ Searcher {
property string actualCurrent
property bool previewColourLock
function getCategoryFor(w: FileSystemEntry): string {
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
if (category.includes("/"))
category = category.slice(0, category.indexOf("/"));
return category;
}
function setRandom(): void {
Quickshell.execDetached(["caelestia", "wallpaper", "-r", ...smartArg]);
}
function setWallpaper(path: string): void {
actualCurrent = path;
Quickshell.execDetached(["caelestia", "wallpaper", "-f", path, ...smartArg]);

View file

@ -21,6 +21,28 @@ Singleton {
readonly property string wm: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP")
readonly property string shell: Quickshell.env("SHELL").split("/").pop()
property string kernel
property string hostname
property string firmware
// DMI vendor/model, combined into a single human-readable device name
property string boardVendor
property string boardName
readonly property string device: {
if (!boardName)
return boardVendor;
if (!boardVendor || boardName.toLowerCase().startsWith(boardVendor.toLowerCase()))
return boardName;
return `${boardVendor} ${boardName}`;
}
// Strips the placeholder strings OEMs commonly leave in DMI fields
function sanitiseDmi(s: string): string {
const t = s.trim();
const junk = ["to be filled by o.e.m.", "system product name", "system manufacturer", "system version", "default string", "o.e.m.", "not specified", "not applicable", "unknown", "none", ""];
return junk.includes(t.toLowerCase()) ? "" : t;
}
FileView {
id: osRelease
@ -57,6 +79,34 @@ Singleton {
target: GlobalConfig.general
}
FileView {
path: "/proc/sys/kernel/osrelease"
onLoaded: root.kernel = text().trim()
}
FileView {
path: "/proc/sys/kernel/hostname"
onLoaded: root.hostname = text().trim()
}
FileView {
path: "/sys/class/dmi/id/sys_vendor"
printErrors: false
onLoaded: root.boardVendor = root.sanitiseDmi(text())
}
FileView {
path: "/sys/class/dmi/id/product_name"
printErrors: false
onLoaded: root.boardName = root.sanitiseDmi(text())
}
FileView {
path: "/sys/class/dmi/id/bios_version"
printErrors: false
onLoaded: root.firmware = root.sanitiseDmi(text())
}
Timer {
running: true
repeat: true