diff --git a/desktopenvs/hyprdrive/kitty/kitty.conf b/desktopenvs/hyprdrive/kitty/kitty.conf index 18dea78..2ab17e9 100644 --- a/desktopenvs/hyprdrive/kitty/kitty.conf +++ b/desktopenvs/hyprdrive/kitty/kitty.conf @@ -12,11 +12,12 @@ scrollback_lines 20000 background_opacity 0.4 background_blur 1 -# Holographic scanlines + noise grain + soft ambient violet/crimson bloom, +# Holographic scanlines + gentle violet tint + sparse magenta/crimson grain, # matching the suite's HologramOverlay treatment (orbit-menu/astro-menu/ -# station-bar/beacon) — a static tiled equivalent since kitty has no -# per-frame draw hook to animate it, and no shader hook to bloom glyphs -# directly, so the "glow" comes from ambient light behind the text instead. +# station-bar/beacon) — a static, seamlessly-tiled equivalent since kitty has +# no per-frame draw hook to animate it and no shader hook to bloom glyphs +# directly. Regenerate with scratch gen_scanlines.py (violet #5018DD stripes on +# a transparent base, so the window's own translucency/blur still show through). background_image scanlines.png background_image_layout tiled background_tint 0.3 diff --git a/desktopenvs/hyprdrive/kitty/scanlines.png b/desktopenvs/hyprdrive/kitty/scanlines.png index 71ca6e0..6603ac8 100644 Binary files a/desktopenvs/hyprdrive/kitty/scanlines.png and b/desktopenvs/hyprdrive/kitty/scanlines.png differ diff --git a/desktopenvs/hyprdrive/station-bar/bar.py b/desktopenvs/hyprdrive/station-bar/bar.py index 958c600..4376e93 100644 --- a/desktopenvs/hyprdrive/station-bar/bar.py +++ b/desktopenvs/hyprdrive/station-bar/bar.py @@ -19,6 +19,7 @@ on hide so windows reflow to use the freed space. from __future__ import annotations import math +import os import subprocess import time from typing import Optional @@ -61,6 +62,24 @@ ICON_SPACESHIP = chr(0xf135) ICON_WINDOW = chr(0xf10ac) ICON_VOLUME = chr(0xf04c3) ICON_FUEL = chr(0xf07c5) # md-fuel — the "battery" reads as a fuel tank +# md-coffee 0xf0176 — a manual caffeine session is holding the idle lock +# md-eye 0xf0208 — presence-detect sees you, so idle is inhibited for you +# md-sleep 0xf04b2 — nothing inhibiting idle; the machine may sleep +# (codepoints verified against Agave Nerd Font's cmap; the ☕ emoji hyprlua's +# eww widget used has no glyph in this font, so md-coffee stands in for it.) +ICON_COFFEE = chr(0xf0176) +ICON_EYE = chr(0xf0208) +ICON_SLEEP = chr(0xf04b2) + +# Shared idle-lock protocol, identical to scripts/caffeine.sh and the status +# scripts it ships alongside — we read the same files rather than shelling out to +# caffeine-manual-status.sh / presence-status.sh on every poll: +# PID alive → the idle inhibitor is held (by anyone) +# PID alive AND no OWNED flag → a MANUAL caffeine toggle holds it +# PRESENCE flag present → the webcam presence daemon currently sees you +_CAFFEINE_PID_FILE = "/tmp/caffeine-inhibit.pid" +_PRESENCE_OWNED_FLAG = "/tmp/presence-inhibit-owned" +_PRESENCE_FLAG = "/tmp/presence-detected" class StationBar(Gtk.Window): @@ -70,6 +89,7 @@ class StationBar(Gtk.Window): BATTERY_POLL_S = 20 CLOCK_POLL_S = 1 VOLUME_POLL_S = 3 # cheap fallback so hardware volume keys still reflect promptly + CAFFEINE_POLL_S = 2 # matches hyprlua's eww caffeine poll; catches external toggles def __init__(self, hologram_enabled: bool = True, monitor=None) -> None: super().__init__() @@ -153,9 +173,11 @@ class StationBar(Gtk.Window): GLib.timeout_add_seconds(self.BATTERY_POLL_S, self._refresh_battery) GLib.timeout_add_seconds(self.CLOCK_POLL_S, self._refresh_clock) GLib.timeout_add_seconds(self.VOLUME_POLL_S, self._refresh_volume) + GLib.timeout_add_seconds(self.CAFFEINE_POLL_S, self._refresh_caffeine) self._refresh_battery() self._refresh_clock() self._refresh_volume() + self._refresh_caffeine() self.set_visible(True) self._ensure_tick() @@ -411,6 +433,16 @@ class StationBar(Gtk.Window): self._volume_btn.add_controller(scroll) self._right.append(self._volume_btn) + # Caffeine — clicking toggles the shared idle-inhibit lock via the same + # scripts/caffeine.sh hyprlua's eww bar drives. Glyph and colour track + # the same two inputs as that widget (manual inhibit + camera presence). + self._caffeine_btn = Gtk.Button(label=ICON_SLEEP) + self._caffeine_btn.add_css_class("station-badge") + self._caffeine_btn.add_css_class("station-caffeine") + self._caffeine_btn.set_has_frame(False) + self._caffeine_btn.connect("clicked", self._on_caffeine_clicked) + self._right.append(self._caffeine_btn) + self._clock_label = Gtk.Label() self._clock_label.add_css_class("station-badge") self._clock_label.add_css_class("station-clock") @@ -465,6 +497,59 @@ class StationBar(Gtk.Window): self._refresh_volume() return False + # -- caffeine (shared idle-inhibit lock) ------------------------------------- + @staticmethod + def _pid_alive(pid_file: str) -> bool: + """True if pid_file holds the PID of a still-running process (kill -0).""" + try: + with open(pid_file) as fh: + pid = int(fh.read().strip()) + except (OSError, ValueError): + return False + try: + os.kill(pid, 0) # signal 0 = liveness probe, sends nothing + except OSError: + return False + return True + + def _caffeine_state(self) -> tuple[bool, bool]: + """(manual, presence) — same two inputs as hyprlua's eww caffeine widget: + a manual toggle holding the lock, and the webcam presence flag.""" + manual = self._pid_alive(_CAFFEINE_PID_FILE) and not os.path.exists(_PRESENCE_OWNED_FLAG) + presence = os.path.exists(_PRESENCE_FLAG) + return manual, presence + + def _refresh_caffeine(self) -> bool: + manual, presence = self._caffeine_state() + if manual: + icon = ICON_COFFEE + tip = "Caffeine: ON (manual + presence)" if presence else "Caffeine: ON (manual)" + elif presence: + icon = ICON_EYE + tip = "Awake: presence detected" + else: + icon = ICON_SLEEP + tip = "Idle: no inhibit" + self._caffeine_btn.set_label(icon) + self._caffeine_btn.set_tooltip_text(tip) + # Colour tracks presence (magenta), exactly like eww's .caffeine-presence; + # the glyph carries the manual/presence/idle distinction on its own. + if presence: + self._caffeine_btn.add_css_class("station-caffeine-active") + else: + self._caffeine_btn.remove_css_class("station-caffeine-active") + return True + + def _on_caffeine_clicked(self, *_a) -> None: + subprocess.Popen(["bash", "-c", "$HOME/.config/scripts/caffeine.sh"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # caffeine.sh flips the lock synchronously; give it a beat, then reflect it. + GLib.timeout_add(250, self._refresh_caffeine_once) + + def _refresh_caffeine_once(self) -> bool: + self._refresh_caffeine() + return False + def _refresh_clock(self) -> bool: self._clock_label.set_label(time.strftime("%H:%M")) return True diff --git a/desktopenvs/hyprdrive/station-bar/style/style.css b/desktopenvs/hyprdrive/station-bar/style/style.css index b6ef45b..28717c6 100644 --- a/desktopenvs/hyprdrive/station-bar/style/style.css +++ b/desktopenvs/hyprdrive/station-bar/style/style.css @@ -75,6 +75,13 @@ button.station-badge, .station-clock { color: @glow_amber; text-shadow: 0 0 7px alpha(@glow_amber, 0.85); } .station-volume:hover { text-shadow: 0 0 13px @glow_cyan; } +/* caffeine — dim violet at rest (idle/manual), magenta glow while the webcam + * presence daemon is keeping the screen awake, mirroring eww's .caffeine-presence */ +.station-caffeine { color: alpha(@glow_violet, 0.8); text-shadow: 0 0 7px alpha(@glow_violet, 0.55); } +.station-caffeine:hover { color: @glow_violet; text-shadow: 0 0 13px @glow_violet; } +.station-caffeine-active { color: @glow_magenta; text-shadow: 0 0 8px alpha(@glow_magenta, 0.9); } +.station-caffeine-active:hover { text-shadow: 0 0 14px @glow_magenta, 0 0 4px @glow_magenta; } + /* Orbit / Astro launchers — larger glowing icon glyphs */ .station-launcher { font-size: 17pt;