control panel: real padding, seat grab, key hints
- GtkBox set_border_width shrinks the widget's own background, so the card was inset 30px while its contents sat flush against the card edge. Padding is css now: 12px transparent gutter outside, 20px inside. Measured on a virtual display rather than guessed. - the panel grabs the seat on map, like rofi, so it keeps keyboard and pointer focus wherever the cursor is; the grab is released on close - every control shows its key in the bottom-right: n b v d p w for the tiles, a c s t for the row beneath, l o z r q for power, m/i to mute, arrows for volume Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cn3dHfbgNYdbZvzb1b3GQt
This commit is contained in:
parent
9853553782
commit
ddf9321901
3 changed files with 109 additions and 28 deletions
|
|
@ -59,7 +59,7 @@ bspc config pointer_action2 resize_side
|
||||||
bspc config pointer_action3 resize_corner
|
bspc config pointer_action3 resize_corner
|
||||||
|
|
||||||
# Rules
|
# Rules
|
||||||
bspc rule -a Control-panel.py state=floating layer=above center=on sticky=on border=off
|
bspc rule -a Control-panel.py state=floating layer=above center=on sticky=on border=off follow=on focus=on
|
||||||
bspc rule -a feh state=floating
|
bspc rule -a feh state=floating
|
||||||
bspc rule -a '*:sun-awt-X11-XWindowPeer' manage=off
|
bspc rule -a '*:sun-awt-X11-XWindowPeer' manage=off
|
||||||
bspc rule -a 'vlc' state=floating center=true
|
bspc rule -a 'vlc' state=floating center=true
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,20 @@ sliders are live and stepping back between views never redraws the panel:
|
||||||
| Wallpaper / Particles | shuffle, toggle the particle layer |
|
| Wallpaper / Particles | shuffle, toggle the particle layer |
|
||||||
| Power | lock, log out, suspend, reboot, shut down |
|
| Power | lock, log out, suspend, reboot, shut down |
|
||||||
|
|
||||||
Escape steps back from a subview; Escape on the main view closes it.
|
Escape steps back from a subview; Escape on the main view closes it. The panel
|
||||||
|
grabs the seat like rofi does, so it keeps focus wherever the pointer is.
|
||||||
|
|
||||||
|
### Panel keys
|
||||||
|
|
||||||
|
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 |
|
||||||
|
| `l` lock | `o` log out | `z` suspend | `r` reboot |
|
||||||
|
| `q` shut down | `←/→` volume | `Esc` back / close | |
|
||||||
|
|
||||||
## Keybindings
|
## Keybindings
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,17 @@ window.panel { background-color: transparent; }
|
||||||
background-color: rgba(15, 13, 23, 0.88);
|
background-color: rgba(15, 13, 23, 0.88);
|
||||||
border: 1px solid rgba(146, 185, 234, 0.16);
|
border: 1px solid rgba(146, 185, 234, 0.16);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title { font-weight: bold; font-size: 15px; color: #e9eefb; }
|
.title { font-weight: bold; font-size: 15px; color: #e9eefb; }
|
||||||
.subtle { color: #8d85a4; font-size: 11px; }
|
.subtle { color: #8d85a4; font-size: 11px; }
|
||||||
.section { color: #92b9ea; font-size: 10px; font-weight: bold; letter-spacing: 1px; }
|
.section { color: #92b9ea; font-size: 10px; font-weight: bold; letter-spacing: 1px; }
|
||||||
|
.hint {
|
||||||
|
color: #6f6889;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
.tile-icon { font-size: 20px; color: #e9eefb; }
|
.tile-icon { font-size: 20px; color: #e9eefb; }
|
||||||
.tile-name { font-size: 12px; color: #e9eefb; }
|
.tile-name { font-size: 12px; color: #e9eefb; }
|
||||||
.tile.active .tile-icon { color: #92b9ea; }
|
.tile.active .tile-icon { color: #92b9ea; }
|
||||||
|
|
@ -255,6 +261,7 @@ class Panel(Gtk.Window):
|
||||||
# the window must not paint, or it paints opaque over the rgba visual
|
# the window must not paint, or it paints opaque over the rgba visual
|
||||||
self.set_app_paintable(True)
|
self.set_app_paintable(True)
|
||||||
|
|
||||||
|
self.keymap = {}
|
||||||
self.stack = Gtk.Stack()
|
self.stack = Gtk.Stack()
|
||||||
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||||
self.stack.set_transition_duration(140)
|
self.stack.set_transition_duration(140)
|
||||||
|
|
@ -266,14 +273,14 @@ class Panel(Gtk.Window):
|
||||||
|
|
||||||
frame = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
frame = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
frame.get_style_context().add_class("frame")
|
frame.get_style_context().add_class("frame")
|
||||||
frame.set_border_width(20) # padding inside the card
|
|
||||||
for setter in ("set_margin_top", "set_margin_bottom",
|
for setter in ("set_margin_top", "set_margin_bottom",
|
||||||
"set_margin_start", "set_margin_end"):
|
"set_margin_start", "set_margin_end"):
|
||||||
getattr(frame, setter)(10) # transparent gutter outside it
|
getattr(frame, setter)(12) # transparent gutter outside the card
|
||||||
frame.add(self.stack)
|
frame.add(self.stack)
|
||||||
self.add(frame)
|
self.add(frame)
|
||||||
|
|
||||||
self.connect("key-press-event", self.on_key)
|
self.connect("key-press-event", self.on_key)
|
||||||
|
self.connect("map-event", self.on_map)
|
||||||
self.connect("destroy", Gtk.main_quit)
|
self.connect("destroy", Gtk.main_quit)
|
||||||
|
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
@ -304,13 +311,12 @@ class Panel(Gtk.Window):
|
||||||
self.t_vpn = self.tile(ICON["vpn_off"], "VPN", self.toggle_vpn)
|
self.t_vpn = self.tile(ICON["vpn_off"], "VPN", self.toggle_vpn)
|
||||||
self.t_dnd = self.tile(ICON["dnd_off"], "Notify", self.toggle_dnd)
|
self.t_dnd = self.tile(ICON["dnd_off"], "Notify", self.toggle_dnd)
|
||||||
self.t_par = self.tile(ICON["particles"], "Particles", self.toggle_particles)
|
self.t_par = self.tile(ICON["particles"], "Particles", self.toggle_particles)
|
||||||
self.t_wall = self.tile(ICON["wall"], "Wallpaper",
|
self.t_wall = self.tile(ICON["wall"], "Wallpaper", lambda *_: self.shuffle_wall())
|
||||||
lambda *_: (spawn("sh ~/.config/scripts/set-wallpaper.sh"),
|
tiles = ((self.t_net, "n"), (self.t_bt, "b"), (self.t_vpn, "v"),
|
||||||
notify("Wallpaper", "Shuffled")))
|
(self.t_dnd, "d"), (self.t_par, "p"), (self.t_wall, "w"))
|
||||||
for i, w in enumerate((self.t_net, self.t_bt, self.t_vpn)):
|
for i, (w, k) in enumerate(tiles):
|
||||||
grid.attach(w, i, 0, 1, 1)
|
grid.attach(self.hint(w, k), i % 3, i // 3, 1, 1)
|
||||||
for i, w in enumerate((self.t_dnd, self.t_par, self.t_wall)):
|
self.keymap[k] = (lambda _w=None, btn=w: btn.emit("clicked"))
|
||||||
grid.attach(w, i, 1, 1, 1)
|
|
||||||
box.add(grid)
|
box.add(grid)
|
||||||
|
|
||||||
# sliders
|
# sliders
|
||||||
|
|
@ -326,41 +332,60 @@ class Panel(Gtk.Window):
|
||||||
|
|
||||||
# power row
|
# power row
|
||||||
prow = Gtk.Box(spacing=8, homogeneous=True)
|
prow = Gtk.Box(spacing=8, homogeneous=True)
|
||||||
for icon, tip, act, danger in (
|
for icon, tip, act, danger, key in (
|
||||||
(ICON["lock"], "Lock", self.do_lock, False),
|
(ICON["lock"], "Lock", self.do_lock, False, "l"),
|
||||||
(ICON["logout"], "Log out", lambda *_: spawn("bspc quit"), False),
|
(ICON["logout"], "Log out", lambda *_: spawn("bspc quit"), False, "o"),
|
||||||
(ICON["suspend"], "Suspend", lambda *_: (self.do_lock(), spawn("systemctl suspend")), False),
|
(ICON["suspend"], "Suspend",
|
||||||
(ICON["reboot"], "Reboot", lambda *_: spawn("systemctl reboot"), True),
|
lambda *_: (self.do_lock(), spawn("systemctl suspend")), False, "z"),
|
||||||
(ICON["power"], "Shut down", lambda *_: spawn("systemctl poweroff"), True),
|
(ICON["reboot"], "Reboot", lambda *_: spawn("systemctl reboot"), True, "r"),
|
||||||
|
(ICON["power"], "Shut down", lambda *_: spawn("systemctl poweroff"), True, "q"),
|
||||||
):
|
):
|
||||||
b = Gtk.Button(label=icon)
|
b = Gtk.Button(label=icon)
|
||||||
b.set_tooltip_text(tip)
|
b.set_tooltip_text(f"{tip} ({key})")
|
||||||
b.get_style_context().add_class("pw")
|
b.get_style_context().add_class("pw")
|
||||||
if danger:
|
if danger:
|
||||||
b.get_style_context().add_class("danger")
|
b.get_style_context().add_class("danger")
|
||||||
b.connect("clicked", act)
|
b.connect("clicked", act)
|
||||||
prow.add(b)
|
self.keymap[key] = act
|
||||||
|
prow.add(self.hint(b, key))
|
||||||
box.add(prow)
|
box.add(prow)
|
||||||
|
|
||||||
extra = Gtk.Box(spacing=8, homogeneous=True)
|
extra = Gtk.Box(spacing=8, homogeneous=True)
|
||||||
for icon, tip, cmd in (
|
for icon, tip, cmd, key in (
|
||||||
(ICON["vol"], "Audio devices & app volumes", "@audio"),
|
(ICON["vol"], "Audio devices & app volumes", "@audio", "a"),
|
||||||
(ICON["clip"], "Clipboard history",
|
(ICON["clip"], "Clipboard history",
|
||||||
"sh ~/.config/scripts/rofi-menus/clipboard-manager.sh"),
|
"sh ~/.config/scripts/rofi-menus/clipboard-manager.sh", "c"),
|
||||||
(ICON["shot"], "Screenshot", "flameshot gui"),
|
(ICON["shot"], "Screenshot", "flameshot gui", "s"),
|
||||||
(ICON["bar"], "Toggle bar", "sh ~/.config/scripts/toggle-bar.sh"),
|
(ICON["bar"], "Toggle bar", "sh ~/.config/scripts/toggle-bar.sh", "t"),
|
||||||
):
|
):
|
||||||
b = Gtk.Button(label=icon)
|
b = Gtk.Button(label=icon)
|
||||||
b.set_tooltip_text(tip)
|
b.set_tooltip_text(f"{tip} ({key})")
|
||||||
b.get_style_context().add_class("pw")
|
b.get_style_context().add_class("pw")
|
||||||
if cmd == "@audio":
|
if cmd == "@audio":
|
||||||
b.connect("clicked", lambda *_: self.go("audio"))
|
act = lambda *_: self.go("audio")
|
||||||
else:
|
else:
|
||||||
b.connect("clicked", lambda _w, c=cmd: (spawn(c), self.close()))
|
act = lambda _w=None, c=cmd: (spawn(c), self.close())
|
||||||
extra.add(b)
|
b.connect("clicked", act)
|
||||||
|
self.keymap[key] = act
|
||||||
|
extra.add(self.hint(b, key))
|
||||||
box.add(extra)
|
box.add(extra)
|
||||||
return box
|
return box
|
||||||
|
|
||||||
|
def hint(self, widget, letter):
|
||||||
|
"""Wrap a widget so a small key hint sits in its bottom-right."""
|
||||||
|
ov = Gtk.Overlay()
|
||||||
|
ov.add(widget)
|
||||||
|
lab = Gtk.Label(label=letter)
|
||||||
|
lab.get_style_context().add_class("hint")
|
||||||
|
lab.set_halign(Gtk.Align.END)
|
||||||
|
lab.set_valign(Gtk.Align.END)
|
||||||
|
lab.set_margin_end(7)
|
||||||
|
lab.set_margin_bottom(5)
|
||||||
|
ov.add_overlay(lab)
|
||||||
|
ov.set_overlay_pass_through(lab, True)
|
||||||
|
ov.btn = widget
|
||||||
|
return ov
|
||||||
|
|
||||||
def tile(self, icon, label, cb):
|
def tile(self, icon, label, cb):
|
||||||
b = Gtk.Button()
|
b = Gtk.Button()
|
||||||
b.get_style_context().add_class("tile")
|
b.get_style_context().add_class("tile")
|
||||||
|
|
@ -541,6 +566,26 @@ class Panel(Gtk.Window):
|
||||||
self.audio_box.add(card)
|
self.audio_box.add(card)
|
||||||
self.audio_box.show_all()
|
self.audio_box.show_all()
|
||||||
|
|
||||||
|
def on_map(self, *_):
|
||||||
|
"""Own the seat the way rofi does: keyboard and pointer both, so the
|
||||||
|
panel keeps focus wherever the cursor is or whatever gets clicked."""
|
||||||
|
self.present()
|
||||||
|
seat = Gdk.Display.get_default().get_default_seat()
|
||||||
|
gdkwin = self.get_window()
|
||||||
|
if seat and gdkwin:
|
||||||
|
seat.grab(gdkwin, Gdk.SeatCapabilities.ALL, True,
|
||||||
|
None, None, None, None)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def release_grab(self):
|
||||||
|
seat = Gdk.Display.get_default().get_default_seat()
|
||||||
|
if seat:
|
||||||
|
seat.ungrab()
|
||||||
|
|
||||||
|
def close(self, *_):
|
||||||
|
self.release_grab()
|
||||||
|
super().close()
|
||||||
|
|
||||||
# -------------------------------------------------------------- actions
|
# -------------------------------------------------------------- actions
|
||||||
def go(self, name):
|
def go(self, name):
|
||||||
if name != "main":
|
if name != "main":
|
||||||
|
|
@ -554,6 +599,25 @@ class Panel(Gtk.Window):
|
||||||
else:
|
else:
|
||||||
self.close()
|
self.close()
|
||||||
return True
|
return True
|
||||||
|
if self.stack.get_visible_child_name() != "main":
|
||||||
|
return False
|
||||||
|
name = Gdk.keyval_name(ev.keyval) or ""
|
||||||
|
if name in ("m", "M"):
|
||||||
|
self.toggle_mute()
|
||||||
|
return True
|
||||||
|
if name in ("i", "I"):
|
||||||
|
self.toggle_micmute()
|
||||||
|
return True
|
||||||
|
if name in ("Left", "Right", "Up", "Down"):
|
||||||
|
step = -5 if name in ("Left", "Down") else 5
|
||||||
|
spawn(f"wpctl set-volume @DEFAULT_AUDIO_SINK@ "
|
||||||
|
f"{abs(step)}%{'-' if step < 0 else '+'} -l 1.3")
|
||||||
|
GLib.timeout_add(180, lambda: (self.refresh(), False)[1])
|
||||||
|
return True
|
||||||
|
act = self.keymap.get(name.lower())
|
||||||
|
if act:
|
||||||
|
act()
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_vol(self, scale):
|
def on_vol(self, scale):
|
||||||
|
|
@ -584,6 +648,10 @@ class Panel(Gtk.Window):
|
||||||
spawn("particles || ~/.local/bin/particles")
|
spawn("particles || ~/.local/bin/particles")
|
||||||
GLib.timeout_add(700, lambda: (self.refresh(), False)[1])
|
GLib.timeout_add(700, lambda: (self.refresh(), False)[1])
|
||||||
|
|
||||||
|
def shuffle_wall(self):
|
||||||
|
spawn("sh ~/.config/scripts/set-wallpaper.sh")
|
||||||
|
notify("Wallpaper", "Shuffled")
|
||||||
|
|
||||||
def toggle_vpn(self, *_):
|
def toggle_vpn(self, *_):
|
||||||
_, _, up = vpn_state()
|
_, _, up = vpn_state()
|
||||||
if up:
|
if up:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue