Add polished system feedback overlays
This commit is contained in:
@@ -30,6 +30,9 @@ local sysmonitor = "flatpak run io.missioncenter.MissionCenter"
|
|||||||
-- Vicinae is the Raycast-style launcher. `vicinae toggle` shows/hides the
|
-- Vicinae is the Raycast-style launcher. `vicinae toggle` shows/hides the
|
||||||
-- window against the already-running server (started in autostart.lua).
|
-- window against the already-running server (started in autostart.lua).
|
||||||
local launcher = "vicinae toggle"
|
local launcher = "vicinae toggle"
|
||||||
|
local osd = function(action)
|
||||||
|
return "$HOME/.config/quickshell/scripts/panama-osd " .. action
|
||||||
|
end
|
||||||
|
|
||||||
-- Quickshell IPC targets. See quickshell/shell.qml for the handlers.
|
-- Quickshell IPC targets. See quickshell/shell.qml for the handlers.
|
||||||
local qs = function(target, fn) return "qs ipc call " .. target .. " " .. fn end
|
local qs = function(target, fn) return "qs ipc call " .. target .. " " .. fn end
|
||||||
@@ -252,23 +255,23 @@ bind("CTRL + ALT + Delete", hl.dsp.exec_cmd(qs("powermenu", "toggle")), { descri
|
|||||||
-- ── Media and volume ────────────────────────────────────────────────────────
|
-- ── Media and volume ────────────────────────────────────────────────────────
|
||||||
-- locked = true keeps these working on the lock screen, as they do in GNOME.
|
-- locked = true keeps these working on the lock screen, as they do in GNOME.
|
||||||
-- 6% steps match the GNOME volume-step setting.
|
-- 6% steps match the GNOME volume-step setting.
|
||||||
bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 6%+"), { locked = true, repeating = true , description = "Volume up" })
|
bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd(osd("volume up 6")), { locked = true, repeating = true , description = "Volume up" })
|
||||||
bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 6%-"), { locked = true, repeating = true , description = "Volume down" })
|
bind("XF86AudioLowerVolume", hl.dsp.exec_cmd(osd("volume down 6")), { locked = true, repeating = true , description = "Volume down" })
|
||||||
bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true , description = "Mute" })
|
bind("XF86AudioMute", hl.dsp.exec_cmd(osd("volume toggle")), { locked = true , description = "Mute" })
|
||||||
bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true , description = "Mute microphone" })
|
bind("XF86AudioMicMute", hl.dsp.exec_cmd(osd("microphone toggle")), { locked = true , description = "Mute microphone" })
|
||||||
|
|
||||||
-- Fine-grained steps, matching GNOME's shift/alt volume modifiers.
|
-- Fine-grained steps, matching GNOME's shift/alt volume modifiers.
|
||||||
bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 1%+"), { locked = true, repeating = true , description = "Volume up (fine)" })
|
bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd(osd("volume up 1")), { locked = true, repeating = true , description = "Volume up (fine)" })
|
||||||
bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%-"), { locked = true, repeating = true , description = "Volume down (fine)" })
|
bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd(osd("volume down 1")), { locked = true, repeating = true , description = "Volume down (fine)" })
|
||||||
|
|
||||||
bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true , description = "Play or pause" })
|
bind("XF86AudioPlay", hl.dsp.exec_cmd(osd("media play-pause")), { locked = true , description = "Play or pause" })
|
||||||
bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true , description = "Play or pause" })
|
bind("XF86AudioPause", hl.dsp.exec_cmd(osd("media play-pause")), { locked = true , description = "Play or pause" })
|
||||||
bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true , description = "Next track" })
|
bind("XF86AudioNext", hl.dsp.exec_cmd(osd("media next")), { locked = true , description = "Next track" })
|
||||||
bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true , description = "Previous track" })
|
bind("XF86AudioPrev", hl.dsp.exec_cmd(osd("media previous")), { locked = true , description = "Previous track" })
|
||||||
bind("XF86AudioStop", hl.dsp.exec_cmd("playerctl stop"), { locked = true , description = "Stop playback" })
|
bind("XF86AudioStop", hl.dsp.exec_cmd(osd("media stop")), { locked = true , description = "Stop playback" })
|
||||||
|
|
||||||
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true , description = "Brightness up" })
|
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd(osd("brightness up 5")), { locked = true, repeating = true , description = "Brightness up" })
|
||||||
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true , description = "Brightness down" })
|
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd(osd("brightness down 5")), { locked = true, repeating = true , description = "Brightness down" })
|
||||||
|
|
||||||
-- Hardware keys GNOME mapped that have obvious equivalents.
|
-- Hardware keys GNOME mapped that have obvious equivalents.
|
||||||
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// Compact, click-through system feedback. Repeated hardware-key presses update
|
||||||
|
// this one surface in place, avoiding a stack of notification cards.
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
import qs.widgets
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var modelData: null
|
||||||
|
readonly property var presentation: OsdState.state
|
||||||
|
readonly property bool belongsHere: !OsdState.monitorName || !root.modelData
|
||||||
|
|| root.modelData.name === OsdState.monitorName
|
||||||
|
readonly property bool requestedVisible: OsdState.active && root.belongsHere
|
||||||
|
readonly property int horizontalPadding: 18
|
||||||
|
readonly property int contentGap: 14
|
||||||
|
readonly property int progressWidth: 156
|
||||||
|
readonly property int progressLabelWidth: 48
|
||||||
|
readonly property int messageWidth: Math.min(300, Math.max(52, Math.ceil(labelMetrics.advanceWidth)))
|
||||||
|
readonly property int desiredWidth: root.horizontalPadding * 2 + 24 + root.contentGap
|
||||||
|
+ (root.presentation.progress
|
||||||
|
? root.progressWidth + root.contentGap + root.progressLabelWidth
|
||||||
|
: root.messageWidth)
|
||||||
|
|
||||||
|
screen: root.modelData
|
||||||
|
anchors.bottom: true
|
||||||
|
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||||
|
exclusiveZone: 0
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
implicitWidth: root.desiredWidth
|
||||||
|
implicitHeight: 64
|
||||||
|
color: "transparent"
|
||||||
|
mask: Region {}
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "qs-popover-osd"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
property bool mapped: false
|
||||||
|
visible: root.mapped
|
||||||
|
|
||||||
|
onRequestedVisibleChanged: {
|
||||||
|
if (root.requestedVisible) {
|
||||||
|
unmapTimer.stop();
|
||||||
|
root.mapped = true;
|
||||||
|
entrance.restart();
|
||||||
|
} else if (root.mapped) {
|
||||||
|
unmapTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (root.requestedVisible)
|
||||||
|
root.mapped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: unmapTimer
|
||||||
|
interval: Theme.durNormal
|
||||||
|
onTriggered: root.mapped = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: surface
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.popoverRadius
|
||||||
|
color: Theme.alpha(Theme.bgPopover, 0.9)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.fg, 0.1)
|
||||||
|
opacity: root.requestedVisible ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: Translate {
|
||||||
|
id: slide
|
||||||
|
y: root.requestedVisible ? 0 : 8
|
||||||
|
Behavior on y {
|
||||||
|
NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrismEdge {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 1
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
inset: parent.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: root.contentGap
|
||||||
|
|
||||||
|
ThemedIcon {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
size: 24
|
||||||
|
icon: root.presentation.icon
|
||||||
|
iconFallback: "dialog-information-symbolic"
|
||||||
|
tint: Theme.fg
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: root.presentation.progress
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: visible ? root.progressWidth : 0
|
||||||
|
height: 8
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: height / 2
|
||||||
|
color: Theme.alpha(Theme.fg, 0.14)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: parent.width * root.presentation.ratio
|
||||||
|
radius: height / 2
|
||||||
|
color: Theme.accent
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: root.presentation.progress ? root.progressLabelWidth : root.messageWidth
|
||||||
|
text: root.presentation.label
|
||||||
|
color: Theme.fg
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
horizontalAlignment: root.presentation.progress ? Text.AlignRight : Text.AlignLeft
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: labelMetrics
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
text: root.presentation.label
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: entrance
|
||||||
|
NumberAnimation {
|
||||||
|
target: surface
|
||||||
|
property: "opacity"
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
duration: Theme.durNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: slide
|
||||||
|
property: "y"
|
||||||
|
from: 8
|
||||||
|
to: 0
|
||||||
|
duration: Theme.durNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
module qs.modules.osd
|
||||||
|
Osd 1.0 Osd.qml
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import QtQuick
|
||||||
|
import "../modules/osd/OsdModel.js" as OsdModel
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool active: false
|
||||||
|
property string monitorName: ""
|
||||||
|
property var state: OsdModel.messageState("dialog-information-symbolic", "", 1400)
|
||||||
|
|
||||||
|
function present(next: var): void {
|
||||||
|
root.state = next;
|
||||||
|
root.monitorName = Hyprland.focusedMonitor?.name ?? "";
|
||||||
|
root.active = true;
|
||||||
|
if (next.duration > 0) {
|
||||||
|
hideTimer.interval = next.duration;
|
||||||
|
hideTimer.restart();
|
||||||
|
} else {
|
||||||
|
hideTimer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function progress(kind: string, value: int, maximum: int, label: string): void {
|
||||||
|
root.present(OsdModel.progressState(kind, value, maximum, label, 1400));
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(kind: string, label: string): void {
|
||||||
|
root.present(OsdModel.messageState(kind, label, 1400));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide(): void {
|
||||||
|
hideTimer.stop();
|
||||||
|
root.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: hideTimer
|
||||||
|
interval: 1400
|
||||||
|
onTriggered: root.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ import qs.modules.datemenu
|
|||||||
import qs.modules.focus
|
import qs.modules.focus
|
||||||
import qs.modules.signals
|
import qs.modules.signals
|
||||||
import qs.modules.settings
|
import qs.modules.settings
|
||||||
|
import qs.modules.osd
|
||||||
|
|
||||||
// Clipboard and datemenu ship explicit qmldir manifests because they were
|
// Clipboard and datemenu ship explicit qmldir manifests because they were
|
||||||
// added while this daily-driver shell was already running; that also keeps
|
// added while this daily-driver shell was already running; that also keeps
|
||||||
@@ -72,6 +73,11 @@ ShellRoot {
|
|||||||
SignalGlass {}
|
SignalGlass {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: Quickshell.screens
|
||||||
|
Osd {}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Single-instance overlays ────────────────────────────────────────────
|
// ── Single-instance overlays ────────────────────────────────────────────
|
||||||
// These are always constructed but only *visible* when ShellState says so.
|
// These are always constructed but only *visible* when ShellState says so.
|
||||||
// They're cheap while hidden, and keeping them alive means opening the
|
// They're cheap while hidden, and keeping them alive means opening the
|
||||||
@@ -194,6 +200,25 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "osd"
|
||||||
|
|
||||||
|
function progress(kind: string, value: int, maximum: int, label: string): void {
|
||||||
|
OsdState.progress(kind, value, maximum, label);
|
||||||
|
}
|
||||||
|
function message(kind: string, label: string): void {
|
||||||
|
OsdState.message(kind, label);
|
||||||
|
}
|
||||||
|
function hide(): void { OsdState.hide(); }
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
active: OsdState.active,
|
||||||
|
monitorName: OsdState.monitorName,
|
||||||
|
state: OsdState.state
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "activity"
|
target: "activity"
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ fail() {
|
|||||||
[[ -f "$osd" ]] || fail 'Prism OSD surface is missing'
|
[[ -f "$osd" ]] || fail 'Prism OSD surface is missing'
|
||||||
[[ -f "$state" ]] || fail 'OSD presentation state is missing'
|
[[ -f "$state" ]] || fail 'OSD presentation state is missing'
|
||||||
[[ -f "$qs_dir/modules/osd/qmldir" ]] || fail 'OSD module manifest is missing'
|
[[ -f "$qs_dir/modules/osd/qmldir" ]] || fail 'OSD module manifest is missing'
|
||||||
|
rg -q '^import Quickshell$' "$state" \
|
||||||
|
|| fail 'OSD singleton does not import the Quickshell Singleton type'
|
||||||
|
|
||||||
rg -Fq 'import qs.modules.osd' "$shell_file" || fail 'shell does not import the OSD module'
|
rg -Fq 'import qs.modules.osd' "$shell_file" || fail 'shell does not import the OSD module'
|
||||||
[[ "$(rg -c '^[[:space:]]*Osd \{\}' "$shell_file")" -eq 1 ]] \
|
[[ "$(rg -c '^[[:space:]]*Osd \{\}' "$shell_file")" -eq 1 ]] \
|
||||||
|
|||||||
Reference in New Issue
Block a user