Merge branch 'main' into sdfs

This commit is contained in:
2 * r + 2 * t 2026-03-30 02:10:22 +11:00
commit b57d596230
35 changed files with 1538 additions and 206 deletions

View file

@ -55,6 +55,7 @@ Elevation {
function onClicked(): void { function onClicked(): void {
root.itemSelected(item.modelData); root.itemSelected(item.modelData);
root.active = item.modelData; root.active = item.modelData;
item.modelData.clicked();
root.expanded = false; root.expanded = false;
} }

View file

@ -267,11 +267,13 @@ Singleton {
function serializeNotifs(): var { function serializeNotifs(): var {
return { return {
expire: notifs.expire, expire: notifs.expire,
fullscreen: notifs.fullscreen,
defaultExpireTimeout: notifs.defaultExpireTimeout, defaultExpireTimeout: notifs.defaultExpireTimeout,
clearThreshold: notifs.clearThreshold, clearThreshold: notifs.clearThreshold,
expandThreshold: notifs.expandThreshold, expandThreshold: notifs.expandThreshold,
actionOnClick: notifs.actionOnClick, actionOnClick: notifs.actionOnClick,
groupPreviewNum: notifs.groupPreviewNum groupPreviewNum: notifs.groupPreviewNum,
openExpanded: notifs.openExpanded
}; };
} }
@ -318,11 +320,30 @@ Singleton {
} }
function serializeUtilities(): var { function serializeUtilities(): var {
const vpnProviders = [];
for (let i = 0; i < utilities.vpn.provider.length; i++) {
const p = utilities.vpn.provider[i];
const provider = {
displayName: p.displayName,
enabled: p.enabled,
iface: p.iface,
name: p.name
};
if (p.connectCmd && p.connectCmd.length > 0) {
provider.connectCmd = p.connectCmd;
}
if (p.disconnectCmd && p.disconnectCmd.length > 0) {
provider.disconnectCmd = p.disconnectCmd;
}
vpnProviders.push(provider);
}
return { return {
enabled: utilities.enabled, enabled: utilities.enabled,
maxToasts: utilities.maxToasts, maxToasts: utilities.maxToasts,
toasts: { toasts: {
configLoaded: utilities.toasts.configLoaded, configLoaded: utilities.toasts.configLoaded,
fullscreen: utilities.toasts.fullscreen,
chargingChanged: utilities.toasts.chargingChanged, chargingChanged: utilities.toasts.chargingChanged,
gameModeChanged: utilities.toasts.gameModeChanged, gameModeChanged: utilities.toasts.gameModeChanged,
dndChanged: utilities.toasts.dndChanged, dndChanged: utilities.toasts.dndChanged,
@ -336,7 +357,7 @@ Singleton {
}, },
vpn: { vpn: {
enabled: utilities.vpn.enabled, enabled: utilities.vpn.enabled,
provider: utilities.vpn.provider provider: vpnProviders
}, },
quickToggles: utilities.quickToggles quickToggles: utilities.quickToggles
}; };

View file

@ -2,6 +2,7 @@ import Quickshell.Io
JsonObject { JsonObject {
property bool expire: true property bool expire: true
property string fullscreen: "on"
property int defaultExpireTimeout: 5000 property int defaultExpireTimeout: 5000
property real clearThreshold: 0.3 property real clearThreshold: 0.3
property int expandThreshold: 20 property int expandThreshold: 20

View file

@ -46,6 +46,7 @@ JsonObject {
component Toasts: JsonObject { component Toasts: JsonObject {
property bool configLoaded: true property bool configLoaded: true
property string fullscreen: "off"
property bool chargingChanged: true property bool chargingChanged: true
property bool gameModeChanged: true property bool gameModeChanged: true
property bool dndChanged: true property bool dndChanged: true
@ -61,6 +62,6 @@ JsonObject {
component Vpn: JsonObject { component Vpn: JsonObject {
property bool enabled: false property bool enabled: false
property list<var> provider: ["netbird"] property list<var> provider: []
} }
} }

View file

@ -9,7 +9,7 @@ Scope {
id: root id: root
property bool launcherInterrupted property bool launcherInterrupted
readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false
// qmllint disable unresolved-type // qmllint disable unresolved-type
CustomShortcut { CustomShortcut {

View file

@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets import Caelestia.Internal
import Caelestia.Services import Caelestia.Services
import qs.components import qs.components
import qs.services import qs.services
@ -55,25 +55,29 @@ Item {
service: Audio.cava service: Audio.cava
} }
Item { VisualiserBars {
id: content id: bars
anchors.fill: parent anchors.fill: parent
anchors.margins: Config.border.thickness anchors.margins: Config.border.thickness
anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing
Side { values: Audio.cava.values
content: content primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7)
} secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7)
Side { rounding: Appearance.rounding.small * Config.background.visualiser.rounding
content: content spacing: Appearance.spacing.small * Config.background.visualiser.spacing
isRight: true animationDuration: Appearance.anim.durations.normal
}
Behavior on anchors.leftMargin { Behavior on anchors.leftMargin {
Anim {} Anim {}
} }
} }
FrameAnimation {
running: root.opacity > 0 && !bars.settled
onTriggered: bars.advance(frameTime)
}
} }
} }
} }
@ -85,69 +89,4 @@ Item {
Behavior on opacity { Behavior on opacity {
Anim {} Anim {}
} }
component Side: Repeater {
id: side
required property Item content
property bool isRight
model: Config.services.visualiserBars
ClippingRectangle {
id: bar
required property int modelData
property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1]))
clip: true
x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0)
implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing
y: side.content.height - height
implicitHeight: bar.value * side.content.height * 0.4
color: "transparent"
topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding
topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding
Rectangle {
topLeftRadius: parent.topLeftRadius
topRightRadius: parent.topRightRadius
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Qt.alpha(Colours.palette.m3primary, 0.7)
Behavior on color {
CAnim {}
}
}
GradientStop {
position: 1
color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7)
Behavior on color {
CAnim {}
}
}
}
anchors.left: parent.left
anchors.right: parent.right
y: parent.height - height
implicitHeight: side.content.height * 0.4
}
Behavior on value {
Anim {
duration: Appearance.anim.durations.small
}
}
}
}
} }

View file

@ -16,6 +16,7 @@ ColumnLayout {
required property ShellScreen screen required property ShellScreen screen
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
required property BarPopouts.Wrapper popouts required property BarPopouts.Wrapper popouts
required property bool fullscreen
readonly property int vPadding: Appearance.padding.large readonly property int vPadding: Appearance.padding.large
function closeTray(): void { function closeTray(): void {
@ -128,6 +129,7 @@ ColumnLayout {
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Workspaces { sourceComponent: Workspaces {
screen: root.screen screen: root.screen
fullscreen: root.fullscreen
} }
} }
} }
@ -135,6 +137,7 @@ ColumnLayout {
roleValue: "activeWindow" roleValue: "activeWindow"
delegate: WrappedLoader { delegate: WrappedLoader {
Layout.fillWidth: true Layout.fillWidth: true
visible: !root.fullscreen
sourceComponent: ActiveWindow { sourceComponent: ActiveWindow {
bar: root bar: root
monitor: Brightness.getMonitorForScreen(root.screen) monitor: Brightness.getMonitorForScreen(root.screen)
@ -144,18 +147,21 @@ ColumnLayout {
DelegateChoice { DelegateChoice {
roleValue: "tray" roleValue: "tray"
delegate: WrappedLoader { delegate: WrappedLoader {
visible: !root.fullscreen
sourceComponent: Tray {} sourceComponent: Tray {}
} }
} }
DelegateChoice { DelegateChoice {
roleValue: "clock" roleValue: "clock"
delegate: WrappedLoader { delegate: WrappedLoader {
visible: !root.fullscreen
sourceComponent: Clock {} sourceComponent: Clock {}
} }
} }
DelegateChoice { DelegateChoice {
roleValue: "statusIcons" roleValue: "statusIcons"
delegate: WrappedLoader { delegate: WrappedLoader {
visible: !root.fullscreen
sourceComponent: StatusIcons {} sourceComponent: StatusIcons {}
} }
} }

View file

@ -13,12 +13,13 @@ Item {
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
required property BarPopouts.Wrapper popouts required property BarPopouts.Wrapper popouts
required property bool disabled required property bool disabled
required property bool fullscreen
readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth)
readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness)
readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2
readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness
readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered) readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered)
property bool isHovered property bool isHovered
function closeTray(): void { function closeTray(): void {
@ -33,8 +34,9 @@ Item {
(content.item as Bar)?.handleWheel(y, angleDelta); (content.item as Bar)?.handleWheel(y, angleDelta);
} }
visible: width > Config.border.thickness clip: true
implicitWidth: Config.border.thickness visible: width > 0
implicitWidth: fullscreen ? 0 : Config.border.thickness
states: State { states: State {
name: "visible" name: "visible"
@ -83,6 +85,7 @@ Item {
screen: root.screen screen: root.screen
visibilities: root.visibilities visibilities: root.visibilities
popouts: root.popouts // qmllint disable incompatible-type popouts: root.popouts // qmllint disable incompatible-type
fullscreen: root.fullscreen
} }
} }
} }

View file

@ -10,6 +10,7 @@ StyledRect {
required property int activeWsId required property int activeWsId
required property Repeater workspaces required property Repeater workspaces
required property Item mask required property Item mask
required property bool fullscreen
readonly property int currentWsIdx: { readonly property int currentWsIdx: {
let i = activeWsId - 1; let i = activeWsId - 1;

View file

@ -12,6 +12,7 @@ StyledClippingRect {
id: root id: root
required property ShellScreen screen required property ShellScreen screen
required property bool fullscreen
readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== ""
readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId
@ -36,6 +37,7 @@ StyledClippingRect {
anchors.fill: parent anchors.fill: parent
scale: root.onSpecial ? 0.8 : 1 scale: root.onSpecial ? 0.8 : 1
opacity: root.onSpecial ? 0.5 : 1 opacity: root.onSpecial ? 0.5 : 1
visible: !root.fullscreen
layer.enabled: root.blur > 0 layer.enabled: root.blur > 0
layer.effect: MultiEffect { layer.effect: MultiEffect {
@ -86,6 +88,7 @@ StyledClippingRect {
activeWsId: root.activeWsId activeWsId: root.activeWsId
workspaces: workspaces workspaces: workspaces
mask: layout mask: layout
fullscreen: root.fullscreen
} }
} }

View file

@ -36,6 +36,12 @@ QtObject {
readonly property string icon: "task_alt" readonly property string icon: "task_alt"
readonly property string component: "taskbar/TaskbarPane.qml" readonly property string component: "taskbar/TaskbarPane.qml"
}, },
QtObject {
readonly property string id: "notifications"
readonly property string label: "notifications"
readonly property string icon: "notifications"
readonly property string component: "notifications/NotificationsPane.qml"
},
QtObject { QtObject {
readonly property string id: "launcher" readonly property string id: "launcher"
readonly property string label: "launcher" readonly property string label: "launcher"
@ -60,7 +66,7 @@ QtObject {
return result; return result;
} }
function getByIndex(index: int): QtObject { function getByIndex(index: int): var {
if (index >= 0 && index < panes.length) { if (index >= 0 && index < panes.length) {
return panes[index]; return panes[index];
} }
@ -76,12 +82,12 @@ QtObject {
return -1; return -1;
} }
function getByLabel(label: string): QtObject { function getByLabel(label: string): var {
const index = getIndexByLabel(label); const index = getIndexByLabel(label);
return getByIndex(index); return getByIndex(index);
} }
function getById(id: string): QtObject { function getById(id: string): var {
for (let i = 0; i < panes.length; i++) { for (let i = 0; i < panes.length; i++) {
if (panes[i].id === id) { if (panes[i].id === id) {
return panes[i]; return panes[i];

View file

@ -5,6 +5,7 @@ import "network"
import "audio" import "audio"
import "appearance" import "appearance"
import "taskbar" import "taskbar"
import "notifications"
import "launcher" import "launcher"
import "dashboard" import "dashboard"
import QtQuick import QtQuick

View file

@ -109,10 +109,13 @@ DeviceDetails {
inactiveOnColour: Colours.palette.m3onSecondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer
onClicked: { onClicked: {
const provider = Config.utilities.vpn.provider[root.vpnProvider.index];
editVpnDialog.editIndex = root.vpnProvider.index; editVpnDialog.editIndex = root.vpnProvider.index;
editVpnDialog.providerName = root.vpnProvider.name; editVpnDialog.providerName = root.vpnProvider.name;
editVpnDialog.displayName = root.vpnProvider.displayName; editVpnDialog.displayName = root.vpnProvider.displayName;
editVpnDialog.interfaceName = root.vpnProvider.interface; editVpnDialog.interfaceName = root.vpnProvider.interface;
editVpnDialog.connectCmd = (provider && provider.connectCmd) ? provider.connectCmd.join(" ") : "";
editVpnDialog.disconnectCmd = (provider && provider.disconnectCmd) ? provider.disconnectCmd.join(" ") : "";
editVpnDialog.open(); editVpnDialog.open();
} }
} }
@ -136,6 +139,30 @@ DeviceDetails {
} }
} }
} }
TextButton {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== ""
text: qsTr("Open Login Page")
inactiveColour: Colours.palette.m3tertiaryContainer
inactiveOnColour: Colours.palette.m3onTertiaryContainer
onClicked: {
Qt.openUrlExternally(VPN.status.authUrl);
}
}
StyledText {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === ""
text: qsTr("Click 'Connect' to generate authentication URL")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
} }
} }
}, },
@ -176,11 +203,30 @@ DeviceDetails {
return qsTr("Disabled"); return qsTr("Disabled");
if (VPN.connecting) if (VPN.connecting)
return qsTr("Connecting..."); return qsTr("Connecting...");
if (VPN.connected)
switch (VPN.status.state) {
case "connected":
return qsTr("Connected"); return qsTr("Connected");
return qsTr("Enabled (Not connected)"); case "disconnected":
return qsTr("Disconnected");
case "connecting":
return qsTr("Connecting...");
case "needs-auth":
return qsTr("Authentication required");
case "error":
return qsTr("Error");
default:
return qsTr("Unknown");
} }
} }
}
PropertyRow {
visible: VPN.status.reason !== ""
showTopMargin: true
label: qsTr("Details")
value: VPN.status.reason
}
PropertyRow { PropertyRow {
showTopMargin: true showTopMargin: true
@ -200,6 +246,8 @@ DeviceDetails {
property string providerName: "" property string providerName: ""
property string displayName: "" property string displayName: ""
property string interfaceName: "" property string interfaceName: ""
property string connectCmd: ""
property string disconnectCmd: ""
function closeWithAnimation(): void { function closeWithAnimation(): void {
close(); close();
@ -349,6 +397,82 @@ DeviceDetails {
} }
} }
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller / 2
visible: editVpnDialog.connectCmd.length > 0
StyledText {
text: qsTr("Connect Command")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
StyledRect {
Layout.fillWidth: true
implicitHeight: 40
color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color {
CAnim {}
}
Behavior on border.color {
CAnim {}
}
StyledTextField {
id: connectCmdFieldEdit
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignLeft
text: editVpnDialog.connectCmd
onTextChanged: editVpnDialog.connectCmd = text
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller / 2
visible: editVpnDialog.disconnectCmd.length > 0
StyledText {
text: qsTr("Disconnect Command")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
StyledRect {
Layout.fillWidth: true
implicitHeight: 40
color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color {
CAnim {}
}
Behavior on border.color {
CAnim {}
}
StyledTextField {
id: disconnectCmdFieldEdit
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignLeft
text: editVpnDialog.disconnectCmd
onTextChanged: editVpnDialog.disconnectCmd = text
}
}
}
RowLayout { RowLayout {
Layout.topMargin: Appearance.spacing.normal Layout.topMargin: Appearance.spacing.normal
Layout.fillWidth: true Layout.fillWidth: true
@ -376,12 +500,23 @@ DeviceDetails {
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
if (i === editVpnDialog.editIndex) { if (i === editVpnDialog.editIndex) {
providers.push({ const hasCommands = editVpnDialog.connectCmd.length > 0 && editVpnDialog.disconnectCmd.length > 0;
name: editVpnDialog.providerName, const newProvider = {
displayName: editVpnDialog.displayName || editVpnDialog.interfaceName, displayName: editVpnDialog.displayName || editVpnDialog.interfaceName,
interface: editVpnDialog.interfaceName, enabled: wasEnabled,
enabled: wasEnabled iface: editVpnDialog.interfaceName,
}); name: editVpnDialog.providerName,
connectCmd: hasCommands ? editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0) : undefined,
disconnectCmd: hasCommands ? editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0) : undefined
};
// Remove undefined properties
if (!hasCommands) {
delete newProvider.connectCmd;
delete newProvider.disconnectCmd;
}
providers.push(newProvider);
} else { } else {
providers.push(Config.utilities.vpn.provider[i]); providers.push(Config.utilities.vpn.provider[i]);
} }

View file

@ -159,15 +159,38 @@ ColumnLayout {
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
text: { text: {
if (modelData.enabled && VPN.connected) if (!modelData.enabled)
return qsTr("Connected");
if (modelData.enabled && VPN.connecting)
return qsTr("Connecting...");
if (modelData.enabled)
return qsTr("Enabled");
return qsTr("Disabled"); return qsTr("Disabled");
if (VPN.connecting)
return qsTr("Connecting...");
switch (VPN.status.state) {
case "connected":
return qsTr("Connected");
case "disconnected":
return qsTr("Enabled");
case "connecting":
return qsTr("Connecting...");
case "needs-auth":
return qsTr("Auth required");
case "error":
return qsTr("Error");
default:
return qsTr("Enabled");
}
}
color: {
if (!modelData.enabled)
return Colours.palette.m3outline;
if (VPN.status.state === "connected")
return Colours.palette.m3primary;
if (VPN.status.state === "error")
return Colours.palette.m3error;
if (VPN.status.state === "needs-auth")
return Colours.palette.m3tertiary;
return Colours.palette.m3onSurface;
} }
color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline
font.pointSize: Appearance.font.size.small font.pointSize: Appearance.font.size.small
font.weight: modelData.enabled && VPN.connected ? 500 : 400 font.weight: modelData.enabled && VPN.connected ? 500 : 400
elide: Text.ElideRight elide: Text.ElideRight
@ -271,6 +294,8 @@ ColumnLayout {
property string providerName: "" property string providerName: ""
property string displayName: "" property string displayName: ""
property string interfaceName: "" property string interfaceName: ""
property string connectCmd: ""
property string disconnectCmd: ""
function showProviderSelection(): void { function showProviderSelection(): void {
currentState = "selection"; currentState = "selection";
@ -286,6 +311,8 @@ ColumnLayout {
providerName = providerType; providerName = providerType;
displayName = defaultDisplayName; displayName = defaultDisplayName;
interfaceName = ""; interfaceName = "";
connectCmd = "";
disconnectCmd = "";
if (currentState === "selection") { if (currentState === "selection") {
transitionToForm.start(); transitionToForm.start();
@ -304,6 +331,8 @@ ColumnLayout {
providerName = isObject ? (provider.name || "custom") : String(provider); providerName = isObject ? (provider.name || "custom") : String(provider);
displayName = isObject ? (provider.displayName || providerName) : providerName; displayName = isObject ? (provider.displayName || providerName) : providerName;
interfaceName = isObject ? (provider.interface || "") : ""; interfaceName = isObject ? (provider.interface || "") : "";
connectCmd = isObject && provider.connectCmd ? provider.connectCmd.join(" ") : "";
disconnectCmd = isObject && provider.disconnectCmd ? provider.disconnectCmd.join(" ") : "";
currentState = "form"; currentState = "form";
open(); open();
@ -491,7 +520,7 @@ ColumnLayout {
TextButton { TextButton {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("WireGuard (Custom)") text: qsTr("WireGuard")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface inactiveOnColour: Colours.palette.m3onSurface
onClicked: { onClicked: {
@ -499,6 +528,16 @@ ColumnLayout {
} }
} }
TextButton {
Layout.fillWidth: true
text: qsTr("Custom")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: {
vpnDialog.showAddForm("custom", "Custom VPN");
}
}
TextButton { TextButton {
Layout.topMargin: Appearance.spacing.normal Layout.topMargin: Appearance.spacing.normal
Layout.fillWidth: true Layout.fillWidth: true
@ -604,6 +643,82 @@ ColumnLayout {
} }
} }
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller / 2
visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom")
StyledText {
text: qsTr("Connect Command (e.g., wg-quick up wg0)")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
StyledRect {
Layout.fillWidth: true
implicitHeight: 40
color: connectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: connectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color {
CAnim {}
}
Behavior on border.color {
CAnim {}
}
StyledTextField {
id: connectCmdField
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignLeft
text: vpnDialog.connectCmd
onTextChanged: vpnDialog.connectCmd = text
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller / 2
visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom")
StyledText {
text: qsTr("Disconnect Command (e.g., wg-quick down wg0)")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
StyledRect {
Layout.fillWidth: true
implicitHeight: 40
color: disconnectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: disconnectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color {
CAnim {}
}
Behavior on border.color {
CAnim {}
}
StyledTextField {
id: disconnectCmdField
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignLeft
text: vpnDialog.disconnectCmd
onTextChanged: vpnDialog.disconnectCmd = text
}
}
}
RowLayout { RowLayout {
Layout.topMargin: Appearance.spacing.normal Layout.topMargin: Appearance.spacing.normal
Layout.fillWidth: true Layout.fillWidth: true
@ -620,19 +735,37 @@ ColumnLayout {
TextButton { TextButton {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Save") text: qsTr("Save")
enabled: vpnDialog.interfaceName.length > 0 enabled: {
const hasCommands = vpnDialog.connectCmd.length > 0 || vpnDialog.disconnectCmd.length > 0;
if (hasCommands) {
return vpnDialog.interfaceName.length > 0 && vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0;
}
return vpnDialog.interfaceName.length > 0;
}
inactiveColour: Colours.palette.m3primaryContainer inactiveColour: Colours.palette.m3primaryContainer
inactiveOnColour: Colours.palette.m3onPrimaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer
onClicked: { onClicked: {
const providers = []; const providers = [];
const hasCommands = vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0;
const newProvider = { const newProvider = {
name: vpnDialog.providerName,
displayName: vpnDialog.displayName || vpnDialog.interfaceName, displayName: vpnDialog.displayName || vpnDialog.interfaceName,
interface: vpnDialog.interfaceName enabled: false,
interface: vpnDialog.interfaceName,
name: vpnDialog.providerName
}; };
if (hasCommands) {
newProvider.connectCmd = vpnDialog.connectCmd.split(" ").filter(s => s.length > 0);
newProvider.disconnectCmd = vpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0);
}
if (vpnDialog.editIndex >= 0) { if (vpnDialog.editIndex >= 0) {
const oldProvider = Config.utilities.vpn.provider[vpnDialog.editIndex];
if (typeof oldProvider === "object" && oldProvider.enabled !== undefined) {
newProvider.enabled = oldProvider.enabled;
}
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
if (i === vpnDialog.editIndex) { if (i === vpnDialog.editIndex) {
providers.push(newProvider); providers.push(newProvider);

View file

@ -0,0 +1,427 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import ".."
import "../components"
import qs.components
import qs.components.controls
import qs.components.effects
import qs.components.containers
import qs.services
import qs.config
Item {
id: root
required property Session session
property bool notificationsExpire: Config.notifs.expire ?? true
property string notificationsFullscreen: Config.notifs.fullscreen ?? "on"
property bool notificationsOpenExpanded: Config.notifs.openExpanded ?? false
property int notificationsDefaultExpireTimeout: Config.notifs.defaultExpireTimeout ?? 5000
property int notificationsGroupPreviewNum: Config.notifs.groupPreviewNum ?? 3
property int maxToasts: Config.utilities.maxToasts ?? 4
property string toastsFullscreen: Config.utilities.toasts.fullscreen ?? "off"
property bool chargingChanged: Config.utilities.toasts.chargingChanged ?? true
property bool gameModeChanged: Config.utilities.toasts.gameModeChanged ?? true
property bool dndChanged: Config.utilities.toasts.dndChanged ?? true
property bool audioOutputChanged: Config.utilities.toasts.audioOutputChanged ?? true
property bool audioInputChanged: Config.utilities.toasts.audioInputChanged ?? true
property bool capsLockChanged: Config.utilities.toasts.capsLockChanged ?? true
property bool numLockChanged: Config.utilities.toasts.numLockChanged ?? true
property bool kbLayoutChanged: Config.utilities.toasts.kbLayoutChanged ?? true
property bool vpnChanged: Config.utilities.toasts.vpnChanged ?? true
property bool nowPlaying: Config.utilities.toasts.nowPlaying ?? false
function saveConfig(): void {
Config.notifs.expire = root.notificationsExpire;
Config.notifs.fullscreen = root.notificationsFullscreen;
Config.notifs.openExpanded = root.notificationsOpenExpanded;
Config.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout;
Config.notifs.groupPreviewNum = root.notificationsGroupPreviewNum;
Config.utilities.maxToasts = root.maxToasts;
Config.utilities.toasts.fullscreen = root.toastsFullscreen;
Config.utilities.toasts.chargingChanged = root.chargingChanged;
Config.utilities.toasts.gameModeChanged = root.gameModeChanged;
Config.utilities.toasts.dndChanged = root.dndChanged;
Config.utilities.toasts.audioOutputChanged = root.audioOutputChanged;
Config.utilities.toasts.audioInputChanged = root.audioInputChanged;
Config.utilities.toasts.capsLockChanged = root.capsLockChanged;
Config.utilities.toasts.numLockChanged = root.numLockChanged;
Config.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged;
Config.utilities.toasts.vpnChanged = root.vpnChanged;
Config.utilities.toasts.nowPlaying = root.nowPlaying;
Config.save();
}
anchors.fill: parent
ClippingRectangle {
id: notificationsClippingRect
anchors.fill: parent
anchors.margins: Appearance.padding.normal
anchors.leftMargin: 0
anchors.rightMargin: Appearance.padding.normal
color: "transparent"
radius: notificationsBorder.innerRadius
Loader {
id: notificationsLoader
anchors.fill: parent
anchors.margins: Appearance.padding.large + Appearance.padding.normal
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large
sourceComponent: notificationsContentComponent
}
}
InnerBorder {
id: notificationsBorder
leftThickness: 0
rightThickness: Appearance.padding.normal
}
Component {
id: notificationsContentComponent
StyledFlickable {
id: notificationsFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: notificationsLayout.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: notificationsFlickable
}
RowLayout {
id: notificationsLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
Layout.maximumWidth: 500
Layout.alignment: Qt.AlignTop
spacing: Appearance.spacing.normal
SectionContainer {
Layout.fillWidth: true
alignTop: true
StyledText {
text: qsTr("Notifications")
font.pointSize: Appearance.font.size.normal
}
SplitButtonRow {
id: notificationsFullscreenSelector
function syncActiveItem(): void {
active = root.notificationsFullscreen === "off" ? notificationsFullscreenOffItem : notificationsFullscreenOnItem;
}
label: qsTr("Show in fullscreen")
menuItems: [notificationsFullscreenOffItem, notificationsFullscreenOnItem]
Component.onCompleted: syncActiveItem()
Connections {
function onNotificationsFullscreenChanged(): void {
notificationsFullscreenSelector.syncActiveItem();
}
target: root
}
MenuItem {
id: notificationsFullscreenOffItem
text: qsTr("Off")
icon: "notifications_off"
activeText: qsTr("Off")
onClicked: {
root.notificationsFullscreen = "off";
root.saveConfig();
}
}
MenuItem {
id: notificationsFullscreenOnItem
text: qsTr("On")
icon: "notifications"
activeText: qsTr("On")
onClicked: {
root.notificationsFullscreen = "on";
root.saveConfig();
}
}
}
SwitchRow {
label: qsTr("Expire automatically")
checked: root.notificationsExpire
onToggled: checked => {
root.notificationsExpire = checked;
root.saveConfig();
}
}
SwitchRow {
label: qsTr("Open expanded")
checked: root.notificationsOpenExpanded
onToggled: checked => {
root.notificationsOpenExpanded = checked;
root.saveConfig();
}
}
SpinBoxRow {
label: qsTr("Default timeout")
value: root.notificationsDefaultExpireTimeout
min: 1000
max: 60000
step: 500
onValueModified: value => {
root.notificationsDefaultExpireTimeout = value;
root.saveConfig();
}
}
SpinBoxRow {
label: qsTr("Group preview count")
value: root.notificationsGroupPreviewNum
min: 1
max: 10
step: 1
onValueModified: value => {
root.notificationsGroupPreviewNum = value;
root.saveConfig();
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: Appearance.spacing.normal
SectionContainer {
Layout.fillWidth: true
alignTop: true
StyledText {
text: qsTr("Toast settings")
font.pointSize: Appearance.font.size.normal
}
SplitButtonRow {
id: toastFullscreenSelector
function syncActiveItem(): void {
if (root.toastsFullscreen === "all") {
active = toastFullscreenAllItem;
return;
}
if (root.toastsFullscreen === "important") {
active = toastFullscreenImportantItem;
return;
}
active = toastFullscreenOffItem;
}
Layout.fillWidth: true
z: expanded ? 100 : 0
label: qsTr("Show in fullscreen")
menuItems: [toastFullscreenOffItem, toastFullscreenImportantItem, toastFullscreenAllItem]
Component.onCompleted: syncActiveItem()
Connections {
function onToastsFullscreenChanged(): void {
toastFullscreenSelector.syncActiveItem();
}
target: root
}
MenuItem {
id: toastFullscreenOffItem
text: qsTr("Off")
icon: "notifications_off"
activeText: qsTr("Off")
onClicked: {
root.toastsFullscreen = "off";
root.saveConfig();
}
}
MenuItem {
id: toastFullscreenImportantItem
text: qsTr("Important")
icon: "priority_high"
activeText: qsTr("Important")
onClicked: {
root.toastsFullscreen = "important";
root.saveConfig();
}
}
MenuItem {
id: toastFullscreenAllItem
text: qsTr("On")
icon: "notifications"
activeText: qsTr("On")
onClicked: {
root.toastsFullscreen = "all";
root.saveConfig();
}
}
}
SpinBoxRow {
Layout.fillWidth: true
label: qsTr("Visible toasts")
value: root.maxToasts
min: 1
max: 10
step: 1
onValueModified: value => {
root.maxToasts = value;
root.saveConfig();
}
}
GridLayout {
Layout.fillWidth: true
columns: 2
columnSpacing: Appearance.spacing.normal
rowSpacing: Appearance.spacing.normal
SwitchRow {
Layout.fillWidth: true
label: qsTr("Charging changes")
checked: root.chargingChanged
onToggled: checked => {
root.chargingChanged = checked;
root.saveConfig();
}
}
SwitchRow {
Layout.fillWidth: true
label: qsTr("Game mode changes")
checked: root.gameModeChanged
onToggled: checked => {
root.gameModeChanged = checked;
root.saveConfig();
}
}
SwitchRow {
Layout.fillWidth: true
label: qsTr("Do not disturb")
checked: root.dndChanged
onToggled: checked => {
root.dndChanged = checked;
root.saveConfig();
}
}
SwitchRow {
Layout.fillWidth: true
label: qsTr("Audio output changes")
checked: root.audioOutputChanged
onToggled: checked => {
root.audioOutputChanged = checked;
root.saveConfig();
}
}
SwitchRow {
Layout.fillWidth: true
label: qsTr("Audio input changes")
checked: root.audioInputChanged
onToggled: checked => {
root.audioInputChanged = checked;
root.saveConfig();
}
}
SwitchRow {
Layout.fillWidth: true
label: qsTr("Caps lock changes")
checked: root.capsLockChanged
onToggled: checked => {
root.capsLockChanged = checked;
root.saveConfig();
}
}
SwitchRow {
Layout.fillWidth: true
label: qsTr("Num lock changes")
checked: root.numLockChanged
onToggled: checked => {
root.numLockChanged = checked;
root.saveConfig();
}
}
SwitchRow {
Layout.fillWidth: true
label: qsTr("Keyboard layout changes")
checked: root.kbLayoutChanged
onToggled: checked => {
root.kbLayoutChanged = checked;
root.saveConfig();
}
}
SwitchRow {
Layout.fillWidth: true
label: qsTr("VPN changes")
checked: root.vpnChanged
onToggled: checked => {
root.vpnChanged = checked;
root.saveConfig();
}
}
SwitchRow {
Layout.fillWidth: true
label: qsTr("Now playing")
checked: root.nowPlaying
onToggled: checked => {
root.nowPlaying = checked;
root.saveConfig();
}
}
}
}
}
}
}
}
}

View file

@ -358,32 +358,29 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
width: parent.width * heroCard.animatedUsage implicitWidth: parent.width * heroCard.animatedUsage
color: Qt.alpha(heroCard.accentColor, 0.15) color: Qt.alpha(heroCard.accentColor, 0.15)
} }
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large
anchors.topMargin: Appearance.padding.normal
anchors.bottomMargin: Appearance.padding.normal
spacing: Appearance.spacing.small
CardHeader { CardHeader {
anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: Appearance.padding.large
anchors.topMargin: Math.round(Appearance.padding.large * 1.2)
width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Appearance.spacing.normal
icon: heroCard.icon icon: heroCard.icon
title: heroCard.title title: heroCard.title
accentColor: heroCard.accentColor accentColor: heroCard.accentColor
} }
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Appearance.spacing.normal
Column { Column {
Layout.alignment: Qt.AlignBottom anchors.left: parent.left
Layout.fillWidth: true anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Math.round(Appearance.padding.large * 1.2)
anchors.bottomMargin: Math.round(Appearance.padding.large * 1.3)
spacing: Appearance.spacing.small spacing: Appearance.spacing.small
Row { Row {
@ -404,21 +401,17 @@ Item {
} }
ProgressBar { ProgressBar {
width: parent.width * 0.5 implicitWidth: parent.width * 0.5
height: 6 implicitHeight: 6
value: heroCard.tempProgress value: heroCard.tempProgress
fgColor: heroCard.accentColor fgColor: heroCard.accentColor
bgColor: Qt.alpha(heroCard.accentColor, 0.2) bgColor: Qt.alpha(heroCard.accentColor, 0.2)
} }
} }
Item {
Layout.fillWidth: true
}
}
}
Column { Column {
id: usageColumn
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large anchors.margins: Appearance.padding.large
@ -426,6 +419,8 @@ Item {
spacing: 0 spacing: 0
StyledText { StyledText {
id: usageLabel
anchors.right: parent.right anchors.right: parent.right
text: heroCard.mainLabel text: heroCard.mainLabel
font.pointSize: Appearance.font.size.normal font.pointSize: Appearance.font.size.normal

View file

@ -15,14 +15,17 @@ Shape {
required property Panels panels required property Panels panels
required property Item bar required property Item bar
required property real borderThickness
required property real borderRounding
anchors.fill: parent anchors.fill: parent
anchors.margins: Config.border.thickness anchors.margins: root.borderThickness
anchors.leftMargin: bar.implicitWidth anchors.leftMargin: bar.implicitWidth
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
Osd.Background { Osd.Background {
wrapper: root.panels.osd // qmllint disable incompatible-type wrapper: root.panels.osd // qmllint disable incompatible-type
rounding: Config.border.rounding
startX: root.width - root.panels.session.width - root.panels.sidebar.width startX: root.width - root.panels.session.width - root.panels.sidebar.width
startY: (root.height - wrapper.height) / 2 - rounding startY: (root.height - wrapper.height) / 2 - rounding
@ -31,6 +34,7 @@ Shape {
Notifications.Background { Notifications.Background {
wrapper: root.panels.notifications // qmllint disable incompatible-type wrapper: root.panels.notifications // qmllint disable incompatible-type
sidebar: sidebar sidebar: sidebar
rounding: Config.border.rounding
startX: root.width startX: root.width
startY: 0 startY: 0
@ -68,6 +72,7 @@ Shape {
Utilities.Background { Utilities.Background {
wrapper: root.panels.utilities // qmllint disable incompatible-type wrapper: root.panels.utilities // qmllint disable incompatible-type
sidebar: sidebar sidebar: sidebar
rounding: root.borderRounding
startX: root.width startX: root.width
startY: root.height startY: root.height
@ -78,6 +83,7 @@ Shape {
wrapper: root.panels.sidebar // qmllint disable incompatible-type wrapper: root.panels.sidebar // qmllint disable incompatible-type
panels: root.panels panels: root.panels
rounding: root.borderRounding
startX: root.width startX: root.width
startY: root.panels.notifications.height startY: root.panels.notifications.height

View file

@ -4,12 +4,13 @@ import QtQuick
import QtQuick.Effects import QtQuick.Effects
import qs.components import qs.components
import qs.services import qs.services
import qs.config
Item { Item {
id: root id: root
required property Item bar required property Item bar
required property real borderThickness
required property real borderRounding
anchors.fill: parent anchors.fill: parent
@ -36,9 +37,9 @@ Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: Config.border.thickness anchors.margins: root.borderThickness
anchors.leftMargin: root.bar.implicitWidth anchors.leftMargin: root.bar.implicitWidth
radius: Config.border.rounding radius: root.borderRounding
} }
} }
} }

View file

@ -26,18 +26,34 @@ Variants {
Exclusions { Exclusions {
screen: scope.modelData screen: scope.modelData
bar: bar bar: bar
borderThickness: Config.border.thickness
} }
StyledWindow { StyledWindow {
id: win id: win
readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false readonly property var monitor: Hypr.monitorFor(screen)
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject?.specialWorkspace?.name.length ?? 0) > 0
readonly property bool hasFullscreen: {
if (hasSpecialWorkspace) {
const specialName = monitor?.lastIpcObject?.specialWorkspace?.name;
if (!specialName)
return false;
const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName);
return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
}
return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
}
property real borderThickness: hasFullscreen ? 0 : Config.border.thickness
readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness
property real borderRounding: hasFullscreen ? 0 : Config.border.rounding
property real shadowOpacity: hasFullscreen ? 0 : 0.7
readonly property int dragMaskPadding: { readonly property int dragMaskPadding: {
if (focusGrab.active || panels.popouts.isDetached) if (focusGrab.active || panels.popouts.isDetached)
return 0; return 0;
const mon = Hypr.monitorFor(screen); const mon = Hypr.monitorFor(screen);
if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject.windows > 0) if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace.lastIpcObject.windows > 0)
return 0; return 0;
const thresholds = []; const thresholds = [];
@ -56,6 +72,7 @@ Variants {
screen: scope.modelData screen: scope.modelData
name: "drawers" name: "drawers"
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
mask: Regions { mask: Regions {
@ -69,6 +86,30 @@ Variants {
anchors.left: true anchors.left: true
anchors.right: true anchors.right: true
Behavior on borderThickness {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on borderRounding {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on shadowOpacity {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
@ -101,7 +142,7 @@ Variants {
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowEnabled: true shadowEnabled: true
blurMax: 15 blurMax: 15
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, win.shadowOpacity))
} }
// Border { // Border {
@ -127,11 +168,11 @@ Variants {
anchors.fill: parent anchors.fill: parent
anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers
group: blobGroup group: blobGroup
radius: Config.border.rounding radius: win.borderRounding
borderLeft: bar.implicitWidth - anchors.margins borderLeft: bar.implicitWidth - anchors.margins
borderRight: Config.border.thickness - anchors.margins borderRight: win.borderThickness - anchors.margins
borderTop: Config.border.thickness - anchors.margins borderTop: win.borderThickness - anchors.margins
borderBottom: Config.border.thickness - anchors.margins borderBottom: win.borderThickness - anchors.margins
} }
PanelBg { PanelBg {
@ -140,6 +181,7 @@ Variants {
blobGroup: blobGroup blobGroup: blobGroup
panel: panels.dashboard panel: panels.dashboard
bar: bar bar: bar
borderThickness: win.borderThickness
deformAmount: 0.1 deformAmount: 0.1
} }
@ -149,6 +191,7 @@ Variants {
blobGroup: blobGroup blobGroup: blobGroup
panel: panels.launcher panel: panels.launcher
bar: bar bar: bar
borderThickness: win.borderThickness
deformAmount: 0.1 deformAmount: 0.1
} }
@ -158,6 +201,7 @@ Variants {
blobGroup: blobGroup blobGroup: blobGroup
panel: panels.sessionWrapper panel: panels.sessionWrapper
bar: bar bar: bar
borderThickness: win.borderThickness
deformAmount: 0.25 deformAmount: 0.25
x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth
implicitWidth: panels.session.width implicitWidth: panels.session.width
@ -169,6 +213,7 @@ Variants {
blobGroup: blobGroup blobGroup: blobGroup
panel: panels.sidebar panel: panels.sidebar
bar: bar bar: bar
borderThickness: win.borderThickness
deformAmount: 0.05 deformAmount: 0.05
implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg]
@ -181,6 +226,7 @@ Variants {
blobGroup: blobGroup blobGroup: blobGroup
panel: panels.osdWrapper panel: panels.osdWrapper
bar: bar bar: bar
borderThickness: win.borderThickness
deformAmount: 0.3 deformAmount: 0.3
x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth
implicitWidth: panels.osd.width implicitWidth: panels.osd.width
@ -192,6 +238,7 @@ Variants {
blobGroup: blobGroup blobGroup: blobGroup
panel: panels.notifications panel: panels.notifications
bar: bar bar: bar
borderThickness: win.borderThickness
} }
PanelBg { PanelBg {
@ -200,6 +247,7 @@ Variants {
blobGroup: blobGroup blobGroup: blobGroup
panel: panels.utilities panel: panels.utilities
bar: bar bar: bar
borderThickness: win.borderThickness
deformAmount: panels.sidebar.visible ? 0.1 : 0.15 deformAmount: panels.sidebar.visible ? 0.1 : 0.15
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius
@ -211,6 +259,7 @@ Variants {
blobGroup: blobGroup blobGroup: blobGroup
panel: panels.popouts panel: panels.popouts
bar: bar bar: bar
borderThickness: win.borderThickness
x: bar.implicitWidth - (panels.popouts.isDetached ? -(win.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5) x: bar.implicitWidth - (panels.popouts.isDetached ? -(win.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5)
implicitWidth: panels.popouts.shownWidth implicitWidth: panels.popouts.shownWidth
@ -243,6 +292,8 @@ Variants {
visibilities: visibilities visibilities: visibilities
panels: panels panels: panels
bar: bar bar: bar
borderThickness: win.borderLayoutThickness
fullscreen: win.hasFullscreen
Panels { Panels {
id: panels id: panels
@ -250,6 +301,7 @@ Variants {
screen: scope.modelData screen: scope.modelData
visibilities: visibilities visibilities: visibilities
bar: bar bar: bar
borderThickness: win.borderThickness
utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1
@ -290,6 +342,7 @@ Variants {
popouts: panels.popouts popouts: panels.popouts
disabled: scope.barDisabled disabled: scope.barDisabled
fullscreen: win.hasFullscreen
Component.onCompleted: Visibilities.bars.set(scope.modelData, this) Component.onCompleted: Visibilities.bars.set(scope.modelData, this)
} }
@ -301,11 +354,12 @@ Variants {
required property BlobGroup blobGroup required property BlobGroup blobGroup
required property Item panel required property Item panel
required property Item bar required property Item bar
required property real borderThickness
property real deformAmount: 0.15 property real deformAmount: 0.15
group: panel.width > 0 && panel.height > 0 ? blobGroup : null group: panel.width > 0 && panel.height > 0 ? blobGroup : null
x: panel.x + bar.implicitWidth x: panel.x + bar.implicitWidth
y: panel.y + Config.border.thickness y: panel.y + borderThickness
implicitWidth: panel.width implicitWidth: panel.width
implicitHeight: panel.height implicitHeight: panel.height
radius: Config.border.rounding radius: Config.border.rounding

View file

@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.components.containers import qs.components.containers
import qs.config
import qs.modules.bar as Bar import qs.modules.bar as Bar
Scope { Scope {
@ -11,6 +10,7 @@ Scope {
required property ShellScreen screen required property ShellScreen screen
required property Bar.BarWrapper bar required property Bar.BarWrapper bar
required property real borderThickness
ExclusionZone { ExclusionZone {
anchors.left: true anchors.left: true
@ -32,7 +32,7 @@ Scope {
component ExclusionZone: StyledWindow { component ExclusionZone: StyledWindow {
screen: root.screen screen: root.screen
name: "border-exclusion" name: "border-exclusion"
exclusiveZone: Config.border.thickness exclusiveZone: root.borderThickness
mask: Region {} mask: Region {}
implicitWidth: 1 implicitWidth: 1
implicitHeight: 1 implicitHeight: 1

View file

@ -15,6 +15,8 @@ CustomMouseArea {
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
required property Panels panels required property Panels panels
required property Bar.BarWrapper bar required property Bar.BarWrapper bar
required property real borderThickness
required property bool fullscreen
property point dragStart property point dragStart
property bool dashboardShortcutActive property bool dashboardShortcutActive
@ -22,7 +24,7 @@ CustomMouseArea {
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
function withinPanelHeight(panel: Item, x: real, y: real): bool { function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = Config.border.thickness + panel.y; const panelY = root.borderThickness + panel.y;
return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding;
} }
@ -49,13 +51,16 @@ CustomMouseArea {
} }
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
if (fullscreen)
return;
if (event.x < bar.implicitWidth) { if (event.x < bar.implicitWidth) {
bar.handleWheel(event.y, event.angleDelta); bar.handleWheel(event.y, event.angleDelta);
} }
} }
anchors.fill: parent anchors.fill: parent
hoverEnabled: true acceptedButtons: fullscreen ? Qt.NoButton : Qt.AllButtons
hoverEnabled: !fullscreen
onPressed: event => dragStart = Qt.point(event.x, event.y) onPressed: event => dragStart = Qt.point(event.x, event.y)
onContainsMouseChanged: { onContainsMouseChanged: {

View file

@ -19,6 +19,7 @@ Item {
required property ShellScreen screen required property ShellScreen screen
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
required property Bar.BarWrapper bar required property Bar.BarWrapper bar
required property real borderThickness
readonly property alias osd: osd readonly property alias osd: osd
readonly property alias osdWrapper: osdWrapper readonly property alias osdWrapper: osdWrapper
@ -33,7 +34,7 @@ Item {
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
anchors.fill: parent anchors.fill: parent
anchors.margins: Config.border.thickness anchors.margins: root.borderThickness
anchors.leftMargin: bar.implicitWidth anchors.leftMargin: bar.implicitWidth
Item { Item {
@ -123,7 +124,7 @@ Item {
if (isDetached) if (isDetached)
return (root.height - nonAnimHeight) / 2; return (root.height - nonAnimHeight) / 2;
const off = currentCenter - Config.border.thickness - nonAnimHeight / 2; const off = currentCenter - root.borderThickness - nonAnimHeight / 2;
const diff = root.height - Math.floor(off + nonAnimHeight); const diff = root.height - Math.floor(off + nonAnimHeight);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;

View file

@ -2,14 +2,13 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.components import qs.components
import qs.services import qs.services
import qs.config
ShapePath { ShapePath {
id: root id: root
required property Wrapper wrapper required property Wrapper wrapper
required property var sidebar required property var sidebar
readonly property real rounding: Config.border.rounding required property real rounding
readonly property bool flatten: wrapper.height < rounding * 2 readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
@ -31,14 +30,14 @@ ShapePath {
relativeY: root.wrapper.height - root.roundingY * 2 relativeY: root.wrapper.height - root.roundingY * 2
} }
PathArc { PathArc {
relativeX: root.sidebar.notifsRoundingX relativeX: root.rounding
relativeY: root.roundingY relativeY: root.roundingY
radiusX: root.sidebar.notifsRoundingX radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height) radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise direction: PathArc.Counterclockwise
} }
PathLine { PathLine {
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding * 2 : root.wrapper.width
relativeY: 0 relativeY: 0
} }
PathArc { PathArc {

View file

@ -2,13 +2,12 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.components import qs.components
import qs.services import qs.services
import qs.config
ShapePath { ShapePath {
id: root id: root
required property Wrapper wrapper required property Wrapper wrapper
readonly property real rounding: Config.border.rounding required property real rounding
readonly property bool flatten: wrapper.width < rounding * 2 readonly property bool flatten: wrapper.width < rounding * 2
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding readonly property real roundingX: flatten ? wrapper.width / 2 : rounding

View file

@ -2,15 +2,13 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.components import qs.components
import qs.services import qs.services
import qs.config
ShapePath { ShapePath {
id: root id: root
required property Wrapper wrapper required property Wrapper wrapper
required property var panels required property var panels
required property real rounding
readonly property real rounding: Config.border.rounding
readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width
readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding

View file

@ -2,14 +2,13 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.components import qs.components
import qs.services import qs.services
import qs.config
ShapePath { ShapePath {
id: root id: root
required property Wrapper wrapper required property Wrapper wrapper
required property var sidebar required property var sidebar
readonly property real rounding: Config.border.rounding required property real rounding
readonly property bool flatten: wrapper.height < rounding * 2 readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding readonly property real roundingY: flatten ? wrapper.height / 2 : rounding

View file

@ -141,8 +141,10 @@ StyledRect {
roleValue: "vpn" roleValue: "vpn"
delegate: Toggle { delegate: Toggle {
icon: "vpn_key" icon: "vpn_key"
checked: VPN.connected checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error"
enabled: !VPN.connecting enabled: !VPN.connecting
toggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error"
inactiveOnColour: Colours.palette.m3onSurfaceVariant
onClicked: VPN.toggle() onClicked: VPN.toggle()
} }
} }

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell import Quickshell
import Caelestia import Caelestia
import qs.components import qs.components
import qs.services
import qs.config import qs.config
Item { Item {
@ -12,6 +13,16 @@ Item {
readonly property int spacing: Appearance.spacing.small readonly property int spacing: Appearance.spacing.small
property bool flag property bool flag
function shouldShowToast(toast: Toast): bool {
if (!Notifs.hasFullscreen())
return true;
if (Config.utilities.toasts.fullscreen === "all")
return true;
if (Config.utilities.toasts.fullscreen === "important")
return toast.type === Toast.Warning || toast.type === Toast.Error;
return false;
}
implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2
implicitHeight: { implicitHeight: {
let h = -spacing; let h = -spacing;
@ -31,6 +42,8 @@ Item {
const toasts = []; const toasts = [];
let count = 0; let count = 0;
for (const toast of Toaster.toasts) { for (const toast of Toaster.toasts) {
if (!root.shouldShowToast(toast))
continue;
toasts.push(toast); toasts.push(toast);
if (!toast.closed) { if (!toast.closed) {
count++; count++;

View file

@ -126,8 +126,6 @@ in
prePatch = '' prePatch = ''
substituteInPlace assets/pam.d/fprint \ substituteInPlace assets/pam.d/fprint \
--replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so --replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so
substituteInPlace shell.qml \
--replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false'
''; '';
postInstall = '' postInstall = ''

View file

@ -9,6 +9,7 @@ qml_module(caelestia-internal
hyprextras.hpp hyprextras.cpp hyprextras.hpp hyprextras.cpp
logindmanager.hpp logindmanager.cpp logindmanager.hpp logindmanager.cpp
sparklineitem.hpp sparklineitem.cpp sparklineitem.hpp sparklineitem.cpp
visualiserbars.hpp visualiserbars.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick

View file

@ -0,0 +1,198 @@
#include "visualiserbars.hpp"
#include <algorithm>
#include <cmath>
#include <qbrush.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
namespace caelestia::internal {
VisualiserBars::VisualiserBars(QQuickItem* parent)
: QQuickPaintedItem(parent) {
setAntialiasing(true);
}
void VisualiserBars::advance(qreal dt) {
if (m_displayValues.isEmpty() || m_settled)
return;
// dt is in seconds (from FrameAnimation.frameTime), convert to ms
const qreal dtMs = dt * 1000.0;
const qreal tau = m_animationDuration / 3.0;
const qreal alpha = 1.0 - std::exp(-dtMs / tau);
bool allSettled = true;
for (qsizetype i = 0; i < m_displayValues.size(); ++i) {
const double diff = m_targetValues[i] - m_displayValues[i];
if (std::abs(diff) > 0.001) {
m_displayValues[i] += diff * alpha;
allSettled = false;
} else {
m_displayValues[i] = m_targetValues[i];
}
}
update();
if (allSettled && !m_settled) {
m_settled = true;
emit settledChanged();
}
}
void VisualiserBars::paint(QPainter* painter) {
if (m_displayValues.isEmpty())
return;
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(Qt::NoPen);
const qreal h = height();
const qreal maxBarHeight = h * 0.4;
QLinearGradient gradient(0, h - maxBarHeight, 0, h);
gradient.setColorAt(0, m_primaryColor);
gradient.setColorAt(1, m_secondaryColor);
painter->setBrush(gradient);
drawSide(painter, false);
drawSide(painter, true);
}
void VisualiserBars::drawSide(QPainter* painter, bool rightSide) {
const qreal w = width();
const qreal h = height();
const auto count = m_displayValues.size();
if (count == 0)
return;
const qreal sideWidth = w * 0.4;
const qreal slotWidth = sideWidth / static_cast<qreal>(count);
const qreal barWidth = slotWidth - m_spacing;
if (barWidth <= 0)
return;
const qreal sideOffset = rightSide ? w * 0.6 : 0;
const qreal maxBarHeight = h * 0.4;
for (qsizetype i = 0; i < count; ++i) {
const qsizetype valueIndex = rightSide ? i : (count - i - 1);
const qreal value = std::clamp(m_displayValues[valueIndex], 0.0, 1.0);
const qreal barHeight = value * maxBarHeight;
if (barHeight <= 0)
continue;
const qreal x = static_cast<qreal>(i) * slotWidth + sideOffset;
const qreal y = h - barHeight;
const qreal r = std::min({ m_rounding, barWidth / 2.0, barHeight });
QPainterPath path;
path.moveTo(x, h);
path.lineTo(x, y + r);
if (r > 0) {
path.arcTo(x, y, r * 2, r * 2, 180, -90);
path.lineTo(x + barWidth - r, y);
path.arcTo(x + barWidth - r * 2, y, r * 2, r * 2, 90, -90);
} else {
path.lineTo(x, y);
path.lineTo(x + barWidth, y);
}
path.lineTo(x + barWidth, h);
path.closeSubpath();
painter->drawPath(path);
}
}
QVector<double> VisualiserBars::values() const {
return m_targetValues;
}
void VisualiserBars::setValues(const QVector<double>& values) {
m_targetValues = values;
if (m_displayValues.size() != values.size()) {
m_displayValues.resize(values.size(), 0.0);
}
if (m_settled) {
m_settled = false;
emit settledChanged();
}
emit valuesChanged();
}
bool VisualiserBars::settled() const {
return m_settled;
}
QColor VisualiserBars::primaryColor() const {
return m_primaryColor;
}
void VisualiserBars::setPrimaryColor(const QColor& color) {
if (m_primaryColor == color)
return;
m_primaryColor = color;
emit primaryColorChanged();
update();
}
QColor VisualiserBars::secondaryColor() const {
return m_secondaryColor;
}
void VisualiserBars::setSecondaryColor(const QColor& color) {
if (m_secondaryColor == color)
return;
m_secondaryColor = color;
emit secondaryColorChanged();
update();
}
qreal VisualiserBars::rounding() const {
return m_rounding;
}
void VisualiserBars::setRounding(qreal rounding) {
if (qFuzzyCompare(m_rounding, rounding))
return;
m_rounding = rounding;
emit roundingChanged();
update();
}
qreal VisualiserBars::spacing() const {
return m_spacing;
}
void VisualiserBars::setSpacing(qreal spacing) {
if (qFuzzyCompare(m_spacing, spacing))
return;
m_spacing = spacing;
emit spacingChanged();
update();
}
int VisualiserBars::animationDuration() const {
return m_animationDuration;
}
void VisualiserBars::setAnimationDuration(int duration) {
if (m_animationDuration == duration)
return;
m_animationDuration = duration;
emit animationDurationChanged();
}
} // namespace caelestia::internal

View file

@ -0,0 +1,72 @@
#pragma once
#include <qcolor.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
#include <qvector.h>
namespace caelestia::internal {
class VisualiserBars : public QQuickPaintedItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QVector<double> values READ values WRITE setValues NOTIFY valuesChanged)
Q_PROPERTY(QColor primaryColor READ primaryColor WRITE setPrimaryColor NOTIFY primaryColorChanged)
Q_PROPERTY(QColor secondaryColor READ secondaryColor WRITE setSecondaryColor NOTIFY secondaryColorChanged)
Q_PROPERTY(qreal rounding READ rounding WRITE setRounding NOTIFY roundingChanged)
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
Q_PROPERTY(int animationDuration READ animationDuration WRITE setAnimationDuration NOTIFY animationDurationChanged)
Q_PROPERTY(bool settled READ settled NOTIFY settledChanged)
public:
explicit VisualiserBars(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
Q_INVOKABLE void advance(qreal dt);
[[nodiscard]] QVector<double> values() const;
void setValues(const QVector<double>& values);
[[nodiscard]] QColor primaryColor() const;
void setPrimaryColor(const QColor& color);
[[nodiscard]] QColor secondaryColor() const;
void setSecondaryColor(const QColor& color);
[[nodiscard]] qreal rounding() const;
void setRounding(qreal rounding);
[[nodiscard]] qreal spacing() const;
void setSpacing(qreal spacing);
[[nodiscard]] int animationDuration() const;
void setAnimationDuration(int duration);
[[nodiscard]] bool settled() const;
signals:
void valuesChanged();
void primaryColorChanged();
void secondaryColorChanged();
void roundingChanged();
void spacingChanged();
void animationDurationChanged();
void settledChanged();
private:
void drawSide(QPainter* painter, bool rightSide);
QVector<double> m_targetValues;
QVector<double> m_displayValues;
QColor m_primaryColor;
QColor m_secondaryColor;
qreal m_rounding = 0.0;
qreal m_spacing = 0.0;
int m_animationDuration = 200;
bool m_settled = true;
};
} // namespace caelestia::internal

View file

@ -21,6 +21,22 @@ Singleton {
property bool loaded property bool loaded
function hasFullscreen(): bool {
for (const monitor of Hypr.monitors.values) {
if (monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1))
return true;
}
return false;
}
function shouldShowPopup(): bool {
if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar))
return false;
if (Config.notifs.fullscreen === "off" && hasFullscreen())
return false;
return true;
}
onDndChanged: { onDndChanged: {
if (!Config.utilities.toasts.dndChanged) if (!Config.utilities.toasts.dndChanged)
return; return;
@ -79,7 +95,7 @@ Singleton {
notif.tracked = true; notif.tracked = true;
const comp = notifComp.createObject(root, { const comp = notifComp.createObject(root, {
popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), popup: root.shouldShowPopup(),
notification: notif notification: notif
}); });
root.list = [comp, ...root.list]; root.list = [comp, ...root.list];

View file

@ -10,6 +10,12 @@ Singleton {
id: root id: root
property bool connected: false property bool connected: false
property var status: ({
connected: false,
state: "disconnected",
reason: "",
authUrl: ""
})
readonly property bool connecting: connectProc.running || disconnectProc.running readonly property bool connecting: connectProc.running || disconnectProc.running
readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false)
@ -19,7 +25,7 @@ Singleton {
} }
readonly property bool isCustomProvider: typeof providerInput === "object" readonly property bool isCustomProvider: typeof providerInput === "object"
readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput) readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput)
readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : "" readonly property string interfaceName: isCustomProvider ? (providerInput.iface || "") : ""
readonly property var currentConfig: { readonly property var currentConfig: {
const name = providerName; const name = providerName;
const iface = interfaceName; const iface = interfaceName;
@ -30,7 +36,7 @@ Singleton {
return { return {
connectCmd: custom.connectCmd || defaults.connectCmd, connectCmd: custom.connectCmd || defaults.connectCmd,
disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd, disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd,
interface: custom.interface || defaults.interface, interface: custom.iface || defaults.interface,
displayName: custom.displayName || defaults.displayName displayName: custom.displayName || defaults.displayName
}; };
} }
@ -53,7 +59,7 @@ Singleton {
displayName: "Warp" displayName: "Warp"
}, },
"netbird": { "netbird": {
connectCmd: ["netbird", "up"], connectCmd: ["netbird", "up", "--no-browser"],
disconnectCmd: ["netbird", "down"], disconnectCmd: ["netbird", "down"],
interface: "wt0", interface: "wt0",
displayName: "NetBird" displayName: "NetBird"
@ -75,6 +81,10 @@ Singleton {
} }
function connect(): void { function connect(): void {
if (status.state === "needs-auth" && status.authUrl) {
emitStatusToast(status);
return;
}
if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) { if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) {
connectProc.exec(root.currentConfig.connectCmd); connectProc.exec(root.currentConfig.connectCmd);
} }
@ -87,11 +97,7 @@ Singleton {
} }
function toggle(): void { function toggle(): void {
if (connected) { connected ? disconnect() : connect();
disconnect();
} else {
connect();
}
} }
function checkStatus(): void { function checkStatus(): void {
@ -100,16 +106,221 @@ Singleton {
} }
} }
onConnectedChanged: { function getStatusCommand(): var {
switch (providerName) {
case "tailscale":
return ["tailscale", "status", "--json"];
case "netbird":
return ["netbird", "status", "--json"];
case "warp":
return ["warp-cli", "status"];
case "wireguard":
return ["ip", "link", "show"];
default:
return ["ip", "link", "show"];
}
}
function parseTailscaleStatus(output: string): var {
const status = {
connected: false,
state: "disconnected",
reason: "",
authUrl: ""
};
// Handle empty or whitespace-only output
if (!output || output.trim().length === 0) {
return status;
}
// Check for common non-JSON states first
if (output.includes("Logged out") || output.includes("Stopped") || output.includes("not running") || output.includes("Tailscale is not running")) {
status.state = "disconnected";
return status;
}
// Try to parse as JSON
try {
const data = JSON.parse(output);
const backendState = data.BackendState || "";
if (backendState === "Running") {
status.connected = true;
status.state = "connected";
} else if (backendState === "Starting") {
status.state = "connecting";
} else if (backendState === "NeedsLogin" || backendState === "NeedsMachineAuth") {
status.state = "needs-auth";
status.reason = backendState === "NeedsLogin" ? "Login required" : "Machine authorization required";
status.authUrl = data.AuthURL || "";
}
} catch (e) {
// JSON parsing failed - treat as disconnected unless it looks like an error
if (output.includes("error") || output.includes("Error") || output.includes("failed")) {
status.state = "disconnected";
status.reason = "Tailscale may not be running";
} else {
status.state = "disconnected";
}
}
return status;
}
function parseNetBirdStatus(output: string): var {
const status = {
connected: false,
state: "disconnected",
reason: "",
authUrl: ""
};
try {
const data = JSON.parse(output);
const mgmtConnected = data.management?.connected;
const signalConnected = data.signal?.connected;
if (mgmtConnected && signalConnected) {
status.connected = true;
status.state = "connected";
} else if (data.management?.error) {
const error = data.management.error;
if (error.includes("auth") || error.includes("login")) {
status.state = "needs-auth";
status.reason = "Authentication required";
} else {
status.reason = error;
}
}
} catch (e) {
status.state = "error";
status.reason = "Failed to parse status";
}
return status;
}
function parseWarpStatus(output: string): var {
const status = {
connected: false,
state: "disconnected",
reason: "",
authUrl: ""
};
if (output.includes("Connected")) {
status.connected = true;
status.state = "connected";
} else if (output.includes("Connecting")) {
status.state = "connecting";
} else if (output.includes("Unable") || output.includes("Registration Missing") || output.includes("registration") || output.includes("register")) {
status.state = "needs-auth";
status.reason = "WARP registration required";
} else if (!output.includes("Disconnected")) {
status.state = "error";
status.reason = "Unknown WARP status";
}
return status;
}
function parseWireGuardStatus(output: string): var {
const status = {
connected: false,
state: "disconnected",
reason: "",
authUrl: ""
};
const iface = root.currentConfig?.interface || "";
if (iface && output.includes(iface + ":")) {
status.connected = true;
status.state = "connected";
}
return status;
}
function parseStatusOutput(output: string): var {
switch (providerName) {
case "tailscale":
return parseTailscaleStatus(output);
case "netbird":
return parseNetBirdStatus(output);
case "warp":
return parseWarpStatus(output);
case "wireguard":
default:
return parseWireGuardStatus(output);
}
}
function extractAuthUrl(text: string): string {
const urlMatch = text.match(/(https?:\/\/[^\s]+)/);
return urlMatch ? urlMatch[1] : "";
}
function createAuthStatus(authUrl: string): var {
return {
connected: false,
state: "needs-auth",
reason: "Authentication required",
authUrl: authUrl
};
}
function updateStatus(newStatus: var): void {
const oldState = status.state;
if (newStatus.state === "needs-auth" && !newStatus.authUrl && status.authUrl) {
newStatus.authUrl = status.authUrl;
}
status = newStatus;
root.connected = newStatus.connected;
if (oldState !== newStatus.state) {
emitStatusToast(newStatus);
}
}
function emitStatusToast(statusObj: var): void {
if (!Config.utilities.toasts.vpnChanged) if (!Config.utilities.toasts.vpnChanged)
return; return;
const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN"; const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN";
if (connected) {
switch (statusObj.state) {
case "connected":
Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key"); Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key");
} else { break;
case "disconnected":
if (status.connected) {
Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off");
} }
break;
case "needs-auth":
const authMsg = statusObj.reason || "Authentication required";
Toaster.toast(qsTr("VPN authentication required"), qsTr("%1: %2").arg(displayName).arg(authMsg), "vpn_lock");
break;
case "error":
if (status.state === "connected" || status.state === "connecting" || status.state === "needs-auth") {
const errMsg = statusObj.reason || "Unknown error";
Toaster.toast(qsTr("VPN error"), qsTr("%1: %2").arg(displayName).arg(errMsg), "error");
}
break;
}
}
onStatusChanged: {
if (providerName === "warp" && status.state === "needs-auth" && status.reason.includes("registration")) {
warpRegisterProc.exec(["warp-cli", "registration", "new"]);
}
}
onProviderNameChanged: {
status = {
connected: false,
state: "disconnected",
reason: "",
authUrl: ""
};
root.connected = false;
statusCheckTimer.start();
} }
Component.onCompleted: root.enabled && statusCheckTimer.start() Component.onCompleted: root.enabled && statusCheckTimer.start()
@ -127,7 +338,7 @@ Singleton {
Process { Process {
id: statusProc id: statusProc
command: ["ip", "link", "show"] command: root.getStatusCommand()
// qmllint disable incompatible-type // qmllint disable incompatible-type
environment: ({ environment: ({
// qmllint enable incompatible-type // qmllint enable incompatible-type
@ -136,8 +347,38 @@ Singleton {
}) })
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const iface = root.currentConfig ? root.currentConfig.interface : ""; const newStatus = root.parseStatusOutput(text);
root.connected = iface && text.includes(iface + ":"); root.updateStatus(newStatus);
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim().length > 0) {
if (text.includes("doesn't appear to be running") || text.includes("failed to connect to local tailscaled") || text.includes("daemon is not running") || text.includes("not running") && (text.includes("netbird") || text.includes("warp"))) {
let cmd = "sudo systemctl start ";
switch (root.providerName) {
case "tailscale":
cmd += "tailscaled";
break;
case "netbird":
cmd += "netbird";
break;
case "warp":
cmd += "warp-svc";
break;
default:
cmd += root.providerName + "d";
break;
}
const errorStatus = {
connected: false,
state: "disconnected",
reason: `Service not running (run: ${cmd})`,
authUrl: ""
};
root.updateStatus(errorStatus);
}
}
} }
} }
} }
@ -145,12 +386,59 @@ Singleton {
Process { Process {
id: connectProc id: connectProc
onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters onExited: exitCode => { // qmllint disable signal-handler-parameters
if (exitCode !== 0) {
return;
}
if (root.providerName === "tailscale") {
Qt.callLater(() => {
if (root.status.state !== "needs-auth") {
statusCheckTimer.start();
}
});
} else if (root.status.state !== "needs-auth") {
statusCheckTimer.start();
}
}
stdout: SplitParser {
onRead: data => {
const authUrl = root.extractAuthUrl(data);
if (authUrl) {
root.updateStatus(root.createAuthStatus(authUrl));
}
}
}
stderr: StdioCollector { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
const error = text.trim(); const error = text.trim();
if (error && !error.includes("[#]") && !error.includes("already exists")) {
console.warn("VPN connection error:", error); if (error.includes("Access denied") || error.includes("checkprefs access denied")) {
const errorStatus = {
connected: false,
state: "disconnected",
reason: "Permission denied. Run in terminal: sudo tailscale set --operator=$USER",
authUrl: ""
};
root.updateStatus(errorStatus);
return;
}
if (error.includes("Unknown device type") || error.includes("Protocol not supported")) {
const errorStatus = {
connected: false,
state: "disconnected",
reason: "WireGuard module not loaded. Run: sudo modprobe wireguard",
authUrl: ""
};
root.updateStatus(errorStatus);
return;
}
const authUrl = root.extractAuthUrl(error);
if (authUrl) {
root.updateStatus(root.createAuthStatus(authUrl));
} else if (error.includes("already exists")) { } else if (error.includes("already exists")) {
root.connected = true; root.connected = true;
} }
@ -172,6 +460,16 @@ Singleton {
} }
} }
Process {
id: warpRegisterProc
onExited: exitCode => { // qmllint disable signal-handler-parameters
if (exitCode === 0) {
statusCheckTimer.start();
}
}
}
Timer { Timer {
id: statusCheckTimer id: statusCheckTimer

View file

@ -121,8 +121,8 @@ Singleton {
humidity: json.current.relative_humidity_2m, humidity: json.current.relative_humidity_2m,
windSpeed: json.current.wind_speed_10m, windSpeed: json.current.wind_speed_10m,
isDay: json.current.is_day, isDay: json.current.is_day,
sunrise: json.daily.sunrise[0], sunrise: json.daily.sunrise[0].replace("T", " "),
sunset: json.daily.sunset[0] sunset: json.daily.sunset[0].replace("T", " ")
}; };
const forecastList = []; const forecastList = [];
@ -218,7 +218,6 @@ Singleton {
target: Config.services target: Config.services
} }
// Refresh current location hourly
Timer { Timer {
interval: 3600000 // 1 hour interval: 3600000 // 1 hour
running: true running: true