From ddf4b5b8d95cd7aeebcc43916d0522db427556e8 Mon Sep 17 00:00:00 2001 From: Sarthak <118442597+sarthak1337-alt@users.noreply.github.com> Date: Fri, 4 Sep 2026 08:33:51 +0530 Subject: [PATCH] control panel: audio devices, icon font, padding, blur - audio view: output/input device switching, master volume + mic, and per-application streams with their own sliders (pactl sink-inputs) - icons rendered black because GTK used the desktop font (Noto Sans), which has no Nerd Font glyphs; Pango's per-glyph fallback then ignored the css colour. Icon classes now name the font explicitly. - bspwm drew a focus border around the panel that fought the css rounded corners: border=off on the rule - picom now blurs translucent surfaces, so the panel reads as glass rather than showing the text behind it - padding and spacing throughout: frame 20px, section margins, card padding Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Cn3dHfbgNYdbZvzb1b3GQt --- .config/bspwm/bspwmrc | 1 + .config/bspwm/picom.conf | 12 + .config/polybar/config.ini | 2 +- .../__pycache__/control-panel.cpython-314.pyc | Bin 0 -> 59295 bytes .config/scripts/control-panel.py | 258 +++++++++++++++--- 5 files changed, 239 insertions(+), 34 deletions(-) create mode 100644 .config/scripts/__pycache__/control-panel.cpython-314.pyc diff --git a/.config/bspwm/bspwmrc b/.config/bspwm/bspwmrc index c4e06fa..29cb690 100755 --- a/.config/bspwm/bspwmrc +++ b/.config/bspwm/bspwmrc @@ -59,6 +59,7 @@ bspc config pointer_action2 resize_side bspc config pointer_action3 resize_corner # Rules +bspc rule -a Control-panel.py state=floating layer=above center=on sticky=on border=off bspc rule -a feh state=floating bspc rule -a '*:sun-awt-X11-XWindowPeer' manage=off bspc rule -a 'vlc' state=floating center=true diff --git a/.config/bspwm/picom.conf b/.config/bspwm/picom.conf index 37df756..bc9113d 100644 --- a/.config/bspwm/picom.conf +++ b/.config/bspwm/picom.conf @@ -68,3 +68,15 @@ fade-exclude = [ animation-exclude = [ "class_g = 'Rofi'", ]; + +# Translucent surfaces over busy content read as noise without blur. +blur-method = "dual_kawase"; +blur-strength = 6; +blur-background = true; +blur-background-frame = true; +blur-background-exclude = [ + # the particle layer is transparent on purpose; blurring it would smear + # the wallpaper behind it + "window_type = 'desktop'", + "class_g = 'Particles'", +]; diff --git a/.config/polybar/config.ini b/.config/polybar/config.ini index 0b16ca5..4b85bbc 100755 --- a/.config/polybar/config.ini +++ b/.config/polybar/config.ini @@ -36,7 +36,7 @@ module-margin = 0 modules-left = bspwm # modules-center = media-window-toggle modules-center = xwindow -modules-right = cpu sep ram sep gpu sep volume sep time sep control +modules-right = cpu sep ram sep gpu sep volume sep time font-0 = JetBrainsMono Nerd Font:style=Bold:pixelsize=10;2 font-1 = JetBrainsMono Nerd Font:style=Bold:pixelsize=12;2 diff --git a/.config/scripts/__pycache__/control-panel.cpython-314.pyc b/.config/scripts/__pycache__/control-panel.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30737203bf64e7060f996043ba01a95ee68a106f GIT binary patch literal 59295 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 literal 0 HcmV?d00001 diff --git a/.config/scripts/control-panel.py b/.config/scripts/control-panel.py index e01d46d..70efaff 100755 --- a/.config/scripts/control-panel.py +++ b/.config/scripts/control-panel.py @@ -17,28 +17,44 @@ gi.require_version("Gdk", "3.0") from gi.repository import Gtk, Gdk, GLib # noqa: E402 CSS = b""" -window.panel { - background-color: rgba(15, 13, 23, 0.86); - border: 1px solid #3d3752; - border-radius: 16px; +/* Glyphs come from the Nerd Font. Without an explicit family, GTK uses the + desktop font (Noto Sans), which lacks them, and Pango's per-glyph fallback + picks a font that ignores the css colour - that is why icons rendered black. */ +.icon, .tile-icon, .pw, .rowbtn, .title { + font-family: "JetBrainsMono Nerd Font", "Symbols Nerd Font", monospace; } + +window.panel { background-color: transparent; } +.frame { + background-color: rgba(15, 13, 23, 0.82); + border: 1px solid rgba(146, 185, 234, 0.16); + border-radius: 18px; +} + .title { font-weight: bold; font-size: 15px; color: #e9eefb; } -.subtle { color: #7f7796; font-size: 12px; } -.section { color: #92b9ea; font-size: 11px; font-weight: bold; } +.subtle { color: #8d85a4; font-size: 11px; } +.section { color: #92b9ea; font-size: 10px; font-weight: bold; letter-spacing: 1px; } +.tile-icon { font-size: 20px; color: #e9eefb; } +.tile-name { font-size: 12px; color: #e9eefb; } +.tile.active .tile-icon, +.tile.active .tile-name, +.tile.active .subtle { color: #0f0d17; } + .card { - background-color: rgba(34, 28, 51, 0.72); - border-radius: 12px; - padding: 10px; + background-color: rgba(34, 28, 51, 0.66); + border: 1px solid rgba(146, 185, 234, 0.08); + border-radius: 14px; + padding: 12px; } .tile { background-image: none; - background-color: rgba(34, 28, 51, 0.72); - border: 1px solid transparent; - border-radius: 12px; + background-color: rgba(38, 32, 58, 0.78); + border: 1px solid rgba(146, 185, 234, 0.10); + border-radius: 14px; color: #e9eefb; - padding: 12px 8px; + padding: 14px 6px; } -.tile:hover { background-color: rgba(58, 51, 80, 0.85); } +.tile:hover { background-color: rgba(58, 51, 80, 0.9); } .tile.active { background-color: #92b9ea; color: #0f0d17; @@ -50,20 +66,24 @@ window.panel { border: none; border-radius: 10px; color: #e9eefb; - padding: 9px 10px; + font-size: 14px; + padding: 10px 12px; } -.rowbtn:hover { background-color: rgba(146, 185, 234, 0.17); } +.rowbtn:hover { background-color: rgba(146, 185, 234, 0.17); } +.rowbtn.chosen { background-color: rgba(146, 185, 234, 0.22); color: #cfe0f7; } .pw { background-image: none; - background-color: rgba(34, 28, 51, 0.72); - border: 1px solid transparent; - border-radius: 10px; + background-color: rgba(38, 32, 58, 0.78); + border: 1px solid rgba(146, 185, 234, 0.10); + border-radius: 12px; color: #e9eefb; - padding: 8px; + font-size: 15px; + padding: 11px 8px; } -.pw:hover { background-color: rgba(146, 185, 234, 0.20); } -.pw.danger:hover { background-color: rgba(207, 106, 128, 0.30); color: #ffe9ee; } -scale { min-height: 22px; } +.pw:hover { background-color: rgba(146, 185, 234, 0.22); } +.pw.danger:hover { background-color: rgba(207, 106, 128, 0.32); color: #ffe9ee; } + +scale { min-height: 24px; } scale > contents > trough { background-color: rgba(61, 55, 82, 0.95); border: none; border-radius: 6px; min-height: 7px; @@ -79,6 +99,8 @@ scale > contents > trough > slider { min-width: 14px; min-height: 14px; margin: -6px; } +scrollbar { background-color: transparent; } +scrollbar slider { background-color: rgba(146, 185, 234, 0.25); border-radius: 8px; } """ ICON = { @@ -127,6 +149,45 @@ def mic_get(): return (round(float(m.group(1)) * 100) if m else 0, "MUTED" in out) +def _pactl_devices(kind): + """kind: 'sinks' or 'sources'. Returns [(name, description, is_default)].""" + default = sh(f"pactl get-default-{'sink' if kind == 'sinks' else 'source'}") + out, name, desc, items = sh(f"pactl list {kind}"), None, None, [] + for line in out.splitlines(): + line = line.strip() + if line.startswith("Name:"): + name = line.split(":", 1)[1].strip() + elif line.startswith("Description:"): + desc = line.split(":", 1)[1].strip() + if name and not name.startswith("alsa_output.platform-snd_dummy"): + items.append((name, desc, name == default)) + name = None + return items + + +def sink_inputs(): + """Per-application streams: [(index, app, volume_pct, muted)].""" + out, cur, items = sh("pactl list sink-inputs"), {}, [] + for line in out.splitlines(): + st = line.strip() + if st.startswith("Sink Input #"): + if cur.get("idx"): + items.append((cur["idx"], cur.get("app", "?"), + cur.get("vol", 0), cur.get("mute", False))) + cur = {"idx": st.split("#")[1]} + elif st.startswith("Volume:") and "vol" not in cur: + m = re.search(r"(\d+)%", st) + cur["vol"] = int(m.group(1)) if m else 0 + elif st.startswith("Mute:"): + cur["mute"] = st.endswith("yes") + elif "application.name" in st: + cur["app"] = st.split("=", 1)[1].strip().strip('"') + if cur.get("idx"): + items.append((cur["idx"], cur.get("app", "?"), + cur.get("vol", 0), cur.get("mute", False))) + return items + + def net_state(): if not has("nmcli"): return ICON["net_off"], "no NetworkManager", False @@ -183,10 +244,13 @@ class Panel(Gtk.Window): self.set_type_hint(Gdk.WindowTypeHint.DIALOG) self.set_keep_above(True) - screen = self.get_screen() - visual = screen.get_rgba_visual() + # An RGBA visual is all that is needed for the css background to blend. + # set_app_paintable(True) would make GTK skip drawing it altogether, + # which is what left the panel fully see-through. + visual = self.get_screen().get_rgba_visual() if visual: self.set_visual(visual) + # the window must not paint, or it paints opaque over the rgba visual self.set_app_paintable(True) self.stack = Gtk.Stack() @@ -196,25 +260,26 @@ class Panel(Gtk.Window): self.stack.add_named(self.build_main(), "main") self.stack.add_named(self.build_list_view("Bluetooth", self.fill_bt), "bt") self.stack.add_named(self.build_list_view("Network", self.fill_net), "net") + self.stack.add_named(self.build_audio_view(), "audio") frame = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - frame.set_border_width(16) + frame.get_style_context().add_class("frame") + frame.set_border_width(20) frame.add(self.stack) self.add(frame) self.connect("key-press-event", self.on_key) self.connect("destroy", Gtk.main_quit) - # close when focus goes elsewhere, like a quick-settings popover - self.connect("focus-out-event", lambda *_: self.close()) self.refresh() GLib.timeout_add_seconds(3, self.refresh_tick) # ---------------------------------------------------------- construction def build_main(self): - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=14) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) head = Gtk.Box(spacing=10) + head.set_margin_bottom(2) who = Gtk.Label(xalign=0) who.set_markup(f"{GLib.get_user_name()}@{GLib.get_host_name()}") who.get_style_context().add_class("title") @@ -274,6 +339,7 @@ class Panel(Gtk.Window): extra = Gtk.Box(spacing=8, homogeneous=True) for icon, tip, cmd in ( + (ICON["vol"], "Audio devices & app volumes", "@audio"), (ICON["clip"], "Clipboard history", "sh ~/.config/scripts/rofi-menus/clipboard-manager.sh"), (ICON["shot"], "Screenshot", "flameshot gui"), @@ -282,7 +348,10 @@ class Panel(Gtk.Window): b = Gtk.Button(label=icon) b.set_tooltip_text(tip) b.get_style_context().add_class("pw") - b.connect("clicked", lambda _w, c=cmd: (spawn(c), self.close())) + if cmd == "@audio": + b.connect("clicked", lambda *_: self.go("audio")) + else: + b.connect("clicked", lambda _w, c=cmd: (spawn(c), self.close())) extra.add(b) box.add(extra) return box @@ -292,8 +361,9 @@ class Panel(Gtk.Window): b.get_style_context().add_class("tile") inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) ic = Gtk.Label(label=icon) + ic.get_style_context().add_class("tile-icon") tx = Gtk.Label(label=label) - tx.get_style_context().add_class("subtle") + tx.get_style_context().add_class("tile-name") sub = Gtk.Label(label="") sub.get_style_context().add_class("subtle") inner.add(ic) @@ -346,10 +416,130 @@ class Panel(Gtk.Window): setattr(self, f"fill_{title.lower()}", filler) return outer + def build_audio_view(self): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + head = Gtk.Box(spacing=8) + head.set_margin_bottom(2) + back = Gtk.Button(label=f"{ICON['back']} Audio") + back.get_style_context().add_class("rowbtn") + back.connect("clicked", lambda *_: self.go("main")) + head.pack_start(back, True, True, 0) + outer.add(head) + + scroller = Gtk.ScrolledWindow() + scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scroller.set_min_content_height(300) + self.audio_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + scroller.add(self.audio_box) + outer.add(scroller) + return outer + + def section(self, text): + lbl = Gtk.Label(label=text.upper(), xalign=0) + lbl.get_style_context().add_class("section") + lbl.set_margin_top(6) + lbl.set_margin_start(4) + lbl.set_margin_bottom(2) + return lbl + + def device_row(self, name, desc, active, kind): + b = Gtk.Button() + b.get_style_context().add_class("rowbtn") + if active: + b.get_style_context().add_class("chosen") + h = Gtk.Box(spacing=10) + mark = Gtk.Label(label="󰄬" if active else "󰝥") + mark.get_style_context().add_class("icon") + h.add(mark) + t = Gtk.Label(label=desc, xalign=0) + t.set_ellipsize(3) + h.pack_start(t, True, True, 0) + b.add(h) + b.connect("clicked", lambda *_: self.set_device(kind, name)) + return b + + def app_row(self, idx, app, vol, muted): + wp = str(idx).startswith("@") + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) + lbl = Gtk.Label(label=app, xalign=0) + lbl.get_style_context().add_class("subtle") + row = Gtk.Box(spacing=10) + mb = Gtk.Button(label=ICON["vol_mute"] if muted else ICON["vol"]) + mb.get_style_context().add_class("rowbtn") + mb.connect("clicked", lambda *_: ( + sh(f"wpctl set-mute {idx} toggle" if wp + else f"pactl set-sink-input-mute {idx} toggle"), + self.fill_audio(), self.refresh())) + sc = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 130, 1) + sc.set_draw_value(False) + sc.set_hexpand(True) + sc.set_value(vol) + pct = Gtk.Label(label=f"{vol}%") + pct.get_style_context().add_class("subtle") + sc.connect("value-changed", lambda w: ( + pct.set_text(f"{int(w.get_value())}%"), + spawn(f"wpctl set-volume {idx} {int(w.get_value())}%" if wp + else f"pactl set-sink-input-volume {idx} {int(w.get_value())}%"))) + row.pack_start(mb, False, False, 0) + row.pack_start(sc, True, True, 0) + row.pack_end(pct, False, False, 0) + card.add(lbl) + card.add(row) + return card + + def set_device(self, kind, name): + sh(f"pactl set-default-{kind} {name}") + if kind == "sink": + # follow existing streams over to the new output + for idx, *_ in sink_inputs(): + sh(f"pactl move-sink-input {idx} {name}") + notify("Audio", f"{'Output' if kind == 'sink' else 'Input'} switched") + self.fill_audio() + self.refresh() + + def fill_audio(self): + for c in self.audio_box.get_children(): + self.audio_box.remove(c) + + self.audio_box.add(self.section("Volume")) + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + card.get_style_context().add_class("card") + v, vm = vol_get() + m, mm = mic_get() + card.add(self.app_row("@DEFAULT_AUDIO_SINK@", "Output", v, vm)) + card.add(self.app_row("@DEFAULT_AUDIO_SOURCE@", "Microphone", m, mm)) + self.audio_box.add(card) + + self.audio_box.add(self.section("Output")) + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + card.get_style_context().add_class("card") + for name, desc, active in _pactl_devices("sinks"): + card.add(self.device_row(name, desc, active, "sink")) + self.audio_box.add(card) + + self.audio_box.add(self.section("Input")) + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + card.get_style_context().add_class("card") + for name, desc, active in _pactl_devices("sources"): + if ".monitor" in name: + continue + card.add(self.device_row(name, desc, active, "source")) + self.audio_box.add(card) + + apps = sink_inputs() + if apps: + self.audio_box.add(self.section("Applications")) + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + card.get_style_context().add_class("card") + for idx, app, vol, muted in apps: + card.add(self.app_row(idx, app, vol, muted)) + self.audio_box.add(card) + self.audio_box.show_all() + # -------------------------------------------------------------- actions def go(self, name): if name != "main": - {"bt": self.fill_bt, "net": self.fill_net}[name]() + {"bt": self.fill_bt, "net": self.fill_net, "audio": self.fill_audio}[name]() self.stack.set_visible_child_name(name) def on_key(self, _w, ev): @@ -426,7 +616,9 @@ class Panel(Gtk.Window): b = Gtk.Button() b.get_style_context().add_class("rowbtn") h = Gtk.Box(spacing=12) - h.add(Gtk.Label(label=icon)) + ic = Gtk.Label(label=icon) + ic.get_style_context().add_class("icon") + h.add(ic) t = Gtk.Label(label=label, xalign=0) h.pack_start(t, True, True, 0) s = Gtk.Label(label=sub)