Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 7s
C++ / build (pull_request) Failing after 12s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 35s
Rust / build (pull_request) Successful in 1m0s
Python / verify (pull_request) Successful in 1m44s
Rust / fmt (pull_request) Successful in 45s
Rust / clippy (pull_request) Successful in 1m7s

This commit is contained in:
2026-10-22 23:21:20 +02:00
469 changed files with 40652 additions and 7947 deletions
+9 -8
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Modules.Bar.Components
import qs.Modules.Bar.Popouts
@@ -16,12 +17,12 @@ GridLayout {
required property Wrapper popouts
required property ClipWrapper popoutsWrapper
required property ShellScreen screen
required property ScreenState screenState
readonly property int vPadding: 6
required property PersistentProperties visibilities
function axisCenterOf(item: Item): real {
const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2);
return horizontal ? c.x : c.y;
return horizontal ? Math.floor(c.x) : Math.floor(c.y);
}
function checkPopout(pos: real): void {
@@ -34,7 +35,7 @@ GridLayout {
return;
}
if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings)
if (screenState.sidebar || screenState.dashboard || screenState.resources || screenState.settings)
return;
if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
@@ -88,10 +89,10 @@ GridLayout {
role: "id"
DelegateChoice {
roleValue: "hyprsunset"
roleValue: "nightlight"
delegate: EntryWrapper {
HyprsunsetWidget {
NightlightWidget {
horizontal: root.horizontal
visible: !root.fullscreen || root.isHovered
}
@@ -149,7 +150,7 @@ GridLayout {
delegate: EntryWrapper {
Resources {
horizontal: root.horizontal
visibilities: root.visibilities
screenState: root.screenState
visible: !root.fullscreen || root.isHovered
}
}
@@ -173,7 +174,7 @@ GridLayout {
NotifBell {
horizontal: root.horizontal
popouts: root.popouts
visibilities: root.visibilities
screenState: root.screenState
visible: !root.fullscreen || root.isHovered
}
}
@@ -187,7 +188,7 @@ GridLayout {
horizontal: root.horizontal
loader: root
popouts: root.popouts
visibilities: root.visibilities
screenState: root.screenState
visible: !root.fullscreen || root.isHovered
}
}
+4 -4
View File
@@ -10,7 +10,7 @@ Item {
id: root
readonly property int clampedExtent: Math.max(Config.bar.border, extent)
readonly property int contentThickness: Math.floor(Math.max(Config.bar.height, Tokens.bar.innerSize * (horizontal ? 1 : 1.5))) + padding * 2
readonly property int contentThickness: Math.floor(Math.max(Config.bar.height, Tokens.sizes.bar.innerSize * (horizontal ? 1 : 1.5))) + padding * 2
readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentThickness
property real extent: fullscreen ? 0 : Config.bar.border
required property bool fullscreen
@@ -21,9 +21,9 @@ Item {
required property ClipWrapper popoutsWrapper
required property string position
required property ShellScreen screen
readonly property bool shouldBeVisible: (!fullscreen && (!Config.bar.autoHide || visibilities.bar)) || isHovered
readonly property bool shouldBeVisible: (!fullscreen && (!Config.bar.autoHide || screenState.bar)) || isHovered
readonly property int vPadding: 6
required property PersistentProperties visibilities
required property ScreenState screenState
function checkPopout(pos: real): void {
(content.item as Bar)?.checkPopout(pos);
@@ -85,7 +85,7 @@ Item {
popouts: root.popouts
popoutsWrapper: root.popoutsWrapper
screen: root.screen
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ Item {
id: root
required property Item bar
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.fill: parent
+12 -25
View File
@@ -13,7 +13,8 @@ import qs.Services
WidgetBase {
id: root
readonly property color contentColor: visibilities.dashboard ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
readonly property font font: Tokens.font.clock.family(Config.appearance.font.family.clock).build()
readonly property color contentColor: Colors.palette.m3onSurface
readonly property string effectiveFormat: {
let fmt = Config.general.dateFormat;
if (!root.horizontal)
@@ -44,10 +45,9 @@ WidgetBase {
}
required property GridLayout loader
required property Wrapper popouts
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.small * 2 : verticalColumn.implicitHeight + Tokens.padding.small * 2
required property PersistentProperties visibilities
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
required property ScreenState screenState
color: visibilities.dashboard ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
implicitHeight: horizontal ? baseSize : size
implicitWidth: horizontal ? size : baseSize
radius: Tokens.rounding.full
@@ -57,14 +57,13 @@ WidgetBase {
anchors.centerIn: parent
color: root.contentColor
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
font: root.font
height: implicitHeight
text: Time.dateStr
visible: root.horizontal
Behavior on color {
CAnim {
}
CAnim {}
}
}
@@ -84,12 +83,11 @@ WidgetBase {
Layout.alignment: Qt.AlignHCenter
color: root.contentColor
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
font: root.font
text: Qt.formatDateTime(Time.date, modelData)
Behavior on color {
CAnim {
}
CAnim {}
}
}
}
@@ -114,7 +112,7 @@ WidgetBase {
Layout.alignment: Qt.AlignHCenter
color: root.contentColor
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
font: root.font
text: {
if (modelData.includes("h"))
return Time.hourStr;
@@ -126,8 +124,7 @@ WidgetBase {
}
Behavior on color {
CAnim {
}
CAnim {}
}
}
}
@@ -135,23 +132,13 @@ WidgetBase {
CustomText {
Layout.alignment: Qt.AlignHCenter
color: root.contentColor
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
font: root.font
text: Qt.formatDateTime(Time.date, "AP")
visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0
Behavior on color {
CAnim {
}
CAnim {}
}
}
}
StateLayer {
acceptedButtons: Qt.LeftButton
color: root.contentColor
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
}
}
}
@@ -28,8 +28,7 @@ Row {
CustomText {
color: Battery.getColors(root.percentage, root.devState).fg
font.pointSize: Tokens.font.size.larger / 1.5
font.weight: 800
font: Tokens.font.body.builders.small.size(11).weight(Font.Bold).build()
height: track.height
horizontalAlignment: Text.AlignHCenter
text: Math.round(root.percentage * 100)
@@ -60,8 +59,7 @@ Row {
clip: true
color: Battery.getColors(root.percentage, root.devState).bg
font.pointSize: Tokens.font.size.larger / 1.5
font.weight: 800
font: Tokens.font.body.builders.small.size(11).weight(Font.Bold).build()
height: track.height
horizontalAlignment: Text.AlignHCenter
text: Math.round(root.percentage * 100)
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Services
CustomRect {
id: root
readonly property int baseSize: horizontal ? Math.max(Config.bar.height, Tokens.bar.innerSize) : Math.max(Config.bar.height, Math.floor(Tokens.bar.innerSize * 1.5))
readonly property int baseSize: horizontal ? Math.max(Config.bar.height, Tokens.sizes.bar.innerSize) : Math.max(Config.bar.height, Math.floor(Tokens.sizes.bar.innerSize * 1.5))
required property bool horizontal
color: Colors.tPalette.m3surfaceContainer
+4 -8
View File
@@ -19,14 +19,12 @@ WidgetBase {
Behavior on implicitHeight {
enabled: !root.horizontal
Anim {
}
Anim {}
}
Behavior on implicitWidth {
enabled: root.horizontal
Anim {
}
Anim {}
}
TextMetrics {
@@ -47,15 +45,14 @@ WidgetBase {
columns: root.horizontal ? -1 : 1
Behavior on implicitWidth {
Anim {
}
Anim {}
}
MaterialIcon {
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
animate: true
color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface
font.pointSize: Tokens.font.size.larger
fontStyle: Tokens.font.icon.small
text: Players.active?.isPlaying ? "music_note" : "music_off"
}
@@ -65,7 +62,6 @@ WidgetBase {
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
animate: true
color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter
implicitHeight: root.horizontal ? root.implicitHeight : root.textWidth
implicitWidth: root.horizontal ? root.textWidth : root.implicitWidth
@@ -8,15 +8,15 @@ import qs.Services
WidgetBase {
id: root
property bool tempEnabled: Hyprsunset.enabled
property bool tempEnabled: Nightlight.enabled
color: root.tempEnabled ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.full
StateLayer {
onClicked: {
Hyprsunset.toggleNightLight();
Hyprsunset.manualToggle = true;
Nightlight.toggleNightLight();
Nightlight.manualToggle = true;
}
}
@@ -25,12 +25,11 @@ WidgetBase {
animate: true
color: root.tempEnabled ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.tempEnabled ? 1 : 0
font.pointSize: Tokens.font.size.larger
fontStyle: Tokens.font.icon.small
text: root.tempEnabled ? "lightbulb" : "light_off"
Behavior on fill {
Anim {
}
Anim {}
}
}
}
+9 -11
View File
@@ -11,35 +11,33 @@ WidgetBase {
id: root
required property Wrapper popouts
required property PersistentProperties visibilities
required property ScreenState screenState
color: visibilities.sidebar ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
color: screenState.sidebar ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.full
MaterialIcon {
id: notificationCenterIcon
anchors.centerIn: parent
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.visibilities.sidebar ? 1 : 0
font.pointSize: Tokens.font.size.larger
color: root.screenState.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.screenState.sidebar ? 1 : 0
fontStyle: Tokens.font.icon.size(16).build()
text: NotifServer.list.length ? "\uf4fe" : "\ue7f4"
Behavior on color {
CAnim {
}
CAnim {}
}
Behavior on fill {
Anim {
}
Anim {}
}
}
StateLayer {
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
color: root.screenState.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: {
root.visibilities.sidebar = !root.visibilities.sidebar;
root.screenState.sidebar = !root.screenState.sidebar;
}
}
}
+30 -17
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import Quickshell
import qs.Components
import ZShell.Config
import qs.Services
@@ -12,6 +12,7 @@ GridLayout {
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
required property bool horizontal
property string icon
required property color iconColor
@@ -22,6 +23,10 @@ GridLayout {
property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80
function snap(v) {
return Math.round(v * dpr) / dpr;
}
columnSpacing: Tokens.spacing.medium
columns: horizontal ? -1 : 1
percentage: 0
@@ -44,31 +49,39 @@ GridLayout {
Layout.preferredWidth: 16
color: root.iconColor
font.pointSize: Tokens.font.size.larger
fontStyle: Tokens.font.icon.small
text: root.icon
}
CustomClippingRect {
CustomRect {
id: track
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
radius: Tokens.rounding.full
radius: Math.min(width, height) / 2
CustomRect {
id: fill
Item {
id: clipper
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: root.horizontal ? parent.right : undefined
anchors.top: root.horizontal ? undefined : parent.top
color: root.mainColor
implicitHeight: root.horizontal ? Math.ceil(root.percentage * parent.height) : 0
implicitWidth: root.horizontal ? 0 : Math.ceil(root.percentage * parent.width)
anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
clip: true
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
// Behavior on implicitHeight {
// Anim {
// }
// }
CustomRect {
anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
color: root.mainColor
height: track.height
radius: track.radius
width: track.width
}
}
}
}
+12 -12
View File
@@ -12,18 +12,18 @@ import qs.Components
WidgetBase {
id: root
required property PersistentProperties visibilities
required property ScreenState screenState
clip: true
color: visibilities.resources ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
color: screenState.resources ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
implicitHeight: horizontal ? baseSize : gridLayout.implicitHeight + Tokens.padding.medium * 2
implicitWidth: horizontal ? gridLayout.implicitWidth + Tokens.padding.medium * 2 : baseSize
radius: Tokens.rounding.full
StateLayer {
color: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
color: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: root.visibilities.resources = !root.visibilities.resources
onClicked: root.screenState.resources = !root.screenState.resources
}
GridLayout {
@@ -53,8 +53,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "memory"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
percentage: Cpu.percentage
warningThreshold: 95
}
@@ -64,8 +64,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "memory_alt"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary
percentage: Memory.percentage
warningThreshold: 80
}
@@ -75,8 +75,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "gamepad"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary
percentage: Gpu.percentage
}
@@ -85,8 +85,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal
horizontal: root.horizontal
icon: "developer_board"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
percentage: Gpu.memoryUsed / Gpu.memoryTotal
}
}
@@ -13,19 +13,16 @@ MaterialIcon {
animate: true
color: Audio.muted ? Colors.palette.m3error : Colors.palette.m3onSurface
fill: 1
font.pointSize: Tokens.font.size.larger
fontStyle: Tokens.font.icon.small
text: Audio.muted ? "volume_off" : "volume_up"
Behavior on Layout.maximumWidth {
Anim {
}
Anim {}
}
Behavior on opacity {
Anim {
}
Anim {}
}
Behavior on scale {
Anim {
}
Anim {}
}
}
@@ -13,19 +13,16 @@ MaterialIcon {
animate: true
color: (Audio.sourceMuted ?? false) ? Colors.palette.m3error : Colors.palette.m3onSurface
fill: 1
font.pointSize: Tokens.font.size.larger
fontStyle: Tokens.font.icon.small
text: Audio.sourceMuted ? "mic_off" : "mic"
Behavior on Layout.maximumWidth {
Anim {
}
Anim {}
}
Behavior on opacity {
Anim {
}
Anim {}
}
Behavior on scale {
Anim {
}
Anim {}
}
}
@@ -15,20 +15,16 @@ Item {
implicitWidth: currentItem?.implicitWidth ?? 0
Behavior on Layout.preferredHeight {
Anim {
}
Anim {}
}
Behavior on Layout.preferredWidth {
Anim {
}
Anim {}
}
Behavior on opacity {
Anim {
}
Anim {}
}
Behavior on scale {
Anim {
}
Anim {}
}
Component.onCompleted: Battery.startDaemon()
@@ -67,7 +63,7 @@ Item {
Layout.alignment: Qt.AlignVCenter
animate: true
fill: 1
font.pointSize: Tokens.font.size.larger
fontStyle: Tokens.font.icon.small
text: {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
+4 -4
View File
@@ -62,10 +62,10 @@ Item {
root.popouts.currentName = `traymenu${root.ind}`;
root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x);
root.popouts.hasCurrent = true;
if (visibilities.sidebar || visibilities.dashboard || visibilities.settings) {
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.settings = false;
if (screenState.sidebar || screenState.dashboard || screenState.settings) {
screenState.sidebar = false;
screenState.dashboard = false;
screenState.settings = false;
}
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ WidgetBase {
Repeater {
id: repeater
model: SystemTray.items
model: SystemTray.items.values.filter(i => i.status !== Status.Passive && i.hasMenu)
TrayItem {
id: trayItem
+1 -2
View File
@@ -27,14 +27,13 @@ WidgetBase {
MaterialIcon {
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
font.pointSize: root.horizontal ? Tokens.font.size.larger : Tokens.font.size.large
fontStyle: Tokens.font.icon.small
text: "package_2"
}
CustomText {
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
color: root.textColor
font.pointSize: Tokens.font.size.normal
text: root.countUpdates
}
}
+2 -4
View File
@@ -52,8 +52,7 @@ Item {
elide: Qt.ElideRight
elideWidth: root.maxSize - (root.horizontal ? root.current.anchors.leftMargin : 0)
font.family: "Rubik"
font.pointSize: Tokens.font.size.normal
font: Tokens.font.body.small
text: Hypr.activeToplevel?.title ?? qsTr("Desktop")
onElideWidthChanged: root.current.text = elidedText
@@ -81,8 +80,7 @@ Item {
width: root.horizontal ? implicitWidth : implicitHeight
Behavior on opacity {
Anim {
}
Anim {}
}
transform: [
Translate {
+3 -5
View File
@@ -17,7 +17,7 @@ Item {
required property bool horizontal
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
required property ShellScreen screen
readonly property real shortSize: Math.max(Config.bar.height, Tokens.bar.innerSize)
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
@@ -30,14 +30,12 @@ Item {
Behavior on implicitHeight {
enabled: !root.horizontal
Anim {
}
Anim {}
}
Behavior on implicitWidth {
enabled: root.horizontal
Anim {
}
Anim {}
}
CustomRect {
+2 -3
View File
@@ -181,8 +181,7 @@ Column {
}
]
transitions: Transition {
AnchorAnim {
}
AnchorAnim {}
}
}
@@ -246,7 +245,7 @@ Column {
anchors.centerIn: parent
color: profiles.current === text ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: profiles.current === text ? 1 : 0
font.pointSize: Tokens.font.size.large * 2
fontStyle: Tokens.font.icon.extraLarge
grade: Colors.light ? 0 : (text === "balance" ? -25 : 0)
text: parent.icon
+10 -8
View File
@@ -36,15 +36,17 @@ CustomClippingRect {
MaterialIcon {
id: noUpdatesIcon
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.centerIn: parent
anchors.verticalCenterOffset: -noUpdatesText.implicitHeight
color: Colors.tPalette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.extraLarge * 3
fontStyle: Tokens.font.icon.builders.large.size(48).weight(Font.DemiBold).build()
horizontalAlignment: Text.AlignHCenter
text: "check"
}
CustomText {
id: noUpdatesText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: noUpdatesIcon.bottom
color: Colors.tPalette.m3onSurfaceVariant
@@ -89,7 +91,7 @@ CustomClippingRect {
anchors.rightMargin: Tokens.padding.small
MaterialIcon {
font.pointSize: Tokens.font.size.large * 2
fontStyle: Tokens.font.icon.extraLarge
text: "package_2"
}
@@ -100,7 +102,7 @@ CustomClippingRect {
Layout.fillWidth: true
Layout.preferredHeight: 25
elide: Text.ElideRight
font.pointSize: Tokens.font.size.large
font: Tokens.font.title.medium
text: update.sections[0]
}
@@ -122,7 +124,7 @@ CustomClippingRect {
Layout.preferredWidth: 125
animate: true
color: Colors.palette.m3tertiary
font.pointSize: Tokens.font.size.large
font: Tokens.font.title.medium
horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true
pauseMs: 4000
@@ -133,7 +135,7 @@ CustomClippingRect {
MaterialIcon {
Layout.fillHeight: true
color: Colors.palette.m3secondary
font.pointSize: Tokens.font.size.extraLarge
fontStyle: Tokens.font.icon.large
horizontalAlignment: Text.AlignHCenter
text: "arrow_right_alt"
verticalAlignment: Text.AlignVCenter
@@ -146,7 +148,7 @@ CustomClippingRect {
Layout.preferredWidth: 120
animate: true
color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.large
font: Tokens.font.title.medium
horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true
pauseMs: 4000
+4 -6
View File
@@ -14,7 +14,7 @@ Item {
readonly property int itemHeight: Config.clipboard.sizes.itemHeight
required property ShellScreen screen
required property PersistentProperties visibilities
required property ScreenState screenState
implicitHeight: search.implicitHeight + entries.anchors.topMargin + ((view.spacing + itemHeight) * Config.clipboard.maxEntriesShown) - view.spacing
implicitWidth: Config.clipboard.sizes.width + preview.width + preview.anchors.leftMargin
@@ -49,7 +49,7 @@ Item {
Keys.onUpPressed: view.decrementCurrentIndex()
onAccepted: {
ClipHistory.copy(view.currentItem.modelData);
root.visibilities.clipboard = false;
root.screenState.clipboard = false;
}
}
}
@@ -66,8 +66,7 @@ Item {
radius: 25
Behavior on implicitWidth {
Anim {
}
Anim {}
}
Column {
@@ -104,7 +103,6 @@ Item {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small
text: lineRow.modelData.line
}
}
@@ -252,7 +250,7 @@ Item {
anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Tokens.font.size.large
fontStyle: Tokens.font.icon.large
text: clipItem.isImage ? "image" : "text_fields"
}
+3 -3
View File
@@ -11,8 +11,8 @@ Item {
property int contentHeight
property real offsetScale: shouldBeActive ? 0 : 1
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.clipboard && Config.clipboard.enabled
required property PersistentProperties visibilities
readonly property bool shouldBeActive: screenState.clipboard && Config.clipboard.enabled
required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
@@ -35,7 +35,7 @@ Item {
sourceComponent: Content {
screen: root.screen
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+123 -36
View File
@@ -1,82 +1,169 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell
import qs.Components
Item {
id: root
required property PersistentProperties dashState
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
readonly property var dashboardTabs: {
const allTabs = [
{
component: dashComponent,
iconName: "dashboard",
text: qsTr("Dashboard"),
enabled: Config.dashboard.showDashboard
},
{
component: weatherComponent,
iconName: "cloud",
text: qsTr("Weather"),
enabled: Config.dashboard.showWeather
},
{
component: mediaComponent,
iconName: "queue_music",
text: qsTr("Media"),
enabled: Config.dashboard.showMedia
},
{
component: procsComponent,
iconName: "speed",
text: qsTr("Processes"),
enabled: Config.dashboard.showProcesses
},
];
return allTabs.filter(tab => tab.enabled);
}
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins + viewWrapper.anchors.topMargin
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
required property PersistentProperties visibilities
required property ScreenState screenState
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
Behavior on implicitHeight {
Anim {
type: Anim.SlowEffects
}
}
Behavior on implicitWidth {
Anim {
type: Anim.SlowEffects
}
}
ClippingRectangle {
Tabs {
id: tabs
anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
nonAnimWidth: view.implicitWidth
screenState: root.screenState
tabs: root.dashboardTabs
}
Item {
id: viewWrapper
property int radius: Tokens.rounding.extraLarge - anchors.margins
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.right: parent.right
anchors.top: parent.top
color: "transparent"
radius: Tokens.rounding.extraLarge - anchors.margins
anchors.top: tabs.bottom
anchors.topMargin: Tokens.spacing.small
clip: view.currentIndex !== 3 || (!view.currentItem?.settled ?? true)
Item {
id: view
readonly property int currentIndex: root.dashState?.currentTab
readonly property Item currentItem: row.children[currentIndex]
readonly property int currentIndex: root.screenState.dashboardTab
readonly property Item currentItem: {
repeater.count;
return repeater.itemAt(currentIndex);
}
anchors.fill: parent
implicitHeight: currentItem.implicitHeight
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem?.implicitHeight ?? 0
implicitWidth: currentItem?.implicitWidth ?? 0
RowLayout {
id: row
Repeater {
id: repeater
Pane {
index: 0
delegate: Loader {
id: paneLoader
sourceComponent: Dash {
dashState: root.dashState
visibilities: root.visibilities
required property int index
required property var modelData
readonly property real targetX: index < view.currentIndex ? -view.implicitWidth : index > view.currentIndex ? view.implicitWidth : 0
readonly property bool settled: x === targetX
active: index === view.currentIndex || opacity > 0
opacity: index === view.currentIndex ? 1 : 0
sourceComponent: modelData.component
visible: opacity > 0
x: targetX
y: 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on x {
Anim {
type: Anim.DefaultEffects
}
}
}
model: ScriptModel {
values: root.dashboardTabs
}
}
Component {
id: dashComponent
Dash {
radius: viewWrapper.radius
screenState: root.screenState
}
}
Component {
id: weatherComponent
WeatherTab {
radius: viewWrapper.radius
}
}
Component {
id: mediaComponent
Media {
radius: viewWrapper.radius
screenState: root.screenState
}
}
Component {
id: procsComponent
ProcessesTab {
radius: viewWrapper.radius
}
}
}
}
component Pane: Loader {
id: pane
required property int index
Layout.alignment: Qt.AlignTop
Component.onCompleted: active = Qt.binding(() => {
// Always keep current tab loaded
if (pane.index === view.currentIndex)
return true;
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
})
}
}
+7 -23
View File
@@ -11,10 +11,9 @@ import qs.Services
GridLayout {
id: root
required property PersistentProperties dashState
readonly property bool dashboardVisible: visibilities.dashboard
property int radius: Tokens.rounding.medium
required property PersistentProperties visibilities
readonly property bool dashboardVisible: screenState.dashboard
required property int radius
required property ScreenState screenState
columnSpacing: Tokens.spacing.small
rowSpacing: Tokens.spacing.small
@@ -61,19 +60,18 @@ GridLayout {
User {
id: user
dashState: root.dashState
screenState: root.screenState
}
}
Rect {
Layout.columnSpan: 2
Layout.fillHeight: true
Layout.preferredWidth: Config.dashboard.sizes.weatherWidth
Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth
Layout.row: 0
radius: root.radius
Weather {
}
Weather {}
}
Rect {
@@ -87,7 +85,7 @@ GridLayout {
Calendar {
id: calendar
dashState: root.dashState
screenState: root.screenState
}
}
@@ -104,20 +102,6 @@ GridLayout {
}
}
CustomClippingRect {
Layout.column: 0
Layout.columnSpan: 5
Layout.fillWidth: true
Layout.preferredHeight: media.implicitHeight
Layout.row: 2
color: Colors.tPalette.m3surfaceContainer
radius: root.radius
Media {
id: media
}
}
component Rect: CustomRect {
color: Colors.tPalette.m3surfaceContainer
}
+9 -9
View File
@@ -12,12 +12,12 @@ CustomMouseArea {
id: root
property int activeGrid: 1
required property PersistentProperties dashState
required property ScreenState screenState
property bool initialized: false
property int month1
property int month2
readonly property int realCurrMonth: dashState.currentDate.getMonth()
readonly property int realCurrYear: dashState.currentDate.getFullYear()
readonly property int realCurrMonth: screenState.dashboardDate.getMonth()
readonly property int realCurrYear: screenState.dashboardDate.getFullYear()
property int year1
property int year2
@@ -43,9 +43,9 @@ CustomMouseArea {
function onWheel(event: WheelEvent): void {
if (event.angleDelta.y > 0)
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1);
root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1);
else if (event.angleDelta.y < 0)
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1);
root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1);
}
acceptedButtons: Qt.MiddleButton
@@ -61,7 +61,7 @@ CustomMouseArea {
initialized = true;
}
onClicked: root.dashState.currentDate = new Date()
onClicked: root.screenState.dashboardDate = new Date()
onRealCurrMonthChanged: handleDateChange()
onRealCurrYearChanged: handleDateChange()
@@ -135,7 +135,7 @@ CustomMouseArea {
padding: Tokens.padding.extraSmall
type: IconButton.Text
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1)
onClicked: root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1)
}
Item {
@@ -158,7 +158,7 @@ CustomMouseArea {
}
}
onClicked: root.dashState.currentDate = new Date()
onClicked: root.screenState.dashboardDate = new Date()
}
Item {
@@ -206,7 +206,7 @@ CustomMouseArea {
padding: Tokens.padding.extraSmall
type: IconButton.Text
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1)
onClicked: root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1)
}
}
-51
View File
@@ -1,51 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Components
import ZShell.Config
import qs.Helpers
import qs.Services
Item {
id: root
anchors.bottom: parent.bottom
anchors.top: parent.top
implicitWidth: 110
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: -(font.pointSize * 0.4)
color: Colors.palette.m3secondary
font.family: "Rubik"
font.pointSize: 18
font.weight: 600
text: Time.hourStr
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3primary
font.family: "Rubik"
font.pointSize: 18 * 0.9
text: "•••"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -(font.pointSize * 0.4)
color: Colors.palette.m3secondary
font.family: "Rubik"
font.pointSize: 18
font.weight: 600
text: Time.minuteStr
}
}
}
-329
View File
@@ -1,329 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import QtQuick.Effects
import ZShell.Components
import ZShell.Services
import ZShell.Config
import qs.Modules.Dashboard.Dash.Media
import qs.Daemons
import qs.Components
import qs.Helpers
import qs.Services
Item {
id: root
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
property real playerProgress: {
const active = Players.active;
return active?.length ? (active.position % active.length) / active.length : 0;
}
property int rowHeight: Tokens.padding.large + Config.dashboard.sizes.mediaProgressThickness + Tokens.spacing.small
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
const hours = Math.floor(length / 3600);
const mins = Math.floor((length % 3600) / 60);
const secs = Math.floor(length % 60).toString().padStart(2, "0");
if (hours > 0)
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
Behavior on playerProgress {
Anim {
duration: Tokens.anim.durations.large
}
}
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
ServiceRef {
service: Audio.cava
}
BackgroundShapes {
anchors.fill: parent
}
Shape {
id: visualizer
readonly property int bars: Config.services.visualizerBars
property color color: Qt.alpha(Colors.palette.m3primary, 0.5)
property color fillColor: Qt.alpha(color, 0.1)
anchors.fill: parent
anchors.leftMargin: -(shape.strokeWidth / 2)
layer.enabled: true
asynchronous: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: shape
strokeColor: visualizer.color
fillColor: visualizer.fillColor
strokeWidth: 2
PathSvg {
path: shape.curvePath
}
property string curvePath: {
const values = Audio.cava.values;
const n = values.length;
if (n < 2)
return "";
const h = root.height + shape.strokeWidth / 2;
const w = root.width + shape.strokeWidth;
function x(i) {
return i * w / (n - 1);
}
function y(i) {
return h - (values[i] * Config.dashboard.sizes.mediaVisualizerSize) / 2;
}
let d = `M 0 ${h} `;
d += `L ${x(0)} ${y(0)} `;
for (let i = 0; i < n - 1; ++i) {
const x0 = x(i);
const y0 = y(i);
const x1 = x(i + 1);
const y1 = y(i + 1);
const prev = Math.max(0, i - 1);
const next = Math.min(n - 1, i + 2);
const c1x = x0 + (x1 - x(prev)) / 6;
const c1y = y0 + (y1 - y(prev)) / 6;
const c2x = x1 - (x(next) - x0) / 6;
const c2y = y1 - (y(next) - y0) / 6;
d += `C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x1} ${y1} `;
}
d += `L ${w} ${h} `;
d += `L 0 ${h} Z`;
return d;
}
}
}
CustomRect {
anchors.fill: visualizer
color: Qt.alpha(Colors.palette.m3shadow, 0.2)
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: visualizer
}
}
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.large
anchors.leftMargin: Tokens.padding.extraLarge
CoverVisualizer {
Layout.fillHeight: true
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.largeIncreased
Layout.rightMargin: Tokens.spacing.largeIncreased
spacing: Tokens.spacing.small
MarqueeText {
id: title
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter
pauseMs: 4000
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
implicitWidth: parent.width - Tokens.padding.large * 4
}
MarqueeText {
id: album
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.small
horizontalAlignment: Text.AlignHCenter
pauseMs: 4000
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
implicitWidth: parent.width - Tokens.padding.large * 4
}
MarqueeText {
id: artist
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
color: Colors.palette.m3secondary
horizontalAlignment: Text.AlignHCenter
pauseMs: 4000
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
implicitWidth: parent.width - Tokens.padding.large * 4
}
RowLayout {
id: positionSliderLayout
Layout.topMargin: Tokens.spacing.large
Layout.fillWidth: true
spacing: Tokens.spacing.small
TextMetrics {
id: timeMetrics
text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
}
CustomText {
id: positionLabel
Layout.preferredWidth: timeMetrics.width
text: root.lengthStr(Players.active?.position ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
CustomSlider {
id: positionSlider
Layout.fillWidth: true
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
wavy: true
animateWave: Players.active?.isPlaying ?? false
waveFrequency: 5
waveDuration: 2000
interactionOnMove: false
onInteraction: value => {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: positionLabel
property: "text"
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
when: positionSlider.dragging
}
}
CustomText {
Layout.preferredWidth: timeMetrics.width
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
}
ButtonRow {
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
Layout.leftMargin: positionSliderLayout.implicitWidth / 4
Layout.rightMargin: positionSliderLayout.implicitWidth / 4
IconButton {
type: IconButton.Tonal
icon: "shuffle"
isRound: true
shapeMorph: true
checked: Players.active?.shuffle ?? false
enabled: Players.active?.shuffleSupported
onClicked: Players.active.shuffle = !Players.active?.shuffle
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
font.pointSize: Tokens.font.size.large
}
IconButton {
id: playPauseBtn
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
isRound: true
shapeMorph: true
fillWidth: true
checked: Players.active?.isPlaying ?? false
enabled: Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
font.pointSize: Tokens.font.size.large
}
IconButton {
id: nextBtn
type: IconButton.Tonal
icon: "skip_next"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
font.pointSize: Tokens.font.size.large
}
IconButton {
type: IconButton.Tonal
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
isRound: true
shapeMorph: true
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
enabled: Players.active?.loopSupported
onClicked: {
const state = Players.active.loopState;
if (state === MprisLoopState.None)
Players.active.loopState = MprisLoopState.Track;
else if (state === MprisLoopState.Track)
Players.active.loopState = MprisLoopState.Playlist;
else
Players.active.loopState = MprisLoopState.None;
}
}
}
}
}
}
+7 -10
View File
@@ -9,7 +9,7 @@ import qs.Helpers
Row {
id: root
required property PersistentProperties dashState
required property ScreenState screenState
padding: 20
spacing: 12
@@ -23,8 +23,7 @@ Row {
MaterialIcon {
anchors.centerIn: parent
fill: 1
font.pointSize: Math.floor(info.implicitHeight / 2) || 1
grade: 200
fontStyle: Tokens.font.icon.large
text: "person"
}
@@ -52,7 +51,7 @@ Row {
id: icon
anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2
anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2
color: Colors.palette.m3primary
implicitSize: Math.floor(13 * 1.34)
source: SystemInfo.osLogo
@@ -65,9 +64,8 @@ Row {
anchors.leftMargin: icon.anchors.leftMargin
anchors.verticalCenter: icon.verticalCenter
elide: Text.ElideRight
font.pointSize: 13
text: `: ${SystemInfo.osPrettyName || SystemInfo.osName}`
width: Config.dashboard.sizes.infoWidth
width: Tokens.sizes.dashboard.infoWidth
}
}
@@ -100,10 +98,10 @@ Row {
id: icon
anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2
anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2
color: line.colour
fill: 1
font.pointSize: 13
fontStyle: Tokens.font.icon.small
text: line.icon
}
@@ -114,9 +112,8 @@ Row {
anchors.leftMargin: icon.anchors.leftMargin
anchors.verticalCenter: icon.verticalCenter
elide: Text.ElideNone
font.pointSize: 13
text: `: ${line.text}`
width: Config.dashboard.sizes.infoWidth
width: Tokens.sizes.dashboard.infoWidth
}
}
}
+2 -3
View File
@@ -19,7 +19,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
animate: true
color: Colors.palette.m3secondary
font.pointSize: 54
fontStyle: Tokens.font.icon.extraLarge
text: Weather.icon
}
@@ -35,8 +35,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
animate: true
color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.extraLarge
font.weight: 500
font: Tokens.font.body.large
text: Weather.temp
}
+158
View File
@@ -0,0 +1,158 @@
import QtQuick
import QtQuick.Layouts
import M3Shapes
import ZShell.Config
import qs.Modules.Dashboard.Media
import qs.Components
import qs.Services
import qs.Helpers
Item {
id: root
property ScreenState screenState
required property int radius
implicitWidth: 1000
implicitHeight: 320
CustomClippingRect {
anchors.fill: parent
radius: root.radius
BackgroundShapes {
anchors.fill: parent
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.extraLarge
CoverVisualizer {
Layout.fillHeight: true
implicitWidth: 250
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
state: Players.active ? "" : "noMedia"
states: State {
name: "noMedia"
PropertyChanges {
noMedia.opacity: 1
content.opacity: 0
}
}
transitions: [
Transition {
from: ""
SequentialAnimation {
Anim {
target: content
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
target: noMedia
property: "opacity"
type: Anim.SlowEffects
}
}
},
Transition {
to: ""
SequentialAnimation {
Anim {
target: noMedia
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
target: content
property: "opacity"
type: Anim.SlowEffects
}
}
}
]
Loader {
id: noMedia
anchors.centerIn: parent
anchors.horizontalCenterOffset: -Tokens.padding.extraLarge * 2
asynchronous: true
active: opacity > 0
opacity: 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
MaterialShape {
Layout.topMargin: (pathBounds().height - implicitSize) / 2
Layout.bottomMargin: (pathBounds().height - implicitSize) / 2 + Tokens.spacing.small
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3primaryContainer
implicitSize: icon.implicitHeight + Tokens.padding.extraLarge * 2
shape: MaterialShape.ClamShell
Behavior on color {
CAnim {}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "queue_music"
fontStyle: Tokens.font.icon.large
color: Colors.palette.m3onPrimaryContainer
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Nothing playing")
font: Tokens.font.body.medium
}
CustomText {
text: qsTr("Play something for it to show up here!")
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.large
}
}
}
Loader {
id: content
anchors.fill: parent
asynchronous: true
active: opacity > 0
sourceComponent: RowLayout {
spacing: Tokens.spacing.extraLarge
Details {
Layout.fillWidth: true
}
LyricsAndSelector {
Layout.fillHeight: true
implicitWidth: 300
}
}
}
}
}
}
@@ -11,8 +11,8 @@ Item {
id: cover
anchors.centerIn: parent
implicitHeight: Config.dashboard.sizes.mediaCoverArtSize
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
implicitHeight: 200
implicitWidth: 200
shape.shape: MaterialShape.Cookie9Sided
}
}
+194
View File
@@ -0,0 +1,194 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Mpris
import ZShell.Components
import ZShell.Config
import qs.Components
import qs.Services
import qs.Helpers
ColumnLayout {
id: root
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
const hours = Math.floor(length / 3600);
const mins = Math.floor((length % 3600) / 60);
const secs = Math.floor(length % 60).toString().padStart(2, "0");
if (hours > 0)
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
spacing: Tokens.spacing.extraSmall
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
CustomText {
Layout.fillWidth: true
text: Players.active?.trackTitle ?? ""
font: Tokens.font.title.large
elide: Text.ElideRight
animate: true
}
CustomText {
Layout.fillWidth: true
text: Players.active?.trackArtist || qsTr("Unknown artist")
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.title.medium
elide: Text.ElideRight
animate: true
}
CustomText {
Layout.fillWidth: true
text: Players.active?.trackAlbum || qsTr("Unknown album")
color: Colors.palette.m3secondary
font: Tokens.font.title.medium
elide: Text.ElideRight
animate: true
}
RowLayout {
Layout.topMargin: Tokens.spacing.extraLargeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.small
TextMetrics {
id: timeMetrics
text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
font: Tokens.font.label.medium
}
CustomText {
id: positionLabel
Layout.preferredWidth: timeMetrics.width
text: root.lengthStr(Players.active?.position ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
CustomSlider {
id: positionSlider
Layout.fillWidth: true
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
wavy: true
animateWave: Players.active?.isPlaying ?? false
waveFrequency: 5
waveDuration: 2000
interactionOnMove: false
onInteraction: value => {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: positionLabel
property: "text"
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
when: positionSlider.dragging
}
}
CustomText {
Layout.preferredWidth: timeMetrics.width
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
}
ButtonRow {
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
IconButton {
type: IconButton.Tonal
icon: "shuffle"
isRound: true
shapeMorph: true
checked: Players.active?.shuffle ?? false
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
enabled: Players.active?.shuffleSupported
onClicked: Players.active.shuffle = !Players.active?.shuffle
implicitWidth: Math.round(implicitHeight * 0.9)
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous"
isRound: true
shapeMorph: true
font: Tokens.font.icon.large
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
}
IconButton {
id: playPauseBtn
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
isRound: true
shapeMorph: true
fillWidth: true
checked: Players.active?.isPlaying ?? false
font: Tokens.font.icon.large
enabled: Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
}
IconButton {
id: nextBtn
type: IconButton.Tonal
icon: "skip_next"
isRound: true
shapeMorph: true
font: Tokens.font.icon.large
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
}
IconButton {
type: IconButton.Tonal
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
isRound: true
shapeMorph: true
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
enabled: Players.active?.loopSupported
onClicked: {
const state = Players.active.loopState;
if (state === MprisLoopState.None)
Players.active.loopState = MprisLoopState.Track;
else if (state === MprisLoopState.Track)
Players.active.loopState = MprisLoopState.Playlist;
else
Players.active.loopState = MprisLoopState.None;
}
implicitWidth: Math.round(implicitHeight * 0.9)
}
}
}
+317
View File
@@ -0,0 +1,317 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Effects
import qs.Services
import qs.Helpers
Item {
id: root
// Funny binding hack to make lyrics update
readonly property var _: {
const p = Players.active;
if (p)
Lyrics.setTrack(p.trackArtist, p.trackTitle, p.trackAlbum, p.length);
else
Lyrics.clearTrack();
}
readonly property real fadeAmount: 0.1
property list<string> lyricList: Lyrics.lyrics
layer.enabled: true
layer.effect: Mask {
maskSource: mask
Rectangle {
id: mask
layer.enabled: true
visible: false
implicitWidth: root.width
implicitHeight: root.height
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
color: Qt.alpha("black", 0)
position: 0
}
GradientStop {
color: Qt.alpha("black", 1)
position: root.fadeAmount
}
GradientStop {
color: Qt.alpha("black", 1)
position: 1 - root.fadeAmount
}
GradientStop {
color: Qt.alpha("black", 0)
position: 1
}
}
}
}
state: {
if (Lyrics.hasLyrics)
return "hasLyrics";
if (Lyrics.loading)
return "loading";
return "noLyrics";
}
states: [
State {
name: "loading"
PropertyChanges {
loadingIndicator.opacity: 1
lyrics.opacity: 0
noLyrics.opacity: 0
}
},
State {
name: "hasLyrics"
PropertyChanges {
loadingIndicator.opacity: 0
lyrics.opacity: 1
noLyrics.opacity: 0
}
},
State {
name: "noLyrics"
PropertyChanges {
loadingIndicator.opacity: 0
lyrics.opacity: 0
noLyrics.opacity: 1
}
}
]
transitions: [
Transition {
from: "loading"
SequentialAnimation {
Anim {
target: loadingIndicator
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
targets: [lyrics, noLyrics]
property: "opacity"
type: Anim.SlowEffects
}
}
},
Transition {
from: "hasLyrics"
SequentialAnimation {
Anim {
target: lyrics
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
targets: [loadingIndicator, noLyrics]
property: "opacity"
type: Anim.SlowEffects
}
}
},
Transition {
from: "noLyrics"
SequentialAnimation {
Anim {
target: noLyrics
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
targets: [loadingIndicator, lyrics]
property: "opacity"
type: Anim.SlowEffects
}
}
}
]
Loader {
id: loadingIndicator
anchors.centerIn: parent
asynchronous: true
active: opacity > 0
opacity: 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.large
CustomRect {
Layout.alignment: Qt.AlignHCenter
implicitWidth: shape.implicitSize + Tokens.padding.medium * 2
implicitHeight: shape.implicitSize + Tokens.padding.medium * 2
color: Colors.palette.m3primaryContainer
radius: Tokens.rounding.full
LoadingIndicator {
id: shape
anchors.centerIn: parent
implicitSize: Math.round(300 / 5)
containsIcon: true // This removes the pentagon, which is not centered
}
}
CustomText {
text: qsTr("Loading lyrics...")
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.medium
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Loader {
id: noLyrics
anchors.centerIn: parent
asynchronous: true
active: opacity > 0
opacity: 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "sentiment_sad"
fontStyle: Tokens.font.icon.extraLarge
color: Colors.palette.m3outline
}
CustomText {
text: qsTr("No lyrics found")
color: Colors.palette.m3outline
font: Tokens.font.body.medium
}
}
}
CustomListView {
id: lyrics
anchors.fill: parent
anchors.topMargin: parent.height * root.fadeAmount / 2
anchors.bottomMargin: parent.height * root.fadeAmount / 2
displayMarginBeginning: anchors.topMargin
displayMarginEnd: anchors.bottomMargin
model: root.lyricList
Component.onCompleted: {
currentIndex = Qt.binding(() => {
model; // Force update when lyrics change
return Lyrics.indexForTime(Players.active?.position ?? 0);
});
positionViewAtIndex(currentIndex, ListView.Center);
}
onModelChanged: Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center))
highlightRangeMode: ListView.ApplyRange
highlightMoveDuration: Tokens.anim.durations.large
highlightMoveVelocity: -1
preferredHighlightBegin: (height - (currentItem?.implicitHeight ?? 0)) / 2
preferredHighlightEnd: (height + (currentItem?.implicitHeight ?? 0)) / 2
spacing: Tokens.spacing.small
opacity: 0
enabled: opacity > 0
delegate: CustomText {
id: lyric
required property string modelData
required property int index
property real effectScale: ListView.isCurrentItem ? 1 : 0
anchors.left: lyrics.contentItem.left
anchors.right: lyrics.contentItem.right
text: modelData || ". . ."
color: ListView.isCurrentItem ? Colors.palette.m3primary : mouse.containsMouse ? Colors.palette.m3onSurface : Colors.palette.m3outline
font: Tokens.font.body.medium
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
layer.enabled: effectScale > 0
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: Colors.palette.m3primary
shadowOpacity: 0.5 * lyric.effectScale
shadowBlur: 0.6 * lyric.effectScale
blur: 0.4 * lyric.effectScale
}
Behavior on effectScale {
Anim {
type: Anim.SlowEffects
}
}
MouseArea {
id: mouse
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
const p = Players.active;
if (p)
p.position = Lyrics.timeForIndex(lyric.index);
}
}
}
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
Behavior on lyricList {
SequentialAnimation {
Anim {
target: lyrics
property: "opacity"
to: 0
type: Anim.DefaultEffects
}
PropertyAction {}
Anim {
target: lyrics
property: "opacity"
to: 1
type: Anim.SlowEffects
}
}
}
}
@@ -0,0 +1,79 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Services
Item {
ColumnLayout {
id: layout
anchors.fill: parent
anchors.leftMargin: Tokens.padding.medium
spacing: Tokens.spacing.medium
RowLayout {
Layout.bottomMargin: -Tokens.spacing.medium
spacing: Tokens.spacing.medium
z: 1
MaterialIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
text: "lyrics"
fontStyle: Tokens.font.icon.large
}
CustomText {
Layout.fillWidth: true
text: qsTr("Lyrics")
font: Tokens.font.body.large
}
LyricsInfo {}
}
LyricList {
Layout.fillWidth: true
Layout.fillHeight: true
}
SplitButton {
Layout.alignment: Qt.AlignHCenter
type: SplitButton.Tonal
enabled: Players.list.length
active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null
menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData
menuItems: playerList.instances
fallbackIcon: "music_off"
fallbackText: qsTr("No players", "no media players active")
minLeftWidth: layout.width - expandBtn.implicitWidth - spacing
label.Layout.maximumWidth: minLeftWidth - iconLabel.implicitWidth - textRow.spacing - textRow.anchors.horizontalCenterOffset / 2 - horizontalPadding * 2
label.elide: Text.ElideRight
stateLayer.enabled: true
menuOnTop: true
Variants {
id: playerList
model: Players.list
PlayerItem {}
}
}
}
component PlayerItem: MenuItem {
required property MprisPlayer modelData
icon: modelData === Players.active ? "check" : ""
text: Players.getIdentity(modelData)
activeIcon: "animated_images"
}
}
+212
View File
@@ -0,0 +1,212 @@
import QtQuick
import QtQuick.Layouts
import ZShell
import ZShell.Blobs
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Services
Item {
id: root
property bool open
readonly property real padding: Tokens.padding.large
implicitWidth: btn.implicitWidth * 0.9
implicitHeight: btn.implicitHeight * 0.9
BlobGroup {
id: blobGroup
color: Colors.palette.m3surfaceContainerHighest
smoothing: Tokens.rounding.medium
cornerFill: false
Behavior on color {
CAnim {}
}
}
BlobRect {
id: btnRect
anchors.fill: parent
anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0
group: blobGroup
radius: Tokens.rounding.medium
Behavior on anchors.margins {
Anim {}
}
}
BlobRect {
id: rect
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: parent.width
implicitHeight: parent.height
group: blobGroup
radius: Tokens.rounding.medium
deformScale: 0.00001
states: State {
name: "open"
when: root.open
PropertyChanges {
rect.anchors.rightMargin: root.width - Tokens.spacing.small
rect.anchors.topMargin: -Tokens.padding.medium
rect.implicitWidth: Math.max(layout.implicitWidth, placeholder.implicitWidth) + root.padding * 2
rect.implicitHeight: Math.max(layout.implicitHeight, placeholder.implicitHeight) + root.padding * 2
content.opacity: 1
}
}
transitions: Transition {
Anim {
properties: "rightMargin,implicitWidth"
}
Anim {
properties: "topMargin,implicitHeight"
easing: Tokens.anim.expressiveFastSpatial
}
Anim {
property: "opacity"
type: Anim.DefaultEffects
}
}
Behavior on implicitWidth {
Anim {}
}
Behavior on implicitHeight {
Anim {}
}
Item {
id: content
anchors.fill: parent
clip: true
opacity: 0
state: Lyrics.loading || !Lyrics.hasLyrics ? "" : "hasLyrics"
states: State {
name: "hasLyrics"
PropertyChanges {
layout.opacity: 1
placeholder.opacity: 0
}
}
transitions: [
Transition {
from: "hasLyrics"
SequentialAnimation {
Anim {
target: layout
property: "opacity"
type: Anim.FastEffects
}
Anim {
target: placeholder
property: "opacity"
type: Anim.DefaultEffects
}
}
},
Transition {
to: "hasLyrics"
SequentialAnimation {
Anim {
target: placeholder
property: "opacity"
type: Anim.FastEffects
}
Anim {
target: layout
property: "opacity"
type: Anim.DefaultEffects
}
}
}
]
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall
opacity: 0
CustomText {
// TRANSLATORS: %1 = lyrics backend name, e.g. LRCLIB
text: qsTr("Backend: %1").arg(ZUtils.enumToString(Lyrics, "backend"))
color: Colors.palette.m3onSurfaceVariant
animate: true
}
CustomText {
Layout.maximumWidth: 1000 / 2
// TRANSLATORS: %1/%2/%3 = matched track title, artist and album
text: qsTr("Selected candidate: %1 | %2 | %3").arg(Lyrics.selectedCandidate.title).arg(Lyrics.selectedCandidate.artist).arg(Lyrics.selectedCandidate.album)
color: Colors.palette.m3onSurfaceVariant
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
animate: true
}
CustomText {
// TRANSLATORS: %1 = lyric timing offset; ms is the millisecond unit, leave untranslated
text: qsTr("Offset: %1 ms").arg(Lyrics.offset)
color: Colors.palette.m3onSurfaceVariant
animate: true
}
}
Item {
id: placeholder
anchors.centerIn: parent
implicitWidth: placeholderText.implicitWidth
implicitHeight: placeholderText.implicitHeight
CustomText {
id: placeholderText
text: Lyrics.loading ? qsTr("Loading...") : qsTr("No lyrics found")
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.medium
animate: true
}
}
}
}
MouseArea {
id: btn
anchors.centerIn: parent
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Tokens.padding.extraSmall * 2
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: root.open = !root.open
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "more_vert"
fontStyle: Tokens.font.icon.medium
}
}
}
@@ -0,0 +1,220 @@
pragma Singleton
import QtQuick
import Quickshell
import ZShell.Config
import ZShell.Services
Singleton {
id: root
readonly property var columnDefs: [
{
id: "command",
title: qsTr("Command"),
width: 200,
field: 3
},
{
id: "pid",
title: qsTr("PID"),
width: 60,
field: 1
},
{
id: "cpuUsage",
title: qsTr("CPU %"),
width: 70,
field: 7
},
{
id: "memoryUsage",
title: qsTr("Memory"),
width: 90,
field: 8
},
{
id: "elapsed",
title: qsTr("Elapsed"),
width: 80,
field: 9
},
{
id: "ppid",
title: qsTr("PPID"),
width: 60,
field: 2
},
{
id: "user",
title: qsTr("User"),
width: 100,
field: 11
},
{
id: "uid",
title: qsTr("UID"),
width: 60,
field: 10
},
{
id: "state",
title: qsTr("State"),
width: 60,
field: 5
},
{
id: "sleeping",
title: qsTr("Sleeping"),
width: 80,
field: 6
},
{
id: "commandLine",
title: qsTr("Command line"),
width: 240,
field: 4
},
{
id: "ioReadBytes",
title: qsTr("IO read"),
width: 90,
field: 12
},
{
id: "ioWriteBytes",
title: qsTr("IO write"),
width: 90,
field: 13
}
]
readonly property var desktopEntries: DesktopEntries.applications.values.map(e => ({
icon: e.icon,
command: e.command
}))
Component.onCompleted: {
Processes.setDesktopEntries(root.desktopEntries);
Qt.callLater(ensurePinnedFirst);
}
onDesktopEntriesChanged: Processes.setDesktopEntries(root.desktopEntries)
readonly property string pinnedColumnId: "command"
readonly property var hiddenColumns: {
const visible = Config.dashboard.processColumns.values.map(e => e.id);
return root.columnDefs.filter(d => !visible.includes(d.id));
}
function pinnedIndex(): int {
const values = Config.dashboard.processColumns.values;
for (let i = 0; i < values.length; i++)
if (values[i].id === root.pinnedColumnId)
return i;
return -1;
}
function ensurePinnedFirst() {
const i = root.pinnedIndex();
if (i > 0)
Qt.callLater(() => {
const idx = root.pinnedIndex();
if (idx > 0)
Config.dashboard.processColumns.move(idx, 0);
});
}
function defFor(id: string): var {
for (const def of root.columnDefs)
if (def.id === id)
return def;
return null;
}
function addColumn(id: string) {
const def = root.defFor(id);
Config.dashboard.processColumns.insert({
id: id,
width: def ? def.width : 100
});
}
function removeColumn(index: int) {
const entry = Config.dashboard.processColumns.at(index);
if (entry && entry.id === root.pinnedColumnId)
return;
Config.dashboard.processColumns.remove(index);
}
function moveColumns(from: int, to: int) {
const list = Config.dashboard.processColumns;
const entry = list.at(from);
if (!entry)
return;
if (entry.id === root.pinnedColumnId)
return;
if (to === 0 && list.at(0)?.id === root.pinnedColumnId)
return;
list.move(from, to);
const i = root.pinnedIndex();
if (i > 0)
list.move(i, 0);
}
function resizeColumn(index: int, width: real) {
const entry = Config.dashboard.processColumns.at(index);
if (entry)
entry.width = Math.max(50, width);
}
function dampOvershoot(overshoot: real, maxOvershoot: real): real {
return overshoot / (1 + overshoot / maxOvershoot);
}
function formatCell(id: string, value: var): string {
switch (id) {
case "cpuUsage":
return value.toFixed(1) + "%";
case "memoryUsage":
return root.formatBytes(value * 1024);
case "ioReadBytes":
case "ioWriteBytes":
return root.formatBytes(value);
case "elapsed":
return root.formatElapsed(value);
case "sleeping":
return value ? qsTr("Yes") : qsTr("No");
default:
return String(value);
}
}
function formatBytes(bytes: var): string {
if (bytes === undefined || bytes === null || bytes <= 0)
return "0 B";
const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
let v = Number(bytes);
let i = 0;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
++i;
}
return (i === 0 ? v.toFixed(0) : v.toFixed(1)) + " " + units[i];
}
function formatElapsed(secs: var): string {
const s = Math.max(0, Math.floor(Number(secs)));
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const ss = s % 60;
if (h > 0)
return h + "h " + m + "m";
if (m > 0)
return m + "m " + ss + "s";
return ss + "s";
}
}
@@ -0,0 +1,665 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Internal
import ZShell.Services
import qs.Components
import qs.Services
Item {
id: root
required property int headerHeight
required property ProcessesList pList
readonly property alias columnList: columnList
readonly property alias columnsModel: columnsData
readonly property bool anyHeld: columnList.contentItem.children.some(c => c.held)
z: 1
implicitHeight: headerHeight
function widthForColumn(id: string, configured: real): real {
for (const item of columnList.contentItem.children) {
if (item.modelData !== undefined && item.modelData.id === id)
return item.liveWidth > 0 ? item.liveWidth : (configured > 0 ? configured : (ProcessesColumns.defFor(id)?.width ?? 100));
}
return configured > 0 ? configured : (ProcessesColumns.defFor(id)?.width ?? 100);
}
function fillMissingWidths() {
const n = columnsData.values.length;
if (n === 0 || columnList.width <= 0)
return;
const spacingTotal = columnList.spacing * Math.max(0, n - 1);
let savedTotal = 0;
const unsavedIndices = [];
for (let i = 0; i < n; i++) {
const w = columnsData.values[i].width;
if (w > 0)
savedTotal += w;
else
unsavedIndices.push(i);
}
if (unsavedIndices.length === 0)
return;
const available = columnList.width - spacingTotal - savedTotal;
if (available <= 0)
return;
const each = Math.max(50, available / unsavedIndices.length);
for (const idx of unsavedIndices)
ProcessesColumns.resizeColumn(idx, each);
}
Connections {
target: columnList
function onWidthChanged() {
root.fillMissingWidths();
}
function onCountChanged() {
resizeOverlay.geometryTick++;
root.fillMissingWidths();
}
function onContentWidthChanged() {
resizeOverlay.geometryTick++;
}
}
Connections {
target: columnsData
function onValuesChanged() {
root.fillMissingWidths();
resizeOverlay.geometryTick++;
}
}
Component.onCompleted: Qt.callLater(fillMissingWidths)
Component.onDestruction: Cursor.clearOverride()
CustomListView {
id: columnList
anchors.fill: parent
orientation: ListView.Horizontal
interactive: false
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
displaced: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "x"
}
}
move: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "x"
}
}
remove: Transition {
Anim {
to: 0
properties: "opacity"
type: Anim.DefaultEffects
}
}
model: DelegateModel {
id: columnsVisual
delegate: ColumnHeaderCell {
parentModel: columnsVisual
}
model: ScriptModel {
id: columnsData
values: Config.dashboard.processColumns.values
}
}
}
Item {
id: resizeOverlay
property real gapBoost: pressed ? Tokens.spacing.small : Tokens.spacing.extraSmall
property bool pressed: false
property int geometryTick: 0
property int activeGapIndex: -1
anchors.fill: columnList
z: 10
Component.onCompleted: Qt.callLater(() => geometryTick++)
Repeater {
model: Math.max(0, columnsData.values.length - 1)
MouseArea {
id: handleRoot
required property int index
readonly property bool hovered: containsMouse
property ColumnHeaderCell leftCell: {
const left = leftCell;
if (resizing && left)
return left;
const tick = resizeOverlay.geometryTick;
return resolveCell(true);
}
property ColumnHeaderCell rightCell: {
const right = rightCell;
if (resizing && right)
return right;
const tick = resizeOverlay.geometryTick;
return resolveCell(false);
}
property bool resizing: false
property real originX: 0
property real originWidth: 0
property real originRightWidth: 0
property real originAbsX: 0
function resolveCell(left: bool): ColumnHeaderCell {
if (left) { // qmllint disable incompatible-type
const leftItem = columnList.itemAtIndex(index);
if (leftItem)
return leftItem;
else
return null;
} else {
const rightItem = columnList.itemAtIndex(index + 1);
if (rightItem)
return rightItem;
else
return null;
}
}
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
preventStealing: true
hoverEnabled: true
enabled: !root.anyHeld
x: {
if (!leftCell)
return 0;
const gapCenter = leftCell.x + leftCell.width + columnList.spacing / 2;
return gapCenter - width / 2;
}
y: 0
implicitWidth: 16
implicitHeight: columnList.height
function pairAvailableWidth(rightCell) {
let othersWidth = 0;
for (const item of columnList.contentItem.children) {
if (item.modelData === undefined || item === handleRoot.leftCell || item === rightCell)
continue;
othersWidth += item.width;
}
const spacingTotal = columnList.spacing * Math.max(0, columnsData.values.length - 1);
return columnList.width - othersWidth - spacingTotal;
}
CustomRect {
id: handle
readonly property bool shouldBeVisible: handleRoot.hovered || handleRoot.pressed
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: shouldBeVisible ? resizeOverlay.gapBoost : 0
color: Colors.palette.m3secondary
radius: Tokens.rounding.full
opacity: shouldBeVisible ? 1.0 : 0
Behavior on implicitWidth {
Anim {
type: Anim.FastSpatial
}
}
Behavior on opacity {
Anim {}
}
transform: Scale {
origin {
x: handle.implicitWidth / 2
y: handle.height / 2
}
yScale: handle.shouldBeVisible ? 1 : 0.5
xScale: handle.shouldBeVisible ? 1 : 0.5
Behavior on xScale {
Anim {}
}
Behavior on yScale {
Anim {}
}
}
}
onHoveredChanged: resizeOverlay.activeGapIndex = hovered || pressed ? handleRoot.index : (resizeOverlay.activeGapIndex === handleRoot.index ? -1 : resizeOverlay.activeGapIndex)
onPressedChanged: {
resizeOverlay.pressed = pressed;
if (!pressed)
resizeOverlay.activeGapIndex = hovered ? handleRoot.index : resizeOverlay.activeGapIndex === handleRoot.index ? -1 : resizeOverlay.activeGapIndex;
}
onPressed: e => {
if (!handleRoot.leftCell)
return;
Cursor.setOverride(Qt.ClosedHandCursor);
resizing = true;
originAbsX = handleRoot.mapToItem(resizeOverlay, e.x, 0).x;
originWidth = handleRoot.leftCell.implicitWidth;
originRightWidth = handleRoot.rightCell ? handleRoot.rightCell.implicitWidth : 0;
}
onPositionChanged: e => {
if (!resizing || !handleRoot.leftCell)
return;
const nowAbsX = handleRoot.mapToItem(resizeOverlay, e.x, 0).x;
let newLeftWidth = Math.max(50, originWidth + (nowAbsX - originAbsX));
const rightCell = handleRoot.rightCell;
let roundedLeft = Math.round(newLeftWidth);
if (rightCell) {
const available = Math.round(handleRoot.pairAvailableWidth(rightCell));
const maxLeftWidth = Math.max(50, available - 50);
roundedLeft = Math.min(roundedLeft, maxLeftWidth);
rightCell.liveWidth = Math.max(50, available - roundedLeft);
}
handleRoot.leftCell.liveWidth = roundedLeft;
}
onReleased: {
Cursor.clearOverride();
resizing = false;
const cell = handleRoot.leftCell;
const rightCell = handleRoot.rightCell;
if (cell.liveWidth !== 0)
ProcessesColumns.resizeColumn(cell.DelegateModel.itemsIndex, cell.liveWidth);
if (rightCell.liveWidth !== 0)
ProcessesColumns.resizeColumn(rightCell.DelegateModel.itemsIndex, rightCell.liveWidth);
if (cell || rightCell) {
cell.liveWidth = 0;
rightCell.liveWidth = 0;
}
resizeOverlay.geometryTick++;
}
}
}
}
component ColumnHeaderCell: Item {
id: cell
required property var modelData
required property int index
required property DelegateModel parentModel
property int leftRadius: pushSide !== 1 && DelegateModel.itemsIndex === 0 ? cell.implicitHeight / 2 : Tokens.rounding.extraSmall
property int rightRadius: pushSide !== -1 && DelegateModel.itemsIndex === parentModel.count - 1 ? cell.implicitHeight / 2 : Tokens.rounding.extraSmall
property real radiusLerpProg
property bool held
property bool moved: false
property int pressIndex: -1
property point pressPos
property real lastMoveX: 0
property real liveWidth: 0
readonly property bool isActiveSort: Processes.sortField === (ProcessesColumns.defFor(cell.modelData.id)?.field ?? 0)
readonly property int pushSide: {
const gapIdx = resizeOverlay.activeGapIndex;
if (gapIdx < 0)
return 0;
const myIdx = cell.DelegateModel.itemsIndex;
if (myIdx === gapIdx)
return -1;
if (myIdx === gapIdx + 1)
return 1;
return 0;
}
property real gapBoost: resizeOverlay.gapBoost
readonly property real gapShrink: gapBoost / 2
property real gapProg: pushSide !== 0 ? 1 : 0
property real gapX: pushSide === 1 ? 1 : 0
function lerpRadius(a: real, b: real): real {
return a + (b - a) * radiusLerpProg;
}
Behavior on gapBoost {
Anim {
type: Anim.FastSpatial
}
}
Behavior on gapX {
Anim {
type: Anim.FastSpatial
}
}
Behavior on gapProg {
Anim {
type: Anim.FastSpatial
}
}
implicitHeight: root.headerHeight
implicitWidth: liveWidth > 0 ? liveWidth : (modelData.width > 0 ? modelData.width : (ProcessesColumns.defFor(modelData.id)?.width ?? 100))
z: held || returnAnim.running ? 1 : 0
states: State {
name: "held"
PropertyChanges {
cellBg.color: Colors.palette.m3primaryContainer
elevation.opacity: 1
placeholder.opacity: 0.1
cell.radiusLerpProg: 1
title.color: Colors.palette.m3onPrimaryContainer
removeBtn.inactiveOnColor: Colors.palette.m3onPrimaryContainer
}
}
Behavior on leftRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on rightRadius {
Anim {
type: Anim.DefaultEffects
}
}
transitions: Transition {
Anim {
properties: "opacity,radiusLerpProg"
type: Anim.SlowEffects
}
PropertyAction {
properties: "color,inactiveOnColor"
}
}
state: held ? "held" : ""
CustomRect {
id: placeholder
anchors.fill: parent
color: Colors.palette.m3primary
opacity: 0
topLeftRadius: cell.leftRadius
bottomLeftRadius: cell.leftRadius
topRightRadius: cell.rightRadius
bottomRightRadius: cell.rightRadius
}
DragHandler {
id: dragHandler
property real centerOffset: 0
enabled: cell.modelData.id !== ProcessesColumns.pinnedColumnId
target: null
xAxis.enabled: true
dragThreshold: 10
grabPermissions: PointerHandler.CanTakeOverFromItems
onActiveChanged: {
if (active) {
Cursor.setOverride(Qt.ClosedHandCursor);
returnAnim.stop();
cell.pressIndex = cell.DelegateModel.itemsIndex;
cell.lastMoveX = cell.x;
cell.moved = true;
cell.held = true;
const p = centroid.position;
cell.pressPos = Qt.point(p.x, p.y);
dragHandler.centerOffset = p.x - cell.width / 2;
cellContent.x = Qt.binding(() => {
const maxOvershoot = Tokens.padding.extraLarge;
const xDiff = cell.x - cell.lastMoveX;
const x = dragHandler.centroid.position.x - cell.pressPos.x - xDiff;
const absX = cell.mapToItem(columnList.contentItem, x, 0).x;
const maxX = columnList.contentWidth - cell.width;
if (absX < 0)
return x - absX - ProcessesColumns.dampOvershoot(-absX, maxOvershoot);
if (absX > maxX)
return x - absX + maxX + ProcessesColumns.dampOvershoot(absX - maxX, maxOvershoot);
return x;
});
cellContent.y = Qt.binding(() => {
const maxOvershoot = Tokens.padding.extraLargeIncreased;
const y = dragHandler.centroid.position.y - cell.pressPos.y;
return ProcessesColumns.dampOvershoot(Math.abs(y), maxOvershoot) * Math.sign(y);
});
} else {
Cursor.clearOverride();
if (cell.held) {
cellContent.x = cellContent.x;
cellContent.y = cellContent.y;
cell.held = false;
returnAnim.start();
if (cell.DelegateModel.itemsIndex !== cell.pressIndex) {
ProcessesColumns.moveColumns(cell.pressIndex, cell.DelegateModel.itemsIndex);
}
}
}
}
onCentroidChanged: {
if (!active)
return;
const p = centroid.position;
cell.lastMoveX = cell.x;
const cursorAbsX = cell.mapToItem(columnList.contentItem, p.x, 0).x;
const centerAbsX = cursorAbsX - dragHandler.centerOffset;
const movingRight = p.x > cell.pressPos.x;
const grabbedRight = dragHandler.centerOffset > 0;
const useCursor = movingRight === grabbedRight;
const refX = useCursor ? cursorAbsX : centerAbsX;
const myIndex = cell.DelegateModel.itemsIndex;
const rightItem = columnList.itemAtIndex(myIndex + 1);
if (rightItem) {
const rightMid = rightItem.x + rightItem.width / 2;
if (refX > rightMid) {
columnsVisual.items.move(myIndex, myIndex + 1);
return;
}
}
const leftItem = columnList.itemAtIndex(myIndex - 1);
if (leftItem) {
const leftMid = leftItem.x + leftItem.width / 2;
const pinnedAtZero = columnList.itemAtIndex(0)?.modelData?.id === ProcessesColumns.pinnedColumnId;
if (refX < leftMid && !(myIndex === 1 && pinnedAtZero)) {
columnsVisual.items.move(myIndex, myIndex - 1);
}
}
}
}
Anim {
id: returnAnim
properties: "x,y"
target: cellContent
to: 0
}
Item {
id: cellVisual
x: cell.gapShrink * cell.gapX
y: 0
width: cell.width - cell.gapShrink * cell.gapProg
height: cell.height
Item {
id: cellContent
width: cellVisual.width
height: cellVisual.height
readonly property int topLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
readonly property int topRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
readonly property int bottomLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
readonly property int bottomRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
Elevation {
id: elevation
anchors.fill: parent
level: 3
opacity: 0
topLeftRadius: parent.topLeftRadius
topRightRadius: parent.topRightRadius
bottomLeftRadius: parent.bottomLeftRadius
bottomRightRadius: parent.bottomRightRadius
}
CustomRect {
id: cellBg
anchors.fill: parent
color: cell.isActiveSort ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
topLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
topRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
bottomLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
bottomRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
StateLayer {
id: layer
color: cell.isActiveSort ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
manualHoverOverride: cell.held
onClicked: {
Processes.sortBy(ProcessesColumns.defFor(cell.modelData.id)?.field ?? 0);
root.pList.positionViewAtBeginning();
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Tokens.padding.medium
anchors.rightMargin: 12
CustomText {
id: title
anchors.left: parent.left
anchors.right: removeBtn.visible ? removeBtn.left : parent.right
anchors.rightMargin: Tokens.spacing.extraSmall
anchors.verticalCenter: parent.verticalCenter
color: cell.isActiveSort ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
elide: CustomText.ElideRight
text: ProcessesColumns.defFor(cell.modelData.id)?.title ?? cell.modelData.id
}
IconButton {
id: removeBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
type: IconButton.Text
icon: "close"
font: Tokens.font.icon.small
inactiveOnColor: Colors.palette.m3onPrimary
visible: cell.modelData.id !== ProcessesColumns.pinnedColumnId
onClicked: ProcessesColumns.removeColumn(cell.DelegateModel.itemsIndex)
}
MaterialIcon {
anchors.right: removeBtn.visible ? removeBtn.left : parent.right
anchors.verticalCenter: parent.verticalCenter
text: "keyboard_arrow_down"
opacity: cell.isActiveSort ? 1 : 0
visible: opacity > 0
rotation: !Processes.sortAscending && opacity > 0 ? 180 : 0
color: Colors.palette.m3onPrimary
Behavior on rotation {
Anim {}
}
Behavior on opacity {
Anim {}
}
}
}
}
}
}
component HiddenColumnItem: MenuItem {
required property var modelData
text: modelData.title
}
}
@@ -0,0 +1,140 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Services
CustomListView {
id: root
required property ProcessesHeader columnHeader
required property int rowHeight
property real rowSpacing: Tokens.spacing.small
model: Processes.list
spacing: root.rowSpacing
clip: true
CustomScrollBar.vertical: CustomScrollBar {
flickable: root
}
delegate: CustomRect {
id: processRow
required property var modelData
required property int index
implicitWidth: ListView.view.width
implicitHeight: rowLayout.implicitHeight
radius: Tokens.rounding.full
StateLayer {
id: rowHover
}
RowLayout {
id: rowLayout
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
spacing: Tokens.spacing.extraSmall / 2
CustomClippingRect {
id: iconClip
Layout.fillHeight: true
Layout.margins: Tokens.padding.medium
implicitWidth: height
radius: Tokens.rounding.full
IconImage {
id: appIcon
anchors.fill: parent
implicitSize: height
source: Quickshell.iconPath(processRow.modelData.iconName, "application-x-executable")
}
}
Repeater {
id: cellRepeater
model: root.columnHeader.columnsModel
Item {
id: rowCell
required property var modelData
required property int index
implicitWidth: root.columnHeader.widthForColumn(modelData.id, modelData.width) - (index === 0 ? iconClip.implicitWidth + iconClip.Layout.margins * 2 + rowLayout.spacing : 0)
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
Layout.preferredWidth: implicitWidth
Layout.fillHeight: true
CustomRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.leftMargin: -(rowLayout.spacing)
implicitWidth: 2
color: Colors.palette.m3outlineVariant
visible: rowCell.index !== 0
}
Item {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.small
anchors.leftMargin: rowCell.index !== 0 ? Tokens.padding.medium : 0
anchors.rightMargin: Tokens.padding.medium
implicitHeight: mainText.implicitHeight + subText.implicitHeight + mainText.anchors.margins * 2 + subText.anchors.topMargin
CustomText {
id: mainText
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
elide: CustomText.ElideRight
text: {
const value = processRow.modelData[rowCell.modelData.id];
return value === undefined ? "" : ProcessesColumns.formatCell(rowCell.modelData.id, value);
}
states: State {
name: "centerAnchored"
when: !subText.visible
AnchorChanges {
target: mainText
anchors.top: undefined
anchors.verticalCenter: layout.verticalCenter
}
}
}
CustomText {
id: subText
anchors.top: mainText.bottom
anchors.left: parent.left
anchors.right: parent.right
visible: rowCell.modelData.id === "command"
elide: CustomText.ElideRight
text: rowCell.modelData.id === "command" ? processRow.modelData["commandLine"] : ""
font: Tokens.font.label.small
color: Colors.palette.m3outline
}
}
}
}
}
}
}
@@ -0,0 +1,86 @@
pragma Singleton
import QtQuick
import Quickshell
import ZShell.Internal
import ZShell.Services
Singleton {
id: root
readonly property int historyLength: 30
readonly property CircularBuffer cpuBuffer: _cpuBuffer
readonly property CircularBuffer memoryBuffer: _memoryBuffer
readonly property list<var> cpuInfo: [
{
label: qsTr("Usage"),
value: `${Math.round(Cpu.percentage * 100)}%`
},
{
label: qsTr("Temp"),
value: `${Math.ceil(Cpu.temperature)}°${"C"}`
}
]
readonly property list<var> memoryInfo: [
{
label: qsTr("Used"),
value: memoryUsageFormat(Memory.used, Memory.total)
},
{
label: qsTr("Cached"),
value: memoryTotalFormat(Memory.cached)
},
{
label: qsTr("Writes"),
value: memoryTotalFormat(Memory.writeTotal)
},
{
label: qsTr("Reads"),
value: memoryTotalFormat(Memory.readTotal)
}
]
function memoryTotalFormat(kib: int): string {
const fmt = UsageFmt.formatKib(kib);
return `${fmt.value.toFixed(1)} ${fmt.unit}`;
}
function memoryUsageFormat(used: int, total: int): string {
const fmt = UsageFmt.formatKib(used, total);
return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`;
}
Connections {
function onUpdateTriggered(): void {
if (Cpu.percentage >= 0 && isFinite(Cpu.percentage))
_cpuBuffer.push(Cpu.percentage);
}
target: Cpu
}
Connections {
function onUpdateTriggered(): void {
if (Memory.percentage >= 0 && isFinite(Memory.percentage))
_memoryBuffer.push(Memory.percentage);
}
target: Memory
}
CircularBuffer {
id: _cpuBuffer
capacity: root.historyLength + 1
smoothingRadius: 2
}
CircularBuffer {
id: _memoryBuffer
capacity: root.historyLength + 1
smoothingRadius: 2
}
}
+187
View File
@@ -0,0 +1,187 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import ZShell.Services
import qs.Components
import qs.Services
Item {
id: root
required property int radius
implicitHeight: layout.implicitHeight
ServiceRef {
service: Cpu
}
ServiceRef {
service: Memory
}
RowLayout {
id: layout
anchors.fill: parent
spacing: Tokens.spacing.medium
UsageCard {
accent: Colors.palette.m3primary
buffer: ResourceUsage.cpuBuffer
icon: "memory"
label: qsTr("CPU")
values: ResourceUsage.cpuInfo
}
UsageCard {
accent: Colors.palette.m3tertiary
buffer: ResourceUsage.memoryBuffer
icon: "memory_alt"
label: qsTr("Memory")
values: ResourceUsage.memoryInfo
}
}
component UsageCard: CustomRect {
id: card
property color accent
required property var buffer
property string icon
property string label
property list<var> values
color: Colors.tPalette.m3surfaceContainer
radius: root.radius
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: mainLayout.implicitHeight + mainLayout.anchors.margins * 2
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: 0
RowLayout {
spacing: Tokens.spacing.small
MaterialIcon {
color: card.accent
text: card.icon
}
CustomText {
text: card.label
}
}
Item {
id: graphWrapper
Layout.fillHeight: true
Layout.fillWidth: true
implicitHeight: infoColumn.implicitHeight
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: infoColumn.left
SparklineItem {
id: sparkline
anchors.fill: parent
historyLength: ResourceUsage.historyLength
line1: card.buffer
line1Color: card.accent
line1FillAlpha: 0.15
maxValue: 1.0
slideProgress: 1
Connections {
function onValuesChanged(): void {
slideAnim.restart();
}
target: card.buffer
}
NumberAnimation {
id: slideAnim
duration: Config.dashboard.resourceUpdateInterval
easing.type: Easing.Linear
from: 0
property: "slideProgress"
target: sparkline
to: 1
}
}
// "Collecting data" placeholder
CustomText {
anchors.centerIn: parent
color: Colors.palette.m3outline
text: qsTr("Collecting data...")
visible: card.buffer.count < 2
}
}
ColumnLayout {
id: infoColumn
anchors.right: parent.right
anchors.bottom: parent.bottom
width: {
const prefWidth = graphWrapper.width / 3;
const minWidth = graphWrapper.width / 4;
if (prefWidth < implicitWidth)
return implicitWidth;
else if (prefWidth >= implicitWidth * 2)
return minWidth;
else
return prefWidth;
}
Repeater {
model: card.values
Item {
id: moreInfo
required property var modelData
Layout.leftMargin: Tokens.spacing.medium
Layout.fillWidth: true
implicitWidth: label.implicitWidth + value.implicitWidth + label.anchors.margins * 2
implicitHeight: label.implicitHeight
CustomText {
id: label
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurfaceVariant
text: moreInfo.modelData.label
}
CustomText {
id: value
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: card.accent
text: moreInfo.modelData.value
}
}
}
}
}
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import QtQuick
import Quickshell
import ZShell.Config
import ZShell.Services
import qs.Modules.Dashboard.Processes
import qs.Components
import qs.Services
Item {
id: root
required property int radius
implicitWidth: Tokens.sizes.dashboard.processesWidth
implicitHeight: Tokens.sizes.dashboard.processesHeight
ServiceRef {
service: Processes
}
ProcessesHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
headerHeight: Tokens.sizes.dashboard.processesHeaderHeight
pList: processList
}
ProcessesList {
id: processList
anchors.top: header.bottom
anchors.bottom: sysInfo.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Tokens.spacing.small
anchors.bottomMargin: Tokens.spacing.small
columnHeader: header
rowHeight: Tokens.sizes.dashboard.processesRowHeight
}
SystemInfo {
id: sysInfo
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
radius: root.radius
}
}
+90 -144
View File
@@ -1,23 +1,24 @@
pragma ComponentBehavior: Bound
import qs.Components
import ZShell.Config
import qs.Helpers
import qs.Modules
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Templates
import Quickshell
import ZShell.Config
import qs.Components
import qs.Services
Item {
id: root
readonly property alias count: bar.count
required property real nonAnimWidth
required property PersistentProperties state
required property ScreenState screenState
required property var tabs
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
readonly property alias count: bar.count
readonly property int tabIndicatorSpacing: Tokens.spacing.small
implicitHeight: bar.implicitHeight + bar.anchors.topMargin + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
TabBar {
id: bar
@@ -25,85 +26,99 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: root.tabIndicatorSpacing
currentIndex: root.screenState.dashboardTab
onCurrentIndexChanged: root.screenState.dashboardTab = currentIndex
implicitHeight: contentHeight
background: null
currentIndex: root.state.currentTab
contentItem: RowLayout {
spacing: 0
onCurrentIndexChanged: root.state.currentTab = currentIndex
Tab {
iconName: "dashboard"
text: qsTr("Dashboard")
Repeater {
model: bar.contentModel
}
}
Tab {
iconName: "queue_music"
text: qsTr("Media")
}
Repeater {
model: ScriptModel {
values: root.tabs
}
Tab {
iconName: "speed"
text: qsTr("Performance")
}
delegate: Tab {
required property var modelData
Tab {
iconName: "cloud"
text: qsTr("Weather")
iconName: modelData.iconName
text: modelData.text
}
}
// Tab {
// iconName: "workspaces"
// text: qsTr("Workspaces")
// }
}
Item {
id: indicator
anchors.top: bar.bottom
clip: true
implicitHeight: 40
implicitWidth: bar.currentItem.implicitWidth
anchors.topMargin: root.tabIndicatorSpacing
implicitWidth: {
const tab = bar.currentItem;
if (tab)
return tab.implicitWidth;
const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
return width;
}
implicitHeight: 3
x: {
const tab = bar.currentItem;
const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
return width * tab.TabBar.index + (width - tab.implicitWidth) / 2;
const tabWidth = tab?.implicitWidth ?? width;
return width * bar.currentIndex + (width - tabWidth) / 2;
}
clip: true
CustomRect {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: parent.implicitHeight * 2
color: Colors.palette.m3primary
radius: Tokens.rounding.full
}
Behavior on x {
Anim {}
}
Behavior on implicitWidth {
Anim {
}
}
Behavior on x {
Anim {
}
}
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
color: Colors.palette.m3primary
implicitHeight: parent.implicitHeight * 2
radius: Tokens.rounding.full
Anim {}
}
}
CustomRect {
id: separator
anchors.top: indicator.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.top: indicator.bottom
color: Colors.palette.m3outlineVariant
implicitHeight: 1
color: Colors.palette.m3outlineVariant
}
component Tab: TabButton {
id: tab
readonly property bool current: TabBar.tabBar.currentItem === this
required property string iconName
readonly property bool current: TabBar.tabBar.currentItem === this
Layout.fillWidth: true
Layout.preferredWidth: 1 // Uniform width across all tabs
implicitWidth: implicitContentWidth
implicitHeight: implicitContentHeight
background: null
contentItem: CustomMouseArea {
@@ -111,117 +126,47 @@ Item {
function onWheel(event: WheelEvent): void {
if (event.angleDelta.y < 0)
root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1);
root.screenState.dashboardTab = Math.min(root.screenState.dashboardTab + 1, bar.count - 1);
else if (event.angleDelta.y > 0)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0);
}
cursorShape: Qt.PointingHandCursor
implicitHeight: icon.height + label.height
implicitWidth: Math.max(icon.width, label.width)
implicitHeight: icon.height + label.height
onPressed: event => {
root.state.currentTab = tab.TabBar.index;
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
const stateY = stateWrapper.y;
rippleAnim.x = event.x;
rippleAnim.y = event.y - stateY;
onPressed: root.screenState.dashboardTab = tab.TabBar.index
const dist = (ox, oy) => ox * ox + oy * oy;
rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY), dist(event.x, stateWrapper.height - event.y), dist(width - event.x, event.y + stateY), dist(width - event.x, stateWrapper.height - event.y)));
rippleAnim.restart();
}
SequentialAnimation {
id: rippleAnim
property real radius
property real x
property real y
PropertyAction {
property: "x"
target: ripple
value: rippleAnim.x
}
PropertyAction {
property: "y"
target: ripple
value: rippleAnim.y
}
PropertyAction {
property: "opacity"
target: ripple
value: 0.08
}
Anim {
from: 0
properties: "implicitWidth,implicitHeight"
target: ripple
to: rippleAnim.radius * 2
}
Anim {
easing.type: Easing.BezierSpline
property: "opacity"
target: ripple
to: 0
}
}
ClippingRectangle {
id: stateWrapper
StateLayer {
id: stateLayer
anchors.fill: undefined
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: "transparent"
implicitHeight: parent.height + 8 * 2
radius: Tokens.rounding.small
implicitHeight: parent.height + root.tabIndicatorSpacing * 2
CustomRect {
id: stateLayer
anchors.fill: parent
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0
Behavior on opacity {
Anim {
}
}
}
CustomRect {
id: ripple
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
opacity: 0
radius: Tokens.rounding.full
transform: Translate {
x: -ripple.width / 2
y: -ripple.height / 2
}
}
radius: Tokens.rounding.medium
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
onClicked: root.screenState.dashboardTab = tab.TabBar.index
}
MaterialIcon {
id: icon
anchors.bottom: label.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: label.top
text: tab.iconName
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
font.pointSize: 18
text: tab.iconName
fontStyle: Tokens.font.icon.medium
Behavior on fill {
Anim {
type: Anim.DefaultEffects
}
}
}
@@ -229,10 +174,11 @@ Item {
CustomText {
id: label
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
anchors.bottom: parent.bottom
text: tab.text
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
}
}
}
+145
View File
@@ -0,0 +1,145 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Services
import qs.Helpers
RowLayout {
id: root
spacing: Tokens.spacing.largeIncreased
readonly property list<var> forecastToday: Weather.hourlyForecast.slice(0, 25)
property var now: new Date()
property bool useTwelveHourClock: Config.services.useTwelveHourClock
property bool curveSmoothing: true
property real smoothing: 2
property real markerGap: Math.min(Math.max(Tokens.spacing.medium / 2, 0), 6)
readonly property real markerX: {
const d = root.now;
return d.getHours() + d.getMinutes() / 60;
}
function formatHour(h) {
if (root.useTwelveHourClock) {
const hr = h % 12 === 0 ? 12 : h % 12;
return hr + (h < 12 ? " AM" : " PM");
}
return h;
}
Timer {
interval: 60000
repeat: true
running: true
onTriggered: root.now = new Date()
}
ColumnLayout {
Layout.fillWidth: true
Layout.preferredWidth: root.width / 2
RowLayout {
Layout.fillWidth: true
MaterialIcon {
text: "device_thermostat"
fontStyle: Tokens.font.icon.medium
color: Colors.palette.m3secondary
}
CustomText {
text: qsTr("Temperature")
}
}
Item {
id: temperature
implicitHeight: 150
Layout.fillWidth: true
LineGraph {
id: tempGraph
anchors.fill: parent
values: root.forecastToday.map(e => e.tempC)
verticalLineStep: 3
xSteps: 24
horizontalLineStep: 10
minHorizontalLines: 4
yPaddingTop: horizontalLineStep
yPaddingBottom: horizontalLineStep
xLabel: i => root.formatHour(i)
yLabel: v => Math.round(v) + "°"
markerX: root.markerX
smoothing: root.smoothing
curveSmoothing: root.curveSmoothing
markerGap: root.markerGap
lineColor: Colors.palette.m3secondary
gridColor: Colors.palette.m3onSurface
markerColor: Colors.palette.m3secondary
labelColor: Colors.palette.m3onSurfaceVariant
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.preferredWidth: root.width / 2
RowLayout {
Layout.fillWidth: true
MaterialIcon {
text: "water_drop"
fontStyle: Tokens.font.icon.medium
color: Colors.palette.m3tertiary
}
CustomText {
text: qsTr("Chance of precipitation")
}
}
Item {
id: precipitation
Layout.fillWidth: true
implicitHeight: 150
LineGraph {
id: graph
anchors.fill: parent
values: root.forecastToday.map(e => e?.precipChance ?? null)
xSteps: 24
yMin: 0
yMax: 100
yPaddingTop: 20
verticalLineStep: 3
lastXLine: false
horizontalLineStep: 20
xLabel: i => root.formatHour(i)
yLabel: v => v + "%"
markerX: root.markerX
smoothing: root.smoothing
curveSmoothing: root.curveSmoothing
markerGap: root.markerGap
lineColor: Colors.palette.m3tertiary
gridColor: Colors.palette.m3onSurface
markerColor: Colors.palette.m3tertiary
labelColor: Colors.palette.m3onSurfaceVariant
}
}
}
}
+263
View File
@@ -0,0 +1,263 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Modules.Dashboard.Weather
import qs.Services
import qs.Helpers
Item {
id: root
required property int radius
readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null
implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840
implicitHeight: layout.implicitHeight
Component.onCompleted: Weather.reload()
ColumnLayout {
id: layout
anchors.fill: parent
spacing: Tokens.spacing.medium
RowLayout {
Layout.leftMargin: Tokens.padding.large
Layout.rightMargin: Tokens.padding.large
Layout.fillWidth: true
Column {
spacing: Tokens.spacing.extraSmall
CustomText {
text: Weather.city || qsTr("Loading...")
font: Tokens.font.body.builders.large.size(24).weight(Font.DemiBold).build()
color: Colors.palette.m3onSurface
}
CustomText {
text: new Date().toLocaleDateString(Qt.locale("en_US"), "dddd, MMMM d")
font: Tokens.font.body.small
color: Colors.palette.m3onSurfaceVariant
}
}
Item {
Layout.fillWidth: true
}
Row {
spacing: Tokens.spacing.largeIncreased
WeatherStat {
icon: "wb_twilight"
label: qsTr("Sunrise")
value: Weather.sunrise
colour: Colors.palette.m3tertiary
}
WeatherStat {
icon: "bedtime"
label: qsTr("Sunset")
value: Weather.sunset
colour: Colors.palette.m3tertiary
}
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: bigInfoRow.implicitHeight + bigInfoRow.anchors.margins * 2
radius: Tokens.rounding.large
color: Colors.tPalette.m3surfaceContainer
Graphs {
id: bigInfoRow
anchors.fill: parent
anchors.margins: Tokens.padding.medium
}
}
RowLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.medium
DetailCard {
icon: Weather.icon
label: qsTr("Temperature")
value: Weather.temp
colour: Colors.palette.m3primary
}
DetailCard {
icon: "thermostat"
label: qsTr("Feels like", "apparent temperature")
value: Weather.feelsLike
colour: Colors.palette.m3primary
}
DetailCard {
icon: "water_drop"
label: qsTr("Humidity")
value: Strings.percent(Weather.humidity)
colour: Colors.palette.m3secondary
}
DetailCard {
icon: "air"
label: qsTr("Wind")
value: Weather.windSpeed ? qsTr("%1 km/h").arg(Weather.windSpeed) : "--"
colour: Colors.palette.m3tertiary
}
}
CustomText {
Layout.topMargin: Tokens.spacing.medium
Layout.leftMargin: Tokens.padding.medium
visible: forecastRepeater.count > 0
text: qsTr("7-day forecast")
font: Tokens.font.body.builders.medium.weight(Font.DemiBold).build()
color: Colors.palette.m3onSurface
}
RowLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.medium
Repeater {
id: forecastRepeater
model: Weather.forecast
CustomRect {
id: forecastItem
required property int index
required property var modelData
Layout.fillWidth: true
implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2
radius: root.radius
color: Colors.tPalette.m3surfaceContainer
ColumnLayout {
id: forecastItemColumn
anchors.centerIn: parent
spacing: Tokens.spacing.small
CustomText {
Layout.alignment: Qt.AlignHCenter
text: forecastItem.index === 0 ? qsTr("Today", "forecast column") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale("en_US"), "ddd")
font: Tokens.font.body.builders.medium.weight(Font.DemiBold).build()
color: Colors.palette.m3primary
}
CustomText {
Layout.topMargin: -Tokens.spacing.extraSmall
Layout.alignment: Qt.AlignHCenter
text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale("en_US"), "MMM d")
font: Tokens.font.body.small
opacity: 0.7
color: Colors.palette.m3onSurfaceVariant
}
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: forecastItem.modelData.icon
fontStyle: Tokens.font.icon.extraLarge
color: Colors.palette.m3secondary
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: {
const min = Weather.formatTemp(forecastItem.modelData.minTempC, true);
const max = Weather.formatTemp(forecastItem.modelData.maxTempC, true);
return qsTr("%1 / %2", "min/max temperature").arg(min).arg(max);
}
font: Tokens.font.body.builders.small.weight(Font.DemiBold).build()
color: Colors.palette.m3tertiary
}
}
}
}
}
}
component DetailCard: CustomRect {
id: detailRoot
property string icon
property string label
property string value
property color colour
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: Tokens.rounding.medium
color: Colors.tPalette.m3surfaceContainer
Row {
anchors.centerIn: parent
spacing: Tokens.spacing.medium
MaterialIcon {
text: detailRoot.icon
color: detailRoot.colour
fontStyle: Tokens.font.icon.large
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
text: detailRoot.label
font: Tokens.font.body.small
opacity: 0.7
horizontalAlignment: Text.AlignLeft
}
CustomText {
text: detailRoot.value
font: Tokens.font.body.builders.small.weight(Font.DemiBold).build()
horizontalAlignment: Text.AlignLeft
}
}
}
}
component WeatherStat: RowLayout {
id: weatherStat
property string icon
property string label
property string value
property color colour
spacing: Tokens.spacing.small
MaterialIcon {
text: weatherStat.icon
fontStyle: Tokens.font.icon.extraLarge
color: weatherStat.colour
}
Column {
Layout.alignment: Qt.AlignVCenter
CustomText {
text: weatherStat.label
font: Tokens.font.body.small
color: Colors.palette.m3onSurfaceVariant
}
CustomText {
text: weatherStat.value
font: Tokens.font.body.builders.small.weight(Font.DemiBold).build()
color: Colors.palette.m3onSurface
}
}
}
}
+7 -22
View File
@@ -8,24 +8,13 @@ import ZShell.Config
Item {
id: root
readonly property PersistentProperties dashState: PersistentProperties {
property date currentDate: new Date()
property int currentTab
reloadableId: "dashboardState"
}
readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0
required property real offsetScale
required property string position
readonly property bool shouldBeActive: root.visibilities.dashboard && Config.dashboard.enabled
required property PersistentProperties visibilities
readonly property bool shouldBeActive: root.screenState.dashboard && Config.dashboard.enabled
required property ScreenState screenState
anchors.bottom: position === "bottom" ? parent.bottom : undefined
anchors.bottomMargin: position === "bottom" ? (-implicitHeight - 5) * offsetScale : 0
anchors.left: position === "left" ? parent.left : undefined
anchors.leftMargin: position === "left" ? (-implicitWidth - 5) * offsetScale : 0
anchors.top: position === "top" ? parent.top : undefined
anchors.topMargin: position === "top" ? (-implicitHeight - 5) * offsetScale : 0
anchors.top: parent.top
anchors.topMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 854
opacity: 1 - offsetScale
@@ -35,15 +24,11 @@ Item {
id: content
active: root.shouldBeActive || root.visible
anchors.bottom: root.position === "top" ? parent.bottom : undefined
anchors.horizontalCenter: root.position !== "left" ? parent.horizontalCenter : undefined
anchors.right: root.position !== "left" ? undefined : parent.right
anchors.top: root.position === "bottom" ? parent.top : undefined
anchors.verticalCenter: root.position === "left" ? undefined : parent.verticalCenter
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: Content {
dashState: root.dashState
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+163 -142
View File
@@ -8,177 +8,198 @@ import ZShell.Config
import qs.Paths
import qs.Helpers
import qs.Services
import qs.Services
Item {
id: root
id: root
anchors.fill: parent
z: 998
visible: false
anchors.fill: parent
z: 998
visible: false
property real menuX: 0
property real menuY: 0
property real menuX: 0
property real menuY: 0
MouseArea {
anchors.fill: parent
onClicked: root.close()
}
MouseArea {
anchors.fill: parent
onClicked: root.close()
}
CustomClippingRect {
id: popupBackground
readonly property real padding: 4
CustomClippingRect {
id: popupBackground
readonly property real padding: 4
x: root.menuX
y: root.menuY
x: root.menuX
y: root.menuY
color: Colors.tPalette.m3surface
radius: Tokens.rounding.medium
color: Colors.tPalette.m3surface
radius: Tokens.rounding.medium
implicitWidth: menuLayout.implicitWidth + padding * 2
implicitHeight: menuLayout.implicitHeight + padding * 2
implicitWidth: menuLayout.implicitWidth + padding * 2
implicitHeight: menuLayout.implicitHeight + padding * 2
Behavior on opacity { Anim {} }
opacity: root.visible ? 1 : 0
Behavior on opacity {
Anim {}
}
opacity: root.visible ? 1 : 0
ColumnLayout {
id: menuLayout
anchors.centerIn: parent
spacing: 0
ColumnLayout {
id: menuLayout
anchors.centerIn: parent
spacing: 0
CustomRect {
Layout.preferredWidth: 200
radius: popupBackground.radius - popupBackground.padding
implicitHeight: openTerminalRow.implicitHeight + Tokens.padding.extraSmall * 2
CustomRect {
Layout.preferredWidth: 200
radius: popupBackground.radius - popupBackground.padding
implicitHeight: openTerminalRow.implicitHeight + Tokens.padding.extraSmall * 2
RowLayout {
id: openTerminalRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Tokens.padding.small
RowLayout {
id: openTerminalRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Tokens.padding.small
MaterialIcon { text: "terminal"; font.pointSize: 20 }
CustomText { text: "Open terminal"; Layout.fillWidth: true }
}
MaterialIcon {
text: "terminal"
fontStyle: Tokens.font.icon.medium
}
CustomText {
text: "Open terminal"
Layout.fillWidth: true
}
}
StateLayer {
anchors.fill: parent
StateLayer {
anchors.fill: parent
onClicked: {
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)])
root.close()
}
}
}
onClicked: {
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)]);
root.close();
}
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: Colors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: Colors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
CustomRect {
Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: settingsRow.implicitHeight + Tokens.padding.extraSmall * 2
CustomRect {
Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: settingsRow.implicitHeight + Tokens.padding.extraSmall * 2
RowLayout {
id: settingsRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Tokens.padding.small
RowLayout {
id: settingsRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Tokens.padding.small
MaterialIcon { text: "settings"; font.pointSize: 20 }
CustomText { text: "ZShell settings"; Layout.fillWidth: true }
}
MaterialIcon {
text: "settings"
fontStyle: Tokens.font.icon.medium
}
CustomText {
text: "ZShell settings"
Layout.fillWidth: true
}
}
StateLayer {
anchors.fill: parent
StateLayer {
anchors.fill: parent
onClicked: {
const visibilities = Visibilities.getForActive();
visibilities.settings = true;
root.close()
}
}
}
onClicked: {
const state = ShellState.forActive();
state.settings = true;
root.close();
}
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: Colors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: Colors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
CustomRect {
Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: logoutRow.implicitHeight + Tokens.padding.extraSmall * 2
CustomRect {
Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: logoutRow.implicitHeight + Tokens.padding.extraSmall * 2
RowLayout {
id: logoutRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Tokens.padding.small
RowLayout {
id: logoutRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Tokens.padding.small
MaterialIcon { text: "logout"; font.pointSize: 20 }
CustomText { text: "Logout"; Layout.fillWidth: true }
}
MaterialIcon {
text: "logout"
fontStyle: Tokens.font.icon.medium
}
CustomText {
text: "Logout"
Layout.fillWidth: true
}
}
StateLayer {
anchors.fill: parent
StateLayer {
anchors.fill: parent
onClicked: {
Hyprland.dispatch("global quickshell:sessionOpen")
root.close()
}
}
}
onClicked: {
Hyprland.dispatch("global quickshell:sessionOpen");
root.close();
}
}
}
// CustomRect {
// Layout.fillWidth: true
// implicitHeight: 1
// color: Colors.palette.m3outlineVariant
// Layout.topMargin: 4
// Layout.bottomMargin: 4
// }
//
// CustomRect {
// Layout.fillWidth: true
// radius: popupBackground.radius - popupBackground.padding
// implicitHeight: desktopIconsRow.implicitHeight + Tokens.padding.extraSmall * 2
//
// RowLayout {
// id: desktopIconsRow
// spacing: 8
// anchors.fill: parent
// anchors.leftMargin: Tokens.padding.small
//
// MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 }
// CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true }
// }
//
// StateLayer {
// anchors.fill: parent
//
// onClicked: {
// Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons
// root.close()
// }
// }
// }
}
}
// CustomRect {
// Layout.fillWidth: true
// implicitHeight: 1
// color: Colors.palette.m3outlineVariant
// Layout.topMargin: 4
// Layout.bottomMargin: 4
// }
//
// CustomRect {
// Layout.fillWidth: true
// radius: popupBackground.radius - popupBackground.padding
// implicitHeight: desktopIconsRow.implicitHeight + Tokens.padding.extraSmall * 2
//
// RowLayout {
// id: desktopIconsRow
// spacing: 8
// anchors.fill: parent
// anchors.leftMargin: Tokens.padding.small
//
// MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 }
// CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true }
// }
//
// StateLayer {
// anchors.fill: parent
//
// onClicked: {
// Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons
// root.close()
// }
// }
// }
}
}
function openAt(mouseX, mouseY, parentW, parentH) {
menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth))
menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight))
visible = true
}
function openAt(mouseX, mouseY, parentW, parentH) {
menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth));
menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight));
visible = true;
}
function close() {
visible = false
}
function close() {
visible = false;
}
}
@@ -54,8 +54,7 @@ Item {
y: contextMenu.menuY
Behavior on opacity {
Anim {
}
Anim {}
}
ColumnLayout {
@@ -77,7 +76,7 @@ Item {
spacing: 8
MaterialIcon {
font.pointSize: 20
fontStyle: Tokens.font.icon.medium
text: "open_in_new"
}
@@ -120,7 +119,7 @@ Item {
spacing: 8
MaterialIcon {
font.pointSize: 20
fontStyle: Tokens.font.icon.medium
text: contextMenu.targetIsDir ? "terminal" : "apps"
}
@@ -165,7 +164,7 @@ Item {
spacing: 8
MaterialIcon {
font.pointSize: 20
fontStyle: Tokens.font.icon.medium
text: "content_copy"
}
@@ -199,7 +198,7 @@ Item {
spacing: 8
MaterialIcon {
font.pointSize: 20
fontStyle: Tokens.font.icon.medium
text: "edit"
}
@@ -241,7 +240,7 @@ Item {
MaterialIcon {
color: deleteButton.hovered ? Colors.palette.m3onError : Colors.palette.m3error
font.pointSize: 20
fontStyle: Tokens.font.icon.medium
text: "delete"
}
+3 -3
View File
@@ -27,7 +27,7 @@ Item {
property var pendingCommitIds: []
readonly property int rounding: Tokens.rounding.largeIncreased
required property ShellScreen screen
required property PersistentProperties visibilities
required property ScreenState screenState
property var visualIds: []
function beginVisualDrag(appId, modelData, item) {
@@ -198,7 +198,7 @@ Item {
anchors.centerIn: parent
appListRoot: root
appToplevel: modelData
visibilities: root.visibilities
screenState: root.screenState
visible: root.draggedAppId !== slot.appId
}
@@ -314,7 +314,7 @@ Item {
appListRoot: root
appToplevel: root.draggedModelData
enabled: false
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ CustomRect {
property real iconSize: implicitHeight - 20
readonly property bool isSeparator: appToplevel?.appId === "__dock_separator__"
property int lastFocused: -1
required property PersistentProperties visibilities
required property ScreenState screenState
implicitHeight: Config.dock.height
implicitWidth: isSeparator ? 1 : implicitHeight
@@ -62,12 +62,12 @@ CustomRect {
onClicked: {
if (appToplevel?.toplevels.length === 0) {
root.desktopEntry?.execute();
root.visibilities.dock = false;
root.screenState.dock = false;
return;
}
lastFocused = (lastFocused + 1) % appToplevel?.toplevels.length;
appToplevel?.toplevels[lastFocused].activate();
root.visibilities.dock = false;
root.screenState.dock = false;
}
}
+3 -3
View File
@@ -12,8 +12,8 @@ Item {
property real offsetScale: shouldBeActive ? 0 : 1
required property var panels
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.dock && Config.dock.enabled
required property PersistentProperties visibilities
readonly property bool shouldBeActive: screenState.dock && Config.dock.enable
required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight
@@ -39,7 +39,7 @@ Item {
sourceComponent: Content {
panels: root.panels
screen: root.screen
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+10 -12
View File
@@ -12,7 +12,7 @@ Item {
readonly property var colors1: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4"]
readonly property var colors2: ["#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"]
required property var drawing
required property var visibilities
required property ScreenState screenState
required property Wrapper wrapper
function syncFromPenColor() {
@@ -20,17 +20,17 @@ Item {
return;
if (!saturationSlider.pressed)
saturationSlider.value = drawing.drawingState.penColor.hsvSaturation;
saturationSlider.value = drawing.screenState.penColor.hsvSaturation;
if (!brightnessSlider.pressed)
brightnessSlider.value = drawing.drawingState.penColor.hsvValue;
brightnessSlider.value = drawing.screenState.penColor.hsvValue;
}
function updatePenColorFromHsv() {
if (!drawing)
return;
drawing.drawingState.penColor = Qt.hsva(huePicker.currentHue, saturationSlider.value, brightnessSlider.value, drawing.drawingState.penColor.a);
drawing.screenState.penColor = Qt.hsva(huePicker.currentHue, saturationSlider.value, brightnessSlider.value, drawing.screenState.penColor.a);
}
implicitHeight: column.height + Tokens.padding.small * 2
@@ -43,7 +43,7 @@ Item {
root.syncFromPenColor();
}
target: root.drawing.drawingState
target: root.drawing.screenState
}
Column {
@@ -133,9 +133,9 @@ Item {
multiplier: 1
orientation: Qt.Horizontal
to: 45
value: root.drawing.drawingState.penWidth
value: root.drawing.screenState.penWidth
onMoved: root.drawing.drawingState.penWidth = value
onMoved: root.drawing.screenState.penWidth = value
}
ButtonRow {
@@ -145,7 +145,6 @@ Item {
IconTextButton {
fillWidth: true
font.pointSize: Tokens.font.size.normal
icon: "close"
inactiveColor: Colors.palette.m3error
inactiveOnColor: Colors.palette.m3onError
@@ -153,12 +152,11 @@ Item {
shapeMorph: true
text: "Exit"
onClicked: root.visibilities.isDrawing = false
onClicked: root.screenState.isDrawing = false
}
IconTextButton {
fillWidth: true
font.pointSize: Tokens.font.size.normal
icon: "ink_eraser"
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
@@ -193,7 +191,7 @@ Item {
required property ButtonRow row
fillWidth: false
font.pointSize: Tokens.font.size.normal
font: Tokens.font.body.medium
icon: ""
implicitHeight: buttonSize
implicitWidth: buttonSize
@@ -203,6 +201,6 @@ Item {
shapeMorph: true
shapeMorphExpansion: pressed ? 12 : 0
onClicked: root.drawing.drawingState.penColor = modelData
onClicked: root.drawing.screenState.penColor = modelData
}
}
+6 -8
View File
@@ -15,8 +15,8 @@ Item {
property real offsetScale: shouldBeActive ? 0 : 1
property bool pinned: false
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.isDrawing
required property var visibilities
readonly property bool shouldBeActive: screenState.isDrawing
required property ScreenState screenState
function collapse(): void {
if (pinned)
@@ -74,11 +74,10 @@ Item {
opacity: root.expanded ? 0 : 1
Behavior on opacity {
Anim {
}
Anim {}
}
sourceComponent: MaterialIcon {
font.pointSize: Tokens.font.size.larger
fontStyle: Tokens.font.icon.medium
text: "arrow_forward_ios"
}
}
@@ -93,12 +92,11 @@ Item {
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {
}
Anim {}
}
sourceComponent: Content {
drawing: root.drawing
visibilities: root.visibilities
screenState: root.screenState
wrapper: root
}
}
+23 -6
View File
@@ -15,7 +15,7 @@ CustomListView {
property string displayText
readonly property string requestedState: stateForText(search.text)
required property SearchBar search
required property PersistentProperties visibilities
required property ScreenState screenState
function resultsForText(text: string): var {
switch (stateForText(text)) {
@@ -25,6 +25,8 @@ CustomListView {
return [0];
case "variant":
return SchemeVariants.query(text);
case "files":
return Files.query(text);
default:
return Apps.search(text);
}
@@ -33,7 +35,7 @@ CustomListView {
function stateForText(text: string): string {
const prefix = Config.launcher.actionPrefix;
if (text.startsWith(prefix)) {
for (const action of ["calc", "scheme", "variant"])
for (const action of ["calc", "scheme", "variant", "files"])
if (text.startsWith(`${prefix}${action} `))
return action;
@@ -44,7 +46,7 @@ CustomListView {
}
function syncDisplayText(): void {
if (visibilities.launcher && requestedState === displayState)
if (screenState.launcher && requestedState === displayState)
displayText = search.text;
}
@@ -55,7 +57,7 @@ CustomListView {
preferredHighlightBegin: 0
preferredHighlightEnd: height
spacing: Tokens.spacing.small
state: visibilities.launcher ? requestedState : displayState
state: screenState.launcher ? requestedState : displayState
verticalLayoutDirection: ListView.BottomToTop
CustomScrollBar.vertical: CustomScrollBar {
@@ -163,6 +165,13 @@ CustomListView {
PropertyChanges {
root.delegate: variantItem
}
},
State {
name: "files"
PropertyChanges {
root.delegate: filesItem
}
}
]
transitions: Transition {
@@ -236,7 +245,7 @@ CustomListView {
id: appItem
AppItem {
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -264,6 +273,14 @@ CustomListView {
}
}
Component {
id: filesItem
FilesItem {
list: root
}
}
Connections {
function onTextChanged() {
root.syncDisplayText();
@@ -277,6 +294,6 @@ CustomListView {
root.syncDisplayText();
}
target: root.visibilities
target: root.screenState
}
}
+3 -3
View File
@@ -13,14 +13,14 @@ CarouselView {
readonly property var screen: QsWindow.window?.screen
required property SearchBar search
readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" "))
required property var visibilities
required property ScreenState screenState
currentIndex: {
const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent);
return idx >= 0 ? idx : 0;
}
delegateProperties: ({
visibilities: root.visibilities
screenState: root.screenState
})
focalWidth: Config.launcher.sizes.wallpaperWidth
glideDuration: 250
@@ -32,7 +32,7 @@ CarouselView {
return 0;
const barMargins = Config.bar.border * 2;
let margins = Math.round(screen.width / 6);
if (visibilities.utilities || visibilities.sidebar)
if (screenState.utilities || screenState.sidebar)
margins = Config.sidebar.sizes.width;
return screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4;
}
+9 -7
View File
@@ -15,7 +15,7 @@ Item {
readonly property int padding: Tokens.padding.largeIncreased / 4
required property var panels
readonly property int rounding: Tokens.rounding.largeIncreased
required property PersistentProperties visibilities
required property ScreenState screenState
implicitHeight: search.height + listWrapper.height + padding * 2
implicitWidth: listWrapper.width + padding * 2
@@ -38,7 +38,7 @@ Item {
panels: root.panels
rounding: root.rounding
search: search
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -55,7 +55,7 @@ Item {
Component.onCompleted: forceActiveFocus()
Keys.onDownPressed: list.currentList?.decrementCurrentIndex()
Keys.onEscapePressed: root.visibilities.launcher = false
Keys.onEscapePressed: root.screenState.launcher = false
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
if (event.key === Qt.Key_J) {
@@ -81,26 +81,28 @@ Item {
if (Colors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent)
Wallpapers.previewColorLock = true;
Wallpapers.setWallpaper(currentItem.modelData.path);
root.visibilities.launcher = false;
root.screenState.launcher = false;
} else if (text.startsWith(Config.launcher.actionPrefix)) {
if (text.startsWith(`${Config.launcher.actionPrefix}calc `))
currentItem.onClicked();
else if (text.startsWith(`${Config.launcher.actionPrefix}files `))
currentItem.onClicked();
else
currentItem.modelData.onClicked(list.currentList);
} else {
Apps.launch(currentItem.modelData);
root.visibilities.launcher = false;
root.screenState.launcher = false;
}
}
}
Connections {
function onLauncherChanged(): void {
if (!root.visibilities.launcher)
if (!root.screenState.launcher)
search.text = "";
}
target: root.visibilities
target: root.screenState
}
}
}
+11 -15
View File
@@ -20,7 +20,7 @@ Item {
required property int rounding
required property SearchBar search
readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `)
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
@@ -28,7 +28,7 @@ Item {
state: showWallpapers ? "wallpapers" : "apps"
Behavior on implicitHeight {
enabled: root.visibilities.launcher
enabled: root.screenState.launcher
Anim {
duration: Tokens.anim.durations.small
@@ -36,7 +36,7 @@ Item {
}
}
Behavior on implicitWidth {
enabled: root.visibilities.launcher
enabled: root.screenState.launcher
Anim {
duration: Tokens.anim.durations.small
@@ -53,8 +53,7 @@ Item {
to: 0
}
PropertyAction {
}
PropertyAction {}
Anim {
duration: Tokens.anim.durations.small
@@ -99,7 +98,7 @@ Item {
sourceComponent: AppList {
search: root.search
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -115,7 +114,7 @@ Item {
sourceComponent: CarouselView {
panels: root.panels
search: root.search
visibilities: root.visibilities
screenState: root.screenState
}
}
@@ -130,18 +129,16 @@ Item {
spacing: Tokens.spacing.medium
Behavior on opacity {
Anim {
}
Anim {}
}
Behavior on scale {
Anim {
}
Anim {}
}
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.extraLarge
fontStyle: Tokens.font.icon.extraLarge
text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search"
}
@@ -150,14 +147,13 @@ Item {
CustomText {
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.larger
font.weight: 500
font: Tokens.font.body.builders.large.weight(Font.Medium).build()
text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results")
}
CustomText {
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
font: Tokens.font.body.medium
text: root.state === "wallpapers" && Wallpapers.list.length === 0 ? qsTr("Try putting some wallpapers in %1").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr("Try searching for something else")
}
}
+3 -4
View File
@@ -16,7 +16,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight
StateLayer {
radius: Tokens.rounding.small
radius: Tokens.rounding.medium
onClicked: {
root.modelData?.onClicked(root.list);
@@ -33,7 +33,7 @@ Item {
id: icon
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Tokens.font.size.extraLarge
fontStyle: Tokens.font.icon.large
text: root.modelData?.icon ?? ""
}
@@ -47,7 +47,6 @@ Item {
CustomText {
id: name
font.pointSize: Tokens.font.size.normal
text: root.modelData?.name ?? ""
}
@@ -57,7 +56,7 @@ Item {
anchors.top: name.bottom
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
text: root.modelData?.desc ?? ""
width: root.width - icon.width - Tokens.rounding.medium * 2
}
+4 -5
View File
@@ -12,18 +12,18 @@ Item {
id: root
required property DesktopEntry modelData
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: Config.launcher.sizes.itemHeight
StateLayer {
radius: Tokens.rounding.small
radius: Tokens.rounding.medium
onClicked: {
Apps.launch(root.modelData);
root.visibilities.launcher = false;
root.screenState.launcher = false;
}
}
@@ -51,7 +51,6 @@ Item {
CustomText {
id: name
font.pointSize: Tokens.font.size.normal
text: root.modelData?.name ?? ""
}
@@ -61,7 +60,7 @@ Item {
anchors.top: name.bottom
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? ""
width: root.width - icon.width - Tokens.rounding.medium - Tokens.padding.medium * 2
}
+7 -8
View File
@@ -16,7 +16,7 @@ Item {
function onClicked(): void {
Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]);
root.list.visibilities.launcher = false;
root.list.screenState.launcher = false;
}
anchors.left: parent?.left
@@ -24,7 +24,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight
StateLayer {
radius: Tokens.rounding.small
radius: Tokens.rounding.medium
onClicked: {
root.onClicked();
@@ -40,7 +40,7 @@ Item {
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
font.pointSize: Tokens.font.size.extraLarge
fontStyle: Tokens.font.icon.large
text: "function"
}
@@ -81,7 +81,7 @@ Item {
onClicked: {
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]);
root.list.visibilities.launcher = false;
root.list.screenState.launcher = false;
}
}
@@ -92,13 +92,12 @@ Item {
anchors.rightMargin: Tokens.spacing.small
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onTertiary
font.pointSize: Tokens.font.size.normal
font: Tokens.font.body.medium
opacity: stateLayer.containsMouse ? 1 : 0
text: qsTr("Open in calculator")
Behavior on opacity {
Anim {
}
Anim {}
}
}
@@ -109,7 +108,7 @@ Item {
anchors.rightMargin: Tokens.padding.small
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onTertiary
font.pointSize: Tokens.font.size.large
fontStyle: Tokens.font.icon.medium
text: "open_in_new"
}
}
+141
View File
@@ -0,0 +1,141 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Modules.Launcher.Services
import qs.Services
Item {
id: root
required property var list
required property var modelData
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: Config.launcher.sizes.itemHeight
function onClicked(): void {
root.list.screenState.launcher = false;
if (root.modelData?.path)
Files.launch(["xdg-open", root.modelData.path]);
}
StateLayer {
id: sLayer
radius: Tokens.rounding.medium
manualHoverOverride: openLocation.hovered
onClicked: {
root.onClicked();
}
}
IconButton {
id: openLocation
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.medium
anchors.verticalCenter: parent.verticalCenter
icon: "open_in_new"
isRound: true
scale: sLayer.containsMouse || sLayer.manualHoverOverride ? 1 : 0.4
opacity: sLayer.containsMouse || sLayer.manualHoverOverride ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on scale {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: {
root.list.screenState.launcher = false;
if (root.modelData?.path)
Files.showInFolder(root.modelData.path);
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Tokens.padding.small
anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small
IconImage {
id: icon
anchors.verticalCenter: parent.verticalCenter
implicitSize: Tokens.font.icon.extraLarge.pointSize
source: Quickshell.iconPath(root.modelData?.icon) ?? ""
visible: !root.modelData?.isImage
}
Loader {
active: opacity > 0
anchors.verticalCenter: parent.verticalCenter
asynchronous: true
opacity: root.modelData?.isImage && image.status === Image.Loading ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
sourceComponent: LoadingIndicator {
color: Colors.palette.m3primaryContainer
implicitSize: Tokens.font.icon.extraLarge.pointSize
}
}
FadeImage {
id: image
property int implicitSize: Tokens.font.icon.extraLarge.pointSize
width: implicitSize
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
source: root.modelData?.isImage ? root.modelData?.path ?? "" : ""
visible: root.modelData?.isImage
}
Item {
anchors.left: icon.right
anchors.leftMargin: Tokens.spacing.medium
anchors.verticalCenter: icon.verticalCenter
implicitHeight: name.implicitHeight + desc.implicitHeight
implicitWidth: parent.width - icon.width
CustomText {
id: name
font: Tokens.font.body.medium
text: root.modelData?.name ?? ""
elide: Text.ElideRight
width: root.width - icon.width - openLocation.implicitWidth - openLocation.anchors.rightMargin - Tokens.spacing.medium - Tokens.rounding.medium * 2
}
CustomText {
id: desc
anchors.top: name.bottom
color: Colors.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.body.small
text: root.modelData?.path ?? ""
width: root.width - icon.width - openLocation.implicitWidth - openLocation.anchors.rightMargin - Tokens.spacing.medium - Tokens.rounding.medium * 2
}
}
}
}
+4 -5
View File
@@ -15,7 +15,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight
StateLayer {
radius: Tokens.rounding.small
radius: Tokens.rounding.medium
onClicked: {
root.modelData?.onClicked(root.list);
@@ -32,7 +32,7 @@ Item {
id: icon
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Tokens.font.size.extraLarge
fontStyle: Tokens.font.icon.large
text: root.modelData?.icon ?? ""
}
@@ -44,7 +44,6 @@ Item {
width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.medium : 0)
CustomText {
font.pointSize: Tokens.font.size.normal
text: root.modelData?.name ?? ""
}
@@ -53,7 +52,7 @@ Item {
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
text: root.modelData?.description ?? ""
}
}
@@ -67,7 +66,7 @@ Item {
sourceComponent: MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large
fontStyle: Tokens.font.icon.large
text: "check"
}
}
+2 -3
View File
@@ -12,7 +12,7 @@ Item {
required property var modelData // FileSystemEntry-like {path, relativePath}
required property real tileHeight
required property real tileWidth
required property PersistentProperties visibilities
required property ScreenState screenState
signal requestActivate
@@ -32,8 +32,7 @@ Item {
MaterialIcon {
anchors.centerIn: parent
color: Colors.tPalette.m3outline
font.pointSize: Tokens.font.size.extraLarge * 2
font.weight: 600
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
text: "image"
}
+2 -2
View File
@@ -42,10 +42,10 @@ Searcher {
if (command[0] === "autocomplete" && command.length > 1) {
list.search.text = `${Config.launcher.actionPrefix}${command[1]} `;
} else if (command[0] === "setMode" && command.length > 1) {
list.visibilities.launcher = false;
list.screenState.launcher = false;
Colors.setMode(command[1]);
} else {
list.visibilities.launcher = false;
list.screenState.launcher = false;
Quickshell.execDetached(command);
}
}
+194
View File
@@ -0,0 +1,194 @@
// qs/Services/Files.qml
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import ZShell.Config
Singleton {
id: root
property list<var> results: []
property string _pendingQuery: ""
property int _generation: 0
readonly property var _imageExts: ({
"png": true,
"jpg": true,
"jpeg": true,
"gif": true,
"bmp": true,
"webp": true,
"svg": true,
"tiff": true,
"tif": true,
"ico": true,
"heic": true,
"avif": true
})
// freedesktop mimetype-icon names; extend as needed
readonly property var _extIcons: ({
// images
"png": "image-x-generic",
"jpg": "image-x-generic",
"jpeg": "image-x-generic",
"gif": "image-x-generic",
"bmp": "image-x-generic",
"webp": "image-x-generic",
"svg": "image-x-generic",
"tiff": "image-x-generic",
"tif": "image-x-generic",
"ico": "image-x-generic",
"heic": "image-x-generic",
"avif": "image-x-generic",
// documents
"pdf": "application-pdf",
"doc": "x-office-document",
"docx": "x-office-document",
"odt": "x-office-document",
"txt": "text-x-generic",
"md": "text-x-generic",
"xls": "x-office-spreadsheet",
"xlsx": "x-office-spreadsheet",
"ods": "x-office-spreadsheet",
"ppt": "x-office-presentation",
"pptx": "x-office-presentation",
"odp": "x-office-presentation",
// archives
"zip": "package-x-generic",
"tar": "package-x-generic",
"gz": "package-x-generic",
"xz": "package-x-generic",
"7z": "package-x-generic",
"rar": "package-x-generic",
// audio/video
"mp3": "audio-x-generic",
"flac": "audio-x-generic",
"wav": "audio-x-generic",
"ogg": "audio-x-generic",
"mp4": "video-x-generic",
"mkv": "video-x-generic",
"webm": "video-x-generic",
"avi": "video-x-generic",
"mov": "video-x-generic",
// code
"js": "text-x-script",
"ts": "text-x-script",
"py": "text-x-script",
"sh": "text-x-script",
"qml": "text-x-script",
"cpp": "text-x-c++src",
"c": "text-x-csrc",
"h": "text-x-chdr",
"json": "application-json",
"html": "text-html",
"css": "text-css"
})
function showInFolder(path: string): void {
const explorer = Config.general.apps.explorer;
let args = [];
if (explorer.includes("dolphin"))
args = [...explorer, "--select", path];
else
args = [...explorer, path];
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--", ...args]);
else
Quickshell.execDetached([...args]);
}
function launch(command: list<string>): void {
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--"].concat(command));
else
Quickshell.execDetached(command);
}
function transformSearch(search: string): string {
return search.slice(Config.launcher.actionPrefix.length + "files ".length);
}
function query(search: string): list<var> {
search = transformSearch(search);
if (search !== _pendingQuery) {
_pendingQuery = search;
debounceTimer.restart();
}
return results;
}
function _extOf(name: string): string {
const i = name.lastIndexOf(".");
// no dot, or dotfile with no real extension (".bashrc" -> treat as no ext)
if (i <= 0)
return "";
return name.slice(i + 1).toLowerCase();
}
function _iconFor(name: string, isDir: bool): string {
if (isDir)
return "folder";
const ext = _extOf(name);
return _extIcons[ext] ?? "text-x-generic";
}
function _buildEntry(path: string): var {
const name = path.split("/").pop();
const ext = _extOf(name);
// trailing slash from plocate's directory output (if -d/dir results ever appear)
const isDir = path.endsWith("/");
return {
path: path,
name: name,
icon: _iconFor(name, isDir),
isImage: !isDir && (_imageExts[ext] ?? false)
};
}
Timer {
id: debounceTimer
interval: 120
onTriggered: root._runQuery(root._pendingQuery)
}
function _runQuery(search: string): void {
_generation++;
if (!search) {
proc.running = false;
results = [];
return;
}
if (proc.running)
proc.running = false;
proc.buffer = [];
proc.myGeneration = _generation;
proc.command = ["plocate", "-i", "-b", "--limit", "50", search];
proc.running = true;
}
Process {
id: proc
property var buffer: []
property int myGeneration: 0
stdout: SplitParser {
onRead: line => proc.buffer.push(root._buildEntry(line))
}
onRunningChanged: {
if (!running && myGeneration === root._generation)
root.results = buffer;
}
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ Searcher {
required property string variant
function onClicked(list: AppList): void {
list.visibilities.launcher = false;
list.screenState.launcher = false;
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]);
Config.colors.schemeType = variant;
}
+4 -4
View File
@@ -12,7 +12,7 @@ Item {
property int contentHeight
readonly property real maxHeight: {
let max = screen.height - Tokens.spacing.largeIncreased * 2;
if (visibilities.resources && panels.resourcesWrapper.x + panels.resourcesWrapper.width > root.x)
if (screenState.resources && panels.resourcesWrapper.x + panels.resourcesWrapper.width > root.x)
max -= panels.resources.nonAnimHeight;
if (panels.popouts.hasCurrent)
if (panels.popouts.current?.x + panels.popouts.current?.width > root.x && panels.popouts.current?.x < root.x + root.width)
@@ -22,8 +22,8 @@ Item {
property real offsetScale: shouldBeActive ? 0 : 1
required property var panels
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.launcher
required property PersistentProperties visibilities
readonly property bool shouldBeActive: screenState.launcher
required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight
@@ -57,7 +57,7 @@ Item {
sourceComponent: Content {
maxHeight: root.maxHeight
panels: root.panels
visibilities: root.visibilities
screenState: root.screenState
}
}
}
+12 -25
View File
@@ -28,27 +28,21 @@ ColumnLayout {
CustomText {
Layout.alignment: Qt.AlignVCenter
color: Colors.palette.m3secondary
font.bold: true
font.family: Config.appearance.font.family.clock
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
text: Time.hourStr
}
CustomText {
Layout.alignment: Qt.AlignVCenter
color: Colors.palette.m3primary
font.bold: true
font.family: Config.appearance.font.family.clock
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
text: ":"
}
CustomText {
Layout.alignment: Qt.AlignVCenter
color: Colors.palette.m3secondary
font.bold: true
font.family: Config.appearance.font.family.clock
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
text: Time.minuteStr
}
}
@@ -57,9 +51,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -Tokens.padding.large * 2
color: Colors.palette.m3tertiary
font.bold: true
font.family: Config.appearance.font.family.mono
font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale)
font: Tokens.font.title.builders.medium.weight(Font.DemiBold).build()
text: Time.format("dddd, d MMMM yyyy")
}
@@ -74,7 +66,7 @@ ColumnLayout {
MaterialIcon {
anchors.centerIn: parent
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Math.floor(root.centerWidth / 4)
fontStyle: Tokens.font.icon.size(root.centerWidth / 4).build()
text: "person"
}
@@ -144,8 +136,7 @@ ColumnLayout {
}
Behavior on opacity {
Anim {
}
Anim {}
}
}
@@ -159,6 +150,7 @@ ColumnLayout {
id: inputField
pam: root.lock.pam
centerScale: root.centerScale
}
CustomRect {
@@ -180,7 +172,6 @@ ColumnLayout {
anchors.centerIn: parent
color: root.lock.pam.buffer ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
font.weight: 500
text: "arrow_forward"
}
}
@@ -193,8 +184,7 @@ ColumnLayout {
implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight)
Behavior on implicitHeight {
Anim {
}
Anim {}
}
CustomText {
@@ -226,7 +216,7 @@ ColumnLayout {
anchors.right: parent.right
animateProp: "opacity"
color: Colors.palette.m3onSurfaceVariant
font.family: Config.appearance.font.family.mono
font: Tokens.font.body.small
horizontalAlignment: Qt.AlignHCenter
lineHeight: 1.2
opacity: shouldBeVisible && !message.msg ? 1 : 0
@@ -234,12 +224,10 @@ ColumnLayout {
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on opacity {
Anim {
}
Anim {}
}
Behavior on scale {
Anim {
}
Anim {}
}
onMsgChanged: {
@@ -295,8 +283,7 @@ ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3error
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
horizontalAlignment: Qt.AlignHCenter
opacity: 0
scale: 0.7
+7 -10
View File
@@ -14,6 +14,7 @@ Item {
property string buffer
required property Pam pam
required property real centerScale
readonly property alias placeholder: placeholder
Layout.fillHeight: true
@@ -41,8 +42,7 @@ Item {
anchors.centerIn: parent
animate: true
color: root.pam.passwd.active ? Colors.palette.m3secondary : Colors.palette.m3outline
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.normal
font: Tokens.font.body.builders.medium.scale(root.centerScale).width(110).build()
opacity: root.buffer ? 0 : 1
text: {
if (root.pam.passwd.active)
@@ -53,8 +53,7 @@ Item {
}
Behavior on opacity {
Anim {
}
Anim {}
}
}
@@ -71,7 +70,7 @@ Item {
anchors.centerIn: parent
anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0
implicitHeight: Tokens.font.size.normal
implicitHeight: Tokens.font.body.medium.pointSize
implicitWidth: fullWidth
interactive: false
orientation: Qt.Horizontal
@@ -84,12 +83,11 @@ Item {
implicitHeight: charList.implicitHeight
implicitWidth: implicitHeight
opacity: 0
radius: Tokens.rounding.small / 2
radius: Tokens.rounding.full
scale: 0
Behavior on opacity {
Anim {
}
Anim {}
}
Behavior on scale {
Anim {
@@ -137,8 +135,7 @@ Item {
Behavior on implicitWidth {
id: imWidthBehavior
Anim {
}
Anim {}
}
model: ScriptModel {
values: root.buffer.split("")
+1 -2
View File
@@ -236,8 +236,7 @@ WlSessionLockSurface {
id: lockIcon
anchors.centerIn: parent
font.bold: true
font.pointSize: Tokens.font.size.extraLarge * 4
fontStyle: Tokens.font.icon.builders.extraLarge.scale(4).weight(Font.Bold).build()
text: "lock"
}
+1 -2
View File
@@ -57,7 +57,7 @@ CustomClippingRect {
animate: true
color: Colors.palette.m3primary
elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal
font: Tokens.font.title.large
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track")
}
@@ -67,7 +67,6 @@ CustomClippingRect {
animate: true
color: Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackArtist ?? qsTr("Try playing some music!")) || qsTr("Unknown artist")
}
+2 -5
View File
@@ -66,9 +66,7 @@ ColumnLayout {
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.large
font.weight: 500
font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications")
}
}
@@ -94,8 +92,7 @@ ColumnLayout {
to: 1
}
}
delegate: NotifGroup {
}
delegate: NotifGroup {}
displaced: Transition {
Anim {
properties: "opacity,scale"
+8 -12
View File
@@ -80,7 +80,7 @@ CustomRect {
MaterialIcon {
color: root.urgency === "critical" ? Colors.palette.m3onError : root.urgency === "low" ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
font.pointSize: Tokens.font.size.large
fontStyle: Tokens.font.icon.large
text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency)
}
}
@@ -133,14 +133,14 @@ CustomRect {
Layout.fillWidth: true
color: Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
text: root.modelData
}
CustomText {
animate: true
color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
text: root.notifs[0]?.timeStr ?? ""
}
@@ -153,12 +153,10 @@ CustomRect {
radius: Tokens.rounding.full
Behavior on Layout.preferredWidth {
Anim {
}
Anim {}
}
Behavior on opacity {
Anim {
}
Anim {}
}
StateLayer {
@@ -181,7 +179,7 @@ CustomRect {
Layout.leftMargin: Tokens.padding.extraSmall / 2
animate: true
color: root.urgency === "critical" ? Colors.palette.m3onError : Colors.palette.m3onSurface
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
text: root.notifs.length
}
@@ -261,8 +259,7 @@ CustomRect {
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {
}
Anim {}
}
sourceComponent: ColumnLayout {
Repeater {
@@ -270,8 +267,7 @@ CustomRect {
values: root.notifs.slice(Config.notifs.groupPreviewNum)
}
NotifLine {
}
NotifLine {}
}
}
}
+2 -3
View File
@@ -70,14 +70,13 @@ CustomRect {
implicitSize: width
Behavior on clampedVal {
Anim {
}
Anim {}
}
MaterialIcon {
anchors.centerIn: parent
color: res.fgColor
font.pointSize: Tokens.font.size.extraLarge
fontStyle: Tokens.font.icon.extraLarge
text: res.icon
}
}
+4 -4
View File
@@ -16,7 +16,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
animate: true
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large
font: Tokens.font.title.large
text: Weather.description
}
@@ -29,7 +29,7 @@ ColumnLayout {
animate: true
color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.large
font: Tokens.font.body.large
text: Weather.temp
}
@@ -44,7 +44,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
animate: true
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large
font: Tokens.font.body.large
text: qsTr("Feels like %1").arg(Weather.temp)
visible: root.rootHeight > 550
}
@@ -53,7 +53,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
animate: true
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
font: Tokens.font.body.normal
text: {
const today = Weather.forecast[0];
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
+8 -9
View File
@@ -24,14 +24,14 @@ CustomRect {
MaterialIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
font.pointSize: Tokens.font.size.normal
fontStyle: Tokens.font.icon.medium
text: "schedule"
}
CustomText {
id: title
font.pointSize: Tokens.font.size.normal
font: Tokens.font.title.medium
text: qsTr("Hourly forecast")
}
}
@@ -60,21 +60,20 @@ CustomRect {
MaterialShape {
Layout.alignment: Qt.AlignHCenter
color: Qt.alpha(Colors.palette.m3primary, hour.index === 0 ? 1 : 0)
color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0)
implicitSize: temp.implicitHeight + Tokens.padding.small * 2
shape: MaterialShape.Cookie4Sided
Behavior on color {
CAnim {
}
CAnim {}
}
CustomText {
id: temp
anchors.centerIn: parent
color: hour.index === 0 ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
font.pointSize: Tokens.font.size.normal
color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
font: Tokens.font.body.medium
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
}
}
@@ -82,7 +81,7 @@ CustomRect {
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3secondary
font.pointSize: Tokens.font.size.extraLarge
fontStyle: Tokens.font.icon.extraLarge
text: hour.cond.icon
}
@@ -96,7 +95,7 @@ CustomRect {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
}
}
+6 -8
View File
@@ -13,7 +13,7 @@ Item {
readonly property int padding: Tokens.padding.smaller
required property Item panels
required property PersistentProperties visibilities
required property ScreenState screenState
anchors.bottom: parent.bottom
anchors.right: parent.right
@@ -27,16 +27,16 @@ Item {
for (let i = 0; i < count; i++)
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || visibilities.dashboard)
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x)
return 0;
if (visibilities.osd) {
if (screenState.osd) {
const h = panels.osd.y - Tokens.spacing.small * 2 - padding * 2;
if (height > h)
height = h;
}
if (visibilities.session) {
if (screenState.session) {
const h = panels.session.y - Tokens.spacing.small * 2 - padding * 2;
if (height > h)
height = h;
@@ -47,8 +47,7 @@ Item {
implicitWidth: Config.notifs.sizes.width + padding * 2
Behavior on implicitHeight {
Anim {
}
Anim {}
}
ClippingWrapperRectangle {
@@ -65,8 +64,7 @@ Item {
orientation: Qt.Vertical
spacing: 0
delegate: NotifWrapper {
}
delegate: NotifWrapper {}
displaced: Transition {
Anim {
property: "y"
+15 -16
View File
@@ -94,8 +94,7 @@ CustomRect {
implicitHeight: root.nonAnimHeight
Behavior on implicitHeight {
Anim {
}
Anim {}
}
Loader {
@@ -171,6 +170,7 @@ CustomRect {
sourceComponent: MaterialIcon {
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
fontStyle: Tokens.font.icon.medium
}
}
}
@@ -222,6 +222,7 @@ CustomRect {
maximumLineCount: 1
opacity: root.expanded ? 1 : 0
text: appNameMetrics.elidedText
font: Tokens.font.label.medium
Behavior on opacity {
Anim {
@@ -246,14 +247,12 @@ CustomRect {
anchors.leftMargin: Tokens.spacing.small
anchors.top: parent.top
animate: true
font.pointSize: Tokens.font.size.small
height: implicitHeight
maximumLineCount: 1
text: summaryMetrics.elidedText
Behavior on height {
Anim {
}
Anim {}
}
states: State {
name: "expanded"
@@ -281,8 +280,7 @@ CustomRect {
property: "topMargin"
}
AnchorAnim {
}
AnchorAnim {}
}
}
@@ -303,6 +301,7 @@ CustomRect {
anchors.top: parent.top
color: Colors.palette.m3onSurfaceVariant
text: "•"
font: Tokens.font.body.small
states: State {
name: "expanded"
@@ -314,8 +313,7 @@ CustomRect {
}
}
transitions: Transition {
AnchorAnim {
}
AnchorAnim {}
}
}
@@ -327,7 +325,7 @@ CustomRect {
anchors.top: parent.top
animate: true
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
horizontalAlignment: Text.AlignLeft
text: root.modelData.timeStr
}
@@ -355,14 +353,13 @@ CustomRect {
anchors.verticalCenterOffset: root.expanded ? -1 : 1
rotation: root.expanded ? 180 : 0
text: "expand_more"
fontStyle: Tokens.font.icon.medium
Behavior on anchors.verticalCenterOffset {
Anim {
}
Anim {}
}
Behavior on rotation {
Anim {
}
Anim {}
}
}
}
@@ -376,7 +373,7 @@ CustomRect {
anchors.top: summary.bottom
animate: true
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
opacity: root.expanded ? 0 : 1
text: bodyPreviewMetrics.elidedText
textFormat: Text.MarkdownText
@@ -407,6 +404,7 @@ CustomRect {
animate: true
color: Colors.palette.m3onSurfaceVariant
height: text ? implicitHeight : 0
font: Tokens.font.body.medium
opacity: root.expanded ? 1 : 0
text: root.modelData.body
textFormat: Text.MarkdownText
@@ -447,6 +445,7 @@ CustomRect {
enabled: root.expanded
fillWidth: root.modelData.actions.length === 0
icon: "close"
font: Tokens.font.icon.builders.small.size(14).build()
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
isRound: true
@@ -464,7 +463,6 @@ CustomRect {
enabled: root.expanded
fillWidth: true
font.pointSize: Tokens.font.size.small
implicitWidth: label.implicitWidth
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
@@ -493,6 +491,7 @@ CustomRect {
label.animate: true
padding: Tokens.padding.extraSmall
shapeMorph: true
font: Tokens.font.icon.builders.small.size(14).build()
onClicked: {
Quickshell.clipboardText = root.modelData.body;
@@ -0,0 +1,581 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import QtQuick.Layouts
import ZShell.Components
import ZShell.Config
import ZShell.Llm
import qs.Modules.Notifications.Sidebar.Chat.Content
import qs.Components
import qs.Services
Item {
id: root
property ChatSession chatData
// qmlformat off
readonly property var keys: [
Qt.Key_A,
Qt.Key_B,
Qt.Key_C,
Qt.Key_D,
Qt.Key_E,
Qt.Key_F,
Qt.Key_G,
Qt.Key_H,
Qt.Key_I,
Qt.Key_J,
Qt.Key_K,
Qt.Key_L,
Qt.Key_M,
Qt.Key_N,
Qt.Key_O,
Qt.Key_P,
Qt.Key_Q,
Qt.Key_R,
Qt.Key_S,
Qt.Key_T,
Qt.Key_U,
Qt.Key_V,
Qt.Key_W,
Qt.Key_X,
Qt.Key_Y,
Qt.Key_Z,
Qt.Key_Agrave,
Qt.Key_Aacute,
Qt.Key_Acircumflex,
Qt.Key_Atilde,
Qt.Key_Adiaeresis,
Qt.Key_Aring,
Qt.Key_AE,
Qt.Key_Ccedilla,
Qt.Key_Egrave,
Qt.Key_Eacute,
Qt.Key_Ecircumflex,
Qt.Key_Ediaeresis,
Qt.Key_Igrave,
Qt.Key_Iacute,
Qt.Key_Icircumflex,
Qt.Key_Idiaeresis,
Qt.Key_ETH,
Qt.Key_Ntilde,
Qt.Key_Ograve,
Qt.Key_Oacute,
Qt.Key_Ocircumflex,
Qt.Key_Otilde,
Qt.Key_Odiaeresis,
Qt.Key_Ooblique,
Qt.Key_Ugrave,
Qt.Key_Uacute,
Qt.Key_Ucircumflex,
Qt.Key_Udiaeresis,
Qt.Key_Yacute,
Qt.Key_ydiaeresis,
Qt.Key_THORN,
Qt.Key_ssharp
]
// qmlformat on
signal requestClose
readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4
property var pendingAttachments: []
function attachFiles(): void {
Files.pickFiles().then(files => {
if (!files || files.length === 0)
return;
const added = chatData.addAttachments(files);
root.pendingAttachments = root.pendingAttachments.concat(added);
});
}
function send(text: string): void {
if (text.trim() === "" && root.pendingAttachments.length === 0)
return;
root.scrollToLatest();
chatData.sendMessage(text, root.pendingAttachments);
input.text = "";
root.pendingAttachments = [];
}
function scrollToLatest(): void {
if (!listLoader.item || root.atNewestEnd)
return;
scrollAnim.stop();
scrollAnim.start();
}
function focusInput(): void {
Qt.callLater(() => input.forceActiveFocus());
}
onChatDataChanged: {
if (chatData)
focusInput();
}
Keys.onPressed: e => {
if (root.keys.includes(e.key)) {
input.insert(input.length, e.text);
focusInput();
}
}
Anim {
id: scrollAnim
target: listLoader.item
property: "contentY"
to: 0
type: Anim.DefaultEffects
onFinished: if (target)
target.contentY = 0
}
RowLayout {
id: header
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Tokens.spacing.large
IconButton {
icon: "arrow_back"
inactiveColor: Colors.tPalette.m3surfaceContainerHigh
inactiveOnColor: Colors.palette.m3onSurfaceVariant
isRound: true
type: IconButton.Tonal
enabled: !ChatState.isWindow
visible: enabled
onClicked: root.requestClose()
}
CustomText {
Layout.fillWidth: ChatState.isWindow ? false : true
Layout.alignment: ChatState.isWindow ? Qt.AlignCenter : Qt.AlignLeft | Qt.AlignVCenter
elide: Text.ElideRight
font: Tokens.font.title.medium
text: qsTr(root.chatData?.title || ChatState.title)
}
}
Loader {
id: listLoader
anchors.bottom: input.top
anchors.bottomMargin: Tokens.spacing.medium
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.topMargin: Tokens.spacing.medium
active: root.chatData && root.chatData.loaded
sourceComponent: Flickable {
id: flick
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: list.layoutHeight
transform: Rotation {
origin.y: flick.height / 2
origin.x: flick.width / 2
angle: 180
}
WheelInverter {
target: flick
}
Anim {
id: restoreAnim
target: flick
type: Anim.DefaultEffects
property: "contentY"
}
ChatListView {
id: list
width: flick.width
height: flick.height
yAnimDuration: Tokens.anim.durations.expressiveDefaultEffects
asynchronous: true
cacheBuffer: 50
estimatedHeight: 100
readyDelay: 5
removeDuration: Tokens.anim.durations.normal
spacing: 0
model: root.chatData.messagesModel
useCustomViewport: true
viewport: {
tWatcher.transform;
return Qt.rect(0, flick.contentY, width, flick.height);
}
onViewportAdjustNeeded: delta => {
restoreAnim.stop();
flick.contentY += delta;
}
onViewportRestoreRequested: target => {
restoreAnim.to = target;
restoreAnim.start();
}
delegate: MessageDelegate {
id: message
rootParent: flick
chatList: list
ChatListView.preferredHeight: implicitHeight
ChatListView.trackViewport: true
ChatListView.visibleHeight: ChatListView.removing ? 0 : implicitHeight
opacity: ChatListView.removing || ChatListView.adding ? 0 : 1
transform: Rotation {
origin.y: message.height / 2
origin.x: message.width / 2
angle: 180
}
Behavior on y {
enabled: message.ChatListView.ready
Anim {
id: yAnim
type: Anim.DefaultEffects
}
}
Behavior on x {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
CustomScrollBar.vertical: CustomScrollBar {
id: scrollBar
flickable: flick
parent: flick.parent
anchors.top: flick.top
anchors.right: flick.right
anchors.bottom: flick.bottom
inverted: true
transform: Rotation {
origin.y: flick.height / 2
origin.x: scrollBar.width / 2
angle: 180
axis {
y: 0
x: 1
z: 0
}
}
}
TransformWatcher {
id: tWatcher
a: flick.contentItem
b: list
}
}
Component.onCompleted: if (root.chatData)
root.chatData.messagesModel
}
EmptyBackground {
id: emptyState
anchors.fill: parent
spacing: Tokens.spacing.small
visible: root.chatData.messageCount < 1
}
Item {
anchors.bottom: input.top
anchors.bottomMargin: Tokens.spacing.extraLarge
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: scrollToBottom.implicitHeight
implicitWidth: scrollToBottom.implicitWidth
scale: root.atNewestEnd || !listLoader.item || listLoader.item.contentHeight <= listLoader.item.height ? 0 : 1
visible: scale > 0
Behavior on scale {
Anim {}
}
Elevation {
anchors.fill: parent
level: 2
radius: scrollToBottom.radius
}
IconButton {
id: scrollToBottom
anchors.centerIn: parent
font: Tokens.font.icon.large
icon: "arrow_downward"
isRound: true
padding: Tokens.padding.extraSmall
type: IconButton.Tonal
onClicked: {
root.scrollToLatest();
}
}
}
GridView {
id: attachments
readonly property int columns: ChatState.isWindow ? 5 : 3
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: input.top
anchors.bottomMargin: Tokens.spacing.medium
flow: GridView.LeftToRight
verticalLayoutDirection: GridView.BottomToTop
cellWidth: width / columns
cellHeight: cellWidth
implicitHeight: Math.ceil(count / columns) * cellHeight
displaced: Transition {
Anim {
property: "x"
}
Anim {
property: "y"
}
}
add: Transition {
id: addTrans
Anim {
property: "y"
from: addTrans.ViewTransition.destination.y + attachments.cellHeight / 4
}
Anim {
property: "opacity"
from: 0
to: 1
}
}
remove: Transition {
id: removeTrans
Anim {
property: "y"
to: removeTrans.ViewTransition.destination.y + attachments.cellHeight / 4
}
Anim {
property: "opacity"
from: 1
to: 0
}
}
model: ScriptModel {
objectProp: "path"
values: root.pendingAttachments
}
delegate: Item {
id: thumb
required property var modelData
function fileIcon(mime): string {
if (mime.startsWith("archive/") || mime === "application/zip")
return "folder_zip";
if (mime === "application/pdf")
return "picture_as_pdf";
if (mime.startsWith("audio/"))
return "audio_file";
if (mime.startsWith("video/"))
return "video_file";
if (mime.startsWith("text/"))
return "description";
return "insert_drive_file";
}
width: attachments.cellWidth
height: width
opacity: modelData ? 1 : 0
CustomClippingRect {
anchors.fill: parent
anchors.margins: Tokens.spacing.extraSmall
radius: Tokens.rounding.medium
color: Colors.tPalette.m3surfaceContainer
FadeImage {
id: attImage
anchors.fill: parent
fillMode: FadeImage.PreserveAspectCrop
asynchronous: true
source: visible ? thumb.modelData?.path ?? "" : ""
visible: thumb.modelData?.isImage ?? false
}
MaterialIcon {
anchors.centerIn: parent
fontStyle: Tokens.font.icon.large
text: thumb.fileIcon(thumb.modelData?.mimeType ?? "")
visible: !thumb.modelData?.isImage ?? false
}
Loader {
id: attIndicatorLoader
anchors.centerIn: parent
opacity: 0
active: opacity > 0 && (thumb.modelData?.isImage ?? false)
sourceComponent: CustomRect {
implicitWidth: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
implicitHeight: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
color: Colors.palette.m3primaryContainer
radius: Tokens.rounding.full
LoadingIndicator {
id: attLoadingIndicator
anchors.centerIn: parent
containsIcon: true
implicitSize: thumb.implicitWidth * 0.4
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Timer {
id: attLoadDebounceTimer
interval: 100
onTriggered: {
if (attImage.status !== Image.Ready)
attIndicatorLoader.opacity = 1;
}
}
MouseArea {
id: hover
anchors.fill: parent
hoverEnabled: true
cursorShape: undefined
IconButton {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Tokens.padding.medium
icon: "close"
visible: opacity > 0
enabled: thumb.opacity > 0
opacity: hover.containsMouse ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: root.pendingAttachments = root.pendingAttachments.filter(a => a !== thumb.modelData)
}
}
}
}
}
ChatInput {
id: input
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Tokens.padding.medium
bg.border.color: Colors.palette.m3outlineVariant
bg.color: Colors.tPalette.m3surfaceContainerLowest
fastGlow: LlmClient.busy
glow.opacity: input.activeFocus && (!LlmClient.busy || ChatState.isWindow)
font: Tokens.font.body.small
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
focus: true
placeholderText: qsTr("Send a message")
sendIcon.font: Tokens.font.icon.medium
sendIcon.icon: LlmClient.busy ? "stop" : "arrow_upward"
sendIcon.padding: Tokens.padding.extraSmall
sendIcon.opacity: text || LlmClient.busy ? 1.0 : 0.5
sendIcon.enabled: text || LlmClient.busy
Component.onCompleted: {
root.focusInput();
}
Keys.onPressed: e => {
if (e.key == Qt.Key_Return) {
if (!(e.modifiers & Qt.ShiftModifier)) {
root.send(text);
e.accepted = true;
}
}
if (e.key == Qt.Key_V && e.modifiers & Qt.ControlModifier) {
if (root.chatData.clipboardHasAttachment()) {
const att = root.chatData.pasteAttachments();
root.pendingAttachments = [...root.pendingAttachments, ...att];
e.accepted = true;
}
}
}
onButtonPressed: {
if (LlmClient.busy)
LlmClient.stop();
else
root.send(text);
}
}
}
@@ -0,0 +1,389 @@
import QtQuick
import ZShell.Components
import ZShell.Config
import ZShell.Llm
import qs.Effects
import qs.Components
import qs.Services
CustomClippingRect {
id: root
property bool expanded: false
property bool highlighted: false
property bool slim: false
required property ChatSession modelData
signal clicked(content: ChatSession)
signal remove(content: ChatSession)
color: highlighted ? Qt.alpha(Colors.palette.m3primaryContainer, 0.3) : Qt.alpha(Colors.tPalette.m3surfaceContainer, 0.2)
implicitHeight: {
let h = 0;
if (slim)
return chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
h += infoContainer.implicitHeight;
if (expanded)
h += body.implicitHeight + body.topMargin + actionRow.implicitHeight + actionRow.anchors.topMargin;
else
h += preview.implicitHeight + preview.topMargin;
h += Tokens.padding.small;
const icon = chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
if (h < icon)
return icon;
return h;
}
radius: Tokens.rounding.large
clip: true
CustomRect {
id: borderHighlight
anchors.fill: parent
border.width: root.highlighted ? 2 : 0
border.color: Colors.palette.m3secondary
radius: root.radius - border.width + 1
Behavior on border.width {
Anim {}
}
}
Behavior on implicitHeight {
enabled: titleText.readOnly
Anim {}
}
MaterialIcon {
id: chatIcon
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: Tokens.padding.large
fontStyle: Tokens.font.icon.extraLarge
text: root.modelData.icon || "check"
color: Colors.palette.m3onSurface
}
Item {
id: infoContainer
anchors.left: chatIcon.right
anchors.leftMargin: Tokens.spacing.medium
anchors.top: parent.top
anchors.right: expandBtn.left
anchors.rightMargin: Tokens.spacing.medium
implicitHeight: {
let h = 0;
h += title.implicitHeight + title.anchors.topMargin;
h += timestamp.implicitHeight + timestamp.anchors.topMargin;
if (root.expanded)
h += created.implicitHeight + created.anchors.topMargin;
return h;
}
opacity: root.slim ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
CustomRect {
id: title
anchors.left: parent.left
anchors.topMargin: Tokens.padding.extraSmall
anchors.top: parent.top
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
radius: Tokens.rounding.full
implicitWidth: titleText.implicitWidth + Tokens.padding.large * 2
opacity: titleText.readOnly ? 0 : 1
Behavior on opacity {
Anim {}
}
}
Item {
id: titleWrapper
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
layer.enabled: true
layer.effect: OpacityMask {
maskSource: fadeMask
}
TextFieldBase {
id: titleText
anchors.left: parent.left
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
font: Tokens.font.body.medium
text: root.modelData.title || qsTr("New chat")
readOnly: true
color: Colors.palette.m3onSurface
Behavior on anchors.leftMargin {
Anim {}
}
Behavior on anchors.topMargin {
Anim {}
}
onEditingFinished: root.modelData.title = text
onReadOnlyChanged: {
if (!readOnly) {
this.forceActiveFocus();
cursorPosition = text.length;
} else {
root.forceActiveFocus();
}
}
}
}
CustomRect {
id: fadeMask
anchors.fill: titleWrapper
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
}
GradientStop {
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
}
}
}
CustomText {
id: created
anchors.top: title.bottom
anchors.topMargin: Tokens.spacing.extraSmall
anchors.left: parent.left
anchors.right: parent.right
font: Tokens.font.label.small
color: Colors.palette.m3onSurfaceVariant
text: qsTr("Created on: %1").arg(root.modelData.createdAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP"))
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
}
CustomText {
id: updated
anchors.top: created.bottom
anchors.topMargin: Tokens.spacing.small
anchors.left: parent.left
font: Tokens.font.label.small
color: Colors.palette.m3onSurfaceVariant
text: qsTr("Updated on: ")
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
}
CustomText {
id: timestamp
anchors.left: parent.left
anchors.top: title.bottom
anchors.topMargin: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.label.small
text: root.modelData.updatedAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP")
states: State {
name: "expanded"
when: root.expanded
AnchorChanges {
target: timestamp
anchors.left: updated.right
anchors.top: undefined
anchors.verticalCenter: updated.verticalCenter
}
}
transitions: Transition {
AnchorAnim {}
}
}
}
CustomText {
id: preview
readonly property int topMargin: Tokens.spacing.extraSmall
anchors.left: chatIcon.right
anchors.margins: Tokens.spacing.medium
anchors.right: parent.right
y: infoContainer.implicitHeight + topMargin
elide: Text.ElideRight
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.small
maximumLineCount: 1
opacity: !root.expanded && !root.slim ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
Behavior on y {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
CustomText {
id: body
readonly property int topMargin: root.expanded ? Tokens.spacing.medium : Tokens.spacing.extraSmall
anchors.left: chatIcon.right
anchors.margins: Tokens.spacing.medium
anchors.right: parent.right
y: infoContainer.implicitHeight + topMargin
animate: true
color: Colors.palette.m3onSurfaceVariant
maximumLineCount: 5
opacity: root.expanded ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.content ?? qsTr("No messages yet")
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on y {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
StateLayer {
id: sLayer
property int startY
onClicked: {
root.clicked(root.modelData);
}
}
ButtonRow {
id: actionRow
anchors.top: body.bottom
anchors.left: chatIcon.right
anchors.right: parent.right
anchors.topMargin: Tokens.spacing.small
anchors.leftMargin: Tokens.spacing.medium
anchors.rightMargin: Tokens.padding.medium
spacing: Tokens.spacing.medium
IconButton {
icon: "close"
fillWidth: true
shapeMorph: true
isRound: true
font: Tokens.font.icon.medium
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
onClicked: root.remove(root.modelData)
}
IconButton {
icon: titleText.readOnly ? "edit" : "check"
label.animate: true
isToggle: true
fillWidth: true
shapeMorph: true
font: Tokens.font.icon.medium
isRound: true
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
activeColor: Colors.palette.m3secondary
activeOnColor: Colors.palette.m3onSecondary
checked: !titleText.readOnly
onClicked: titleText.readOnly = !titleText.readOnly
}
}
CustomRect {
id: expandBtn
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Tokens.padding.small
implicitHeight: expandIcon.implicitHeight
implicitWidth: expandIcon.implicitHeight
radius: Tokens.rounding.full
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
opacity: root.slim ? 0 : 1
visible: opacity > 0
enabled: visible
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
StateLayer {
onClicked: root.expanded = !root.expanded
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
anchors.verticalCenterOffset: root.expanded ? -1 : 1
rotation: root.expanded ? 180 : 0
color: Colors.palette.m3onSurface
text: "expand_more"
Behavior on anchors.verticalCenterOffset {
Anim {}
}
Behavior on rotation {
Anim {}
}
}
}
}
@@ -0,0 +1,142 @@
import QtQuick
import ZShell.Config
import qs.Components
import qs.Services
TextAreaBase {
id: root
readonly property alias bg: bg
readonly property alias sendIcon: sendIcon
readonly property alias glow: glow
property bool fastGlow: false
signal buttonPressed
bottomPadding: Tokens.padding.large
leftPadding: Tokens.padding.extraLarge
rightPadding: sendIcon.width + sendIcon.anchors.rightMargin + Tokens.spacing.small
topPadding: Tokens.padding.large
wrapMode: TextAreaBase.Wrap
onFastGlowChanged: {
if (fastGlow) {
resetAnim.stop();
boostAnim.restart();
} else {
boostAnim.stop();
resetAnim.restart();
}
}
background: CustomRect {
id: bg
anchors.fill: parent
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraLarge
GeminiGlow {
id: glow
glowSize: 32
z: 5
opacity: root.activeFocus ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {}
}
SequentialAnimation {
id: boostAnim
NumberAnimation {
target: glow
property: "speed"
to: 4.0
duration: 250
easing.type: Easing.OutCubic
}
NumberAnimation {
target: glow
property: "speed"
to: 2.25
duration: 1500
easing.type: Easing.InOutQuad
}
}
// Ease back down to normal once fastGlow is false
NumberAnimation {
id: resetAnim
target: glow
property: "speed"
to: 1.0
duration: 800
easing.type: Easing.InOutQuad
}
}
StateLayer {
id: stateLayer
cursorShape: Qt.IBeamCursor
enabled: !root.activeFocus
manualPressOverride: tapHandler.pressed
onClicked: root.forceActiveFocus()
}
}
onPressed: {
if (stateLayer.enabled)
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
}
CustomText {
id: placeholder
anchors.left: parent.left
anchors.leftMargin: root.leftPadding
anchors.verticalCenter: parent.verticalCenter
color: root.placeholderTextColor
font: root.font
opacity: root.text ? 0 : 1
text: root.placeholderText
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
IconButton {
id: sendIcon
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.small
anchors.verticalCenter: parent.verticalCenter
enabled: root.text
icon: "upward_arrow"
opacity: root.text ? 1 : 0
radius: Tokens.rounding.full
radiusMorph: false
stateLayer.hoverEnabled: enabled
type: IconButton.Text
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: root.buttonPressed()
}
TapHandler {
id: tapHandler
}
}
@@ -0,0 +1,306 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import ZShell.Llm
import ZShell.Config
import qs.Modules.Notifications.Sidebar.Chat.Content
import qs.Services
import qs.Components
Item {
id: root
property alias list: list
property alias model: list.model
property bool highlight: false
property bool slim: false
signal requestExpand
signal deleteChatRequest(content: ChatSession)
signal loadChatRequest(content: ChatSession, index: int)
signal newChatRequest
onSlimChanged: {
if (slim)
modelsContainer.open = false;
}
CustomText {
id: chatHeader
anchors.left: parent.left
anchors.margins: Tokens.padding.extraSmall
anchors.right: parent.right
anchors.top: parent.top
color: Colors.palette.m3outline
font.family: "CaskaydiaCove NF"
font.pointSize: 13
font.weight: 500
text: qsTr("%1 Chat%2").arg(list.count).arg(list.count > 1 ? "s" : "")
}
CustomListView {
id: list
anchors.bottom: modelsContainer.top
anchors.left: parent.left
anchors.right: parent.right
anchors.top: chatHeader.bottom
anchors.topMargin: Tokens.spacing.medium
clip: true
spacing: Tokens.spacing.medium
add: Transition {
Anim {
from: list.width
property: "x"
}
}
remove: Transition {
Anim {
to: list.width
property: "x"
}
Anim {
to: 0
property: "opacity"
}
}
displaced: Transition {
Anim {
property: "y"
}
}
move: Transition {
Anim {
property: "y"
}
}
delegate: ChatDelegate {
id: chat
required property int index
implicitWidth: ListView.view.width
highlighted: root.highlight && ChatState.chatSession === modelData
slim: root.slim
onHighlightedChanged: if (highlighted)
list.currentIndex = index
onClicked: content => root.loadChatRequest(content, index)
onRemove: content => root.deleteChatRequest(content)
}
TapHandler {
onTapped: {
root.focus = true;
ChatState.fabExpanded = false;
modelsContainer.expanded = false;
}
}
}
ModelsContainer {
id: modelsContainer
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: fabRoot.left
anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.spacing.small
rootParent: root
fabRoot: fabRoot
opacity: root.slim ? 0 : 1
visible: opacity > 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
IconButton {
id: fabRoot
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: Tokens.padding.small
padding: 8
font.pointSize: Math.round(18 * 1.2)
icon: root.slim ? "left_panel_open" : "add"
isRound: ChatState.fabExpanded
label.transform: Rotation {
origin.y: fabRoot.label.height / 2
origin.x: fabRoot.label.width / 2
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded && !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
Behavior on angle {
Anim {}
}
}
onClicked: {
if (root.slim && !ChatState.narrowSidebarExpanded) {
root.requestExpand();
return;
}
ChatState.fabExpanded = !ChatState.fabExpanded;
}
Elevation {
anchors.fill: parent
level: fabRoot.stateLayer.containsMouse ? 4 : 3
radius: parent.radius
z: -1
}
}
ColumnLayout {
id: fabMenu
anchors.bottom: fabRoot.top
anchors.right: fabRoot.right
anchors.bottomMargin: Tokens.padding.medium
Repeater {
id: fabRep
model: ListModel {
ListElement {
name: "new chat"
icon: "add"
}
ListElement {
name: "pop out in window"
icon: "open_in_new"
}
}
CustomClippingRect {
id: fabMenuItem
required property var modelData
required property int index
Layout.alignment: Qt.AlignRight
implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.medium * 2
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
color: Colors.palette.m3primaryContainer
visible: !(modelData.name === "pop out in window" && ChatState.isWindow)
Behavior on radius {
Anim {
type: Anim.DefaultEffects
}
}
opacity: 0
states: State {
name: "visible"
when: ChatState.fabExpanded
PropertyChanges {
fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Tokens.padding.large * 2
fabMenuItem.opacity: 1
fabMenuItemInner.opacity: 1
}
}
transitions: [
Transition {
to: "visible"
SequentialAnimation {
PauseAnimation {
duration: (fabRep.count - 1 - fabMenuItem.index) * 50
}
ParallelAnimation {
Anim {
property: "implicitWidth"
type: Anim.FastSpatial
}
Anim {
property: "opacity"
duration: Tokens.anim.durations.small
easing: Tokens.anim.standard
}
}
}
},
Transition {
from: "visible"
SequentialAnimation {
PauseAnimation {
duration: fabMenuItem.index * Tokens.anim.durations.small / 8
}
ParallelAnimation {
Anim {
property: "implicitWidth"
type: Anim.FastSpatial
}
Anim {
property: "opacity"
duration: Tokens.anim.durations.small
easing: Tokens.anim.standard
}
}
}
}
]
RowLayout {
id: fabMenuItemInner
anchors.centerIn: parent
spacing: Tokens.spacing.medium
opacity: 0
MaterialIcon {
text: fabMenuItem.modelData.icon
color: Colors.palette.m3onPrimaryContainer
fontStyle: Tokens.font.icon.large
fill: 1
}
CustomText {
animate: true
text: fabMenuItem.modelData.name
font.capitalization: Font.Capitalize
color: Colors.palette.m3onPrimaryContainer
Layout.preferredWidth: implicitWidth
Behavior on Layout.preferredWidth {
Anim {}
}
}
}
StateLayer {
id: sLayer
onClicked: {
const name = fabMenuItem.modelData.name;
if (name === "new chat")
root.newChatRequest();
else if (name === "pop out in window")
Detach.create();
ChatState.fabExpanded = false;
}
}
}
}
}
}
@@ -0,0 +1,83 @@
pragma ComponentBehavior: Bound
import ZShell.Config
import ZShell.Llm
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Helpers
import qs.Modules.Notifications.Sidebar
import qs.Components
import qs.Services
Item {
id: root
anchors.fill: parent
anchors.margins: Tokens.padding.small
Component.onCompleted: {
if (ChatState.firstOpen) {
ChatState.openDraft();
stack.push(chatList);
stack.push(chatContent);
ChatState.firstOpen = false;
return;
}
if (ChatState.inChat) {
stack.push(chatList);
stack.push(chatContent);
} else
stack.push(chatList);
}
function openChat(): void {
if (stack.depth < 2)
stack.push(chatContent);
}
StackView {
id: stack
anchors.fill: parent
}
Component {
id: chatList
ChatList {
model: LlmClient.chats
onDeleteChatRequest: chat => {
const wasCurrent = chat === ChatState.chatSession;
LlmClient.chats.remove(chat);
if (wasCurrent)
ChatState.openDraft();
}
onLoadChatRequest: (chat, index) => {
ChatState.chatSession = chat;
openChat();
ChatState.inChat = true;
}
onNewChatRequest: {
ChatState.openDraft();
openChat();
ChatState.inChat = true;
}
}
}
Component {
id: chatContent
ChatContent {
chatData: ChatState.chatSession
onRequestClose: {
stack.pop();
ChatState.inChat = false;
ChatState.openDraft();
}
}
}
}
@@ -0,0 +1,33 @@
pragma Singleton
import QtQuick
import Quickshell
import ZShell.Llm
import qs.Services
Singleton {
id: root
property ChatSession chatSession: LlmClient.chats.draft()
readonly property string title: chatSession?.title || "Chat"
property int currentIdx: -1
property bool firstOpen: true
property bool inChat: false
property bool fabExpanded: false
property bool isWindow: false
property bool isWide: false
property bool settledSlim: false
property bool settledWide: false
property bool narrowSidebarExpanded: false
property bool windowIsWide: false
property ShellScreen screen
property bool sidebarVisible: ShellState.forActive().sidebar
function openDraft(): void {
root.chatSession = LlmClient.chats.draft();
root.currentIdx = -1;
}
onSidebarVisibleChanged: if (!sidebarVisible)
fabExpanded = false
}
@@ -0,0 +1,118 @@
import QtQuick
import Quickshell
import ZShell.Config
import ZShell.Components
import ZShell.Llm
import qs.Components
import qs.Services
Item {
id: root
required property TextEdit edit
required property bool hovered
required property bool isUser
required property ChatMessage message
required property bool running
implicitHeight: editButton.implicitHeight
implicitWidth: generationTools.implicitWidth + editTools.implicitWidth + Tokens.spacing.small
ButtonRow {
id: generationTools
enabled: visible
visible: !root.isUser
IconButton {
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== 0
icon: "chevron_left"
type: IconButton.Tonal
isRound: true
onClicked: {
if (!root.edit.readOnly)
root.edit.readOnly = true;
root.message.setActiveGeneration(root.message.activeGenerationIndex - 1);
}
}
IconButton {
enabled: false
icon: `${root.message.activeGenerationIndex + 1}`
label.font: Tokens.font.body.small
type: IconButton.Text
}
IconButton {
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== root.message.generationCount - 1
icon: "chevron_right"
type: IconButton.Tonal
isRound: true
onClicked: {
if (!root.edit.readOnly)
root.edit.readOnly = true;
root.message.setActiveGeneration(root.message.activeGenerationIndex + 1);
}
}
}
ButtonRow {
id: editTools
anchors.right: parent.right
opacity: !root.running && root.hovered ? 1 : 0
spacing: Tokens.spacing.small
visible: opacity > 0
Behavior on opacity {
Anim {}
}
IconButton {
icon: "refresh"
inactiveColor: Colors.palette.m3secondary
inactiveOnColor: Colors.palette.m3onSecondary
scale: root.edit.readOnly ? 1 : 0
visible: !root.isUser
isRound: true
Behavior on scale {
Anim {}
}
onClicked: {
root.message.retry();
}
}
IconButton {
icon: "content_copy"
inactiveColor: Colors.palette.m3secondary
inactiveOnColor: Colors.palette.m3onSecondary
scale: root.edit.readOnly ? 1 : 0
isRound: true
Behavior on scale {
Anim {}
}
onClicked: Quickshell.clipboardText = root.message.content
}
IconButton {
id: editButton
icon: root.edit.readOnly ? "edit" : "check"
inactiveColor: Colors.palette.m3tertiary
inactiveOnColor: Colors.palette.m3onTertiary
visible: root.isUser
isRound: true
onClicked: {
root.edit.readOnly = !root.edit.readOnly;
}
}
}
}
@@ -0,0 +1,400 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import ZShell.Llm
import ZShell.Blobs
import qs.Components
import qs.Services
Item {
id: root
required property Item rootParent
required property ChatGeneration current
implicitHeight: approvalLayout.implicitHeight + approvalLayout.anchors.margins * 2
CustomRect {
id: approvalBg
anchors.fill: parent
radius: Tokens.rounding.medium
color: Colors.palette.m3surface
border.width: 1
border.color: Colors.palette.m3outlineVariant
ColumnLayout {
id: approvalLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
spacing: Tokens.spacing.small
CustomText {
id: requestHeader
text: qsTr("Tool call request")
font: Tokens.font.headline.medium
color: Colors.palette.m3onSurface
}
Item {
id: toolsBg
property bool open: false
property int maxHeight: root.rootParent.height - Tokens.padding.medium * 2
property int openHeight: Math.min(toolsContent.item?.implicitHeight, maxHeight)
property int openWidth: root.rootParent.width - Tokens.padding.medium * 2
property real openHeightProg: openHeight / maxHeight
property int collapsedY: 0
implicitHeight: toolList.implicitHeight + toolList.anchors.margins * 2
z: open || dialogTransition.running ? 2 : 0
Layout.fillWidth: true
function reparentWrapper(): void {
const newParent = open ? root.rootParent : toolsBg;
const c = toolsWrapper.parent.mapToItem(newParent, toolsWrapper.x + toolsWrapper.width / 2, toolsWrapper.y + toolsWrapper.height / 2);
toolsBg.collapsedY = toolsWrapper.y;
toolsWrapper.parent = newParent;
toolsWrapper.x = c.x - toolsWrapper.width / 2;
toolsWrapper.y = c.y - toolsWrapper.height / 2;
}
onOpenChanged: {
if (open)
root.rootParent.interactive = false;
else
root.rootParent.interactive = true;
}
BlobGroup {
id: blobGroup
color: toolsBg.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3primaryContainer
Behavior on color {
CAnim {}
}
}
MouseArea {
id: backdrop
anchors.fill: parent
enabled: false
hoverEnabled: enabled
preventStealing: true
parent: toolsBg.open ? root.rootParent : toolsBg
onClicked: toolsBg.open = false
}
Item {
id: toolsWrapper
width: toolsBg.width
height: toolBox.implicitHeight
rotation: parent === root.rootParent ? 180 : 0
z: 1
states: State {
name: "open"
when: toolsBg.open
PropertyChanges {
backdrop.enabled: true
toolsBackground.bottomLeftRadius: Tokens.rounding.largeIncreased
toolsBackground.bottomRightRadius: Tokens.rounding.largeIncreased
toolsBackground.topRightRadius: Tokens.rounding.largeIncreased
toolsBackground.topLeftRadius: Tokens.rounding.largeIncreased
toolsContent.opacity: 1
toolsWrapper.height: toolsBg.openHeight
toolsWrapper.width: toolsBg.openWidth
toolsWrapper.x: (root.rootParent.width - toolsBg.openWidth) / 2
toolsWrapper.y: (confirmationRow.implicitHeight + Tokens.spacing.small + Tokens.padding.medium) * (1 - toolsBg.openHeightProg) + Tokens.padding.medium
elevation.opacity: 1
toolBox.opacity: 0
}
}
transitions: Transition {
id: dialogTransition
SequentialAnimation {
ScriptAction {
script: toolsBg.reparentWrapper()
}
Anim {
properties: "x,y"
}
}
PropertyAction {
property: "enabled"
}
Anim {
properties: "opacity,topLeftRadius,topRightRadius,bottomLeftRadius,bottomRightRadius"
type: Anim.DefaultEffects
}
Anim {
properties: "width,height"
}
}
Elevation {
id: elevation
anchors.fill: parent
bottomLeftRadius: toolsBackground.bottomLeftRadius
bottomRightRadius: toolsBackground.bottomRightRadius
level: 4
opacity: 0
radius: toolsBackground.radius
transform: Matrix4x4 {
matrix: toolsBackground.deformMatrix
}
}
BlobRect {
id: toolsBackground
anchors.fill: parent
bottomLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
bottomRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
topRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
topLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
deformScale: 0
group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
}
CustomRect {
id: toolBox
anchors.left: parent.left
anchors.right: parent.right
height: Math.min(implicitHeight, parent.height)
implicitHeight: toolList.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.small
StateLayer {
onClicked: {
toolsBg.open = true;
}
}
ColumnLayout {
id: toolList
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Tokens.padding.medium
Repeater {
id: repeater
model: root.current.pendingToolCalls?.slice(0, 4) ?? []
delegate: RowLayout {
id: toolText
required property var modelData
required property int index
spacing: Tokens.spacing.medium
function toolTarget(seg: var): string {
var a = null;
try {
a = JSON.parse(seg.arguments);
} catch (e) {}
if (a && typeof a === "object") {
const v = a.url ?? a.path;
if (v !== undefined && v !== null)
return String(v);
}
return String(seg.arguments);
}
MaterialIcon {
color: Colors.palette.m3onPrimaryContainer
text: toolText.modelData.name === "webfetch" ? "web" : "text_snippet"
fill: 1
}
CustomText {
color: Colors.palette.m3onPrimaryContainer
text: qsTr("%1").arg(toolText.modelData.name)
}
MaterialIcon {
color: Colors.palette.m3onPrimaryContainer
text: "arrow_right_alt"
}
CustomText {
color: Colors.palette.m3onPrimaryContainer
text: qsTr("%1").arg(toolText.toolTarget(toolText.modelData))
}
}
}
CustomText {
color: Colors.palette.m3onPrimaryContainer
text: qsTr("And %1 more").arg(root.current.pendingToolCalls.slice(4).length)
visible: repeater.count < root.current.pendingToolCalls.length
}
}
}
Loader {
id: toolsContent
active: opacity > 0
anchors.fill: parent
asynchronous: false
opacity: 0
sourceComponent: MouseArea {
onWheel: event => event.accepted = true
preventStealing: true
implicitHeight: innerLayout.contentHeight + innerLayout.anchors.margins * 2
VerticalFadeListView {
id: innerLayout
anchors.fill: parent
anchors.margins: Tokens.padding.small
cacheBuffer: height * 10
fadeAmount: 0.1
fadeEnabled: interactive
spacing: 0
clip: true
interactive: contentHeight > toolsBg.maxHeight
model: root.current.pendingToolCalls ?? []
delegate: Item {
id: toolCall
required property var modelData
property bool expanded: false
implicitWidth: CustomListView.view.width
implicitHeight: collapsedRow.implicitHeight + collapsedRow.anchors.margins + (expanded ? expandedTool.implicitHeight + expandedTool.anchors.margins + expandedTool.anchors.topMargin : collapsedRow.anchors.margins)
function toolTarget(seg: var): string {
var a = null;
try {
a = JSON.parse(seg.arguments);
} catch (e) {}
if (a && typeof a === "object") {
const v = a.url ?? a.path;
if (v !== undefined && v !== null)
return String(v);
}
return String(seg.arguments);
}
Behavior on implicitHeight {
Anim {}
}
RowLayout {
id: collapsedRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Tokens.padding.medium
IconButton {
id: toolExpandBtn
font: Tokens.font.icon.medium
icon: "keyboard_arrow_down"
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
rotation: toolCall.expanded ? 180 : 0
type: IconButton.Text
Behavior on rotation {
Anim {}
}
onClicked: toolCall.expanded = !toolCall.expanded
}
CustomText {
id: preview
Layout.fillWidth: true
text: toolCall.modelData.name
}
}
CustomClippingRect {
id: expandedTool
anchors.top: collapsedRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium
anchors.topMargin: Tokens.spacing.medium
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
CustomText {
id: fullToolCall
anchors.fill: parent
anchors.margins: Tokens.padding.medium
text: toolCall.toolTarget(toolCall.modelData)
wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
}
}
}
}
}
}
}
}
ButtonRow {
id: confirmationRow
Layout.alignment: Qt.AlignRight
spacing: Tokens.spacing.medium
IconTextButton {
icon: "check_circle"
text: qsTr("Allow")
horizontalPadding: Tokens.padding.large
verticalPadding: Tokens.padding.small
isRound: true
shapeMorph: true
onClicked: root.current.approveTools()
}
IconTextButton {
icon: "cancel"
text: qsTr("Deny")
isRound: true
shapeMorph: true
inactiveColor: Colors.palette.m3error
inactiveOnColor: Colors.palette.m3onError
horizontalPadding: Tokens.padding.large
verticalPadding: Tokens.padding.small
onClicked: root.current.denyTools()
}
}
}
}
}
@@ -0,0 +1,27 @@
import Quickshell
import QtQuick
import ZShell.Config
import qs.Components
import qs.Services
TextEditBase {
id: root
color: Colors.palette.m3onSurface
readOnly: true
textFormat: Text.MarkdownText
font: Tokens.font.body.small
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
onLinkActivated: link => {
Qt.openUrlExternally(link);
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: root.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor
preventStealing: false
hoverEnabled: true
}
}
@@ -0,0 +1,147 @@
import QtQuick
import ZShell.Llm
import ZShell.Config
import qs.Components
import qs.Services
import qs.Modules.Notifications.Sidebar.Chat
Item {
id: root
property int animOff
property Item currentItem
property bool noAnim: false
property bool loading: false
property int lastIdx: -1
readonly property Component conversationComp: ChatContent {}
signal requestClose
function loadConversation(chat): void {
if (currentItem) {
currentItem.destroy();
currentItem = null;
}
if (!chat)
return;
root.loading = true;
const incubator = root.conversationComp.incubateObject(container, {
chatData: chat
});
const attach = () => {
incubator.object.anchors.top = container.top;
incubator.object.anchors.bottom = container.bottom;
incubator.object.anchors.margins = Tokens.padding.medium;
incubator.object.anchors.horizontalCenter = container.horizontalCenter;
incubator.object.requestClose.connect(root.requestClose);
incubator.object.implicitWidth = Qt.binding(() => Math.max(Math.min(container.width - Tokens.padding.medium * 2, 800), Config.sidebar.sizes.width));
currentItem = incubator.object;
root.loading = false;
if (!noAnim)
enterAnim.start();
};
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
objectName: "ConversationContainer"
Component.onCompleted: {
if (ChatState.chatSession)
root.loadConversation(ChatState.chatSession);
}
}
LoadingIndicator {
anchors.centerIn: parent
implicitSize: Tokens.font.icon.extraLarge.pointSize * 4
opacity: root.loading ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {}
}
}
Connections {
function onChatSessionChanged(): void {
exitAnim.complete();
enterAnim.complete();
if (root.noAnim) {
root.loadConversation(ChatState.chatSession);
return;
}
root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
root.lastIdx = ChatState.currentIdx;
exitAnim.start();
}
target: ChatState
}
SequentialAnimation {
id: exitAnim
Anim {
property: "opacity"
target: container
to: 0
type: Anim.DefaultEffects
}
ScriptAction {
script: root.loadConversation(ChatState.chatSession)
}
}
SequentialAnimation {
id: enterAnim
PropertyAction {
property: "topMargin"
target: container.anchors
value: root.animOff
}
PropertyAction {
property: "bottomMargin"
target: container.anchors
value: -root.animOff
}
ParallelAnimation {
Anim {
from: 0
property: "opacity"
target: container
to: 1
type: Anim.SlowEffects
}
Anim {
properties: "topMargin,bottomMargin"
target: container.anchors
to: 0
type: Anim.SlowEffects
}
}
}
}
@@ -0,0 +1,296 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Services
CustomRect {
id: root
required property string language
required property string code
property bool copied: false
property color codeBackgroundColor: Colors.palette.m3surfaceContainerHigh
property color codeHeaderColor: Colors.palette.m3outline
property string html: ""
property bool colored: false
property string requestedCode: ""
property string requestedLanguage: ""
property int highlightToken: 0
function refresh() {
requestedCode = root.code;
requestedLanguage = root.language;
const token = ++root.highlightToken;
CodeHighlighter.highlight(root.code, root.language, root, token);
}
function onHighlightSpans(token, spans) {
if (token !== root.highlightToken)
return;
if (root.code !== root.requestedCode || root.language !== root.requestedLanguage)
return;
html = spans.length ? root.highlightedHtml(root.code, spans) : "";
colored = root.visible && html.length > 0;
}
function roleColor(kind) {
var s = CodeColors.active;
switch (kind) {
case "comment":
return s.comment;
case "string":
return s.string;
case "string.key":
return s.stringKey;
case "number":
case "constant":
return s.number;
case "keyword":
return s.keyword;
case "type":
return s.type;
case "function":
return s.functions;
case "method":
return s.method ?? s.functions;
case "macro":
return s.macro;
case "preproc":
return s.preproc ?? s.macro;
case "operator":
return s.operator ?? s.normal;
case "property":
return s.property ?? s.normal;
case "label":
return s.label;
case "attribute":
return s.attribute ?? s.label;
default:
return s.normal;
}
}
function escapeHtml(text): string {
return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// Mirrors the whitespace rules of the rich text rendering below (leading
// whitespace kept, mid-line runs collapsed, tabs as single spaces) so the
// plain and colored layouts are identical.
function plainForDisplay(code): string {
return code.split("\n").map(line => {
const i = line.search(/\S/);
if (i < 0)
return "";
const lead = line.slice(0, i).replace(/\t/g, " ");
return lead + line.slice(i).replace(/\s+/g, " ").replace(/\s+$/, "");
}).join("\n");
}
function highlightedHtml(code, spans): string {
let out;
if (!spans.length) {
out = escapeHtml(code);
} else {
out = "";
let pos = 0;
for (let i = 0; i < spans.length; i++) {
const span = spans[i];
const start = Math.min(span.start, code.length);
const end = Math.min(span.start + span.length, code.length);
if (end <= pos)
continue;
if (start > pos)
out += escapeHtml(code.slice(pos, start));
out += `<font color="${roleColor(span.kind)}">` + escapeHtml(code.slice(start, end)) + "</font>";
pos = end;
}
if (pos < code.length)
out += escapeHtml(code.slice(pos));
}
return out.replace(/(^|\n)[ \t]+/g, ws => ws.replace(/[ \t]/g, "&nbsp;")).replace(/\n/g, "<br>");
}
implicitWidth: headerRow.implicitWidth + headerRow.anchors.leftMargin + headerRow.anchors.rightMargin
implicitHeight: headerRow.anchors.topMargin + headerRow.implicitHeight + codeRect.implicitHeight + codeRect.anchors.margins + codeRect.anchors.topMargin
color: root.codeBackgroundColor
radius: Tokens.rounding.medium
onLanguageChanged: {
html = "";
colored = false;
if (root.visible)
refreshTimer.restart();
}
onCodeChanged: {
colored = false;
if (root.visible)
refreshTimer.restart();
}
onVisibleChanged: if (root.visible)
refreshTimer.restart()
Timer {
id: refreshTimer
interval: 200
repeat: false
onTriggered: refresh()
}
RowLayout {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Tokens.padding.small
anchors.leftMargin: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small
spacing: Tokens.spacing.small
Item {
id: iconItem
readonly property string iconPath: CodeIcons.path(root.language)
Layout.fillHeight: true
implicitWidth: height
Shape {
id: shape
anchors.centerIn: parent
visible: iconItem.iconPath !== ""
preferredRendererType: Shape.CurveRenderer
scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width)
ShapePath {
strokeColor: "transparent"
fillColor: Colors.palette.m3tertiary
fillRule: ShapePath.WindingFill
PathSvg {
path: iconItem.iconPath
}
}
}
Shape {
id: fallbackShape
anchors.centerIn: parent
anchors.verticalCenterOffset: -1
visible: iconItem.iconPath === ""
preferredRendererType: Shape.CurveRenderer
scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width)
ShapePath {
strokeColor: Colors.palette.m3tertiary
strokeWidth: 16
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathSvg {
path: "M 40 64 L 112 128 L 40 192"
}
}
ShapePath {
strokeColor: Colors.palette.m3tertiary
strokeWidth: 16
capStyle: ShapePath.RoundCap
fillColor: "transparent"
PathSvg {
path: "M 120 192 L 216 192"
}
}
}
}
CustomText {
id: langText
Layout.alignment: Qt.AlignVCenter
color: root.codeHeaderColor
font: Tokens.font.label.small
text: CodeIcons.name(root.language)
visible: root.language.length > 0
}
Item {
Layout.fillWidth: true
}
IconButton {
icon: root.copied ? "check" : "content_copy"
inactiveColor: "transparent"
inactiveOnColor: root.codeHeaderColor
label.animate: true
type: IconButton.Text
onClicked: {
Quickshell.clipboardText = root.code;
root.copied = true;
copyResetTimer.restart();
}
Timer {
id: copyResetTimer
interval: 1500
onTriggered: root.copied = false
}
}
}
CustomClippingRect {
id: codeRect
anchors.left: parent.left
anchors.top: headerRow.bottom
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Tokens.padding.small
radius: root.radius - anchors.margins
anchors.margins: Tokens.padding.extraSmall
implicitWidth: codeText.implicitWidth + codeFlick.anchors.margins * 2
implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2
color: CodeColors.active.bg
Flickable {
id: codeFlick
anchors.fill: parent
anchors.margins: Tokens.padding.small
CustomScrollBar.horizontal: CustomScrollBar {
flickable: codeFlick
parent: codeFlick.parent
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
}
TextAreaBase.flickable: TextAreaBase {
id: codeText
color: CodeColors.active.normal
layer.enabled: true
clip: false
font: Tokens.font.mono.small
textFormat: root.colored ? Text.RichText : Text.PlainText
text: root.colored ? root.html : root.plainForDisplay(root.code)
readOnly: true
}
}
}
}
@@ -0,0 +1,231 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
import ZShell.Config
Singleton {
id: root
readonly property JsonObject schemes: JsonObject {
readonly property Scheme oneDark: Scheme {
comment: "#5c6370"
string: "#98c379"
stringKey: "#e06c75"
number: "#d19a66"
keyword: "#c678dd"
type: "#e5c07b"
functions: "#61afef"
method: "#61afef"
macro: "#98c379"
preproc: "#abb2bf"
operator: "#abb2bf"
property: "#abb2bf"
label: "#e06c75"
attribute: "#d19a66"
bg: "#282c34"
normal: "#abb2bf"
}
readonly property Scheme nord: Scheme {
comment: "#4c566a"
string: "#a3be8c"
stringKey: "#ebcb8b"
number: "#b48ead"
keyword: "#81a1c1"
type: "#8fbcbb"
functions: "#88c0d0"
method: "#88c0d0"
macro: "#5e81ac"
preproc: "#5e81ac"
operator: "#81a1c1"
property: "#d8dee9"
label: "#d08770"
attribute: "#d8dee9"
bg: "#2e3440"
normal: "#d8dee9"
}
readonly property Scheme dracula: Scheme {
comment: "#6272a4"
string: "#f1fa8c"
stringKey: "#f8f8f2"
number: "#ffb86c"
keyword: "#ff79c6"
type: "#8be9fd"
functions: "#50fa7b"
method: "#50fa7b"
macro: "#ff79c6"
preproc: "#ff79c6"
operator: "#f8f8f2"
property: "#f8f8f2"
label: "#6272a4"
attribute: "#8be9fd"
bg: "#282a36"
normal: "#f8f8f2"
}
readonly property Scheme githubDark: Scheme {
comment: "#8b949e"
string: "#a5d6ff"
stringKey: "#79c0ff"
number: "#79c0ff"
keyword: "#ff7b72"
type: "#ffa657"
functions: "#d2a8ff"
method: "#d2a8ff"
macro: "#ff7b72"
preproc: "#79c0ff"
operator: "#ff7b72"
property: "#79c0ff"
label: "#7ee787"
attribute: "#7ee787"
bg: "#0d1117"
normal: "#c9d1d9"
}
readonly property Scheme solarizedDark: Scheme {
comment: "#586e75"
string: "#2aa198"
stringKey: "#268bd2"
number: "#2aa198"
keyword: "#859900"
type: "#b58900"
functions: "#268bd2"
method: "#268bd2"
macro: "#cb4b16"
preproc: "#cb4b16"
operator: "#859900"
property: "#268bd2"
label: "#6c71c4"
attribute: "#657b83"
bg: "#002b36"
normal: "#839496"
}
readonly property Scheme monokai: Scheme {
comment: "#75715e"
string: "#e6db74"
stringKey: "#f8f8f2"
number: "#ae81ff"
keyword: "#f92672"
type: "#a6e22e"
functions: "#a6e22e"
method: "#a6e22e"
macro: "#a6e22e"
preproc: "#f92672"
operator: "#f92672"
property: "#fda5ff"
label: "#f92672"
attribute: "#a6e22e"
bg: "#272822"
normal: "#f8f8f2"
}
readonly property Scheme gruvboxDark: Scheme {
comment: "#928374"
string: "#b8bb26"
stringKey: "#ebdbb2"
number: "#d3869b"
keyword: "#fb4934"
type: "#fabd2f"
functions: "#b8bb26"
method: "#b8bb26"
macro: "#8ec07c"
preproc: "#8ec07c"
operator: "#ebdbb2"
property: "#83a598"
label: "#fb4934"
attribute: "#8ec07c"
bg: "#1d2021"
normal: "#ebdbb2"
}
readonly property Scheme catppuccinMocha: Scheme {
comment: "#9399b2"
string: "#a6e3a1"
stringKey: "#b4befe"
number: "#fab387"
keyword: "#cba6f7"
type: "#f9e2af"
functions: "#89b4fa"
method: "#89b4fa"
macro: "#cba6f7"
preproc: "#f5c2e7"
operator: "#89dceb"
property: "#b4befe"
label: "#74c7ec"
attribute: "#f9e2af"
bg: "#1e1e2e"
normal: "#cdd6f4"
}
readonly property Scheme tokyoNight: Scheme {
comment: "#565f89"
string: "#9ece6a"
stringKey: "#73daca"
number: "#ff9e64"
keyword: "#9d7cd8"
type: "#2ac3de"
functions: "#7aa2f7"
method: "#7aa2f7"
macro: "#7dcfff"
preproc: "#7dcfff"
operator: "#89ddff"
property: "#73daca"
label: "#7aa2f7"
attribute: "#73daca"
bg: "#1a1b26"
normal: "#c0caf5"
}
readonly property Scheme ayuDark: Scheme {
comment: "#5a6673"
string: "#aad94c"
stringKey: "#aad94c"
number: "#d2a6ff"
keyword: "#ff8f40"
type: "#59c2ff"
functions: "#ffb454"
method: "#ffb454"
macro: "#59c2ff"
preproc: "#ff8f40"
operator: "#f29668"
property: "#f07178"
label: "#59c2ff"
attribute: "#ffb454"
bg: "#0a0e14"
normal: "#bfbdb6"
}
readonly property Scheme palenight: Scheme {
comment: "#697098"
string: "#c3e88d"
stringKey: "#82b1ff"
number: "#f78c6c"
keyword: "#ff5370"
type: "#ffcb6b"
functions: "#82b1ff"
method: "#82b1ff"
macro: "#c792ea"
preproc: "#ffcb6b"
operator: "#89ddff"
property: "#c3e88d"
label: "#c792ea"
attribute: "#ffcb6b"
bg: "#292d3e"
normal: "#bfc7d5"
}
}
readonly property var active: schemes[Config.llm.appearance.scheme]
component Scheme: JsonObject {
property color comment: "#697098"
property color string: "#c3e88d"
property color stringKey: "#82b1ff"
property color number: "#f78c6c"
property color keyword: "#ff5370"
property color type: "#ffcb6b"
property color functions: "#82b1ff"
property color method: "#82b1ff"
property color macro: "#c792ea"
property color preproc: "#ffcb6b"
property color operator: "#89ddff"
property color property: "#c3e88d"
property color label: "#c792ea"
property color attribute: "#ffcb6b"
property color bg: "#292d3e"
property color normal: "#bfc7d5"
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,166 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Llm
import ZShell.Config
import qs.Modules.Notifications.Sidebar.Chat
import qs.Components
import qs.Services
Item {
id: root
required property bool isUser
required property string text
required property var blocks
required property bool running
required property bool hovered
required property Repeater repeater
required property int index
required property ChatMessage message
readonly property real contentMaxWidth: width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge
signal edit(text: string)
implicitHeight: bubble.implicitHeight + (actionsRow.shouldBeActive ? actionsRow.implicitHeight + actionsRow.anchors.topMargin : 0)
CustomClippingRect {
id: bubble
property real liveTargetWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth
property real frozenWidth: 0.0
property bool layoutFrozen: false
property real layoutWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth
radius: Tokens.rounding.medium
color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
implicitWidth: layoutFrozen ? frozenWidth : layoutWidth
implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : markdown.implicitHeight + markdown.anchors.topMargin * 2
anchors.right: root.isUser ? parent.right : undefined
layer.enabled: false
layer.smooth: true
transform: Scale {
id: stretchScale
origin.x: root.isUser ? bubble.width : 0
xScale: bubble.layoutFrozen && bubble.frozenWidth > 0 ? bubble.liveTargetWidth / bubble.frozenWidth : 1.0
}
states: [
State {
name: "toSlim"
when: ChatState.isWindow && !ChatState.isWide && !ChatState.settledSlim
PropertyChanges {
bubble.layer.enabled: true
bubble.layoutFrozen: true
}
},
State {
name: "toWide"
when: ChatState.isWindow && ChatState.isWide && !ChatState.settledWide
PropertyChanges {
bubble.layer.enabled: true
bubble.layoutFrozen: true
}
}
]
onLayoutFrozenChanged: {
if (layoutFrozen)
frozenWidth = width;
}
TextEditBase {
id: msgText
property string cachedText: root.text
property bool cancelled: false
visible: root.isUser
anchors.left: parent.left
anchors.margins: Tokens.padding.medium
anchors.right: parent.right
anchors.top: parent.top
animateCursor: false
color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
cursor.color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary
font: Tokens.font.body.small
readOnly: true
selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4)
text: root.isUser ? root.text : ""
textFormat: CustomText.MarkdownText
wrapMode: Text.WordWrap
Keys.onPressed: event => {
if (event.key == Qt.Key_Return) {
if (!(event.modifiers & Qt.ShiftModifier)) {
readOnly = true;
event.accepted = true;
}
} else if (event.key == Qt.Key_Escape) {
cancelled = true;
readOnly = true;
event.accepted = true;
}
}
onEditingFinished: {
readOnly = true;
}
onReadOnlyChanged: {
if (readOnly) {
animateCursor = false;
textFormat = CustomText.MarkdownText;
if (cancelled) {
text = cachedText;
cancelled = false;
return;
}
root.edit(text);
} else {
textFormat = CustomText.PlainText;
text = cachedText;
forceActiveFocus();
cursorPosition = text.length;
animateCursor = true;
}
}
}
MarkdownBlocks {
id: markdown
visible: !root.isUser
anchors.topMargin: Tokens.padding.medium
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
blocks: root.blocks
}
}
Actions {
id: actionsRow
readonly property bool shouldBeActive: !root.running && root.repeater.count === (root.index + 1)
anchors.left: parent.left
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
anchors.top: bubble.bottom
opacity: shouldBeActive ? 1 : 0
visible: opacity > 0
anchors.topMargin: Tokens.spacing.medium
edit: msgText
hovered: root.hovered
isUser: root.isUser
running: root.running
message: root.message
Behavior on opacity {
Anim {}
}
}
}
@@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Services
ColumnLayout {
id: root
spacing: Tokens.spacing.small
Item {
Layout.fillHeight: true
}
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: Colors.tPalette.m3outlineVariant
fontStyle: Tokens.font.icon.extraLarge
text: "chat"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.tPalette.m3onSurfaceVariant
text: qsTr("Send a message to get started")
}
Item {
Layout.fillHeight: true
}
}
@@ -0,0 +1,128 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Llm
import ZShell.Config
import qs.Components
import qs.Services
Item {
id: root
required property ChatGeneration current
signal dismiss
implicitHeight: 0
Component.onCompleted: {
root.implicitHeight = Qt.binding(() => errorInner.implicitHeight + errorInner.anchors.margins * 2);
enterAnim.start();
}
Behavior on implicitHeight {
Anim {}
}
ParallelAnimation {
id: enterAnim
Anim {
target: container
property: "y"
from: container.implicitHeight
to: 0
}
Anim {
target: container
property: "opacity"
from: 0
to: 1
}
}
ParallelAnimation {
id: exitAnim
Anim {
target: root
property: "implicitHeight"
from: root.implicitHeight
to: 0
}
Anim {
target: container
property: "y"
to: container.implicitHeight
from: 0
}
Anim {
target: container
property: "opacity"
to: 0
from: 1
}
onFinished: root.dismiss()
}
CustomRect {
id: container
anchors.left: parent.left
anchors.right: parent.right
radius: Tokens.rounding.medium
color: Colors.palette.m3errorContainer
implicitHeight: errorInner.implicitHeight + errorInner.anchors.margins * 2
ColumnLayout {
id: errorInner
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Tokens.padding.medium
spacing: Tokens.spacing.small
CustomText {
id: header
Layout.fillWidth: true
font: Tokens.font.headline.small
elide: CustomText.ElideRight
color: Colors.palette.m3onErrorContainer
text: qsTr("An error occured")
}
CustomText {
id: body
Layout.fillWidth: true
wrapMode: CustomText.WordWrap
color: Colors.palette.m3onErrorContainer
text: root.current.errorMessage || LlmClient.serverError
}
}
IconButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Tokens.padding.medium
isRound: true
icon: "close"
type: IconButton.Text
font: Tokens.font.icon.medium
onClicked: exitAnim.start()
}
}
}
@@ -0,0 +1,90 @@
import QtQuick
import Quickshell
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Services
Column {
id: root
required property var blocks
spacing: Tokens.spacing.small
Repeater {
id: blockRep
model: ScriptModel {
values: root.blocks
objectProp: "id"
}
delegate: DelegateChooser {
role: "type"
DelegateChoice {
roleValue: MarkdownBlock.Type.Code
delegate: CodeBlockView {
required property MarkdownBlock modelData
language: modelData.language
code: modelData.code
anchors.right: parent.right
anchors.margins: Tokens.spacing.small
anchors.left: parent.left
}
}
DelegateChoice {
roleValue: MarkdownBlock.Type.Math
delegate: MathBlockView {
required property MarkdownBlock modelData
latex: modelData.latex
anchors.right: parent.right
anchors.left: parent.left
anchors.margins: Tokens.padding.medium
}
}
DelegateChoice {
roleValue: MarkdownBlock.Type.Heading
delegate: TextEditBase {
required property MarkdownBlock modelData
color: Colors.palette.m3onSurface
anchors.right: parent.right
anchors.left: parent.left
anchors.margins: Tokens.padding.medium
text: modelData.text
textFormat: Text.MarkdownText
readOnly: true
font: {
if (modelData.level <= 1)
return Tokens.font.title.builders.large.weight(Font.Bold).build();
if (modelData.level === 2)
return Tokens.font.title.builders.medium.weight(Font.Bold).build();
return Tokens.font.title.builders.small.weight(Font.Bold).build();
}
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
}
DelegateChoice {
roleValue: MarkdownBlock.Type.Text
delegate: BubbleEdit {
required property MarkdownBlock modelData
anchors.right: parent.right
anchors.left: parent.left
anchors.margins: Tokens.padding.medium
text: modelData.text
}
}
}
}
}
@@ -0,0 +1,47 @@
import QtQuick
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Services
Item {
id: root
required property string latex
property color mathColor: Colors.palette.m3onSurface
property font font: Tokens.font.headline.medium
implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2
LlmMathText {
id: math
latex: root.latex
color: root.mathColor
fontPointSize: root.font.pointSize
devicePixelRatio: Screen.devicePixelRatio
}
Image {
id: equationImage
anchors.centerIn: parent
source: math.imageId.length ? "image://llmmath/" + math.imageId : ""
visible: math.ok
fillMode: Image.PreserveAspectFit
smooth: true
width: Math.min(math.width, root.width)
height: math.width > 0 ? width * (math.height / math.width) : 0
}
CustomText {
id: fallbackText
anchors.centerIn: parent
color: root.mathColor
font: Tokens.font.mono.medium
text: root.latex
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
visible: !math.ok
}
}
@@ -0,0 +1,363 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Components
import ZShell.Config
import ZShell.Llm
import qs.Modules.Notifications.Sidebar.Chat
import qs.Components
import qs.Services
MouseArea {
id: root
required property int index
required property Item rootParent
required property ChatMessage modelData
required property ChatListView chatList
readonly property bool isUser: modelData.role === ChatMessage.Role.User
readonly property var blocks: root.isUser ? [
{
kind: "content",
id: 0,
text: root.modelData.content,
markdown: [],
running: false
}
] : blockify(current.segments, current.error)
property ChatGeneration current: modelData.activeGeneration
property bool reasoningExpanded: false
property real savedOffset: NaN
property bool anchored: false
function handleReasoningToggle(expanded: bool, block: Item): void {
if (!root.rootParent)
return;
if (expanded) {
root.savedOffset = root.rootParent.contentY - root.y;
root.anchored = true;
chatList.anchorItem(block, ChatListView.TopEdge);
settleTimer.start();
} else {
settleTimer.stop();
root.anchored = false;
chatList.clearAnchor();
if (!isNaN(root.savedOffset)) {
chatList.requestRestore(root.y + root.savedOffset);
root.savedOffset = NaN;
}
}
}
function blockify(segs, errored) {
const blocks = [];
for (let i = 0; i < segs.length; i++) {
const seg = segs[i];
if (seg.type === LlmSegment.Type.Content) {
blocks.push({
kind: "content",
id: i,
text: seg.text,
markdown: seg.markdown,
running: seg.running
});
} else {
const last = blocks.length ? blocks[blocks.length - 1] : null;
if (last && last.kind === "process") {
last.segments.push(seg);
} else {
blocks.push({
kind: "process",
id: i,
segments: [seg]
});
}
}
}
if (errored)
blocks.push({
kind: "error",
id: segs.length
});
return blocks;
}
hoverEnabled: true
preventStealing: false
implicitHeight: layout.implicitHeight + Tokens.padding.medium + (attachments.visible ? attachments.implicitHeight : 0)
onImplicitHeightChanged: if (root.anchored)
settleTimer.restart()
Timer {
id: settleTimer
interval: Tokens.anim.durations.expressiveDefaultEffects
repeat: false
onTriggered: {
if (root.anchored) {
root.anchored = false;
chatList.clearAnchor();
}
}
}
Behavior on current {
id: changeAnim
property var object: targetProperty
readonly property bool next: {
const cond = root.modelData.generations.indexOf(current) < root.modelData.generations.indexOf(changeAnim.targetValue);
return cond;
}
animation: SequentialAnimation {
ParallelAnimation {
Anim {
target: root
property: "x"
to: changeAnim.next ? root.width / 4 : -root.width / 4
from: 0
type: Anim.FastEffects
}
Anim {
target: root
property: "opacity"
from: 1
to: 0
type: Anim.FastEffects
}
}
PropertyAction {}
ParallelAnimation {
Anim {
target: root
property: "x"
from: changeAnim.next ? -root.width / 4 : root.width / 4
type: Anim.FastEffects
to: 0
}
Anim {
target: root
property: "opacity"
from: 0
type: Anim.FastEffects
to: 1
}
}
}
}
GridView {
id: attachments
readonly property int columns: ChatState.isWindow ? 5 : 3
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
flow: GridView.LeftToRight
verticalLayoutDirection: GridView.BottomToTop
cellWidth: width / columns
cellHeight: cellWidth
implicitHeight: Math.ceil(count / columns) * cellHeight
displaced: Transition {
Anim {
property: "x"
}
Anim {
property: "y"
}
}
model: ScriptModel {
objectProp: "path"
values: {
const list = [...root.modelData.attachments];
if (list.length > 0 && list.length < attachments.columns) {
while (list.length < attachments.columns)
list.unshift(null);
}
return list;
}
}
delegate: Item {
id: thumb
required property var modelData
function fileIcon(mime): string {
if (mime.startsWith("archive/") || mime === "application/zip")
return "folder_zip";
if (mime === "application/pdf")
return "picture_as_pdf";
if (mime.startsWith("audio/"))
return "audio_file";
if (mime.startsWith("video/"))
return "video_file";
if (mime.startsWith("text/"))
return "description";
return "insert_drive_file";
}
width: attachments.cellWidth
height: width
opacity: modelData ? 1 : 0
CustomClippingRect {
anchors.fill: parent
anchors.margins: Tokens.spacing.extraSmall
radius: Tokens.rounding.medium
color: Colors.tPalette.m3surfaceContainer
FadeImage {
id: attImage
anchors.fill: parent
fillMode: FadeImage.PreserveAspectCrop
asynchronous: true
source: visible ? thumb.modelData?.path ?? "" : ""
visible: thumb.modelData?.isImage ?? false
}
MaterialIcon {
anchors.centerIn: parent
fontStyle: Tokens.font.icon.extraLarge
text: thumb.fileIcon(thumb.modelData?.mimeType ?? "")
visible: !thumb.modelData?.isImage ?? false
}
Loader {
id: attIndicatorLoader
anchors.centerIn: parent
opacity: 0
active: opacity > 0 && (thumb.modelData?.isImage ?? false)
sourceComponent: CustomRect {
implicitWidth: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
implicitHeight: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
color: Colors.palette.m3primaryContainer
radius: Tokens.rounding.full
LoadingIndicator {
id: attLoadingIndicator
anchors.centerIn: parent
containsIcon: true
implicitSize: thumb.implicitWidth * 0.4
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Timer {
id: attLoadDebounceTimer
interval: 100
onTriggered: {
if (attImage.status !== Image.Ready)
attIndicatorLoader.opacity = 1;
}
}
}
}
}
Column {
id: layout
anchors.top: attachments.bottom
anchors.left: parent.left
anchors.right: parent.right
spacing: Tokens.spacing.medium
Repeater {
id: segmentRep
model: ScriptModel {
values: root.blocks
objectProp: "id"
}
delegate: DelegateChooser {
role: "kind"
DelegateChoice {
roleValue: "error"
delegate: ErrorBubble {
width: root.width
current: root.current
onDismiss: root.current.dismissError()
}
}
DelegateChoice {
roleValue: "process"
delegate: ProcessBlock {
width: root.width
blocks: root.blocks
current: root.current
rootParent: root.rootParent
onExpandedChanged: root.handleReasoningToggle(expanded, this)
}
}
DelegateChoice {
roleValue: "content"
delegate: ContentBubble {
required property var modelData
isUser: root.isUser
text: modelData.text
blocks: modelData.markdown
running: modelData.running
width: root.width
repeater: segmentRep
message: root.modelData
hovered: root.containsMouse
onEdit: text => {
root.modelData.edit(text);
if (root.isUser)
root.modelData.generate();
}
}
}
}
}
}
}
@@ -0,0 +1,351 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import ZShell.Llm
import ZShell.Blobs
import qs.Modules.Notifications.Sidebar.Chat
import qs.Components
import qs.Services
Item {
id: root
required property Item rootParent
required property Item fabRoot
property bool open: false
property int maxHeight: root.rootParent.height - Tokens.padding.medium * 2
property int openHeight: Math.min(modelsContent.item?.implicitHeight, maxHeight)
property int openWidth: root.rootParent.width - Tokens.padding.medium * 2
property real openHeightProg: openHeight / maxHeight
property int collapsedY: 0
property int horizontalContentMargin
property string header: qsTr("Available models")
property string acceptLabel: qsTr("Accept")
z: open || dialogTransition.running ? 2 : 0
Layout.fillWidth: true
implicitHeight: fabRoot.implicitHeight
function prettyModelName(): string {
if (modelsContent.item?.modelsList.count < 1)
return qsTr("No models found");
const pathList = LlmClient.model.split("\/");
const name = pathList[pathList.length - 1];
if (!name)
return qsTr("No model selected");
return name;
}
Behavior on implicitHeight {
Anim {}
}
function reparentWrapper(): void {
const newParent = open ? root.rootParent : root;
const c = modelsWrapper.parent.mapToItem(newParent, modelsWrapper.x + modelsWrapper.width / 2, modelsWrapper.y + modelsWrapper.height / 2);
root.collapsedY = modelsWrapper.y;
modelsWrapper.parent = newParent;
modelsWrapper.x = c.x - modelsWrapper.width / 2;
modelsWrapper.y = c.y - modelsWrapper.height / 2;
}
BlobGroup {
id: blobGroup
color: root.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainer
Behavior on color {
CAnim {}
}
}
MouseArea {
id: backdrop
anchors.fill: parent
enabled: false
hoverEnabled: enabled
preventStealing: true
parent: root.open ? root.rootParent : root
onClicked: root.open = false
}
Item {
id: modelsWrapper
width: root.width
height: modelBox.implicitHeight
z: 1
states: State {
name: "open"
when: root.open
PropertyChanges {
backdrop.enabled: true
modelsBackground.radius: Tokens.rounding.largeIncreased
modelsContent.opacity: 1
modelsWrapper.height: root.openHeight
modelsWrapper.width: root.openWidth
modelsWrapper.x: (root.rootParent.width - root.openWidth) / 2
modelsWrapper.y: root.rootParent.height - root.openHeight - Tokens.spacing.medium - Tokens.padding.medium - root.fabRoot.implicitHeight
elevation.opacity: 1
modelBox.opacity: 0
}
}
transitions: Transition {
id: dialogTransition
SequentialAnimation {
ScriptAction {
script: root.reparentWrapper()
}
Anim {
properties: "x,y"
}
}
PropertyAction {
property: "enabled"
}
Anim {
properties: "opacity,radius"
type: Anim.DefaultEffects
}
Anim {
properties: "width,height"
}
}
Elevation {
id: elevation
anchors.fill: parent
level: 4
opacity: 0
radius: modelsBackground.radius
transform: Matrix4x4 {
matrix: modelsBackground.deformMatrix
}
}
BlobRect {
id: modelsBackground
anchors.fill: parent
radius: modelLayer.pressed ? Tokens.rounding.small : Tokens.rounding.extraLarge
deformScale: 0
group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
Behavior on radius {
Anim {
type: Anim.DefaultEffects
}
}
}
CustomRect {
id: modelBox
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: root.fabRoot.implicitHeight
radius: modelsBackground.radius
CustomText {
id: label
text: root.prettyModelName()
color: Colors.palette.m3onSurfaceVariant
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
}
MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
text: "unfold_more"
fontStyle: Tokens.font.icon.large
}
StateLayer {
id: modelLayer
onClicked: {
ChatState.fabExpanded = false;
root.open = true;
}
}
}
Loader {
id: modelsContent
active: opacity > 0
anchors.fill: parent
asynchronous: false
opacity: 0
sourceComponent: MouseArea {
readonly property alias modelsList: modelsList
onWheel: event => event.accepted = true
preventStealing: true
clip: true
implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins * 2
ColumnLayout {
id: innerLayout
anchors.bottomMargin: Tokens.padding.largeIncreased
anchors.fill: parent
anchors.margins: Tokens.padding.extraLarge
spacing: 0
CustomText {
font: Tokens.font.title.builders.large.weight(Font.Normal).build()
text: root.header
}
VerticalFadeListView {
id: modelsList
Layout.fillWidth: true
Layout.leftMargin: root.horizontalContentMargin
Layout.topMargin: Tokens.spacing.medium
Layout.bottomMargin: Tokens.spacing.medium
Layout.rightMargin: root.horizontalContentMargin
Layout.preferredHeight: 400
Layout.maximumHeight: 400
model: LlmClient.availableModels
currentIndex: LlmClient.availableModels.indexOf(LlmClient.model)
visible: opacity > 0
Behavior on opacity {
Anim {}
}
CustomScrollBar.vertical: CustomScrollBar {
flickable: modelsList
}
delegate: CustomRect {
id: model
required property var modelData
required property int index
readonly property bool selected: ListView.view.currentIndex === model.index
implicitWidth: ListView.view.width
implicitHeight: name.implicitHeight + Tokens.padding.small * 2
radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
Behavior on radius {
Anim {
type: Anim.SlowEffects
}
}
RowLayout {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Tokens.padding.small
anchors.leftMargin: Tokens.padding.large
anchors.rightMargin: Tokens.padding.large
CustomText {
id: name
text: model.modelData
elide: CustomText.ElideRight
color: model.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
Layout.fillWidth: true
}
MaterialIcon {
id: check
color: Colors.palette.m3onTertiaryContainer
fontStyle: Tokens.font.icon.medium
opacity: model.selected ? 1 : 0
visible: opacity > 0
text: "check"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
}
StateLayer {
id: stateLayer
onClicked: {
model.ListView.view.currentIndex = model.index;
}
}
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
spacing: Tokens.spacing.extraSmall
TextButton {
horizontalPadding: Tokens.padding.largeIncreased
isRound: true
text: qsTr("Cancel")
type: TextButton.Text
verticalPadding: Tokens.padding.small
stateLayer.preventStealing: true
onClicked: {
root.open = false;
}
}
TextButton {
horizontalPadding: Tokens.padding.largeIncreased
isRound: true
text: root.acceptLabel
type: TextButton.Text
stateLayer.preventStealing: true
verticalPadding: Tokens.padding.small
onClicked: {
LlmClient.selectModel(modelsList.currentItem.modelData);
root.open = false;
}
}
}
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More