Initial commit: sway + uwsm desktop dotfiles

Nord-themed Wayland setup on sway (via uwsm), including:

- sway/swayidle/swaylock/swaddle — WM, idle/lock/inhibit
- waybar, rofi — bar and launcher
- dunst — notifications
- foot — terminal
- zsh (+fzf, syntax highlighting, autosuggestions) — shell
- mpd/rmpc/mpc, mpv — music and video
- nvim — editor (lazy.nvim)
- ly — login manager
- rbw + pinentry-rofi wrapper — Bitwarden/Vaultwarden CLI
- per-window-layout, clipvault — helper systemd services
This commit is contained in:
2026-08-11 10:30:07 +04:00
commit a213b10a88
35 changed files with 2775 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
client.focused #88c0d0 #88c0d0 #eceff4 #88c0d0 #88c0d0
client.focused_inactive #00000000 #00000000 #81a1c1 #00000000 #00000000
client.unfocused #00000000 #00000000 #81a1c1 #00000000 #00000000
client.urgent #d08770 #d08770 #eceff4 #d08770 #d08770
font pango:Noto Sans Mono 10
smart_gaps off
smart_borders off
gaps inner 3
gaps outer 1
default_border pixel 1
default_floating_border pixel 1
titlebar_border_thickness 0
default_orientation auto
input type:keyboard {
xkb_layout "us,ru"
xkb_options "grp:win_space_toggle"
}
exec uwsm finalize WAYLAND_DISPLAY SWAYSOCK QT_QPA_PLATFORMTHEME QT_QPA_PLATFORMTHEME_QT6
exec_always xrandr --output HDMI-A-2 --primary
set $mod Mod4
set $left h
set $down j
set $up k
set $right l
set $term foot
set $menu rofi -show drun
set $file foot mc
workspace 1 output HDMI-A-2
workspace 2 output HDMI-A-2
workspace 3 output HDMI-A-2
workspace 4 output HDMI-A-2
workspace 5 output HDMI-A-2
workspace 6 output HDMI-A-2
workspace 7 output DP-1
workspace 8 output DP-1
workspace 9 output DP-2
workspace 0 output DP-2
workspace 1
output DP-1 bg ~/Pictures/wallhaven-43qlkd.jpg fill
output HDMI-A-2 bg ~/Pictures/wallhaven-478kjo.jpg fill
output DP-2 bg ~/Pictures/wallhaven-4gx713.jpg fill
output DP-1 resolution 1280x1024 position 0,220
output HDMI-A-2 resolution 1920x1080 position 1280,0
output DP-2 resolution 1280x102 position 3200,220
floating_modifier $mod normal
bindsym --to-code $mod+$left focus left
bindsym --to-code $mod+$down focus down
bindsym --to-code $mod+$up focus up
bindsym --to-code $mod+$right focus right
bindsym --to-code $mod+Shift+$left move left
bindsym --to-code $mod+Shift+$down move down
bindsym --to-code $mod+Shift+$up move up
bindsym --to-code $mod+Shift+$right move right
bindsym --to-code $mod+1 workspace number 1
bindsym --to-code $mod+2 workspace number 2
bindsym --to-code $mod+3 workspace number 3
bindsym --to-code $mod+4 workspace number 4
bindsym --to-code $mod+5 workspace number 5
bindsym --to-code $mod+6 workspace number 6
bindsym --to-code $mod+7 workspace number 7
bindsym --to-code $mod+8 workspace number 8
bindsym --to-code $mod+9 workspace number 9
bindsym --to-code $mod+0 workspace number 10
bindsym --to-code $mod+Shift+1 move container to workspace number 1
bindsym --to-code $mod+Shift+2 move container to workspace number 2
bindsym --to-code $mod+Shift+3 move container to workspace number 3
bindsym --to-code $mod+Shift+4 move container to workspace number 4
bindsym --to-code $mod+Shift+5 move container to workspace number 5
bindsym --to-code $mod+Shift+6 move container to workspace number 6
bindsym --to-code $mod+Shift+7 move container to workspace number 7
bindsym --to-code $mod+Shift+8 move container to workspace number 8
bindsym --to-code $mod+Shift+9 move container to workspace number 9
bindsym --to-code $mod+Shift+0 move container to workspace number 10
bindsym --to-code $mod+d exec $menu
bindsym --to-code $mod+v exec rofi -show clipboard
bindsym --to-code $mod+e exec $file
bindsym --to-code $mod+Return exec $term
bindsym --to-code $mod+c exec pkill -SIGRTMIN+13 waybar
bindsym --to-code $mod+Shift+q kill
bindsym --to-code $mod+f fullscreen
bindsym --to-code $mod+m focus mode_toggle
bindsym --to-code $mod+Shift+m floating toggle
bindsym --to-code $mod+s scratchpad show
bindsym --to-code $mod+Shift+s move scratchpad
bindsym --to-code --locked XF86AudioRaiseVolume exec wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+
bindsym --to-code --locked XF86AudioLowerVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
bindsym --to-code --locked XF86AudioMute exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
bindsym --to-code --locked $mod+F12 exec wpctl set-volume -l 1 @DEFAULT_AUDIO_SOURCE@ 5%+
bindsym --to-code --locked $mod+F11 exec wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%-
bindsym --to-code --locked $mod+F10 exec wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle
bindsym --to-code --locked XF86AudioNext exec playerctl next
bindsym --to-code --locked XF86AudioPause exec playerctl play-pause
bindsym --to-code --locked XF86AudioPlay exec playerctl play-pause
bindsym --to-code --locked XF86AudioPrev exec playerctl previous
bindsym --to-code Print exec area=$(slurp) && grim -g "$area" - | wl-copy && notify-send -u low "Screenshot" "Captured: $area"
bindsym --to-code $mod+Print exec active_class=$(swaymsg -t get_tree | jq -r '.. | select(.focused? == true) | .app_id // .window_properties.class // "unknown"') && geom=$(swaymsg -t get_tree | jq -r '.. | select(.focused? == true) | "\(.rect.x),\(.rect.y) \(.rect.width)x\(.rect.height)"') && grim -g "$geom" - | wl-copy && notify-send -u low "Screenshot" "Captured: $active_class"
bindsym --to-code ctrl+Print exec monitor=$(swaymsg -t get_outputs | jq -r '.[] | select(.focused==true) | .name') && grim -o "$monitor" - | wl-copy && notify-send -u low "Screenshot" "Captured: $monitor"
bindsym --to-code $mod+u exec sh -c 'rbw get ed-ssh | jq -r ".fields[]? | select(.name==\"privateKey\") | .text" | ssh-add -'
bindsym --to-code $mod+b exec rofi-rbw --selector rofi --selector-args="-theme ~/.config/rofi/bitwarden.rasi -mesg 'alt+1: <span foreground=\\\"#d8dee9\\\">type username/password</span>\nalt+2: <span foreground=\\\"#d8dee9\\\">type username</span>\talt+3: <span foreground=\\\"#d8dee9\\\">type password</span>\talt+4: <span foreground=\\\"#d8dee9\\\">type totp</span>\talt+u: <span foreground=\\\"#d8dee9\\\">copy username</span>\talt+c: <span foreground=\\\"#d8dee9\\\">copy password</span>\talt+t: <span foreground=\\\"#d8dee9\\\">copy totp</span>\talt+m: <span foreground=\\\"#d8dee9\\\">menu</span>\t\talt+s: <span foreground=\\\"#d8dee9\\\">sync logins</span>'"
mode "resize" {
bindsym --to-code $left resize shrink width 10px
bindsym --to-code $down resize grow height 10px
bindsym --to-code $up resize shrink height 10px
bindsym --to-code $right resize grow width 10px
bindsym --to-code Return mode "default"
bindsym --to-code Escape mode "default"
}
mode "reload sway? [y/n]" {
bindsym --to-code y reload; mode "default"
bindsym --to-code n mode "default"
bindsym Escape mode "default"
}
mode "lock sway? [y/n]" {
bindsym --to-code y exec swaylock; mode "default"
bindsym --to-code n mode "default"
bindsym Escape mode "default"
}
mode "exit sway? [y/n]" {
bindsym --to-code y exec swaymsg exit; mode "default"
bindsym --to-code n mode "default"
bindsym Escape mode "default"
}
bindsym --to-code $mod+r mode "resize"
bindsym --to-code $mod+Shift+r mode "reload sway? [y/n]"
bindsym --to-code $mod+x mode "lock sway? [y/n]"
bindsym --to-code $mod+Shift+x mode "exit sway? [y/n]"
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
from __future__ import annotations
import asyncio
import logging
import os
import signal
from dataclasses import dataclass, field
from typing import Dict, Optional
try:
from systemd import daemon
except ImportError:
daemon = None
import i3ipc
from i3ipc.aio import Connection
DEFAULT_MODE = os.getenv("PWL_DEFAULT_MODE", "1") == "1"
DEBUG = os.getenv("PWL_DEBUG", "0") == "1"
INFO = os.getenv("PWL_INFO", "1") == "1"
logging.basicConfig(
level=logging.DEBUG if DEBUG else (logging.INFO if INFO else logging.WARNING),
format="[pwl] %(asctime)s %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("pwl")
def notify_systemd(msg: str) -> None:
if daemon:
try:
daemon.notify(msg)
except Exception as e:
log.debug("Failed to notify systemd: %s", e)
def notify_ready(status: str) -> None:
notify_systemd(f"READY=1\nSTATUS={status}")
def notify_status(status: str) -> None:
notify_systemd(f"STATUS={status}")
def notify_watchdog() -> None:
notify_systemd("WATCHDOG=1")
@dataclass
class Keyboard:
identifier: str
default_index: int
current_index: int
layout_map: Dict[str, int] = field(default_factory=dict)
class PerWindowLayout:
def __init__(self) -> None:
self.conn: Optional[Connection] = None
self.keyboard: Optional[Keyboard] = None
self.window_layouts: Dict[int, int] = {}
self.current_window: Optional[int] = None
self.stop_event = asyncio.Event()
self._layout_map_built = False
async def connect(self) -> None:
self.conn = await Connection(auto_reconnect=True).connect()
def _find_main_keyboard(self, inputs) -> Optional[object]:
keyboards = [i for i in inputs if i.type == "keyboard"]
if not keyboards:
return None
real = [k for k in keyboards if "virtual" not in (k.identifier or "").lower()]
return (real or keyboards)[0]
async def wait_until_ready(self) -> None:
notify_status("Waiting for sway...")
while not self.stop_event.is_set():
try:
inputs = await self.conn.get_inputs()
except Exception:
await asyncio.sleep(0.2)
continue
main = self._find_main_keyboard(inputs)
if main is None:
await asyncio.sleep(0.2)
continue
idx = main.xkb_active_layout_index or 0
self.keyboard = Keyboard(
identifier=main.identifier,
default_index=idx,
current_index=idx,
)
return
async def build_layout_map(self, force: bool = False) -> None:
assert self.keyboard is not None
inputs = await self.conn.get_inputs()
current = self.keyboard.current_index
kbd = next((i for i in inputs if i.identifier == self.keyboard.identifier), None)
if not kbd:
return
layouts = kbd.xkb_layout_names or []
count = len(layouts)
if self._layout_map_built and not force and len(self.keyboard.layout_map) == count:
return
notify_status("Building layout map...")
self.keyboard.layout_map.clear()
for i in range(count):
await self.conn.command(f'input "{self.keyboard.identifier}" xkb_switch_layout {i}')
inputs = await self.conn.get_inputs()
kbd = next((k for k in inputs if k.identifier == self.keyboard.identifier), kbd)
name = kbd.xkb_active_layout_name or str(i)
self.keyboard.layout_map[name] = i
await self.conn.command(f'input "{self.keyboard.identifier}" xkb_switch_layout {current}')
self.keyboard.current_index = current
self._layout_map_built = True
log.info("Layout map initialized (%d layouts)", len(self.keyboard.layout_map))
async def init_windows(self) -> None:
assert self.keyboard is not None
inputs = await self.conn.get_inputs()
kbd = next((i for i in inputs if i.identifier == self.keyboard.identifier), None)
idx = kbd.xkb_active_layout_index if kbd else self.keyboard.default_index
tree = await self.conn.get_tree()
active_ids = set()
for con in tree.leaves():
active_ids.add(con.id)
if con.id not in self.window_layouts:
self.window_layouts[con.id] = idx
stale = [wid for wid in self.window_layouts if wid not in active_ids]
for wid in stale:
self.window_layouts.pop(wid, None)
notify_status(f"Tracking {len(self.window_layouts)} windows")
async def switch_layout(self, idx: int) -> None:
assert self.keyboard is not None
if idx == self.keyboard.current_index:
return
await self.conn.command(f'input "{self.keyboard.identifier}" xkb_switch_layout {idx}')
self.keyboard.current_index = idx
async def handle_active_window(self, con_id: int) -> None:
assert self.keyboard is not None
self.current_window = con_id
if con_id in self.window_layouts:
idx = self.window_layouts[con_id]
else:
idx = self.keyboard.default_index if DEFAULT_MODE else self.keyboard.current_index
self.window_layouts[con_id] = idx
notify_status(f"Tracking {len(self.window_layouts)} windows")
await self.switch_layout(idx)
def handle_active_layout(self, identifier: str, layout_name: Optional[str]) -> None:
assert self.keyboard is not None
if identifier != self.keyboard.identifier or not layout_name:
return
idx = self.keyboard.layout_map.get(layout_name)
if idx is None:
return
self.keyboard.current_index = idx
if self.current_window is not None:
self.window_layouts[self.current_window] = idx
def handle_close_window(self, con_id: int) -> None:
if self.window_layouts.pop(con_id, None) is not None:
notify_status(f"Tracking {len(self.window_layouts)} windows")
async def handle_device_change(self) -> None:
inputs = await self.conn.get_inputs()
main = self._find_main_keyboard(inputs)
if main and self.keyboard and main.identifier != self.keyboard.identifier:
log.info("Main keyboard changed: %s -> %s", self.keyboard.identifier, main.identifier)
idx = main.xkb_active_layout_index or 0
self.keyboard.identifier = main.identifier
self.keyboard.default_index = idx
self.keyboard.current_index = idx
await self.build_layout_map(force=True)
await self.init_windows()
def _register_handlers(self) -> None:
assert self.conn is not None
async def on_window(_conn, event):
change = event.change
if change == "focus" and event.container is not None:
await self.handle_active_window(event.container.id)
elif change == "close" and event.container is not None:
self.handle_close_window(event.container.id)
async def on_input(_conn, event):
change = event.change
if change == "xkb_layout" and event.input is not None:
self.handle_active_layout(
event.input.identifier, event.input.xkb_active_layout_name
)
elif change in ("added", "removed"):
await self.handle_device_change()
self.conn.on(i3ipc.Event.WINDOW, on_window)
self.conn.on(i3ipc.Event.INPUT, on_input)
async def watchdog_loop(self) -> None:
interval = 10.0
if daemon:
try:
wd = daemon.watchdog_enabled()
if isinstance(wd, (int, float)) and wd > 0:
interval = wd / 2.0
except Exception:
pass
while not self.stop_event.is_set():
notify_watchdog()
await asyncio.sleep(interval)
async def run(self) -> None:
await self.connect()
await self.wait_until_ready()
await self.build_layout_map()
await self.init_windows()
self._register_handlers()
notify_ready(f"Tracking {len(self.window_layouts)} windows")
async with asyncio.TaskGroup() as tg:
tg.create_task(self.conn.main())
tg.create_task(self.watchdog_loop())
await self.stop_event.wait()
await self.conn.main_quit()
async def amain() -> None:
pwl = PerWindowLayout()
loop = asyncio.get_running_loop()
def shutdown_handler() -> None:
pwl.stop_event.set()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, shutdown_handler)
try:
await pwl.run()
except asyncio.CancelledError:
raise
except Exception:
log.exception("Fatal error")
raise
def main() -> None:
try:
asyncio.run(amain())
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()