From 7a0073de88e92c925116c114e476c04183fc3115 Mon Sep 17 00:00:00 2001 From: mjkwiatkowski Date: Tue, 10 Feb 2026 08:13:51 +0100 Subject: feat: added disappearing waybar --- hypr/dynamic-cursor.conf | 139 +++++++++++++++++++++++++++++++ hypr/hyprland.conf | 10 ++- hypr/hyprlock.conf | 2 +- hypr/scripts/waybar_peek.py | 193 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 hypr/dynamic-cursor.conf create mode 100755 hypr/scripts/waybar_peek.py (limited to 'hypr') diff --git a/hypr/dynamic-cursor.conf b/hypr/dynamic-cursor.conf new file mode 100644 index 0000000..f343737 --- /dev/null +++ b/hypr/dynamic-cursor.conf @@ -0,0 +1,139 @@ +plugin:dynamic-cursors { + + # enables the plugin + enabled = true + + # sets the cursor behaviour, supports these values: + # tilt - tilt the cursor based on x-velocity + # rotate - rotate the cursor based on movement direction + # stretch - stretch the cursor shape based on direction and velocity + # none - do not change the cursors behaviour + mode = stretch + + # minimum angle difference in degrees after which the shape is changed + # smaller values are smoother, but more expensive for hw cursors + threshold = 2 + + # override the mode behaviour per shape + # this is a keyword and can be repeated many times + # by default, there are no rules added + # see the dedicated `shape rules` section below! + #shaperule = , (optional), : , ... + # shaperule = , (optional), : , ... + + # for mode = rotate + rotate { + + # length in px of the simulated stick used to rotate the cursor + # most realistic if this is your actual cursor size + length = 20 + + # clockwise offset applied to the angle in degrees + # this will apply to ALL shapes + offset = 0.0 + } + + # for mode = tilt + tilt { + + # controls how powerful the tilt is, the lower, the more power + # this value controls at which speed (px/s) the full tilt is reached + limit = 5000 + + # relationship between speed and tilt, supports these values: + # linear - a linear function is used + # quadratic - a quadratic function is used (most realistic to actual air drag) + # negative_quadratic - negative version of the quadratic one, feels more aggressive + # see `activation` in `src/mode/utils.cpp` for how exactly the calculation is done + function = quadratic + + # time window (ms) over which the speed is calculated + # higher values will make slow motions smoother but more delayed + window = 100 + + # full tilt for each side (°) + full_tilt = 60 + } + + # for mode = stretch + stretch { + + # controls how much the cursor is stretched + # this value controls at which speed (px/s) the full stretch is reached + # the full stretch being twice the original length + limit = 4000 + + # relationship between speed and stretch amount, supports these values: + # linear - a linear function is used + # quadratic - a quadratic function is used + # negative_quadratic - negative version of the quadratic one, feels more aggressive + # see `activation` in `src/mode/utils.cpp` for how exactly the calculation is done + function = negative_quadratic + + # time window (ms) over which the speed is calculated + # higher values will make slow motions smoother but more delayed + window = 100 + } + + # configure shake to find + # magnifies the cursor if its is being shaken + shake { + + # enables shake to find + enabled = true + + # use nearest-neighbour (pixelated) scaling when shaking + # may look weird when effects are enabled + nearest = true + + # controls how soon a shake is detected + # lower values mean sooner + threshold = 6.0 + + # magnification level immediately after shake start + base = 4.0 + # magnification increase per second when continuing to shake + speed = 4.0 + # how much the speed is influenced by the current shake intensitiy + influence = 0.0 + + # maximal magnification the cursor can reach + # values below 1 disable the limit (e.g. 0) + limit = 0.0 + + # time in millseconds the cursor will stay magnified after a shake has ended + timeout = 2000 + + # show cursor behaviour `tilt`, `rotate`, etc. while shaking + effects = false + + # enable ipc events for shake + # see the `ipc` section below + ipc = false + } + + # use hyprcursor to get a higher resolution texture when the cursor is magnified + # see the `hyprcursor` section below + hyprcursor { + + # use nearest-neighbour (pixelated) scaling when magnifing beyond texture size + # this will also have effect without hyprcursor support being enabled + # 0 / false - never use pixelated scaling + # 1 / true - use pixelated when no highres image + # 2 - always use pixleated scaling + nearest = true + + # enable dedicated hyprcursor support + enabled = true + + # resolution in pixels to load the magnified shapes at + # be warned that loading a very high-resolution image will take a long time and might impact memory consumption + # -1 means we use [normal cursor size] * [shake:base option] + resolution = -1 + + # shape to use when clientside cursors are being magnified + # see the shape-name property of shape rules for possible names + # specifying clientside will use the actual shape, but will be pixelated + fallback = clientside + } +} diff --git a/hypr/hyprland.conf b/hypr/hyprland.conf index 3120920..1c4354f 100644 --- a/hypr/hyprland.conf +++ b/hypr/hyprland.conf @@ -2,11 +2,12 @@ ### ENVIRONMENT VARIABLES ### ############################# + # See https://wiki.hypr.land/Configuring/Environment-variables/ #These are some of the environment variables suggested by the Hyprland wiki. env = HYPRCURSOR_SIZE,24 -#env = HYPRCURSOR_THEME,rose-pine-cursor +env = HYPRCURSOR_THEME,notawita-gray # Enables hyrpland traces for debugging. env = HYPRLAND_TRACE,1 @@ -71,6 +72,9 @@ $menu = rofi -show drun # Or execute your favorite apps at launch like this: exec-once = dunst +exec-once = hyprpm reload +# To toggle on and off waybar +exec-once = ~/.config/hypr/scripts/waybar_peek.py & # Forces applications to require a password prompt to elevate priviliges. exec-once = systemctl --user start hyprpolkitagent @@ -331,6 +335,8 @@ bindl = , XF86AudioPause, exec, playerctl play-pause bindl = , XF86AudioPlay, exec, playerctl play-pause bindl = , XF86AudioPrev, exec, playerctl previous +bind = , xf86poweroff , exec, hyprlock + ############################## ### WINDOWS AND WORKSPACES ### ############################## @@ -346,3 +352,5 @@ bindl = , XF86AudioPrev, exec, playerctl previous # Fix some dragging issues with XWayland # windowrule = nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned:0 + +source = ~/.config/hypr/dynamic-cursor.conf diff --git a/hypr/hyprlock.conf b/hypr/hyprlock.conf index ee67978..e700fea 100644 --- a/hypr/hyprlock.conf +++ b/hypr/hyprlock.conf @@ -35,7 +35,7 @@ animations { background { monitor = - color = rgba(120, 120, 120, 1) + color = rgba(148, 148, 148, 1) } auth { diff --git a/hypr/scripts/waybar_peek.py b/hypr/scripts/waybar_peek.py new file mode 100755 index 0000000..227b425 --- /dev/null +++ b/hypr/scripts/waybar_peek.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Waybar Peek - Auto-hide for Hyprland (Multi-monitor) +Shows waybar when cursor is at top edge, hides when workspace has windows. + +Toggle auto-hide: pkill -HUP -f waybar_peek +""" + +import json +import os +import signal +import socket +import time +from pathlib import Path + +# Configuration +PIXEL_THRESHOLD = 5 # Show bar when within 5px of top +PIXEL_THRESHOLD_HIDE = 50 # Hide when cursor goes below 50px +POLL_INTERVAL = 0.1 # Poll every 100ms + +class WaybarPeek: + def __init__(self): + self.xdg_runtime = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}") + self.hypr_sig = os.environ.get("HYPRLAND_INSTANCE_SIGNATURE", "") + self.socket_path = f"{self.xdg_runtime}/hypr/{self.hypr_sig}/.socket.sock" + + self.cursor_at_top = False + self.last_visibility = None + self.waybar_pid = None + self.enabled = True # Auto-hide enabled by default + + # Setup signal handler for toggle + signal.signal(signal.SIGHUP, self.toggle_handler) + + def toggle_handler(self, signum, frame): + """Handle SIGHUP to toggle auto-hide on/off""" + self.enabled = not self.enabled + status = "enabled" if self.enabled else "disabled" + print(f"Auto-hide {status}") + + if not self.enabled: + # When disabled, always show the bar + self.set_waybar_visible(True) + self.last_visibility = True + else: + # When re-enabled, force state recalculation + self.last_visibility = None + + def hypr_query(self, cmd: str) -> str: + """Query Hyprland via socket""" + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.socket_path) + sock.send(cmd.encode()) + response = b"" + while True: + chunk = sock.recv(4096) + if not chunk: + break + response += chunk + sock.close() + return response.decode() + except Exception: + return "" + + def get_cursor_pos(self) -> tuple: + """Get cursor position as (x, y)""" + try: + data = json.loads(self.hypr_query("j/cursorpos")) + return (data.get("x", 0), data.get("y", 0)) + except Exception: + return (0, 0) + + def get_monitors(self) -> list: + """Get all monitors with their bounds""" + try: + return json.loads(self.hypr_query("j/monitors")) + except Exception: + return [] + + def check_windows(self) -> bool: + """Check if active workspace has windows""" + try: + data = json.loads(self.hypr_query("j/activeworkspace")) + return data.get("windows", 0) > 0 + except Exception: + return False + + def find_waybar_pid(self) -> int: + """Find waybar process ID""" + try: + for entry in Path("/proc").iterdir(): + if not entry.is_dir() or not entry.name.isdigit(): + continue + comm_file = entry / "comm" + if comm_file.exists(): + comm = comm_file.read_text().strip() + if comm in ("waybar", ".waybar-wrapped"): + return int(entry.name) + except Exception: + pass + return None + + def set_waybar_visible(self, visible: bool) -> bool: + """Send signal to waybar to show/hide""" + if self.waybar_pid is None: + self.waybar_pid = self.find_waybar_pid() + + if self.waybar_pid is None: + return False + + try: + sig = signal.SIGUSR2 if visible else signal.SIGUSR1 + os.kill(self.waybar_pid, sig) + return True + except ProcessLookupError: + self.waybar_pid = self.find_waybar_pid() + if self.waybar_pid: + try: + sig = signal.SIGUSR2 if visible else signal.SIGUSR1 + os.kill(self.waybar_pid, sig) + return True + except Exception: + pass + except Exception: + pass + return False + + def is_cursor_at_top(self) -> bool: + """Check if cursor is at top edge of any monitor""" + cursor_x, cursor_y = self.get_cursor_pos() + monitors = self.get_monitors() + + for m in monitors: + mx, my = m.get("x", 0), m.get("y", 0) + mw, mh = m.get("width", 0), m.get("height", 0) + + # Check if cursor is on this monitor + if mx <= cursor_x <= mx + mw and my <= cursor_y <= my + mh: + # Calculate local Y position relative to this monitor + local_y = cursor_y - my + + # Use different thresholds based on current state + threshold = PIXEL_THRESHOLD_HIDE if self.cursor_at_top else PIXEL_THRESHOLD + return local_y <= threshold + + return False + + def run(self): + """Main loop""" + print(f"waybar-peek started (PID: {os.getpid()})") + print(f"Socket: {self.socket_path}") + print(f"Toggle auto-hide: pkill -HUP -f waybar_peek") + + # Initial state + windows_opened = self.check_windows() + self.last_visibility = not windows_opened + + while True: + try: + # Skip auto-hide logic if disabled + if not self.enabled: + time.sleep(POLL_INTERVAL) + continue + + # Check cursor position + new_cursor_at_top = self.is_cursor_at_top() + if new_cursor_at_top != self.cursor_at_top: + self.cursor_at_top = new_cursor_at_top + + # Check windows + windows_opened = self.check_windows() + + # Determine visibility: show if cursor at top OR no windows + should_be_visible = self.cursor_at_top or not windows_opened + + # Update waybar if state changed + if should_be_visible != self.last_visibility: + self.set_waybar_visible(should_be_visible) + self.last_visibility = should_be_visible + + time.sleep(POLL_INTERVAL) + + except KeyboardInterrupt: + print("\nExiting...") + break + except Exception as e: + print(f"Error: {e}") + time.sleep(1) + +if __name__ == "__main__": + app = WaybarPeek() + app.run() -- cgit v1.2.3