control panel: unscroll the device lists, add display, drop toggle bar
Bluetooth and Network were still a ScrolledWindow around a ListBox, so short lists were pinned to a fixed height and scrolled for no reason. They are plain cards now, sized to content, matching the audio page. Display is back as its own view: modes from xrandr with the active one ticked, showing the rate actually in use rather than the highest the mode supports. Toggle bar is gone from the action row - super+b still does it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cn3dHfbgNYdbZvzb1b3GQt
This commit is contained in:
parent
8cf900b58b
commit
10fca7d9e4
2 changed files with 64 additions and 29 deletions
|
|
@ -47,7 +47,7 @@ sliders are live and stepping back between views never redraws the panel:
|
|||
| Audio | live volume and microphone sliders; click the icon to mute |
|
||||
| VPN | WireGuard up/down via `systemctl wg-quick@…` |
|
||||
| Notifications | do-not-disturb toggle |
|
||||
| Clipboard / Screenshot / Bar | greenclip, flameshot, hide the bar |
|
||||
| Clipboard / Screenshot / Display | greenclip, flameshot, resolution |
|
||||
| Wallpaper / Particles | shuffle, toggle the particle layer |
|
||||
| Power | lock, log out, suspend, reboot, shut down |
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ Each control shows its key in the bottom-right corner.
|
|||
|---|---|---|---|
|
||||
| `n` network | `b` bluetooth | `v` vpn | `d` do-not-disturb |
|
||||
| `p` particles | `w` wallpaper | `a` audio | `c` clipboard |
|
||||
| `s` screenshot | `t` toggle bar | `m` mute | `i` mute mic |
|
||||
| `s` screenshot | `e` display | `m` mute | `i` mute mic |
|
||||
| `l` lock | `o` log out | `z` suspend | `r` reboot |
|
||||
| `q` shut down | `←/→` volume | `Esc` back / close | |
|
||||
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ ICON = {
|
|||
"dnd_on": "", "dnd_off": "", "particles": "", "wall": "",
|
||||
"lock": "", "logout": "", "suspend": "", "reboot": "", "power": "",
|
||||
"back": "", "refresh": "", "vpn_on": "", "vpn_off": "",
|
||||
"clip": "", "shot": "", "bar": "",
|
||||
"clip": "", "shot": "", "bar": "", "screen": "",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -315,6 +315,8 @@ class Panel(Gtk.Window):
|
|||
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")
|
||||
self.stack.add_named(
|
||||
self.build_list_view("Display", None, rescan=False), "display")
|
||||
|
||||
frame = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
frame.get_style_context().add_class("frame")
|
||||
|
|
@ -401,13 +403,13 @@ class Panel(Gtk.Window):
|
|||
(ICON["clip"], "Clipboard history",
|
||||
"sh ~/.config/scripts/rofi-menus/clipboard-manager.sh", "c"),
|
||||
(ICON["shot"], "Screenshot", "flameshot gui", "s"),
|
||||
(ICON["bar"], "Toggle bar", "sh ~/.config/scripts/toggle-bar.sh", "t"),
|
||||
(ICON["screen"], "Display resolution", "@display", "e"),
|
||||
):
|
||||
b = Gtk.Button(label=icon)
|
||||
b.set_tooltip_text(f"{tip} ({key})")
|
||||
b.get_style_context().add_class("pw")
|
||||
if cmd == "@audio":
|
||||
act = lambda *_: self.go("audio")
|
||||
if cmd.startswith("@"):
|
||||
act = lambda _w=None, v=cmd[1:]: self.go(v)
|
||||
else:
|
||||
act = lambda _w=None, c=cmd: (spawn(c), self.close())
|
||||
b.connect("clicked", act)
|
||||
|
|
@ -466,31 +468,25 @@ class Panel(Gtk.Window):
|
|||
scale._pct = pct
|
||||
return row, btn, scale
|
||||
|
||||
def build_list_view(self, title, filler):
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
def build_list_view(self, title, filler=None, rescan=True):
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
|
||||
head = Gtk.Box(spacing=8)
|
||||
back = Gtk.Button(label=f"{ICON['back']} {title}")
|
||||
back.get_style_context().add_class("rowbtn")
|
||||
back.connect("clicked", lambda *_: self.go("main"))
|
||||
head.pack_start(back, True, True, 0)
|
||||
rescan = Gtk.Button(label=ICON["refresh"])
|
||||
rescan.get_style_context().add_class("rowbtn")
|
||||
rescan.connect("clicked", lambda *_: self.rescan(title))
|
||||
head.pack_end(rescan, False, False, 0)
|
||||
if rescan:
|
||||
rb = Gtk.Button(label=ICON["refresh"])
|
||||
rb.get_style_context().add_class("rowbtn")
|
||||
rb.connect("clicked", lambda *_: self.rescan(title))
|
||||
head.pack_end(rb, False, False, 0)
|
||||
outer.add(head)
|
||||
|
||||
scroller = Gtk.ScrolledWindow()
|
||||
scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
scroller.set_min_content_height(120)
|
||||
scroller.set_max_content_height(520)
|
||||
scroller.set_propagate_natural_height(True)
|
||||
listbox = Gtk.ListBox()
|
||||
listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
listbox.get_style_context().add_class("card")
|
||||
scroller.add(listbox)
|
||||
outer.add(scroller)
|
||||
setattr(self, f"list_{title.lower()}", listbox)
|
||||
setattr(self, f"fill_{title.lower()}", filler)
|
||||
# a plain card rather than a scrolled ListBox: these lists are short,
|
||||
# and scrolling made them feel broken
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
box.get_style_context().add_class("card")
|
||||
outer.add(box)
|
||||
setattr(self, f"list_{title.lower()}", box)
|
||||
return outer
|
||||
|
||||
def build_audio_view(self):
|
||||
|
|
@ -667,7 +663,8 @@ class Panel(Gtk.Window):
|
|||
# -------------------------------------------------------------- actions
|
||||
def go(self, name):
|
||||
if name != "main":
|
||||
{"bt": self.fill_bt, "net": self.fill_net, "audio": self.fill_audio}[name]()
|
||||
{"bt": self.fill_bt, "net": self.fill_net,
|
||||
"audio": self.fill_audio, "display": self.fill_display}[name]()
|
||||
self.stack.set_visible_child_name(name)
|
||||
GLib.idle_add(lambda: (self.resize(1, 1), False)[1])
|
||||
|
||||
|
|
@ -764,7 +761,6 @@ class Panel(Gtk.Window):
|
|||
GLib.timeout_add_seconds(3, lambda: (self.fill_net(), False)[1])
|
||||
|
||||
def row(self, listbox, icon, label, sub, cb):
|
||||
r = Gtk.ListBoxRow()
|
||||
b = Gtk.Button()
|
||||
b.get_style_context().add_class("rowbtn")
|
||||
h = Gtk.Box(spacing=12)
|
||||
|
|
@ -778,8 +774,7 @@ class Panel(Gtk.Window):
|
|||
h.pack_end(s, False, False, 0)
|
||||
b.add(h)
|
||||
b.connect("clicked", cb)
|
||||
r.add(b)
|
||||
listbox.add(r)
|
||||
listbox.add(b)
|
||||
|
||||
def fill_bt(self):
|
||||
lb = self.list_bluetooth
|
||||
|
|
@ -812,6 +807,46 @@ class Panel(Gtk.Window):
|
|||
spawn(f"bluetoothctl trust {mac} && bluetoothctl connect {mac}")
|
||||
GLib.timeout_add_seconds(3, lambda: (self.fill_bt(), self.refresh(), False)[2])
|
||||
|
||||
def fill_display(self):
|
||||
box = self.list_display
|
||||
for c in box.get_children():
|
||||
box.remove(c)
|
||||
out = sh("xrandr --current")
|
||||
name = None
|
||||
for line in out.splitlines():
|
||||
m = re.match(r"^(\S+) connected", line)
|
||||
if m:
|
||||
name = m.group(1)
|
||||
continue
|
||||
if name and line.startswith(" "):
|
||||
parts = line.split()
|
||||
if not parts:
|
||||
continue
|
||||
mode = parts[0]
|
||||
rates = parts[1:]
|
||||
cur = any("*" in r for r in rates)
|
||||
def num(r):
|
||||
r = r.rstrip("*+")
|
||||
try:
|
||||
return float(r)
|
||||
except ValueError:
|
||||
return None
|
||||
vals = [v for v in (num(r) for r in rates) if v]
|
||||
# show the rate actually in use for the active mode, not the
|
||||
# highest one the mode happens to support
|
||||
active = next((num(r) for r in rates if "*" in r), None)
|
||||
hz = active or (max(vals) if vals else 0)
|
||||
self.row(box, "" if cur else "", mode, f"{hz:.0f} Hz",
|
||||
lambda _w, o=name, md=mode, r=hz: self.set_mode(o, md, r))
|
||||
if not box.get_children():
|
||||
self.row(box, "", "No outputs found", "", lambda *_: None)
|
||||
box.show_all()
|
||||
|
||||
def set_mode(self, output, mode, rate):
|
||||
if sh(f"xrandr --output {output} --mode {mode} --rate {rate:.2f}") == "":
|
||||
notify("Display", f"{output} set to {mode} @ {rate:.0f} Hz")
|
||||
self.fill_display()
|
||||
|
||||
def fill_net(self):
|
||||
lb = self.list_network
|
||||
for c in lb.get_children():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue