added quickshell
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// Focused window title, glitching on every change the way a Persona 5 caption
|
||||
// snaps into place.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(screenRef)
|
||||
readonly property HyprlandToplevel toplevel: Hyprland.activeToplevel
|
||||
|
||||
// Only speak for the monitor this bar lives on.
|
||||
readonly property bool mine: toplevel !== null
|
||||
&& (root.monitor === null || toplevel.monitor === null
|
||||
|| toplevel.monitor.id === root.monitor.id)
|
||||
|
||||
readonly property string title: mine && toplevel.title ? toplevel.title : "—"
|
||||
|
||||
// The bar shrinks this when the three clusters would otherwise collide.
|
||||
property real maxWidth: 240
|
||||
|
||||
implicitWidth: Math.min(maxWidth, Math.max(70, label.implicitWidth + 4))
|
||||
implicitHeight: 20
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutExpo }
|
||||
}
|
||||
|
||||
Marquee {
|
||||
id: label
|
||||
anchors.fill: parent
|
||||
text: root.title
|
||||
color: root.mine ? Theme.subtext : Theme.muted
|
||||
pixelSize: Theme.fsSmall
|
||||
family: Theme.fontDisplay
|
||||
italic: true
|
||||
weight: Font.DemiBold
|
||||
}
|
||||
|
||||
onTitleChanged: flash.restart()
|
||||
|
||||
// A quick crimson wipe under the title on focus change.
|
||||
Rectangle {
|
||||
id: wipe
|
||||
anchors.bottom: parent.bottom
|
||||
height: 2
|
||||
width: 0
|
||||
color: Theme.accent
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: flash
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: wipe; property: "width"
|
||||
from: 0; to: root.width
|
||||
duration: Theme.durBase; easing.type: Easing.OutExpo
|
||||
}
|
||||
NumberAnimation {
|
||||
target: wipe; property: "opacity"
|
||||
from: 0; to: 1; duration: Theme.durFast
|
||||
}
|
||||
}
|
||||
NumberAnimation {
|
||||
target: wipe; property: "opacity"
|
||||
to: 0; duration: Theme.durSlow
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// One bar per monitor.
|
||||
//
|
||||
// Layout is three clusters: identity and system state on the left, the clock
|
||||
// and player dead centre, controls on the right. The centre is genuinely
|
||||
// centred, so the left cluster's window title is capped at whatever space is
|
||||
// actually left over — otherwise the clusters overlap on a 1080p panel.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "takemi-bar"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
margins {
|
||||
top: Theme.barMargin
|
||||
left: Theme.barMargin
|
||||
right: Theme.barMargin
|
||||
}
|
||||
|
||||
implicitHeight: Theme.barHeight
|
||||
exclusiveZone: Theme.barHeight + Theme.barMargin
|
||||
color: "transparent"
|
||||
|
||||
// Half the space not taken by the centre cluster, minus breathing room.
|
||||
readonly property real sideBudget:
|
||||
(width - centreCluster.width) / 2 - Theme.barGap * 2
|
||||
|
||||
// ------------------------------------------------------------------ left
|
||||
Row {
|
||||
id: leftCluster
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.barGap
|
||||
|
||||
Sigil {
|
||||
id: sigil
|
||||
screenRef: root.modelData
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: spacesPill
|
||||
accent: Theme.primary
|
||||
padding: Theme.padS
|
||||
interactive: false
|
||||
implicitWidth: spacesRow.implicitWidth + contentPad * 2
|
||||
|
||||
Row {
|
||||
id: spacesRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
spacing: Theme.padM
|
||||
|
||||
Workspaces {
|
||||
id: spaces
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
screenRef: root.modelData
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 1
|
||||
height: 18
|
||||
color: Theme.alpha(Theme.outline, 0.8)
|
||||
}
|
||||
|
||||
ActiveWindow {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
screenRef: root.modelData
|
||||
|
||||
// Everything else on this side is fixed width, so the title
|
||||
// gets whatever remains.
|
||||
maxWidth: Math.max(70, root.sideBudget
|
||||
- sigil.implicitWidth
|
||||
- resources.implicitWidth
|
||||
- spaces.implicitWidth
|
||||
- Theme.barGap * 2
|
||||
- Theme.padM * 3
|
||||
- spacesPill.leanInset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Resources {
|
||||
id: resources
|
||||
screenRef: root.modelData
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- centre
|
||||
Row {
|
||||
id: centreCluster
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.barGap
|
||||
|
||||
MediaPill {
|
||||
screenRef: root.modelData
|
||||
}
|
||||
|
||||
Clock {
|
||||
screenRef: root.modelData
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- right
|
||||
Row {
|
||||
id: rightCluster
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.barGap
|
||||
|
||||
Tray {
|
||||
screenRef: root.modelData
|
||||
}
|
||||
|
||||
Status {
|
||||
screenRef: root.modelData
|
||||
}
|
||||
|
||||
NotifButton {
|
||||
screenRef: root.modelData
|
||||
}
|
||||
|
||||
PowerButton {
|
||||
screenRef: root.modelData
|
||||
}
|
||||
}
|
||||
|
||||
// A hairline of crimson under the whole bar — the Persona 5 underscore.
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: "transparent" }
|
||||
GradientStop { position: 0.18; color: Theme.alpha(Theme.accent, 0.55) }
|
||||
GradientStop { position: 0.5; color: Theme.alpha(Theme.primary, 0.35) }
|
||||
GradientStop { position: 0.82; color: Theme.alpha(Theme.accent, 0.55) }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import QtQuick
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// Shared chrome for every bar module: a leaning slab that lifts and lights up
|
||||
// under the pointer, and forwards clicks and scrolls to the widget.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color accent: Theme.primary
|
||||
property bool active: false
|
||||
property bool slash: false
|
||||
property real lean: Theme.skew
|
||||
property real padding: Theme.padM
|
||||
property bool interactive: true
|
||||
property bool halftone: false
|
||||
|
||||
readonly property bool hovered: hover.hovered
|
||||
readonly property real leanInset: panel.leanInset
|
||||
|
||||
// Total horizontal inset a pill must add on top of its content width.
|
||||
readonly property real contentPad: panel.contentPad
|
||||
readonly property alias panelItem: panel
|
||||
|
||||
default property alias content: holder.data
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
signal middleClicked()
|
||||
signal scrolled(real delta)
|
||||
|
||||
implicitHeight: Theme.barHeight
|
||||
|
||||
P5Panel {
|
||||
id: panel
|
||||
anchors.fill: parent
|
||||
lean: root.lean
|
||||
padding: root.padding
|
||||
halftone: root.halftone
|
||||
halftoneColor: root.accent
|
||||
sheen: true
|
||||
|
||||
fill: root.active ? Theme.surfaceAlt : Theme.surface
|
||||
fillOpacity: root.hovered ? 0.98 : 0.88
|
||||
border: root.hovered || root.active
|
||||
? Theme.alpha(root.accent, 0.9)
|
||||
: Theme.alpha(Theme.outline, 0.7)
|
||||
borderWidth: root.hovered || root.active ? Theme.stroke : 1
|
||||
|
||||
slash: root.slash || root.active
|
||||
slashColor: root.accent
|
||||
|
||||
Behavior on fillOpacity {
|
||||
NumberAnimation { duration: Theme.durFast }
|
||||
}
|
||||
Behavior on border {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: holder
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
// Lift toward the pointer. Subtle, but it makes the bar feel alive.
|
||||
transform: Translate {
|
||||
y: root.hovered && root.interactive ? -2 : 0
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.durBase
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
enabled: root.interactive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: root.interactive
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: root.clicked()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: root.interactive
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: root.rightClicked()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: root.interactive
|
||||
acceptedButtons: Qt.MiddleButton
|
||||
onTapped: root.middleClicked()
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
enabled: root.interactive
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => root.scrolled(event.angleDelta.y)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/popouts"
|
||||
|
||||
// Centre-stage clock. Hovering drops the calendar.
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
accent: Theme.accent
|
||||
slash: true
|
||||
padding: Theme.padS
|
||||
implicitWidth: layout.implicitWidth + contentPad * 2
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Seconds
|
||||
}
|
||||
|
||||
onClicked: calendar.pinned = !calendar.pinned
|
||||
|
||||
Row {
|
||||
id: layout
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.padM
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: -1
|
||||
|
||||
Text {
|
||||
text: Qt.formatDateTime(clock.date, "ddd").toUpperCase()
|
||||
color: Theme.accent
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2.5
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Qt.formatDateTime(clock.date, "dd MMM").toUpperCase()
|
||||
color: Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 1
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 2
|
||||
height: 22
|
||||
color: Theme.alpha(Theme.accent, 0.6)
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
SplitText {
|
||||
id: timeText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Qt.formatDateTime(clock.date, "HH:mm")
|
||||
pixelSize: Theme.fsTitle
|
||||
split: 1.4
|
||||
}
|
||||
|
||||
// Seconds, kept quiet so they read as a ticking detail rather than noise.
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: 4
|
||||
text: Qt.formatDateTime(clock.date, "ss")
|
||||
color: Theme.alpha(Theme.primary, 0.8)
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
// Glitch the time on the hour, because it should feel like something happened.
|
||||
property int lastHour: -1
|
||||
Connections {
|
||||
target: clock
|
||||
function onDateChanged() {
|
||||
const h = clock.date.getHours();
|
||||
if (root.lastHour !== -1 && h !== root.lastHour) timeText.glitch();
|
||||
root.lastHour = h;
|
||||
}
|
||||
}
|
||||
|
||||
CalendarPopout {
|
||||
id: calendar
|
||||
anchorItem: root
|
||||
triggerHovered: root.hovered
|
||||
clockDate: clock.date
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
import "root:/popouts"
|
||||
|
||||
// Now-playing module. Collapses to nothing when no player is around.
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
accent: Theme.primary
|
||||
padding: Theme.padS
|
||||
visible: Media.hasPlayer || width > 1
|
||||
|
||||
implicitWidth: Media.hasPlayer
|
||||
? layout.implicitWidth + contentPad * 2
|
||||
: 0
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutExpo }
|
||||
}
|
||||
|
||||
clip: true
|
||||
|
||||
onClicked: Media.playPause()
|
||||
onRightClicked: Media.next()
|
||||
onMiddleClicked: Media.raise()
|
||||
onScrolled: delta => delta > 0 ? Media.next() : Media.previous()
|
||||
|
||||
Row {
|
||||
id: layout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
spacing: Theme.padS
|
||||
|
||||
// Album art, chopped into the house shape.
|
||||
Item {
|
||||
id: artFrame
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 24
|
||||
height: 24
|
||||
|
||||
Shape {
|
||||
id: artMask
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
fillColor: "white"
|
||||
strokeColor: "transparent"
|
||||
PathPolyline {
|
||||
path: Geom.chamfer(Geom.parallelogram(24, 24, 0.18), 5, [1, 3])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: art
|
||||
anchors.fill: parent
|
||||
source: Media.artUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
layer.enabled: true
|
||||
layer.effect: MaskedArt { maskItem: artMask }
|
||||
}
|
||||
|
||||
// Fallback: the source app's glyph.
|
||||
Icon {
|
||||
anchors.centerIn: parent
|
||||
visible: art.status !== Image.Ready
|
||||
text: Media.sourceIcon
|
||||
font.pixelSize: Theme.fsLarge
|
||||
color: Theme.primary
|
||||
pulsing: Media.playing
|
||||
pulseScale: 1.12
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 0
|
||||
|
||||
Marquee {
|
||||
width: 130
|
||||
height: 14
|
||||
text: Media.title || Media.identity
|
||||
color: Theme.text
|
||||
pixelSize: Theme.fsSmall
|
||||
family: Theme.fontDisplay
|
||||
weight: Font.DemiBold
|
||||
italic: true
|
||||
}
|
||||
|
||||
Marquee {
|
||||
width: 130
|
||||
height: 12
|
||||
text: Media.artist || Media.album || Media.identity
|
||||
color: Theme.muted
|
||||
pixelSize: Theme.fsMicro
|
||||
family: Theme.fontMono
|
||||
}
|
||||
}
|
||||
|
||||
// Play state, as a two-bar equaliser that freezes when paused.
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: 3
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
width: 3
|
||||
height: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Media.playing ? Theme.primary : Theme.muted
|
||||
|
||||
SequentialAnimation on height {
|
||||
running: Media.playing
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
to: 4 + (index * 5) % 13
|
||||
duration: 320 + index * 90
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
to: 14 - (index * 4) % 9
|
||||
duration: 280 + index * 70
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track progress, hairline along the bottom of the pill.
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 3
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.leanInset
|
||||
width: Math.max(0, (root.width - root.leanInset * 2) * Media.progress)
|
||||
height: 2
|
||||
color: Theme.accent
|
||||
visible: Media.canSeek
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 480; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: if (visible) titleGlitch.restart()
|
||||
|
||||
Connections {
|
||||
target: Media
|
||||
function onTrackChanged() { titleGlitch.restart(); }
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: titleGlitch
|
||||
NumberAnimation {
|
||||
target: artFrame; property: "rotation"
|
||||
from: -8; to: 0; duration: Theme.durSlow; easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
MediaPopout {
|
||||
id: popout
|
||||
anchorItem: root
|
||||
triggerHovered: root.hovered && Media.hasPlayer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
import "root:/popouts"
|
||||
|
||||
// Notification bell with an unread badge. Click opens the history, right click
|
||||
// toggles do-not-disturb.
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
accent: Notifs.dnd ? Theme.muted : Theme.primary
|
||||
padding: Theme.padS
|
||||
implicitWidth: Theme.barHeight - 4 + contentPad
|
||||
|
||||
onClicked: {
|
||||
popout.pinned = !popout.pinned;
|
||||
Notifs.markRead();
|
||||
}
|
||||
onRightClicked: Notifs.toggleDnd()
|
||||
|
||||
Icon {
|
||||
id: bell
|
||||
anchors.centerIn: parent
|
||||
text: Notifs.icon
|
||||
font.pixelSize: Theme.fsLarge
|
||||
color: {
|
||||
if (Notifs.dnd) return Theme.muted;
|
||||
if (Notifs.hasUnread) return Theme.accent;
|
||||
return root.hovered ? Theme.glow : Theme.primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Unread badge, chopped like everything else.
|
||||
Rectangle {
|
||||
visible: Notifs.unread > 0 && !Notifs.dnd
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 2
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 4
|
||||
width: Math.max(12, badge.implicitWidth + 5)
|
||||
height: 12
|
||||
color: Theme.accent
|
||||
|
||||
Text {
|
||||
id: badge
|
||||
anchors.centerIn: parent
|
||||
text: Notifs.unread > 9 ? "9+" : Notifs.unread
|
||||
color: Theme.text
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 8
|
||||
font.weight: Font.Bold
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 12 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Notifs
|
||||
function onPopupRequested() { bell.bump(); }
|
||||
}
|
||||
|
||||
NotifPopout {
|
||||
id: popout
|
||||
anchorItem: root
|
||||
triggerHovered: root.hovered
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Far right. Opens the full-screen power menu.
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
accent: Theme.accent
|
||||
slash: true
|
||||
padding: Theme.padS
|
||||
active: Actions.powerMenuOpen
|
||||
implicitWidth: Theme.barHeight - 4 + contentPad
|
||||
|
||||
onClicked: Actions.togglePowerMenu()
|
||||
|
||||
Icon {
|
||||
id: glyph
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
font.pixelSize: Theme.fsLarge
|
||||
color: root.hovered || Actions.powerMenuOpen ? Theme.accent : Theme.subtext
|
||||
}
|
||||
|
||||
// A crimson halo that swells under the pointer.
|
||||
Rectangle {
|
||||
anchors.centerIn: glyph
|
||||
z: -1
|
||||
width: root.hovered ? 26 : 0
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: Theme.alpha(Theme.accent, 0.18)
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
import "root:/popouts"
|
||||
|
||||
// Compact CPU / MEM / GPU readout. Each metric is a number over a segmented
|
||||
// meter; hovering opens the full gauge cluster.
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
accent: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
|
||||
padding: Theme.padS
|
||||
implicitWidth: layout.implicitWidth + contentPad * 2
|
||||
|
||||
onClicked: Actions.openSystemMonitor()
|
||||
|
||||
Row {
|
||||
id: layout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
spacing: Theme.padM
|
||||
|
||||
Metric {
|
||||
label: "CPU"
|
||||
value: Sys.cpuUsage
|
||||
readout: Math.round(Sys.cpuUsage) + "%"
|
||||
}
|
||||
|
||||
Divider {}
|
||||
|
||||
Metric {
|
||||
label: "MEM"
|
||||
value: Sys.memPercent
|
||||
readout: Sys.memUsed.toFixed(1) + "G"
|
||||
}
|
||||
|
||||
Divider { visible: Sys.gpuAvailable }
|
||||
|
||||
Metric {
|
||||
visible: Sys.gpuAvailable
|
||||
label: "GPU"
|
||||
value: Sys.gpuUsage
|
||||
readout: Math.round(Sys.gpuUsage) + "%"
|
||||
}
|
||||
}
|
||||
|
||||
component Divider: Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 1
|
||||
height: 18
|
||||
color: Theme.alpha(Theme.outline, 0.8)
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 18 * Theme.skew,
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
component Metric: Column {
|
||||
id: metric
|
||||
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
property string readout: ""
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 1
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: metric.label
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 1.5
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: metric.readout
|
||||
color: Theme.heat(metric.value)
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Font.DemiBold
|
||||
renderType: Text.NativeRendering
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MeterBar {
|
||||
value: metric.value
|
||||
segments: 9
|
||||
segmentWidth: 3
|
||||
spacing: 2
|
||||
height: 5
|
||||
}
|
||||
}
|
||||
|
||||
SysPopout {
|
||||
id: popout
|
||||
anchorItem: root
|
||||
triggerHovered: root.hovered
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// The house mark, far left. Click launches the app menu; right click reloads
|
||||
// the shell, which is handy while tweaking the config.
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
accent: Theme.accent
|
||||
slash: true
|
||||
padding: Theme.padS
|
||||
lean: Theme.skew
|
||||
implicitWidth: Theme.barHeight + contentPad
|
||||
|
||||
onClicked: Actions.launch("fuzzel")
|
||||
onRightClicked: Actions.reloadShell()
|
||||
|
||||
Icon {
|
||||
id: mark
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
font.pixelSize: Theme.fsTitle
|
||||
color: root.hovered ? Theme.accent : Theme.primary
|
||||
}
|
||||
|
||||
// Spin the mark on click, then settle.
|
||||
Connections {
|
||||
target: root
|
||||
function onClicked() { spin.restart(); }
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: spin
|
||||
NumberAnimation {
|
||||
target: mark; property: "rotation"
|
||||
from: 0; to: 360; duration: Theme.durSlow
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
import "root:/popouts"
|
||||
|
||||
// Network, bluetooth, audio, mic, backlight and battery, in one slab. Each
|
||||
// glyph carries its own popout.
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
accent: Theme.primary
|
||||
padding: Theme.padS
|
||||
interactive: false
|
||||
implicitWidth: layout.implicitWidth + contentPad * 2
|
||||
|
||||
Row {
|
||||
id: layout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
spacing: Theme.padM
|
||||
|
||||
StatusIcon {
|
||||
id: netIcon
|
||||
icon: Net.icon
|
||||
label: Sys.netUp ? Sys.rate(Sys.netRx + Sys.netTx) : ""
|
||||
labelWidth: 62
|
||||
tint: Theme.primary
|
||||
dim: !Sys.netUp
|
||||
onClicked: netPopout.pinned = !netPopout.pinned
|
||||
onRightClicked: Net.toggleWifi()
|
||||
onMiddleClicked: Actions.openNetworkSettings()
|
||||
}
|
||||
|
||||
StatusIcon {
|
||||
id: btIcon
|
||||
icon: Bt.icon
|
||||
tint: Bt.connected.length > 0 ? Theme.blue : Theme.primary
|
||||
dim: !Bt.enabled
|
||||
onClicked: btPopout.pinned = !btPopout.pinned
|
||||
onRightClicked: Bt.toggle()
|
||||
}
|
||||
|
||||
StatusIcon {
|
||||
id: volIcon
|
||||
icon: Audio.icon
|
||||
label: Audio.muted ? "" : Audio.volumePercent + "%"
|
||||
labelWidth: Audio.muted ? 0 : 34
|
||||
alert: Audio.muted
|
||||
onClicked: Audio.toggleMute()
|
||||
onRightClicked: audioPopout.pinned = !audioPopout.pinned
|
||||
onMiddleClicked: Actions.openAudioSettings()
|
||||
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
|
||||
}
|
||||
|
||||
StatusIcon {
|
||||
id: micIcon
|
||||
icon: Audio.micIcon
|
||||
alert: Audio.micMuted
|
||||
onClicked: Audio.toggleMicMute()
|
||||
onRightClicked: audioPopout.pinned = !audioPopout.pinned
|
||||
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
|
||||
}
|
||||
|
||||
StatusIcon {
|
||||
id: brightIcon
|
||||
visible: Brightness.available
|
||||
icon: Brightness.icon
|
||||
label: Math.round(Brightness.percent) + "%"
|
||||
labelWidth: 34
|
||||
tint: Theme.warn
|
||||
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
|
||||
}
|
||||
|
||||
StatusIcon {
|
||||
id: battIcon
|
||||
visible: Battery.available
|
||||
icon: Battery.icon
|
||||
label: Math.round(Battery.percent) + "%"
|
||||
labelWidth: 34
|
||||
tint: Battery.tint
|
||||
alert: Battery.critical
|
||||
pulsing: Battery.critical
|
||||
onClicked: battPopout.pinned = !battPopout.pinned
|
||||
}
|
||||
}
|
||||
|
||||
NetPopout {
|
||||
id: netPopout
|
||||
anchorItem: netIcon
|
||||
triggerHovered: netIcon.hovered
|
||||
}
|
||||
|
||||
BtPopout {
|
||||
id: btPopout
|
||||
anchorItem: btIcon
|
||||
triggerHovered: btIcon.hovered
|
||||
}
|
||||
|
||||
AudioPopout {
|
||||
id: audioPopout
|
||||
anchorItem: volIcon
|
||||
triggerHovered: volIcon.hovered || micIcon.hovered
|
||||
}
|
||||
|
||||
BatteryPopout {
|
||||
id: battPopout
|
||||
anchorItem: battIcon
|
||||
triggerHovered: battIcon.hovered
|
||||
visible: Battery.available && shown
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import QtQuick
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// One indicator inside the status cluster: a glyph, an optional readout, and a
|
||||
// crimson underline that wipes in on hover.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string label: ""
|
||||
property color tint: Theme.primary
|
||||
property bool alert: false
|
||||
property bool dim: false
|
||||
property bool pulsing: false
|
||||
|
||||
// Fixed label width. Readouts that change width constantly (throughput,
|
||||
// percentages) would otherwise shove the whole cluster around.
|
||||
property real labelWidth: 0
|
||||
|
||||
readonly property bool hovered: hover.hovered
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
signal middleClicked()
|
||||
signal scrolled(real delta)
|
||||
|
||||
implicitWidth: layout.implicitWidth
|
||||
implicitHeight: Theme.barHeight - Theme.padS * 2
|
||||
|
||||
Row {
|
||||
id: layout
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
Icon {
|
||||
id: glyph
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.icon
|
||||
pulsing: root.pulsing
|
||||
color: {
|
||||
if (root.alert) return Theme.accent;
|
||||
if (root.dim) return Theme.muted;
|
||||
if (root.hovered) return Theme.glow;
|
||||
return root.tint;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.label !== ""
|
||||
text: root.label
|
||||
width: root.labelWidth > 0 ? root.labelWidth : implicitWidth
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideRight
|
||||
color: root.dim ? Theme.muted : Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Font.DemiBold
|
||||
renderType: Text.NativeRendering
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: 2
|
||||
width: root.hovered ? layout.implicitWidth : 0
|
||||
color: root.alert ? Theme.accent : root.tint
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
|
||||
}
|
||||
}
|
||||
|
||||
scale: root.hovered ? 1.06 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
|
||||
}
|
||||
|
||||
onAlertChanged: if (alert) glyph.bump()
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: root.clicked()
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: root.rightClicked()
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.MiddleButton
|
||||
onTapped: root.middleClicked()
|
||||
}
|
||||
WheelHandler {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => root.scrolled(event.angleDelta.y)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// StatusNotifier tray. Left click activates, right click opens the app's own
|
||||
// menu, middle click is the secondary action.
|
||||
BarPill {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
accent: Theme.blue
|
||||
padding: Theme.padS
|
||||
interactive: false
|
||||
visible: SystemTray.items.values.length > 0
|
||||
implicitWidth: visible ? layout.implicitWidth + contentPad * 2 : 0
|
||||
|
||||
Row {
|
||||
id: layout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
spacing: Theme.padM
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
Item {
|
||||
id: entry
|
||||
|
||||
required property SystemTrayItem modelData
|
||||
|
||||
width: 18
|
||||
height: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
IconImage {
|
||||
id: img
|
||||
anchors.fill: parent
|
||||
source: entry.modelData.icon
|
||||
asynchronous: true
|
||||
opacity: itemHover.hovered ? 1.0 : 0.82
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Theme.durFast }
|
||||
}
|
||||
}
|
||||
|
||||
scale: itemHover.hovered ? 1.18 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: itemHover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
if (entry.modelData.onlyMenu) {
|
||||
menuAnchor.open();
|
||||
} else {
|
||||
entry.modelData.activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: menuAnchor.open()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.MiddleButton
|
||||
onTapped: entry.modelData.secondaryActivate()
|
||||
}
|
||||
|
||||
QsMenuAnchor {
|
||||
id: menuAnchor
|
||||
menu: entry.modelData.menu
|
||||
anchor.item: entry
|
||||
anchor.edges: Edges.Bottom
|
||||
anchor.gravity: Edges.Bottom
|
||||
anchor.margins.top: Theme.popoutGap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// Workspace indicator: leaning ticks that stretch open when focused. Occupied
|
||||
// workspaces glow teal, urgent ones snap to crimson.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var screenRef
|
||||
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(screenRef)
|
||||
readonly property int focusedId: monitor?.activeWorkspace?.id ?? -1
|
||||
|
||||
// Live workspaces on this monitor, keyed by id, ignoring special workspaces.
|
||||
readonly property var occupancy: {
|
||||
const map = {};
|
||||
for (const w of (Hyprland.workspaces?.values ?? [])) {
|
||||
if (w.id < 1) continue;
|
||||
if (root.monitor && w.monitor && w.monitor.id !== root.monitor.id) continue;
|
||||
map[w.id] = {
|
||||
count: w.toplevels?.values?.length ?? 0,
|
||||
urgent: w.urgent,
|
||||
fullscreen: w.hasFullscreen
|
||||
};
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// Always show 1..5, plus anything beyond that which is actually in use.
|
||||
readonly property var slots: {
|
||||
const ids = [1, 2, 3, 4, 5];
|
||||
for (const key in root.occupancy) {
|
||||
const id = parseInt(key);
|
||||
if (ids.indexOf(id) === -1) ids.push(id);
|
||||
}
|
||||
return ids.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
implicitWidth: row.implicitWidth
|
||||
implicitHeight: 20
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: root.slots
|
||||
|
||||
Item {
|
||||
id: pip
|
||||
|
||||
required property var modelData
|
||||
readonly property int wsId: modelData
|
||||
readonly property var info: root.occupancy[wsId]
|
||||
readonly property bool occupied: info !== undefined && info.count > 0
|
||||
readonly property bool urgent: info !== undefined && info.urgent
|
||||
readonly property bool focused: root.focusedId === wsId
|
||||
readonly property bool hovered: pipHover.hovered
|
||||
|
||||
width: focused ? 34 : (occupied ? 16 : 10)
|
||||
height: 18
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.durSlow
|
||||
easing.type: Easing.OutBack
|
||||
easing.overshoot: 1.6
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: tick
|
||||
anchors.fill: parent
|
||||
color: {
|
||||
if (pip.urgent) return Theme.accent;
|
||||
if (pip.focused) return Theme.primary;
|
||||
if (pip.hovered) return Theme.glow;
|
||||
if (pip.occupied) return Theme.alpha(Theme.primary, 0.55);
|
||||
return Theme.alpha(Theme.outline, 0.75);
|
||||
}
|
||||
|
||||
transform: Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(
|
||||
1, -Theme.skew, 0, pip.height * Theme.skew,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1)
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
}
|
||||
}
|
||||
|
||||
// The focused workspace wears its number.
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
opacity: pip.focused ? 1 : 0
|
||||
visible: opacity > 0.01
|
||||
text: pip.wsId
|
||||
color: Theme.base
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Theme.weightDisplay
|
||||
font.italic: true
|
||||
renderType: Text.NativeRendering
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Theme.durBase }
|
||||
}
|
||||
}
|
||||
|
||||
// Fullscreen marker.
|
||||
Rectangle {
|
||||
visible: pip.info !== undefined && pip.info.fullscreen && !pip.focused
|
||||
width: 4; height: 4
|
||||
color: Theme.accent
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.bottom
|
||||
anchors.topMargin: 1
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: pipHover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: Hyprland.dispatch("workspace " + pip.wsId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user