diff --git a/config/sway/config b/config/sway/config
index 9b1bf27..ade0963 100644
--- a/config/sway/config
+++ b/config/sway/config
@@ -129,7 +129,7 @@ exec dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DE
exec blueman-applet
exec wl-paste -t text --watch clipman store # Clipboard manager
-exec mako # Notification daemon
+exec swaync # Notification daemon (see home/swaync.nix)
exec nm-applet # Network Manager applet
exec kdeconnect-indicator
@@ -157,4 +157,10 @@ exec xfce4-power-manager
#exec /usr/bin/uxplay -avdec
bindsym $mod+l exec /home/shihaam/scripts/lock.sh
+# Notification centre: $mod+n opens the panel (past notifications, mpris,
+# DND toggle), $mod+Shift+n flips do-not-disturb. -sw closes it again on the
+# second press instead of stacking.
+bindsym $mod+n exec swaync-client -t -sw
+bindsym $mod+Shift+n exec swaync-client -d -sw
+
exec_always export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
diff --git a/config/swaync/config.json b/config/swaync/config.json
new file mode 100644
index 0000000..a977c2b
--- /dev/null
+++ b/config/swaync/config.json
@@ -0,0 +1,54 @@
+{
+ "positionX": "right",
+ "positionY": "top",
+ "layer": "overlay",
+ "control-center-layer": "top",
+ "layer-shell": true,
+ "cssPriority": "user",
+
+ "notification-window-width": 420,
+ "notification-icon-size": 48,
+ "notification-body-image-height": 160,
+ "notification-body-image-width": 200,
+ "image-visibility": "when-available",
+ "transition-time": 200,
+
+ "timeout": 8,
+ "timeout-low": 4,
+ "timeout-critical": 0,
+
+ "control-center-width": 440,
+ "control-center-height": 700,
+ "control-center-margin-top": 8,
+ "control-center-margin-bottom": 8,
+ "control-center-margin-right": 8,
+ "control-center-margin-left": 0,
+ "fit-to-screen": true,
+
+ "keyboard-shortcuts": true,
+ "hide-on-clear": false,
+ "hide-on-action": true,
+ "script-fail-notify": true,
+
+ "widgets": [
+ "title",
+ "dnd",
+ "mpris",
+ "notifications"
+ ],
+
+ "widget-config": {
+ "title": {
+ "text": "Notifications",
+ "clear-all-button": true,
+ "button-text": "Clear all"
+ },
+ "dnd": {
+ "text": "Do not disturb"
+ },
+ "mpris": {
+ "image-size": 88,
+ "image-radius": 8
+ }
+ }
+}
diff --git a/config/swaync/style.css b/config/swaync/style.css
new file mode 100644
index 0000000..1d153d4
--- /dev/null
+++ b/config/swaync/style.css
@@ -0,0 +1,218 @@
+/* Material Design SwayNC theme -- same palette as config/waybar/style.css:
+ #1E1E1E background, #2D2D2D surface, #E1E1E1 text, #90CAF9 accent,
+ #CF6679 critical. */
+
+* {
+ font-family: "Roboto", "JetBrainsMono Nerd Font", "Material Design Icons", sans-serif;
+ font-size: 14px;
+}
+
+/* ============= Popup notifications ============= */
+
+/* The toplevel window must stay transparent or the whole layer surface paints
+ a rectangle around the rounded cards. */
+.control-center,
+.notification-row,
+.notification-background,
+.floating-notifications.background {
+ background: transparent;
+}
+
+.notification {
+ margin: 8px;
+ padding: 0;
+ border-radius: 12px;
+ background: #2D2D2D;
+ color: #E1E1E1;
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
+}
+
+.notification-content {
+ padding: 12px;
+}
+
+.summary {
+ font-size: 14px;
+ font-weight: 500;
+ color: #E1E1E1;
+}
+
+.time {
+ font-size: 12px;
+ color: #B0B0B0;
+}
+
+.body {
+ font-size: 13px;
+ color: #B0B0B0;
+}
+
+.notification.critical {
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4), inset 3px 0 0 #CF6679;
+}
+
+.notification.low .summary {
+ color: #B0B0B0;
+}
+
+/* ============= Action buttons =============
+ What bluetooth pairing prompts, Nextcloud-style "Accept/Reject" and browser
+ notifications render into. .notification-action is one button per action;
+ the first child gets the left rounding, the last the right. */
+
+.notification-action {
+ margin: 0;
+ padding: 8px 12px;
+ border-top: 1px solid #383838;
+ background: #2D2D2D;
+ color: #90CAF9;
+ font-weight: 500;
+ transition: background 200ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.notification-action:hover {
+ background: rgba(144, 202, 249, 0.12);
+ color: #90CAF9;
+}
+
+.notification-action:active {
+ background: rgba(144, 202, 249, 0.2);
+}
+
+.notification-action:first-child {
+ border-radius: 0 0 0 12px;
+}
+
+.notification-action:last-child {
+ border-radius: 0 0 12px 0;
+ border-right: none;
+}
+
+.notification-default-action {
+ border-radius: 12px;
+}
+
+/* Rounding only collapses on the bottom when action buttons sit below. */
+.notification-default-action:not(:only-child) {
+ border-radius: 12px 12px 0 0;
+}
+
+.close-button {
+ margin: 6px;
+ padding: 0;
+ min-width: 24px;
+ min-height: 24px;
+ border-radius: 12px;
+ background: transparent;
+ color: #B0B0B0;
+ box-shadow: none;
+}
+
+.close-button:hover {
+ background: #CF6679;
+ color: #1E1E1E;
+}
+
+.inline-reply {
+ margin-top: 8px;
+}
+
+.inline-reply-entry {
+ padding: 6px 8px;
+ border: 1px solid #383838;
+ border-radius: 6px;
+ background: #1E1E1E;
+ color: #E1E1E1;
+}
+
+.inline-reply-button {
+ margin-left: 4px;
+ padding: 6px 12px;
+ border: none;
+ border-radius: 6px;
+ background: #90CAF9;
+ color: #1E1E1E;
+}
+
+/* ============= Control center ============= */
+
+.control-center {
+ margin: 8px;
+ padding: 8px;
+ border-radius: 12px;
+ background: #1E1E1E;
+ color: #E1E1E1;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
+}
+
+.control-center .notification {
+ margin: 4px 0;
+}
+
+.widget-title {
+ margin: 8px 8px 4px 8px;
+ font-size: 15px;
+ font-weight: 500;
+ color: #E1E1E1;
+}
+
+.widget-title > button {
+ padding: 4px 12px;
+ border: none;
+ border-radius: 6px;
+ background: #2D2D2D;
+ color: #90CAF9;
+ font-size: 13px;
+}
+
+.widget-title > button:hover {
+ background: rgba(144, 202, 249, 0.12);
+}
+
+.widget-dnd {
+ margin: 4px 8px 8px 8px;
+ font-size: 13px;
+ color: #B0B0B0;
+}
+
+.widget-dnd > switch {
+ border-radius: 12px;
+ background: #383838;
+ box-shadow: none;
+}
+
+.widget-dnd > switch:checked {
+ background: #90CAF9;
+}
+
+.widget-dnd > switch slider {
+ border-radius: 12px;
+ background: #E1E1E1;
+}
+
+.widget-mpris {
+ margin: 4px 0;
+ padding: 8px;
+ border-radius: 12px;
+ background: #2D2D2D;
+ color: #E1E1E1;
+}
+
+.widget-mpris-player {
+ padding: 8px;
+}
+
+.widget-mpris-title {
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.widget-mpris-subtitle {
+ font-size: 12px;
+ color: #B0B0B0;
+}
+
+.widget-mpris button:hover {
+ background: rgba(144, 202, 249, 0.12);
+ color: #90CAF9;
+}
diff --git a/config/waybar/config b/config/waybar/config
new file mode 100644
index 0000000..282bf2d
--- /dev/null
+++ b/config/waybar/config
@@ -0,0 +1,576 @@
+[
+ {
+ "output": "DP-1",
+ "position": "bottom",
+ "height": 42,
+ "layer": "top",
+ "spacing": 4,
+
+ "modules-left": [
+ "sway/workspaces",
+ "sway/mode",
+ "custom/media"
+ ],
+
+ "modules-center": [
+ "sway/window"
+ ],
+
+ "modules-right": [
+ "idle_inhibitor",
+ "privacy",
+ "custom/updates",
+ "custom/weather",
+ "bluetooth",
+ "network",
+ "custom/vpn",
+ "pulseaudio",
+ "custom/monitor-brightness",
+ "cpu",
+ "memory",
+ "temperature",
+ "battery",
+ "custom/ibus",
+ "tray",
+ "clock",
+ "custom/power"
+ ],
+
+ "sway/workspaces": {
+ "disable-scroll": false,
+ "all-outputs": false,
+ "format": "{name}"
+ },
+
+ "sway/mode": {
+ "format": " {}",
+ "tooltip": false
+ },
+
+ "sway/window": {
+ "format": "{}",
+ "max-length": 50,
+ "icon": true,
+ "icon-size": 16,
+ "rewrite": {
+ "(.*) ā Mozilla Firefox": "ó°¹ $1",
+ "(.*) - Visual Studio Code": "ó°Ø $1",
+ "(.*) - vim": " $1",
+ "(.*) - zsh": " $1"
+ }
+ },
+
+ "custom/media": {
+ "format": " {}",
+ "return-type": "json",
+ "max-length": 40,
+ "escape": true,
+ "exec": "$HOME/.config/waybar/scripts/mediaplayer.py 2> /dev/null",
+ "on-click": "playerctl play-pause",
+ "on-click-right": "playerctl next",
+ "on-scroll-up": "playerctl previous",
+ "on-scroll-down": "playerctl next"
+ },
+
+ "clock": {
+ "interval": 1,
+ "format": "{:%H:%M:%S}",
+ "format-alt": "{:%A, %B %d, %Y (%R)}",
+ "tooltip-format": "{calendar}",
+ "calendar": {
+ "mode": "year",
+ "mode-mon-col": 3,
+ "weeks-pos": "right",
+ "on-scroll": 1,
+ "on-click-right": "mode",
+ "format": {
+ "months": "{}",
+ "days": "{}",
+ "weeks": "W{}",
+ "weekdays": "{}",
+ "today": "{}"
+ }
+ },
+ "actions": {
+ "on-click-right": "mode",
+ "on-scroll-up": "shift_up",
+ "on-scroll-down": "shift_down"
+ }
+ },
+
+ "idle_inhibitor": {
+ "format": "{icon}",
+ "format-icons": {
+ "activated": "",
+ "deactivated": ""
+ },
+ "tooltip-format-activated": "Idle inhibitor: ON",
+ "tooltip-format-deactivated": "Idle inhibitor: OFF"
+ },
+
+ "privacy": {
+ "icon-spacing": 4,
+ "icon-size": 18,
+ "transition-duration": 250,
+ "modules": [
+ {
+ "type": "screenshare",
+ "tooltip": true,
+ "tooltip-icon-size": 24
+ },
+ {
+ "type": "audio-in",
+ "tooltip": true,
+ "tooltip-icon-size": 24
+ }
+ ]
+ },
+
+ "custom/updates": {
+ "format": "{} ",
+ "interval": 3600,
+ "exec": "checkupdates 2>/dev/null | wc -l",
+ "exec-if": "exit 0",
+ "on-click": "alacritty -e paru -Syu",
+ "tooltip": true,
+ "tooltip-format": "{} pending updates"
+ },
+
+ "custom/weather": {
+ "format": "{}",
+ "tooltip": true,
+ "interval": 1800,
+ "exec": "$HOME/.config/waybar/scripts/weather.sh",
+ "return-type": "json",
+ "on-click": "xdg-open https://weather.com"
+ },
+
+ "bluetooth": {
+ "format": " {status}",
+ "format-disabled": "",
+ "format-connected": " {num_connections}",
+ "tooltip-format": "{controller_alias}\t{controller_address}",
+ "tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{device_enumerate}",
+ "tooltip-format-enumerate-connected": "{device_alias}\t{device_address}",
+ "on-click": "blueman-manager"
+ },
+
+ "network": {
+ "interval": 5,
+ "format-wifi": "š¶ {signalStrength}%",
+ "format-ethernet": "š {ipaddr}",
+ "format-linked": "š {ifname} (No IP)",
+ "format-disconnected": "ā Disconnected",
+ "tooltip-format": "{ifname}: {ipaddr}/{cidr}\nGateway: {gwaddr}\nStrength: {signalStrength}%\nFrequency: {frequency}MHz\nUp: {bandwidthUpBits} Down: {bandwidthDownBits}",
+ "tooltip-format-wifi": "š¶ {essid} ({signalStrength}%)\n{ipaddr}/{cidr}\nGateway: {gwaddr}\nFrequency: {frequency}MHz\nUp: {bandwidthUpBits} Down: {bandwidthDownBits}",
+ "tooltip-format-ethernet": "š {ifname}\n{ipaddr}/{cidr}\nGateway: {gwaddr}\nUp: {bandwidthUpBits} Down: {bandwidthDownBits}",
+ "on-click": "nm-connection-editor",
+ "on-click-right": "alacritty -e nmtui"
+ },
+
+ "custom/vpn": {
+ "format": "{}",
+ "interval": 5,
+ "exec": "$HOME/.config/waybar/scripts/vpn-check.sh",
+ "return-type": "json",
+ "on-click": "nm-connection-editor",
+ "tooltip": true
+ },
+
+ "pulseaudio": {
+ "scroll-step": 5,
+ "format": "{icon} {volume}%",
+ "format-bluetooth": "{icon} {volume}%",
+ "format-bluetooth-muted": " {icon}",
+ "format-muted": " {volume}%",
+ "format-icons": {
+ "headphone": "",
+ "hands-free": "",
+ "headset": "",
+ "phone": "",
+ "portable": "",
+ "car": "",
+ "default": ["", "", ""]
+ },
+ "on-click": "if pgrep -x pavucontrol > /dev/null; then pkill pavucontrol; else pavucontrol; fi",
+ "on-click-right": "pactl set-sink-mute @DEFAULT_SINK@ toggle",
+ "on-scroll-up": "pactl set-sink-volume @DEFAULT_SINK@ +5%",
+ "on-scroll-down": "pactl set-sink-volume @DEFAULT_SINK@ -5%",
+ "tooltip-format": "{desc}\nVolume: {volume}%"
+ },
+
+ "custom/monitor-brightness": {
+ "format": " {}%",
+ "interval": 2,
+ "exec": "$HOME/.config/waybar/scripts/monitor-brightness.sh get",
+ "on-click": "gddccontrol",
+ "on-scroll-up": "$HOME/.config/waybar/scripts/monitor-brightness.sh up && pkill -RTMIN+10 waybar",
+ "on-scroll-down": "$HOME/.config/waybar/scripts/monitor-brightness.sh down && pkill -RTMIN+10 waybar",
+ "signal": 10,
+ "tooltip": true,
+ "tooltip-format": "Monitor Brightness: {}%"
+ },
+
+ "cpu": {
+ "interval": 2,
+ "format": " {usage}%",
+ "max-length": 10,
+ "on-click": "alacritty -e htop",
+ "tooltip": true
+ },
+
+ "memory": {
+ "interval": 5,
+ "format": " {}%",
+ "tooltip-format": "RAM: {used:0.1f}G / {total:0.1f}G\nSwap: {swapUsed:0.1f}G / {swapTotal:0.1f}G",
+ "on-click": "alacritty -e htop"
+ },
+
+ "temperature": {
+ "thermal-zone": 2,
+ "hwmon-path": "/sys/class/hwmon/hwmon2/temp1_input",
+ "critical-threshold": 80,
+ "interval": 5,
+ "format": "{icon} {temperatureC}°C",
+ "format-icons": ["", "", "", "", ""],
+ "tooltip": true
+ },
+
+ "battery": {
+ "states": {
+ "good": 95,
+ "warning": 30,
+ "critical": 15
+ },
+ "interval": 30,
+ "format": "{icon} {capacity}%",
+ "format-charging": " {capacity}%",
+ "format-plugged": " {capacity}%",
+ "format-alt": "{icon} {time}",
+ "format-icons": ["", "", "", "", ""],
+ "tooltip-format": "{timeTo}\nCapacity: {capacity}%\nHealth: {health}%\nPower: {power}W"
+ },
+
+ "custom/ibus": {
+ "exec": "ibus engine",
+ "interval": 1,
+ "format": "⨠{}",
+ "on-click": "ibus engine Thaana",
+ "tooltip-format": "Current keyboard: {}"
+ },
+
+ "tray": {
+ "icon-size": 18,
+ "spacing": 8,
+ "show-passive-items": true
+ },
+
+ "custom/power": {
+ "format": "ā»",
+ "on-click": "$HOME/.config/waybar/scripts/power-menu.sh",
+ "tooltip": false
+ }
+ },
+ {
+ "output": ["DP-2", "DP-3", "DP-4", "DP-5", "DP-6", "DP-7", "DP-8", "HDMI-A-1", "HDMI-A-2", "HDMI-A-3"],
+ "position": "bottom",
+ "height": 42,
+ "layer": "top",
+ "spacing": 4,
+
+ "modules-left": [
+ "sway/workspaces",
+ "sway/mode"
+ ],
+
+ "modules-center": [
+ "sway/window"
+ ],
+
+ "modules-right": [
+ "privacy",
+ "network",
+ "pulseaudio",
+ "battery",
+ "tray",
+ "clock",
+ "custom/power"
+ ],
+
+ "sway/workspaces": {
+ "disable-scroll": false,
+ "all-outputs": false,
+ "format": "{name}"
+ },
+
+ "sway/mode": {
+ "format": " {}",
+ "tooltip": false
+ },
+
+ "sway/window": {
+ "format": "{}",
+ "max-length": 50,
+ "icon": true,
+ "icon-size": 16,
+ "rewrite": {
+ "(.*) ā Mozilla Firefox": "ó°¹ $1",
+ "(.*) - Visual Studio Code": "ó°Ø $1",
+ "(.*) - vim": " $1",
+ "(.*) - zsh": " $1"
+ }
+ },
+
+ "privacy": {
+ "icon-spacing": 4,
+ "icon-size": 18,
+ "transition-duration": 250,
+ "modules": [
+ {
+ "type": "screenshare",
+ "tooltip": true,
+ "tooltip-icon-size": 24
+ },
+ {
+ "type": "audio-in",
+ "tooltip": true,
+ "tooltip-icon-size": 24
+ }
+ ]
+ },
+
+ "network": {
+ "interval": 5,
+ "format-wifi": "š¶ {signalStrength}%",
+ "format-ethernet": "š {ipaddr}",
+ "format-linked": "š {ifname} (No IP)",
+ "format-disconnected": "ā Disconnected",
+ "tooltip-format": "{ifname}: {ipaddr}/{cidr}\nGateway: {gwaddr}\nStrength: {signalStrength}%\nFrequency: {frequency}MHz\nUp: {bandwidthUpBits} Down: {bandwidthDownBits}",
+ "tooltip-format-wifi": "š¶ {essid} ({signalStrength}%)\n{ipaddr}/{cidr}\nGateway: {gwaddr}\nFrequency: {frequency}MHz\nUp: {bandwidthUpBits} Down: {bandwidthDownBits}",
+ "tooltip-format-ethernet": "š {ifname}\n{ipaddr}/{cidr}\nGateway: {gwaddr}\nUp: {bandwidthUpBits} Down: {bandwidthDownBits}",
+ "on-click": "nm-connection-editor",
+ "on-click-right": "alacritty -e nmtui"
+ },
+
+ "pulseaudio": {
+ "scroll-step": 5,
+ "format": "{icon} {volume}%",
+ "format-bluetooth": "{icon} {volume}%",
+ "format-bluetooth-muted": " {icon}",
+ "format-muted": " {volume}%",
+ "format-icons": {
+ "headphone": "",
+ "hands-free": "",
+ "headset": "",
+ "phone": "",
+ "portable": "",
+ "car": "",
+ "default": ["", "", ""]
+ },
+ "on-click": "if pgrep -x pavucontrol > /dev/null; then pkill pavucontrol; else pavucontrol; fi",
+ "on-click-right": "pactl set-sink-mute @DEFAULT_SINK@ toggle",
+ "on-scroll-up": "pactl set-sink-volume @DEFAULT_SINK@ +5%",
+ "on-scroll-down": "pactl set-sink-volume @DEFAULT_SINK@ -5%",
+ "tooltip-format": "{desc}\nVolume: {volume}%"
+ },
+
+ "battery": {
+ "states": {
+ "good": 95,
+ "warning": 30,
+ "critical": 15
+ },
+ "interval": 30,
+ "format": "{icon} {capacity}%",
+ "format-charging": " {capacity}%",
+ "format-plugged": " {capacity}%",
+ "format-alt": "{icon} {time}",
+ "format-icons": ["", "", "", "", ""],
+ "tooltip-format": "{timeTo}\nCapacity: {capacity}%\nHealth: {health}%\nPower: {power}W"
+ },
+
+ "clock": {
+ "interval": 1,
+ "format": "{:%H:%M:%S}",
+ "format-alt": "{:%A, %B %d, %Y (%R)}",
+ "tooltip-format": "{calendar}",
+ "calendar": {
+ "mode": "year",
+ "mode-mon-col": 3,
+ "weeks-pos": "right",
+ "on-scroll": 1,
+ "on-click-right": "mode",
+ "format": {
+ "months": "{}",
+ "days": "{}",
+ "weeks": "W{}",
+ "weekdays": "{}",
+ "today": "{}"
+ }
+ },
+ "actions": {
+ "on-click-right": "mode",
+ "on-scroll-up": "shift_up",
+ "on-scroll-down": "shift_down"
+ }
+ },
+
+ "tray": {
+ "icon-size": 18,
+ "spacing": 8,
+ "show-passive-items": true
+ },
+
+ "custom/power": {
+ "format": "ā»",
+ "on-click": "$HOME/.config/waybar/scripts/power-menu.sh",
+ "tooltip": false
+ }
+ },
+ {
+ "output": "eDP-1",
+ "position": "bottom",
+ "height": 42,
+ "layer": "top",
+ "spacing": 4,
+
+ "modules-left": [
+ "sway/workspaces"
+ ],
+
+ "modules-center": [
+ "sway/window"
+ ],
+
+ "modules-right": [
+ "privacy",
+ "pulseaudio",
+ "backlight",
+ "battery",
+ "tray",
+ "clock",
+ "custom/power"
+ ],
+
+ "sway/workspaces": {
+ "disable-scroll": false,
+ "all-outputs": false,
+ "format": "{name}"
+ },
+
+ "sway/window": {
+ "format": "{}",
+ "max-length": 50,
+ "icon": true,
+ "icon-size": 16,
+ "rewrite": {
+ "(.*) ā Mozilla Firefox": "ó°¹ $1",
+ "(.*) - Visual Studio Code": "ó°Ø $1",
+ "(.*) - vim": " $1",
+ "(.*) - zsh": " $1"
+ }
+ },
+
+ "privacy": {
+ "icon-spacing": 4,
+ "icon-size": 18,
+ "transition-duration": 250,
+ "modules": [
+ {
+ "type": "screenshare",
+ "tooltip": true,
+ "tooltip-icon-size": 24
+ },
+ {
+ "type": "audio-in",
+ "tooltip": true,
+ "tooltip-icon-size": 24
+ }
+ ]
+ },
+
+ "pulseaudio": {
+ "scroll-step": 5,
+ "format": "{icon} {volume}%",
+ "format-bluetooth": "{icon} {volume}%",
+ "format-bluetooth-muted": " {icon}",
+ "format-muted": " {volume}%",
+ "format-icons": {
+ "headphone": "",
+ "hands-free": "",
+ "headset": "",
+ "phone": "",
+ "portable": "",
+ "car": "",
+ "default": ["", "", ""]
+ },
+ "on-click": "if pgrep -x pavucontrol > /dev/null; then pkill pavucontrol; else pavucontrol; fi",
+ "on-click-right": "pactl set-sink-mute @DEFAULT_SINK@ toggle",
+ "on-scroll-up": "pactl set-sink-volume @DEFAULT_SINK@ +5%",
+ "on-scroll-down": "pactl set-sink-volume @DEFAULT_SINK@ -5%",
+ "tooltip-format": "{desc}\nVolume: {volume}%"
+ },
+
+ "backlight": {
+ "device": "intel_backlight",
+ "format": "{icon} {percent}%",
+ "format-icons": ["", "", "", "", "", "", "", "", ""],
+ "on-scroll-up": "brightnessctl set +5%",
+ "on-scroll-down": "[ $(brightnessctl -m | cut -d',' -f4 | tr -d '%') -le 5 ] && brightnessctl set 1% || brightnessctl set 5%-",
+ "tooltip-format": "Brightness: {percent}%"
+ },
+
+ "battery": {
+ "states": {
+ "good": 95,
+ "warning": 30,
+ "critical": 15
+ },
+ "interval": 30,
+ "format": "{icon} {capacity}%",
+ "format-charging": " {capacity}%",
+ "format-plugged": " {capacity}%",
+ "format-alt": "{icon} {time}",
+ "format-icons": ["", "", "", "", ""],
+ "tooltip-format": "{timeTo}\nCapacity: {capacity}%\nHealth: {health}%\nPower: {power}W"
+ },
+
+ "clock": {
+ "interval": 1,
+ "format": "{:%H:%M:%S}",
+ "format-alt": "{:%A, %B %d, %Y (%R)}",
+ "tooltip-format": "{calendar}",
+ "calendar": {
+ "mode": "year",
+ "mode-mon-col": 3,
+ "weeks-pos": "right",
+ "on-scroll": 1,
+ "on-click-right": "mode",
+ "format": {
+ "months": "{}",
+ "days": "{}",
+ "weeks": "W{}",
+ "weekdays": "{}",
+ "today": "{}"
+ }
+ },
+ "actions": {
+ "on-click-right": "mode",
+ "on-scroll-up": "shift_up",
+ "on-scroll-down": "shift_down"
+ }
+ },
+
+ "tray": {
+ "icon-size": 18,
+ "spacing": 8,
+ "show-passive-items": true
+ },
+
+ "custom/power": {
+ "format": "ā»",
+ "on-click": "$HOME/.config/waybar/scripts/power-menu.sh",
+ "tooltip": false
+ }
+ }
+]
diff --git a/config/waybar/scripts/mediaplayer.py b/config/waybar/scripts/mediaplayer.py
new file mode 100755
index 0000000..9aa0167
--- /dev/null
+++ b/config/waybar/scripts/mediaplayer.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+import json
+import subprocess
+import sys
+import signal
+
+def signal_handler(sig, frame):
+ sys.exit(0)
+
+signal.signal(signal.SIGTERM, signal_handler)
+signal.signal(signal.SIGINT, signal_handler)
+
+def get_output(status, artist, title, player):
+ if not status or status == "Stopped" or not title:
+ return {"text": "", "tooltip": "No media playing", "class": "stopped"}
+
+ if artist and title:
+ text = f"{artist} - {title}"
+ elif title:
+ text = title
+ else:
+ return {"text": "", "tooltip": "No media playing", "class": "stopped"}
+
+ if status == "Playing":
+ tooltip = f"ā¶ {text}"
+ display = text
+ elif status == "Paused":
+ tooltip = f"āø {text}"
+ display = f"āø {text}"
+ else:
+ tooltip = text
+ display = text
+
+ return {
+ "text": display[:40] + "..." if len(display) > 40 else display,
+ "tooltip": tooltip,
+ "class": status.lower() if status else "stopped"
+ }
+
+def main():
+ # Use playerctl follow to get real-time updates
+ try:
+ proc = subprocess.Popen(
+ ['playerctl', '--follow', 'metadata', '--format', '{{status}}|||{{artist}}|||{{title}}|||{{playerName}}'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ text=True
+ )
+
+ for line in proc.stdout:
+ line = line.strip()
+ if not line:
+ output = {"text": "", "tooltip": "No media playing", "class": "stopped"}
+ else:
+ parts = line.split('|||')
+ if len(parts) >= 4:
+ status, artist, title, player = parts[0], parts[1], parts[2], parts[3]
+ output = get_output(status, artist, title, player)
+ else:
+ output = {"text": "", "tooltip": "No media playing", "class": "stopped"}
+
+ print(json.dumps(output), flush=True)
+
+ except Exception as e:
+ print(json.dumps({"text": "", "tooltip": "No media playing", "class": "stopped"}), flush=True)
+
+if __name__ == "__main__":
+ main()
diff --git a/config/waybar/scripts/monitor-brightness.sh b/config/waybar/scripts/monitor-brightness.sh
new file mode 100755
index 0000000..664a8b8
--- /dev/null
+++ b/config/waybar/scripts/monitor-brightness.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+
+CACHE_FILE="/tmp/monitor-brightness-cache"
+PID_FILE="/tmp/monitor-brightness-pid"
+SCROLL_FILE="/tmp/monitor-brightness-scroll"
+LOCK_FILE="/tmp/monitor-brightness-lock"
+
+get_actual() {
+ # Prevent concurrent ddcutil calls
+ exec 9>"$LOCK_FILE"
+ flock -n 9 || { cat "$CACHE_FILE" 2>/dev/null || echo "50"; return; }
+ val=$(ddcutil getvcp 10 --display 1 2>/dev/null | grep -oP 'current value =\s*\K[0-9]+')
+ exec 9>&-
+ echo "$val"
+}
+
+# Initialize cache if missing
+[ ! -f "$CACHE_FILE" ] && echo "50" > "$CACHE_FILE"
+
+apply_brightness() {
+ local value=$1
+ [ -f "$PID_FILE" ] && kill "$(cat "$PID_FILE")" 2>/dev/null
+ (
+ echo $$ > "$PID_FILE"
+ sleep 0.3
+ exec 9>"$LOCK_FILE"
+ flock 9
+ ddcutil setvcp 10 "$value" --display 1 --noverify 2>/dev/null
+ exec 9>&-
+ rm -f "$PID_FILE"
+ ) &
+}
+
+case "$1" in
+ get)
+ # If scrolled recently, use cache for instant feedback
+ if [ -f "$SCROLL_FILE" ]; then
+ age=$(( $(date +%s) - $(stat -c %Y "$SCROLL_FILE") ))
+ if [ "$age" -lt 3 ]; then
+ cat "$CACHE_FILE"
+ exit 0
+ fi
+ rm -f "$SCROLL_FILE"
+ fi
+ # Otherwise fetch actual value
+ val=$(get_actual)
+ if [ -n "$val" ]; then
+ echo "$val" > "$CACHE_FILE"
+ echo "$val"
+ else
+ cat "$CACHE_FILE"
+ fi
+ ;;
+ up)
+ touch "$SCROLL_FILE"
+ current=$(cat "$CACHE_FILE")
+ new=$((current + 5))
+ [ "$new" -gt 100 ] && new=100
+ echo "$new" > "$CACHE_FILE"
+ apply_brightness "$new"
+ ;;
+ down)
+ touch "$SCROLL_FILE"
+ current=$(cat "$CACHE_FILE")
+ new=$((current - 5))
+ [ "$new" -lt 0 ] && new=0
+ echo "$new" > "$CACHE_FILE"
+ apply_brightness "$new"
+ ;;
+esac
diff --git a/config/waybar/scripts/power-menu.sh b/config/waybar/scripts/power-menu.sh
new file mode 100755
index 0000000..2feb6cf
--- /dev/null
+++ b/config/waybar/scripts/power-menu.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+
+# Power menu using rofi or wofi
+# Detect which one is available
+if command -v rofi &> /dev/null; then
+ MENU="rofi -dmenu -i -p Power"
+elif command -v wofi &> /dev/null; then
+ MENU="wofi --dmenu -i -p Power"
+else
+ notify-send "Power Menu" "Neither rofi nor wofi is installed"
+ exit 1
+fi
+
+# Menu options
+OPTIONS="ā» Shutdown\nā³ Reboot\nā¾ Suspend\n Lock\n Logout"
+
+# Show menu and get choice
+CHOICE=$(echo -e "$OPTIONS" | $MENU)
+
+# Execute based on choice
+case $CHOICE in
+ *Shutdown)
+ systemctl poweroff
+ ;;
+ *Reboot)
+ systemctl reboot
+ ;;
+ *Suspend)
+ systemctl suspend
+ ;;
+ *Lock)
+ # Try different lock commands
+ if command -v swaylock &> /dev/null; then
+ swaylock -f -c 000000
+ elif command -v gtklock &> /dev/null; then
+ gtklock
+ else
+ notify-send "Lock" "No lock screen program found"
+ fi
+ ;;
+ *Logout)
+ swaymsg exit
+ ;;
+esac
diff --git a/config/waybar/scripts/vpn-check.sh b/config/waybar/scripts/vpn-check.sh
new file mode 100755
index 0000000..19cecb2
--- /dev/null
+++ b/config/waybar/scripts/vpn-check.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+
+# Check for VPN connections using nmcli device show
+
+# Initialize variables
+vpn_status=""
+vpn_tooltip=""
+vpn_class="disconnected"
+vpn_count=0
+
+# Get all VPN devices and their details
+while IFS= read -r device; do
+ if [ -n "$device" ]; then
+ # Get device type and connection
+ device_info=$(nmcli device show "$device" 2>/dev/null)
+ connection=$(echo "$device_info" | grep "GENERAL.CONNECTION:" | awk -F: '{print $2}' | xargs)
+ device_type=$(echo "$device_info" | grep "GENERAL.TYPE:" | awk -F: '{print $2}' | xargs)
+ ip_addr=$(echo "$device_info" | grep "IP4.ADDRESS" | head -1 | awk -F: '{print $2}' | xargs)
+
+ if [ -n "$connection" ] && [ "$connection" != "--" ]; then
+ ((vpn_count++))
+
+ # Determine VPN type
+ if [[ "$device" == wg* ]]; then
+ vpn_type="WG"
+ vpn_icon="š"
+ elif [[ "$device" == tun* ]]; then
+ vpn_type="OVPN"
+ vpn_icon="š"
+ else
+ vpn_type="VPN"
+ vpn_icon="š"
+ fi
+
+ # Build status text
+ if [ -z "$vpn_status" ]; then
+ vpn_status="$vpn_icon $vpn_type"
+ vpn_tooltip="$vpn_type: $connection"
+ else
+ vpn_status="$vpn_status | $vpn_icon $vpn_type"
+ vpn_tooltip="$vpn_tooltip\n$vpn_type: $connection"
+ fi
+
+ # Add IP if available
+ if [ -n "$ip_addr" ]; then
+ vpn_tooltip="$vpn_tooltip\nIP: $ip_addr"
+ fi
+
+ # Set class based on first VPN type found
+ if [ "$vpn_class" = "disconnected" ]; then
+ if [[ "$device" == wg* ]]; then
+ vpn_class="connected-wg"
+ elif [[ "$device" == tun* ]]; then
+ vpn_class="connected-ovpn"
+ else
+ vpn_class="connected"
+ fi
+ fi
+ fi
+ fi
+done < <(nmcli -t -f DEVICE device status | grep -E '^(tun|wg)' | cut -d: -f1)
+
+# If no VPN found, show empty
+if [ $vpn_count -eq 0 ]; then
+ echo "{\"text\":\"\", \"tooltip\":\"No VPN connected\", \"class\":\"disconnected\"}"
+else
+ echo "{\"text\":\"$vpn_status\", \"tooltip\":\"$vpn_tooltip\", \"class\":\"$vpn_class\"}"
+fi
diff --git a/config/waybar/scripts/weather.sh b/config/waybar/scripts/weather.sh
new file mode 100755
index 0000000..e9e2c9a
--- /dev/null
+++ b/config/waybar/scripts/weather.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+# Get weather data from wttr.in with geolocation from ipinfo.io
+# You can override with WEATHER_LOCATION environment variable
+LOCATION="${WEATHER_LOCATION:-}"
+
+# If no location is set, fetch it from ipinfo.io
+if [ -z "$LOCATION" ]; then
+ LOCATION=$(curl -s "https://ipinfo.io/" | jq -r '.city // empty')
+fi
+
+# Fetch weather using wttr.in
+if [ -n "$LOCATION" ]; then
+ WEATHER_DATA=$(curl -s "wttr.in/$LOCATION?format=%c+%t")
+ TOOLTIP=$(curl -s "wttr.in/$LOCATION?format=%l:+%c+%t+%w+%p+%h")
+else
+ WEATHER_DATA=$(curl -s "wttr.in/?format=%c+%t")
+ TOOLTIP=$(curl -s "wttr.in/?format=%l:+%c+%t+%w+%p+%h")
+fi
+
+# Check if curl was successful
+if [ $? -eq 0 ] && [ -n "$WEATHER_DATA" ]; then
+ echo "{\"text\":\"$WEATHER_DATA\", \"tooltip\":\"$TOOLTIP\"}"
+else
+ echo "{\"text\":\"š” N/A\", \"tooltip\":\"Weather data unavailable\"}"
+fi
diff --git a/config/waybar/style.css b/config/waybar/style.css
new file mode 100644
index 0000000..a741132
--- /dev/null
+++ b/config/waybar/style.css
@@ -0,0 +1,329 @@
+/* Material Design Waybar Theme */
+
+* {
+ font-family: "Roboto", "JetBrainsMono Nerd Font", "Material Design Icons", sans-serif;
+ font-size: 13px;
+ font-weight: 500;
+ border: none;
+ border-radius: 0;
+ min-height: 0;
+}
+
+window#waybar {
+ background: #1E1E1E;
+ color: #E1E1E1;
+}
+
+window#waybar.hidden {
+ opacity: 0;
+}
+
+tooltip {
+ background: #2D2D2D;
+ border-radius: 8px;
+ padding: 12px 16px;
+ color: #E1E1E1;
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
+}
+
+tooltip label {
+ color: #E1E1E1;
+}
+
+/* ============= Workspaces ============= */
+#workspaces {
+ margin: 4px 8px;
+ padding: 0;
+ background: #2D2D2D;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+}
+
+#workspaces button {
+ padding: 0 16px;
+ margin: 4px;
+ background: transparent;
+ color: #B0B0B0;
+ border-radius: 6px;
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+#workspaces button:hover {
+ background: rgba(144, 202, 249, 0.12);
+ color: #90CAF9;
+}
+
+#workspaces button.active,
+#workspaces button.focused {
+ background: #90CAF9;
+ color: #1E1E1E;
+ box-shadow: 0 2px 4px rgba(144, 202, 249, 0.3);
+}
+
+#workspaces button.urgent {
+ background: #CF6679;
+ color: #1E1E1E;
+}
+
+/* ============= Mode & Window ============= */
+#mode {
+ margin: 4px 8px;
+ padding: 0 16px;
+ background: #CF6679;
+ color: #1E1E1E;
+ border-radius: 8px;
+ font-weight: 500;
+}
+
+#window {
+ margin: 4px 8px;
+ padding: 0 16px;
+ color: #B0B0B0;
+}
+
+/* ============= Module Cards ============= */
+#battery,
+#cpu,
+#memory,
+#temperature,
+#backlight,
+#network,
+#pulseaudio,
+#bluetooth,
+#idle_inhibitor,
+#privacy,
+#tray,
+#custom-media,
+#custom-updates,
+#custom-weather,
+#custom-vpn,
+#custom-ibus,
+#custom-monitor-brightness {
+ margin: 4px 2px;
+ padding: 0 12px;
+ background: #2D2D2D;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+#battery:hover,
+#cpu:hover,
+#memory:hover,
+#temperature:hover,
+#backlight:hover,
+#network:hover,
+#pulseaudio:hover,
+#bluetooth:hover,
+#idle_inhibitor:hover,
+#custom-media:hover,
+#custom-updates:hover,
+#custom-weather:hover,
+#custom-vpn:hover,
+#custom-ibus:hover,
+#custom-monitor-brightness:hover {
+ background: #383838;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
+}
+
+/* ============= Clock - Elevated Card ============= */
+#clock {
+ margin: 4px 8px;
+ padding: 0 20px;
+ background: #2D2D2D;
+ color: #E1E1E1;
+ border-radius: 8px;
+ font-weight: 500;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+#clock:hover {
+ background: #383838;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
+}
+
+/* ============= Idle Inhibitor ============= */
+#idle_inhibitor {
+ color: #B0B0B0;
+}
+
+#idle_inhibitor.activated {
+ background: #03DAC6;
+ color: #1E1E1E;
+}
+
+/* ============= Privacy ============= */
+#privacy {
+ padding: 0 8px;
+}
+
+#privacy-item {
+ padding: 0 8px;
+ margin: 0 2px;
+}
+
+#privacy-item.screenshare {
+ color: #CF6679;
+}
+
+#privacy-item.audio-in {
+ color: #03DAC6;
+}
+
+#privacy-item.audio-out {
+ color: #90CAF9;
+}
+
+/* ============= Updates ============= */
+#custom-updates {
+ color: #FFB74D;
+}
+
+/* ============= Weather ============= */
+#custom-weather {
+ color: #90CAF9;
+}
+
+/* ============= Bluetooth ============= */
+#bluetooth {
+ color: #90CAF9;
+}
+
+#bluetooth.disabled {
+ color: #666666;
+}
+
+#bluetooth.connected {
+ background: #90CAF9;
+ color: #1E1E1E;
+}
+
+/* ============= Network ============= */
+#network {
+ color: #03DAC6;
+}
+
+#network.disconnected {
+ color: #CF6679;
+}
+
+/* ============= VPN ============= */
+#custom-vpn {
+ color: #81C784;
+}
+
+#custom-vpn.connected,
+#custom-vpn.connected-ovpn,
+#custom-vpn.connected-wg {
+ background: #81C784;
+ color: #1E1E1E;
+}
+
+#custom-vpn.disconnected {
+ background: transparent;
+ box-shadow: none;
+}
+
+/* ============= Pulseaudio ============= */
+#pulseaudio {
+ color: #CE93D8;
+}
+
+#pulseaudio.muted {
+ color: #666666;
+}
+
+/* ============= Backlight & Monitor Brightness ============= */
+#backlight,
+#custom-monitor-brightness {
+ color: #FFE082;
+}
+
+/* ============= CPU ============= */
+#cpu {
+ color: #81C784;
+}
+
+/* ============= Memory ============= */
+#memory {
+ color: #CE93D8;
+}
+
+/* ============= Temperature ============= */
+#temperature {
+ color: #FFAB91;
+}
+
+#temperature.critical {
+ background: #CF6679;
+ color: #1E1E1E;
+}
+
+/* ============= Battery ============= */
+#battery {
+ color: #81C784;
+}
+
+#battery.charging,
+#battery.plugged {
+ background: #81C784;
+ color: #1E1E1E;
+}
+
+#battery.warning:not(.charging) {
+ color: #FFB74D;
+}
+
+#battery.critical:not(.charging) {
+ background: #CF6679;
+ color: #1E1E1E;
+ animation: blink 1s ease-in-out infinite;
+}
+
+@keyframes blink {
+ 50% { opacity: 0.6; }
+}
+
+/* ============= Media ============= */
+#custom-media {
+ color: #F48FB1;
+ min-width: 100px;
+}
+
+/* ============= IBus ============= */
+#custom-ibus {
+ color: #90CAF9;
+}
+
+/* ============= Tray ============= */
+#tray {
+ padding: 0 8px;
+}
+
+#tray > .passive {
+ -gtk-icon-effect: dim;
+}
+
+#tray > .needs-attention {
+ -gtk-icon-effect: highlight;
+ background: rgba(207, 102, 121, 0.3);
+ border-radius: 6px;
+}
+
+/* ============= Power Button ============= */
+#custom-power {
+ margin: 4px 8px 4px 2px;
+ padding: 0 14px;
+ background: #2D2D2D;
+ color: #CF6679;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+#custom-power:hover {
+ background: #CF6679;
+ color: #1E1E1E;
+ box-shadow: 0 4px 8px rgba(207, 102, 121, 0.4);
+}
diff --git a/home/packages.nix b/home/packages.nix
index 9f03c7c..326c1ee 100644
--- a/home/packages.nix
+++ b/home/packages.nix
@@ -4,6 +4,8 @@
{
home.packages = with pkgs; [
ripdrag
+ playerctl # waybar mediaplayer module + swaync's mpris widget
+ libnotify # notify-send, used by config/waybar/scripts/monitor-brightness.sh
fastfetch
p7zip
unrar # unfree
diff --git a/home/shihaam.nix b/home/shihaam.nix
index a2bc75a..62915e5 100644
--- a/home/shihaam.nix
+++ b/home/shihaam.nix
@@ -10,7 +10,9 @@
./nano.nix
./obs.nix
./sway.nix
+ ./swaync.nix
./theme.nix
+ ./waybar.nix
];
home.username = "shihaam";
diff --git a/home/swaync.nix b/home/swaync.nix
new file mode 100644
index 0000000..3ea42ed
--- /dev/null
+++ b/home/swaync.nix
@@ -0,0 +1,29 @@
+# SwayNotificationCenter -- the notification daemon, replacing mako.
+#
+# mako shows a notification's actions only as "click the popup to fire the
+# default one"; swaync draws one button per action, which is what makes
+# blueman's pairing prompts (Accept / Reject / Always accept), browser
+# permission prompts and the like usable without hunting for a CLI.
+#
+# Nothing else may own org.freedesktop.Notifications -- two daemons racing for
+# that name means whichever lost silently eats every notification. Sway starts
+# it from its exec block (config/sway/config), same as waybar; there is no
+# graphical-session.target running here, so a systemd user unit would never
+# fire.
+#
+# Config stays in swaync's own syntax at config/swaync/ and is symlinked, like
+# sway and waybar. cssPriority = "user" in config.json is what lets style.css
+# override the GTK theme instead of losing to it.
+#
+# Iteration loop: edit config/swaync/... -> git add if new ->
+# nixos-rebuild switch -> swaync-client -rs (reload config + css).
+{ pkgs, ... }:
+
+{
+ home.packages = with pkgs; [ swaynotificationcenter ];
+
+ xdg.configFile."swaync" = {
+ source = ../config/swaync;
+ recursive = true;
+ };
+}
diff --git a/home/waybar.nix b/home/waybar.nix
new file mode 100644
index 0000000..f044469
--- /dev/null
+++ b/home/waybar.nix
@@ -0,0 +1,10 @@
+{ ... }:
+
+{
+ xdg.configFile."waybar" = {
+ source = ../config/waybar;
+ recursive = true;
+ };
+}
+# Iteration loop: edit config/waybar/... -> git add if new ->
+# doas nixos-rebuild switch --flake /home/shihaam/git/sargit/dotfiles#la2410 && pkill -SIGUSR2 waybar