mirror of
https://github.com/Nexoniarz/nexoniarz-config-nixos.git
synced 2026-11-03 04:24:27 +00:00
Add on-screen display, sidebar keybinds, function keys, gh package
- Add Osd.qml: themed top-center popup for volume/mic/brightness/media changes. Volume and mic react directly to Pipewire so they fire for any change, not just our own keybinds; brightness is notified explicitly (no live service exists for DDC/CI writes); media is driven by two playerctl --follow watchers (status + metadata) so track changes, pause/resume, and stop all show correctly without double-firing - Add Super+Shift+Z/X to toggle the Left/RightBar sidebars, via a new Quickshell IpcHandler wired to the same expand/collapse logic as the BottomBar arrows - Add function-key binds for volume up/down/mute, mic mute, brightness up/down, and media play/pause/stop/next/prev (wpctl/playerctl); extend brightness-ctl with relative up/down steps for both the backlight and DDC/CI paths - Add gh and playerctl to packages
This commit is contained in:
parent
ea7fefbba8
commit
1f5f9795bc
5 changed files with 267 additions and 1 deletions
199
dotfiles/quickshell/Osd.qml
Normal file
199
dotfiles/quickshell/Osd.qml
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
|
||||||
|
// On-screen display: a themed top-center popup (same placement/layer-shell
|
||||||
|
// pattern as PolkitDialog and mako notifications) for volume, mic mute,
|
||||||
|
// brightness, and media changes — the transient overlay every other DE
|
||||||
|
// shows instead of making you open a panel to check.
|
||||||
|
//
|
||||||
|
// Volume/mic react directly to Pipewire, so they fire for ANY change
|
||||||
|
// (function keys, the RightBar sliders, pavucontrol, etc.), not just ones
|
||||||
|
// that went through our own keybinds. Brightness has no such live service
|
||||||
|
// to bind to (DDC/CI writes aren't broadcast anywhere), so its two
|
||||||
|
// hyprland.conf binds explicitly call `quickshell ipc call osd
|
||||||
|
// brightness <pct>` after brightness-ctl. Media is driven by two
|
||||||
|
// long-running `playerctl --follow` processes (status, and metadata for
|
||||||
|
// track-change detection).
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string osdText: ""
|
||||||
|
property bool showBar: false
|
||||||
|
property real barValue: 0
|
||||||
|
property bool barDanger: false
|
||||||
|
|
||||||
|
function present(text, bar, value, danger) {
|
||||||
|
root.osdText = text;
|
||||||
|
root.showBar = bar;
|
||||||
|
root.barValue = value || 0;
|
||||||
|
root.barDanger = !!danger;
|
||||||
|
osdWindow.visible = true;
|
||||||
|
hideTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "osd"
|
||||||
|
function brightness(pct: string): void {
|
||||||
|
var v = parseInt(pct);
|
||||||
|
if (isNaN(v)) return;
|
||||||
|
root.present("Brightness " + v + "%", true, v / 100, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: hideTimer
|
||||||
|
interval: 1600
|
||||||
|
onTriggered: osdWindow.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Volume / mic --
|
||||||
|
PwObjectTracker {
|
||||||
|
objects: {
|
||||||
|
var arr = [];
|
||||||
|
if (Pipewire.defaultAudioSink) arr.push(Pipewire.defaultAudioSink);
|
||||||
|
if (Pipewire.defaultAudioSource) arr.push(Pipewire.defaultAudioSource);
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipewire reports the already-current volume/mute state once as soon
|
||||||
|
// as it connects — without these guards that fires an OSD on every
|
||||||
|
// Quickshell start/login, not just on real changes.
|
||||||
|
property bool sinkReady: false
|
||||||
|
property bool sourceReady: false
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.audio : null
|
||||||
|
function onVolumesChanged() {
|
||||||
|
if (!root.sinkReady) { root.sinkReady = true; return; }
|
||||||
|
var sink = Pipewire.defaultAudioSink;
|
||||||
|
if (sink.audio.muted) return; // covered by onMutedChanged instead
|
||||||
|
root.present("Volume " + Math.round(sink.audio.volume * 100) + "%", true, sink.audio.volume, false);
|
||||||
|
}
|
||||||
|
function onMutedChanged() {
|
||||||
|
if (!root.sinkReady) { root.sinkReady = true; return; }
|
||||||
|
var sink = Pipewire.defaultAudioSink;
|
||||||
|
root.present(sink.audio.muted ? "Volume Muted" : "Volume " + Math.round(sink.audio.volume * 100) + "%",
|
||||||
|
!sink.audio.muted, sink.audio.volume, sink.audio.muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null
|
||||||
|
function onMutedChanged() {
|
||||||
|
if (!root.sourceReady) { root.sourceReady = true; return; }
|
||||||
|
var source = Pipewire.defaultAudioSource;
|
||||||
|
root.present(source.audio.muted ? "Microphone Muted" : "Microphone Unmuted",
|
||||||
|
false, 0, source.audio.muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Media (MPRIS via playerctl) --
|
||||||
|
property string lastTitle: ""
|
||||||
|
property double lastMetaAt: 0
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: statusFollow
|
||||||
|
command: ["playerctl", "--follow", "status"]
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (data) => {
|
||||||
|
var status = data.trim();
|
||||||
|
if (status === "Paused") {
|
||||||
|
root.present("⏸ Paused", false, 0, false);
|
||||||
|
} else if (status === "Stopped") {
|
||||||
|
root.present("⏹ Stopped", false, 0, false);
|
||||||
|
root.lastTitle = "";
|
||||||
|
} else if (status === "Playing") {
|
||||||
|
// A track change fires both a metadata line and a
|
||||||
|
// Playing status line in short order — the metadata
|
||||||
|
// handler below already popped the OSD for that case,
|
||||||
|
// so only announce "Resumed" if nothing else just did.
|
||||||
|
if (Date.now() - root.lastMetaAt > 400) {
|
||||||
|
root.present("▶ Resumed", false, 0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: metadataFollow
|
||||||
|
command: ["playerctl", "--follow", "metadata", "--format", "{{title}}\t{{artist}}"]
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (data) => {
|
||||||
|
var parts = data.split("\t");
|
||||||
|
var title = (parts[0] || "").trim();
|
||||||
|
var artist = (parts[1] || "").trim();
|
||||||
|
if (!title || title === root.lastTitle) return;
|
||||||
|
root.lastTitle = title;
|
||||||
|
root.lastMetaAt = Date.now();
|
||||||
|
root.present("▶ " + title + (artist ? " — " + artist : ""), false, 0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
statusFollow.running = true;
|
||||||
|
metadataFollow.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: osdWindow
|
||||||
|
visible: false
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.namespace: "osd"
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
focusable: false
|
||||||
|
anchors { top: true }
|
||||||
|
margins.top: 12
|
||||||
|
exclusiveZone: 0
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
implicitWidth: 300
|
||||||
|
implicitHeight: osdContent.implicitHeight + 24
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.bg
|
||||||
|
border.width: 2
|
||||||
|
border.color: root.barDanger ? Theme.danger : Theme.accent
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: osdContent
|
||||||
|
x: 14
|
||||||
|
y: 12
|
||||||
|
width: parent.width - 28
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.osdText
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: root.showBar
|
||||||
|
width: parent.width
|
||||||
|
height: 6
|
||||||
|
color: Theme.bgAlt
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.border
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width * Math.max(0, Math.min(1, root.barValue))
|
||||||
|
height: parent.height
|
||||||
|
color: root.barDanger ? Theme.danger : Theme.accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,9 +9,11 @@
|
||||||
// menu silently does nothing.
|
// menu silently does nothing.
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
PolkitDialog {}
|
PolkitDialog {}
|
||||||
|
Osd {}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
@ -22,6 +24,22 @@ ShellRoot {
|
||||||
LeftBar { id: leftBarInstance; screen: modelData }
|
LeftBar { id: leftBarInstance; screen: modelData }
|
||||||
RightBar { id: rightBarInstance; screen: modelData }
|
RightBar { id: rightBarInstance; screen: modelData }
|
||||||
BottomBar { screen: modelData; leftBar: leftBarInstance; rightBar: rightBarInstance }
|
BottomBar { screen: modelData; leftBar: leftBarInstance; rightBar: rightBarInstance }
|
||||||
|
|
||||||
|
// Lets Hyprland keybinds (Super+Shift+Z/X, see hyprland.conf)
|
||||||
|
// toggle the sidebars via `quickshell ipc call sidebars
|
||||||
|
// toggleLeft/toggleRight` — same open/close behavior as
|
||||||
|
// clicking the arrows on BottomBar.
|
||||||
|
IpcHandler {
|
||||||
|
target: "sidebars"
|
||||||
|
function toggleLeft(): void {
|
||||||
|
leftBarInstance.expanded = !leftBarInstance.expanded;
|
||||||
|
if (!leftBarInstance.expanded) leftBarInstance.activePanel = "";
|
||||||
|
}
|
||||||
|
function toggleRight(): void {
|
||||||
|
rightBarInstance.expanded = !rightBarInstance.expanded;
|
||||||
|
if (!rightBarInstance.expanded) rightBarInstance.activePanel = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,11 @@ bind = $mod, T, exec, $terminal
|
||||||
bind = $mod, E, exec, $fileManager
|
bind = $mod, E, exec, $fileManager
|
||||||
bind = $mod, A, exec, $launcher
|
bind = $mod, A, exec, $launcher
|
||||||
bind = $mod, D, exec, show-desktop-toggle
|
bind = $mod, D, exec, show-desktop-toggle
|
||||||
|
|
||||||
|
# Sidebar toggles — mirrors clicking the arrows on BottomBar, via the
|
||||||
|
# IpcHandler registered in shell.qml.
|
||||||
|
bind = $mod SHIFT, Z, exec, quickshell ipc call sidebars toggleLeft
|
||||||
|
bind = $mod SHIFT, X, exec, quickshell ipc call sidebars toggleRight
|
||||||
bind = $mod, Q, killactive,
|
bind = $mod, Q, killactive,
|
||||||
bind = $mod SHIFT, Q, exit,
|
bind = $mod SHIFT, Q, exit,
|
||||||
bind = $mod, V, togglefloating,
|
bind = $mod, V, togglefloating,
|
||||||
|
|
@ -173,6 +178,24 @@ bind = $mod SHIFT, 8, movetoworkspace, 8
|
||||||
bind = $mod SHIFT, 9, movetoworkspace, 9
|
bind = $mod SHIFT, 9, movetoworkspace, 9
|
||||||
bind = $mod SHIFT, 0, movetoworkspace, 10
|
bind = $mod SHIFT, 0, movetoworkspace, 10
|
||||||
|
|
||||||
|
# Function keys — media/volume/brightness. bindl (not bind) so these still
|
||||||
|
# work with the screen locked; bindel additionally repeats while held, for
|
||||||
|
# the ones you'd actually want to hold down (volume/brightness ramps).
|
||||||
|
bindel = , XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+
|
||||||
|
bindel = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
|
||||||
|
bindl = , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
|
||||||
|
bindl = , XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle
|
||||||
|
# Chained with an OSD call: unlike volume/mic, brightness has no live
|
||||||
|
# service Quickshell can react to (DDC/CI writes aren't broadcast
|
||||||
|
# anywhere), so the keybind has to tell the OSD explicitly.
|
||||||
|
bindel = , XF86MonBrightnessUp, exec, brightness-ctl up && quickshell ipc call osd brightness "$(brightness-ctl get)"
|
||||||
|
bindel = , XF86MonBrightnessDown, exec, brightness-ctl down && quickshell ipc call osd brightness "$(brightness-ctl get)"
|
||||||
|
bindl = , XF86AudioPlay, exec, playerctl play-pause
|
||||||
|
bindl = , XF86AudioPause, exec, playerctl play-pause
|
||||||
|
bindl = , XF86AudioStop, exec, playerctl stop
|
||||||
|
bindl = , XF86AudioNext, exec, playerctl next
|
||||||
|
bindl = , XF86AudioPrev, exec, playerctl previous
|
||||||
|
|
||||||
# Mouse: drag/resize floating windows
|
# Mouse: drag/resize floating windows
|
||||||
bindm = $mod, mouse:272, movewindow
|
bindm = $mod, mouse:272, movewindow
|
||||||
bindm = $mod, mouse:273, resizewindow
|
bindm = $mod, mouse:273, resizewindow
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ in
|
||||||
bibata-cursors # Cursor theme(s) for the cursor picker
|
bibata-cursors # Cursor theme(s) for the cursor picker
|
||||||
xcur2png # Renders Xcursor files to PNG for the cursor picker's previews
|
xcur2png # Renders Xcursor files to PNG for the cursor picker's previews
|
||||||
imagemagick # `identify`, used to pick the right cursor-preview size
|
imagemagick # `identify`, used to pick the right cursor-preview size
|
||||||
|
playerctl # MPRIS media control, for the function-key media binds
|
||||||
|
|
||||||
# Apps
|
# Apps
|
||||||
blender
|
blender
|
||||||
|
|
@ -66,6 +67,7 @@ in
|
||||||
# Global command-line utilities available everywhere
|
# Global command-line utilities available everywhere
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
git
|
git
|
||||||
|
gh # GitHub CLI — was only installed ad-hoc before (login didn't survive)
|
||||||
vim
|
vim
|
||||||
fastfetch
|
fastfetch
|
||||||
gparted
|
gparted
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,32 @@ case "${1:-get}" in
|
||||||
ddcutil setvcp 10 "$pct" > /dev/null
|
ddcutil setvcp 10 "$pct" > /dev/null
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
up|down)
|
||||||
|
# brightnessctl takes a relative step directly, but ddcutil's
|
||||||
|
# setvcp only takes an absolute value — so the DDC/CI path (this
|
||||||
|
# machine's AOC 27G2G8 has no backlight device, so it always takes
|
||||||
|
# this path) has to read the current level and clamp the step
|
||||||
|
# itself.
|
||||||
|
step="${2:-5}"
|
||||||
|
if has_backlight; then
|
||||||
|
if [ "$1" = "up" ]; then
|
||||||
|
brightnessctl set "+${step}%" > /dev/null
|
||||||
|
else
|
||||||
|
brightnessctl set "${step}%-" > /dev/null
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
cur=$(ddcutil getvcp 10 --brief 2>/dev/null | awk '{print $4}')
|
||||||
|
cur="${cur:-50}"
|
||||||
|
if [ "$1" = "up" ]; then
|
||||||
|
new=$(( cur + step > 100 ? 100 : cur + step ))
|
||||||
|
else
|
||||||
|
new=$(( cur - step < 0 ? 0 : cur - step ))
|
||||||
|
fi
|
||||||
|
ddcutil setvcp 10 "$new" > /dev/null
|
||||||
|
fi
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "usage: brightness-ctl [get|set <0-100>]" >&2
|
echo "usage: brightness-ctl [get|set <0-100>|up [step]|down [step]]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue