added quickshell

This commit is contained in:
2026-08-10 23:57:26 +02:00
parent cbaf55cfc5
commit 47ff5233bd
69 changed files with 6945 additions and 80 deletions
+182
View File
@@ -0,0 +1,182 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// Volume / mic / brightness heads-up, bottom centre. Appears on change and
// fades out on its own.
PanelWindow {
id: root
required property var modelData
screen: modelData
readonly property bool primaryScreen: Quickshell.screens.length === 0
|| Quickshell.screens[0] === modelData
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-osd"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors { bottom: true }
margins.bottom: 90
implicitWidth: 300
implicitHeight: 78
color: "transparent"
exclusionMode: ExclusionMode.Ignore
visible: primaryScreen && (shown || fade.running)
property bool shown: false
property string kind: "volume"
// Suppress the burst of change signals that fires while services warm up.
property bool armed: false
Timer {
interval: 1500
running: true
onTriggered: root.armed = true
}
readonly property string glyph: {
switch (root.kind) {
case "mic": return Audio.micIcon;
case "brightness": return Brightness.icon;
default: return Audio.icon;
}
}
readonly property string title: {
switch (root.kind) {
case "mic": return Audio.micMuted ? "Mic muted" : "Microphone";
case "brightness": return "Brightness";
default: return Audio.muted ? "Muted" : "Volume";
}
}
readonly property real level: {
switch (root.kind) {
case "mic": return Audio.micMuted ? 0 : Audio.micVolume;
case "brightness": return Brightness.percent / 100;
default: return Audio.muted ? 0 : Audio.volume;
}
}
readonly property color tone: {
switch (root.kind) {
case "mic": return Audio.micMuted ? Theme.accent : Theme.blue;
case "brightness": return Theme.warn;
default: return Audio.muted ? Theme.accent : Theme.primary;
}
}
function flash(k: string) {
if (!root.armed) return;
root.kind = k;
root.shown = true;
glyphIcon.bump();
hold.restart();
}
Timer {
id: hold
interval: 1600
onTriggered: root.shown = false
}
Connections {
target: Audio
function onVolumeBumped() { root.flash("volume"); }
function onMicBumped() { root.flash("mic"); }
}
Connections {
target: Brightness
function onBumped() { root.flash("brightness"); }
}
P5Panel {
id: panel
anchors.fill: parent
anchors.margins: 4
fill: Theme.mantle
fillOpacity: 0.97
border: Theme.alpha(root.tone, 0.85)
borderWidth: Theme.stroke
slash: true
slashColor: root.tone
slashWidth: 5
halftone: true
halftoneColor: root.tone
halftoneOpacity: 0.08
sheen: true
lean: 0.09
chop: 14
padding: Theme.padM
opacity: root.shown ? 1 : 0
scale: root.shown ? 1 : 0.9
Behavior on opacity {
NumberAnimation { id: fade; duration: Theme.durBase }
}
Behavior on scale {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutBack }
}
Icon {
id: glyphIcon
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.glyph
color: root.tone
font.pixelSize: 30
width: 36
}
Column {
anchors.left: glyphIcon.right
anchors.leftMargin: Theme.padM
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 4
Row {
width: parent.width
Text {
text: root.title.toUpperCase()
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Item {
width: parent.width - 90
height: 1
}
SplitText {
text: Math.round(root.level * 100) + "%"
pixelSize: Theme.fsBody
split: 1
splitOpacity: 0.5
}
}
MeterBar {
width: parent.width
height: 10
segments: 20
segmentWidth: (width - 19 * 3) / 20
spacing: 3
value: root.level * 100
activeColor: root.tone
}
}
}
}
+285
View File
@@ -0,0 +1,285 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// Full-screen power menu. Crimson stripes sweep in behind a stack of leaning
// buttons; Escape or a click on the backdrop dismisses it.
PanelWindow {
id: root
required property var modelData
screen: modelData
visible: Actions.powerMenuOpen || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-power"
WlrLayershell.keyboardFocus: Actions.powerMenuOpen
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
readonly property bool open: Actions.powerMenuOpen
onOpenChanged: {
if (open) {
openAnim.restart();
} else {
closeAnim.restart();
}
}
// ------------------------------------------------------------- backdrop
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.82)
opacity: 0
TapHandler {
onTapped: Actions.closePowerMenu()
}
}
// Diagonal stripe field sweeping in from the left.
Image {
id: stripes
anchors.fill: parent
source: Theme.texStripes
fillMode: Image.Tile
opacity: 0
smooth: false
layer.enabled: true
layer.effect: TintEffect {
tintColor: Theme.accent
}
transform: Translate {
id: stripeShift
x: -root.width
}
}
// A crimson wedge behind the buttons.
Shape {
id: wedge
anchors.fill: parent
opacity: 0
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.alpha(Theme.accentDim, 0.35)
strokeColor: "transparent"
PathPolyline {
path: [
Qt.point(root.width * 0.18, 0),
Qt.point(root.width * 0.52, 0),
Qt.point(root.width * 0.34, root.height),
Qt.point(0, root.height),
Qt.point(root.width * 0.18, 0)
]
}
}
}
// ---------------------------------------------------------------- title
Column {
id: title
anchors.left: parent.left
anchors.leftMargin: root.width * 0.12
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -60
spacing: -14
opacity: 0
SplitText {
text: "TAKE"
pixelSize: Theme.fsMega
split: 4
splitOpacity: 0.9
}
SplitText {
text: "YOUR"
pixelSize: Theme.fsMega
color: Theme.accent
split: 4
splitOpacity: 0.5
}
SplitText {
text: "TIME."
pixelSize: Theme.fsMega
split: 4
splitOpacity: 0.9
}
Text {
text: "— TAE TAKEMI"
color: Theme.primary
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 5
topPadding: 24
renderType: Text.NativeRendering
}
}
// -------------------------------------------------------------- actions
Column {
id: actions
anchors.right: parent.right
anchors.rightMargin: root.width * 0.12
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padM
Repeater {
id: buttonRepeater
model: [
{ label: "Lock", icon: "󰌾", danger: false, act: "lock" },
{ label: "Suspend", icon: "󰤄", danger: false, act: "suspend" },
{ label: "Hibernate", icon: "󰒲", danger: false, act: "hibernate" },
{ label: "Log Out", icon: "󰗽", danger: true, act: "logout" },
{ label: "Reboot", icon: "󰜉", danger: true, act: "reboot" },
{ label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" }
]
// A carrier item owns the staggered slide so the button keeps its
// own hover transform.
Item {
id: entry
required property var modelData
required property int index
width: 280
height: 52
opacity: 0
transform: Translate { id: slide; x: 90 }
P5Button {
anchors.fill: parent
text: entry.modelData.label
icon: entry.modelData.icon
destructive: entry.modelData.danger
accent: entry.modelData.danger ? Theme.accent : Theme.primary
lunge: -10
onClicked: root.invoke(entry.modelData.act)
}
// Staggered entry, one button after another.
Connections {
target: root
function onOpenChanged() {
if (root.open) entryAnim.restart();
else entry.opacity = 0;
}
}
SequentialAnimation {
id: entryAnim
PauseAnimation { duration: 40 + entry.index * 55 }
ParallelAnimation {
NumberAnimation {
target: entry; property: "opacity"
to: 1; duration: Theme.durBase
}
NumberAnimation {
target: slide; property: "x"
from: 90; to: 0
duration: Theme.durSlow
easing.type: Easing.OutBack
}
}
}
}
}
}
// Hint line.
Text {
anchors.bottom: parent.bottom
anchors.bottomMargin: 40
anchors.horizontalCenter: parent.horizontalCenter
text: "ESC TO CANCEL"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 4
opacity: backdrop.opacity
renderType: Text.NativeRendering
}
function invoke(what: string) {
Actions.closePowerMenu();
switch (what) {
case "lock": Actions.lock(); break;
case "suspend": Actions.suspend(); break;
case "hibernate": Actions.hibernate(); break;
case "logout": Actions.logout(); break;
case "reboot": Actions.reboot(); break;
case "shutdown": Actions.shutdown(); break;
}
}
// Escape closes. The layer takes exclusive focus while open.
Item {
anchors.fill: parent
focus: root.open
Keys.onEscapePressed: Actions.closePowerMenu()
}
ParallelAnimation {
id: openAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
}
NumberAnimation {
target: stripes; property: "opacity"
to: 0.1; duration: Theme.durSlow
}
NumberAnimation {
target: stripeShift; property: "x"
from: -root.width; to: 0
duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: wedge; property: "opacity"
to: 1; duration: Theme.durSlow; easing.type: Easing.OutExpo
}
NumberAnimation {
target: title; property: "opacity"
to: 1; duration: Theme.durSlow
}
}
ParallelAnimation {
id: closeAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: stripes; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: wedge; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: title; property: "opacity"
to: 0; duration: Theme.durFast
}
}
}
+148
View File
@@ -0,0 +1,148 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import "root:/config"
import "root:/components"
import "root:/services"
// Floating notification stack under the bar, top right. Replaces dunst.
PanelWindow {
id: root
required property var modelData
screen: modelData
// Only the primary bar screen shows toasts, to avoid duplicates.
readonly property bool primaryScreen: Quickshell.screens.length === 0
|| Quickshell.screens[0] === modelData
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-toasts"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors { top: true; right: true }
margins {
top: Theme.barHeight + Theme.barMargin * 2
right: Theme.barMargin
}
implicitWidth: 400
implicitHeight: Math.max(1, stack.implicitHeight)
color: "transparent"
exclusionMode: ExclusionMode.Ignore
visible: primaryScreen && stack.children.length > 0
// Toasts currently on screen, newest first.
property list<Notification> live: []
property int maxLive: 5
Connections {
target: Notifs
enabled: root.primaryScreen
function onPopupRequested(notif) {
root.live = [notif, ...root.live].slice(0, root.maxLive);
}
}
function retire(notif) {
const idx = root.live.indexOf(notif);
if (idx === -1) return;
const next = root.live.slice();
next.splice(idx, 1);
root.live = next;
}
Column {
id: stack
anchors.right: parent.right
anchors.top: parent.top
width: parent.width
spacing: Theme.padS
Repeater {
model: root.live
Item {
id: toast
required property var modelData
width: stack.width
height: card.implicitHeight
NotifCard {
id: card
width: parent.width
notif: toast.modelData
onDismissed: {
Notifs.dismiss(toast.modelData);
root.retire(toast.modelData);
}
}
// Countdown rule that drains along the bottom edge.
Rectangle {
id: fuse
anchors.bottom: parent.bottom
anchors.left: parent.left
height: 2
width: parent.width
color: Theme.alpha(card.tone, 0.7)
visible: life.running
}
NumberAnimation {
id: life
target: fuse
property: "width"
from: toast.width
to: 0
running: false
duration: Notifs.timeoutFor(toast.modelData)
easing.type: Easing.Linear
onFinished: root.retire(toast.modelData)
}
// Slide in from the right, Persona-style.
opacity: 0
transform: Translate { id: shove; x: 60 }
Component.onCompleted: {
enter.start();
const ms = Notifs.timeoutFor(toast.modelData);
if (ms > 0) life.start();
}
ParallelAnimation {
id: enter
NumberAnimation {
target: toast; property: "opacity"
to: 1; duration: Theme.durBase
}
NumberAnimation {
target: shove; property: "x"
from: 60; to: 0
duration: Theme.durSlow
easing.type: Easing.OutBack
}
}
// Hovering a toast pauses its fuse.
HoverHandler {
id: toastHover
onHoveredChanged: {
if (!life.duration) return;
if (hovered) life.pause();
else if (life.paused) life.resume();
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.retire(toast.modelData)
}
}
}
}
}