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.
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
ShellRoot {
|
||||
PolkitDialog {}
|
||||
Osd {}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
|
@ -22,6 +24,22 @@ ShellRoot {
|
|||
LeftBar { id: leftBarInstance; screen: modelData }
|
||||
RightBar { id: rightBarInstance; screen: modelData }
|
||||
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, A, exec, $launcher
|
||||
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 SHIFT, Q, exit,
|
||||
bind = $mod, V, togglefloating,
|
||||
|
|
@ -173,6 +178,24 @@ bind = $mod SHIFT, 8, movetoworkspace, 8
|
|||
bind = $mod SHIFT, 9, movetoworkspace, 9
|
||||
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
|
||||
bindm = $mod, mouse:272, movewindow
|
||||
bindm = $mod, mouse:273, resizewindow
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ in
|
|||
bibata-cursors # Cursor theme(s) for the cursor picker
|
||||
xcur2png # Renders Xcursor files to PNG for the cursor picker's previews
|
||||
imagemagick # `identify`, used to pick the right cursor-preview size
|
||||
playerctl # MPRIS media control, for the function-key media binds
|
||||
|
||||
# Apps
|
||||
blender
|
||||
|
|
@ -66,6 +67,7 @@ in
|
|||
# Global command-line utilities available everywhere
|
||||
environment.systemPackages = with pkgs; [
|
||||
git
|
||||
gh # GitHub CLI — was only installed ad-hoc before (login didn't survive)
|
||||
vim
|
||||
fastfetch
|
||||
gparted
|
||||
|
|
|
|||
|
|
@ -25,8 +25,32 @@ case "${1:-get}" in
|
|||
ddcutil setvcp 10 "$pct" > /dev/null
|
||||
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
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
Loading…
Reference in a new issue