rework: more TUIs! (#62)

Signed-off-by: Hadi <[email protected]>
This commit is contained in:
Hadi
2026-09-28 20:18:12 +02:00
committed by GitHub
parent 1a3f23f248
commit 07a60e91d2
98 changed files with 1280 additions and 1093 deletions
+13 -2
View File
@@ -3,8 +3,19 @@
pkgs,
scripts,
...
}: {
home.packages = [pkgs.wl-clipboard];
}: let
tofiMenu = "${pkgs.tofi}/bin/tofi --horizontal false --anchor center --width 700 --height 500 --margin-top 0 --margin-left 0 --margin-right 0 --num-results 10";
in {
scripts.clipboard-menu = pkgs.writeShellScriptBin "clipboard-menu" ''
list=$(${pkgs.cliphist}/bin/cliphist list)
selected=$(printf '%s\n' "$list" | cut -f2- | ${tofiMenu} --prompt-text "Clipboard: ")
[ -n "$selected" ] && printf '%s\n' "$list" \
| awk -F'\t' -v s="$selected" '$2 == s {print; exit}' \
| ${pkgs.cliphist}/bin/cliphist decode \
| ${pkgs.wl-clipboard}/bin/wl-copy
'';
home.packages = [pkgs.wl-clipboard scripts.clipboard-menu];
services.cliphist = {
enable = true;
+32 -2
View File
@@ -1,6 +1,24 @@
# hypridle handles idle management: lock the screen, turn the display off, and
# suspend after periods of inactivity. The `caffeine-toggle` script pauses it.
{pkgs, ...}: {
{
pkgs,
scripts,
...
}: {
scripts.caffeine-toggle = pkgs.writeShellScriptBin "caffeine-toggle" ''
# Pause hypridle (stay awake) or resume it.
if systemctl --user is-active --quiet hypridle; then
systemctl --user stop hypridle
OSD_TEXT="󰅶 Keep Awake On"
else
systemctl --user start hypridle
OSD_TEXT="󰾫 Keep Awake Off"
fi
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
home.packages = [scripts.caffeine-toggle];
services.hypridle = {
enable = true;
settings = {
@@ -17,7 +35,7 @@
on-timeout = "loginctl lock-session";
}
{
timeout = 360; # 6 min → screen off
timeout = 720; # 12 min → screen off
on-timeout = "${pkgs.hyprland}/bin/hyprctl dispatch dpms off";
on-resume = "${pkgs.hyprland}/bin/hyprctl dispatch dpms on";
}
@@ -28,4 +46,16 @@
];
};
};
xdg.desktopEntries = {
caffeine-toggle = {
name = "Keep Awake";
exec = "${scripts.caffeine-toggle}/bin/caffeine-toggle";
icon = "my-caffeine-on-symbolic";
comment = "Pause or resume idle locking and suspend";
categories = ["System"];
terminal = false;
settings.Keywords = "caffeine;idle;awake;inhibit;suspend;";
};
};
}
+6 -63
View File
@@ -46,63 +46,6 @@ in {
bind =
[
# Applications
(
"$shiftMod, A, exec, "
+ lib.getExe (mkMenu [
{
key = "a";
desc = "Proton Authenticator";
cmd = "env WEBKIT_DISABLE_COMPOSITING_MODE=1 ${pkgs.proton-authenticator}/bin/proton-authenticator";
}
{
key = "p";
desc = "Proton Pass";
cmd = "${pkgs.proton-pass}/bin/proton-pass";
}
{
key = "v";
desc = "Proton VPN";
cmd = "${pkgs.proton-vpn}/bin/protonvpn-app";
}
{
key = "c";
desc = "Proton Calendar";
cmd = "${config.programs.helium.package}/bin/helium 'https://calendar.proton.me/'";
}
{
key = "m";
desc = "Proton Mail";
cmd = "${config.programs.helium.package}/bin/helium 'https://mail.proton.me/'";
}
{
key = "o";
desc = "Obsidian";
cmd = "${pkgs.obsidian}/bin/obsidian";
}
{
key = "s";
desc = "Signal";
cmd = "${pkgs.signal-desktop}/bin/signal-desktop";
}
{
key = "t";
desc = "TickTick";
cmd = "${pkgs.ticktick}/bin/ticktick";
}
{
key = "b";
desc = "Helium";
cmd = "${config.programs.helium.package}/bin/helium";
}
{
key = "i";
desc = "Helium (Incognito)";
cmd = "${config.programs.helium.package}/bin/helium --incognito";
}
])
)
"$mod,B, exec, uwsm app -- ${config.programs.helium.package}/bin/helium" # Browser
# Power
@@ -112,22 +55,22 @@ in {
{
key = "l";
desc = "Lock";
cmd = "${pkgs.hyprlock}/bin/hyprlock";
cmd = config.xdg.desktopEntries.lock.exec;
}
{
key = "s";
desc = "Suspend";
cmd = "systemctl suspend";
cmd = config.xdg.desktopEntries.suspend.exec;
}
{
key = "r";
desc = "Reboot";
cmd = "systemctl reboot";
cmd = config.xdg.desktopEntries.reboot.exec;
}
{
key = "p";
desc = "Power Off";
cmd = "systemctl poweroff";
cmd = config.xdg.desktopEntries.shutdown.exec;
}
])
)
@@ -159,8 +102,8 @@ in {
"$shiftMod, S, movetoworkspace, special:scratch" # Move to scratch workspace
# Utilities
", Print, exec, ${pkgs.hyprshot}/bin/hyprshot -m region" # Capture region
"$shiftMod, Print, exec, ${pkgs.hyprshot}/bin/hyprshot -m output" # Capture screen
", Print, exec, ${config.xdg.desktopEntries.screenshot-region.exec}" # Capture region
"$shiftMod, Print, exec, ${config.xdg.desktopEntries.screenshot-screen.exec}" # Capture screen
]
++ (builtins.concatLists (
builtins.genList (
+1
View File
@@ -21,6 +21,7 @@ in {
./bindings.nix
./polkitagent.nix
./hyprpaper.nix
./scripts.nix
];
home.packages =
+61
View File
@@ -0,0 +1,61 @@
# Compositor-level scripts: focus mode (temporarily flattens Hyprland's
# animations/gaps/decoration) and night shift (hyprsunset).
{
pkgs,
scripts,
...
}: {
scripts.focus-toggle = pkgs.writeShellScriptBin "focus-toggle" ''
if test -f "$XDG_RUNTIME_DIR/hypr-focus-mode"; then
rm "$XDG_RUNTIME_DIR/hypr-focus-mode"
OSD_TEXT="󰈈 Focus Off"
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
${pkgs.hyprland}/bin/hyprctl reload
${pkgs.hyprland}/bin/hyprctl dispatch exec waybar
else
touch "$XDG_RUNTIME_DIR/hypr-focus-mode"
OSD_TEXT="󰈈 Focus On"
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
${pkgs.procps}/bin/pkill waybar || true
${pkgs.hyprland}/bin/hyprctl keyword animations:enabled false
${pkgs.hyprland}/bin/hyprctl keyword general:gaps_in 0
${pkgs.hyprland}/bin/hyprctl keyword general:gaps_out 0
${pkgs.hyprland}/bin/hyprctl keyword decoration:active_opacity 1
${pkgs.hyprland}/bin/hyprctl keyword decoration:inactive_opacity 1
${pkgs.hyprland}/bin/hyprctl keyword decoration:rounding 0
fi
'';
scripts.nightshift-toggle = pkgs.writeShellScriptBin "nightshift-toggle" ''
if ${pkgs.procps}/bin/pidof "hyprsunset" > /dev/null; then
pkill hyprsunset
OSD_TEXT="󰖔 Night Shift Off"
else
${pkgs.hyprsunset}/bin/hyprsunset -t 4500 &
OSD_TEXT="󰖔 Night Shift On"
fi
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
home.packages = [scripts.focus-toggle scripts.nightshift-toggle];
xdg.desktopEntries = {
focus-toggle = {
name = "Focus Mode";
exec = "${scripts.focus-toggle}/bin/focus-toggle";
icon = "do-not-disturb-symbolic";
comment = "Toggle focus mode";
categories = ["System"];
terminal = false;
};
nightshift-toggle = {
name = "Night Shift";
exec = "${scripts.nightshift-toggle}/bin/nightshift-toggle";
icon = "night-light-symbolic";
comment = "Toggle night shift";
categories = ["System"];
terminal = false;
};
};
}
+9
View File
@@ -63,6 +63,15 @@
in {
stylix.targets.hyprlock.enable = false;
xdg.desktopEntries.lock = {
name = "Lock";
exec = "${pkgs.hyprlock}/bin/hyprlock";
icon = "system-lock-screen-symbolic";
comment = "Lock the screen";
categories = ["System"];
terminal = false;
};
programs.hyprlock = {
enable = true;
settings = {
+10 -5
View File
@@ -8,11 +8,11 @@
text = ["nvim-ghostty.desktop"];
code = ["nvim-ghostty.desktop"];
image = ["imv-dir.desktop"];
audio = ["vlc.desktop"];
audio = ["kew.desktop"];
video = ["vlc.desktop"];
directory = ["elio.desktop"];
office = ["onlyoffice-desktopeditors.desktop"];
pdf = ["onlyoffice-desktopeditors.desktop"];
pdf = ["helium.desktop"];
archive = ["elio.desktop"];
browser = ["helium.desktop"];
};
@@ -53,21 +53,24 @@
"image/bmp"
"image/gif"
"image/jpeg"
"image/jpg"
"image/png"
"image/svg+xml"
"image/tiff"
"image/vnd.microsoft.icon"
"image/webp"
"image/heif"
"image/avif"
];
audio = [
"audio/aac"
"audio/mpeg"
"audio/ogg"
"audio/opus"
"audio/wav"
"audio/vnd.wave"
"audio/webm"
"audio/x-matroska"
"audio/flac"
"audio/mp4"
];
video = [
"video/mp2t"
@@ -78,6 +81,7 @@
"video/x-flv"
"video/x-matroska"
"video/x-msvideo"
"video/quicktime"
];
directory = ["inode/directory"];
office = [
@@ -104,6 +108,8 @@
"application/x-xz"
"application/x-zstd"
"application/x-compressed-tar"
"application/x-bzip2-compressed-tar"
"application/x-xz-compressed-tar"
];
browser = [
"text/html"
@@ -145,7 +151,6 @@ in {
enable = true;
createDirectories = true;
desktop = null;
music = null;
publicShare = null;
templates = null;
extraConfig = {
+42
View File
@@ -0,0 +1,42 @@
{
pkgs,
scripts,
...
}: {
scripts.power-cycle = pkgs.writeShellScriptBin "power-cycle" ''
ppd=${pkgs.power-profiles-daemon}/bin/powerprofilesctl
cur=$("$ppd" get)
case "$cur" in
power-saver) next=balanced; icon="󰾅" ;;
balanced) next=performance; icon="󰓅" ;;
performance) next=power-saver; icon="󰌪" ;;
*) next=balanced; icon="󰾅" ;;
esac
"$ppd" set "$next"
OSD_TEXT="$icon ''${next^}"
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
home.packages = [pkgs.poweralertd scripts.power-cycle];
xdg.desktopEntries.power-cycle = {
name = "Power Profile";
exec = "${scripts.power-cycle}/bin/power-cycle";
icon = "power-profile-balanced-symbolic";
comment = "Cycle power-saver / balanced / performance";
categories = ["System"];
terminal = false;
settings.Keywords = "power;profile;performance;battery;";
};
# Event-driven low-battery/critical/full notifications via UPower dbus signals
# (thresholds set in nixos/utils.nix, services.upower.percentage*).
systemd.user.services.poweralertd = {
Unit.Description = "UPower-powered battery notifications";
Service = {
ExecStart = "${pkgs.poweralertd}/bin/poweralertd";
Restart = "on-failure";
};
Install.WantedBy = ["graphical-session.target"];
};
}
+16
View File
@@ -0,0 +1,16 @@
# Shared script registry: any module that owns a script contributes it here
# (`scripts.<name> = ...`), and every module reads it back via the `scripts`
# module argument.
{
lib,
config,
...
}: {
options.scripts = lib.mkOption {
type = lib.types.attrsOf lib.types.package;
default = {};
internal = true;
};
config._module.args.scripts = config.scripts;
}
+22 -8
View File
@@ -18,6 +18,28 @@
in "${builtins.substring high 1 digits}${builtins.substring low 1 digits}";
alphaHex = opacityToHex config.theme.inactive-opacity;
in {
scripts.dnd-toggle = pkgs.writeShellScriptBin "dnd-toggle" ''
state=$(${pkgs.swaynotificationcenter}/bin/swaync-client -d)
if [ "$state" = "true" ]; then
OSD_TEXT="󰂛 Do Not Disturb On"
else
OSD_TEXT="󰂚 Do Not Disturb Off"
fi
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
home.packages = [scripts.dnd-toggle];
xdg.desktopEntries.dnd-toggle = {
name = "Do Not Disturb";
exec = "${scripts.dnd-toggle}/bin/dnd-toggle";
icon = "notifications-disabled-symbolic";
comment = "Toggle Do Not Disturb";
categories = ["System"];
terminal = false;
settings.Keywords = "dnd;notifications;disturb;swaync;silence;";
};
services.swaync = {
enable = true;
@@ -409,13 +431,5 @@ in {
stylix.targets.swaync.enable = false;
# GTK 4.22 defaults to the Vulkan renderer, which compiles its graphics
# pipelines lazily on the first frame. The control center has many distinct
# visual elements, so the first open triggers a big burst of pipeline
# compilation (a multi-second stall on a cold boot) while later opens reuse
# the in-process cache. The GL (ngl) renderer avoids this: it stays
# GPU-accelerated and benefits from Mesa's on-disk shader cache, so the
# first open is as fast as the rest. Scoped to swaync so other GTK4 apps
# keep the Vulkan renderer.
systemd.user.services.swaync.Service.Environment = ["GSK_RENDERER=ngl"];
}
+2 -6
View File
@@ -1,6 +1,5 @@
# Use elio as the file picker for portal dialogs (browser "select file(s)" and
# "save download to..."). The backend is xdg-desktop-portal-termfilechooser,
# enabled system-wide in nixos/utils.nix (extraPortals + FileChooser routing).
# "save download to...").
{
config,
pkgs,
@@ -48,9 +47,6 @@
fi
fi
'';
# ghostty must run in the foreground instead of handing off to the
# single-instance daemon, otherwise the wrapper returns before you've picked
# anything and the portal reads an empty selection.
termcmd = "ghostty --gtk-single-instance=false --title=termfilechooser -e";
in {
xdg.configFile."xdg-desktop-portal-termfilechooser/config".text = ''
@@ -60,6 +56,6 @@ in {
open_mode=suggested
save_mode=suggested
env=TERMCMD=${termcmd}
PATH=${config.home.profileDirectory}/bin:/run/current-system/sw/bin:/run/wrappers/bin
PATH=${config.home.profileDirectory}/bin:/run/current-system/sw/bin:/run/wrappers/bin
'';
}
+1 -1
View File
@@ -12,7 +12,7 @@
then barHeight / 2
else config.theme.rounding;
in {
imports = [./desktop-actions.nix];
imports = [./desktop-actions.nix ./scripts.nix];
programs.tofi = {
enable = true;
settings = {
+9 -66
View File
@@ -4,24 +4,6 @@
...
}: {
xdg.desktopEntries = {
focus-toggle = {
name = "Focus Mode";
exec = "${scripts.focus-toggle}/bin/focus-toggle";
icon = "do-not-disturb-symbolic";
comment = "Toggle focus mode";
categories = ["System"];
terminal = false;
};
nightshift-toggle = {
name = "Night Shift";
exec = "${scripts.nightshift-toggle}/bin/nightshift-toggle";
icon = "night-light-symbolic";
comment = "Toggle night shift";
categories = ["System"];
terminal = false;
};
mic-mute = {
name = "Mute Microphone";
exec = "${scripts.mic-mute}/bin/mic-mute";
@@ -31,15 +13,6 @@
terminal = false;
};
lock = {
name = "Lock";
exec = "${pkgs.hyprlock}/bin/hyprlock";
icon = "system-lock-screen-symbolic";
comment = "Lock the screen";
categories = ["System"];
terminal = false;
};
reboot = {
name = "Reboot";
exec = "systemctl reboot";
@@ -49,6 +22,15 @@
terminal = false;
};
suspend = {
name = "Suspend";
exec = "systemctl suspend";
icon = "system-suspend-symbolic";
comment = "Suspend the system";
categories = ["System"];
terminal = false;
};
shutdown = {
name = "Shutdown";
exec = "systemctl poweroff";
@@ -67,15 +49,6 @@
terminal = false;
};
waybar-toggle = {
name = "Toggle Waybar";
exec = "${scripts.waybar-toggle}/bin/waybar-toggle";
icon = "panel-applets-symbolic";
comment = "Show or hide the status bar";
categories = ["System"];
terminal = false;
};
wifi-toggle = {
name = "Toggle Wi-Fi";
exec = "${scripts.wifi-toggle}/bin/wifi-toggle";
@@ -154,16 +127,6 @@
settings.Keywords = "record;recording;screencast;video;";
};
power-cycle = {
name = "Power Profile";
exec = "${scripts.power-cycle}/bin/power-cycle";
icon = "power-profile-balanced-symbolic";
comment = "Cycle power-saver / balanced / performance";
categories = ["System"];
terminal = false;
settings.Keywords = "power;profile;performance;battery;";
};
airplane-toggle = {
name = "Airplane Mode";
exec = "${scripts.airplane-toggle}/bin/airplane-toggle";
@@ -194,16 +157,6 @@
settings.Keywords = "icon;nerdfont;glyph;symbol;";
};
caffeine-toggle = {
name = "Keep Awake";
exec = "${scripts.caffeine-toggle}/bin/caffeine-toggle";
icon = "my-caffeine-on-symbolic";
comment = "Pause or resume idle locking and suspend";
categories = ["System"];
terminal = false;
settings.Keywords = "caffeine;idle;awake;inhibit;suspend;";
};
logout = {
name = "Logout";
exec = "${pkgs.hyprland}/bin/hyprctl dispatch exit";
@@ -213,16 +166,6 @@
terminal = false;
};
dnd-toggle = {
name = "Do Not Disturb";
exec = "${scripts.dnd-toggle}/bin/dnd-toggle";
icon = "notifications-disabled-symbolic";
comment = "Toggle Do Not Disturb";
categories = ["System"];
terminal = false;
settings.Keywords = "dnd;notifications;disturb;swaync;silence;";
};
vol-mute = {
name = "Mute Volume";
exec = "${scripts.vol-mute}/bin/vol-mute";
+35
View File
@@ -0,0 +1,35 @@
# Picker scripts built on tofi's dmenu mode: emoji and Nerd Font icon input.
{
pkgs,
config,
scripts,
...
}: let
tofiMenu = "${pkgs.tofi}/bin/tofi --horizontal false --anchor center --width 700 --height 500 --margin-top 0 --margin-left 0 --margin-right 0 --num-results 10";
nerdFontGlyphnames = pkgs.fetchurl {
url = "https://raw.githubusercontent.com/ryanoasis/nerd-fonts/v3.4.0/glyphnames.json";
sha256 = "sha256-4tENI/W/8L1vBnbpsB2XifzcZW3ntJiilVwncW6kQ5w=";
};
nerdFontIconList = pkgs.runCommand "nerd-font-icons.txt" {nativeBuildInputs = [pkgs.jq];} ''
jq -r 'to_entries[] | select(.key != "METADATA") | "\(.value.char) \(.key)"' \
${nerdFontGlyphnames} > "$out"
'';
in {
scripts.emoji-picker = pkgs.writeShellScriptBin "emoji-picker" ''
export PATH="${pkgs.wtype}/bin:${pkgs.wl-clipboard}/bin:$PATH"
export BEMOJI_PICKER_CMD="${tofiMenu} --prompt-text 'emoji: '"
# -t types the emoji, -c also copies it.
exec ${pkgs.bemoji}/bin/bemoji -t -c
'';
scripts.icon-picker = pkgs.writeShellScriptBin "icon-picker" ''
selected=$(${tofiMenu} --font "${config.stylix.fonts.monospace.name}" --prompt-text 'icon: ' < ${nerdFontIconList})
[ -z "$selected" ] || icon=$(printf '%s\n' "$selected" | awk '{print $1}')
[ -n "''${icon:-}" ] || exit 0
${pkgs.wtype}/bin/wtype "$icon"
printf '%s' "$icon" | ${pkgs.wl-clipboard}/bin/wl-copy
'';
home.packages = [scripts.emoji-picker scripts.icon-picker];
}
+1 -1
View File
@@ -2,7 +2,7 @@
{
services.udiskie = {
enable = true;
notify = true;
notify = false;
automount = true;
tray = "never";
};
-103
View File
@@ -1,103 +0,0 @@
# battery-monitor: low-battery alerts for waybar/swaync.
# Runs main once (triggered by the battery-monitor systemd timer).
POWER_SUPPLY_DIR="${POWER_SUPPLY_DIR:-/sys/class/power_supply}"
STATE_FILE="${BATTERY_STATE_FILE:-/tmp/battery-alert-state}"
CRIT_ID_FILE="${BATTERY_CRIT_ID_FILE:-/tmp/battery-crit-id}"
WARN_THRESHOLD=10
CRIT_THRESHOLD=5
# Pure decision. Prints "<action>:<newstate>".
# action ∈ noop | osd_warn | crit_show | crit_clear ; state ∈ none | warn10 | crit5
battery_decide() {
status=$1
cap=$2
prev=$3
if [ "$status" = "Discharging" ]; then
if [ "$cap" -le "$CRIT_THRESHOLD" ]; then
echo "crit_show:crit5"
elif [ "$cap" -le "$WARN_THRESHOLD" ]; then
if [ "$prev" = "crit5" ]; then
echo "crit_clear:warn10"
elif [ "$prev" = "warn10" ]; then
echo "noop:warn10"
else
echo "osd_warn:warn10"
fi
else
if [ "$prev" = "crit5" ]; then
echo "crit_clear:none"
else
echo "noop:none"
fi
fi
else
if [ "$prev" = "crit5" ]; then
echo "crit_clear:none"
else
echo "noop:none"
fi
fi
}
# First real battery (type == Battery, with status + capacity).
resolve_battery() {
for d in "$POWER_SUPPLY_DIR"/*; do
[ -r "$d/type" ] || continue
[ "$(cat "$d/type")" = "Battery" ] || continue
[ -r "$d/status" ] && [ -r "$d/capacity" ] || continue
printf '%s' "$d"
return 0
done
return 1
}
show_warn_osd() {
waybar-osd "󰂃 Low battery · $1%"
}
show_crit_notification() {
cap=$1
id=$(cat "$CRIT_ID_FILE" 2>/dev/null || echo 0)
newid=$(notify-send --print-id --replace-id="$id" \
--urgency=critical --expire-time=0 \
"󰂃 Critical battery" "Battery at $cap%: plug in the charger")
printf '%s' "$newid" >"$CRIT_ID_FILE"
}
clear_crit_notification() {
id=$(cat "$CRIT_ID_FILE" 2>/dev/null || echo 0)
if [ "$id" != "0" ]; then
gdbus call --session \
--dest org.freedesktop.Notifications \
--object-path /org/freedesktop/Notifications \
--method org.freedesktop.Notifications.CloseNotification "$id" \
>/dev/null 2>&1 || true
fi
rm -f "$CRIT_ID_FILE"
}
main() {
dir=$(resolve_battery) || exit 0
status=$(cat "$dir/status")
cap=$(cat "$dir/capacity")
prev=$(cat "$STATE_FILE" 2>/dev/null || echo none)
result=$(battery_decide "$status" "$cap" "$prev")
action=${result%%:*}
newstate=${result##*:}
case "$action" in
osd_warn) show_warn_osd "$cap" ;;
crit_show) show_crit_notification "$cap" ;;
crit_clear) clear_crit_notification ;;
noop) : ;;
esac
printf '%s' "$newstate" >"$STATE_FILE"
}
main "$@"
+10 -36
View File
@@ -2,23 +2,13 @@
pkgs,
config,
...
}: let
scripts = import ./scripts.nix {inherit pkgs config;};
in {
}: {
imports = [
./scripts
./settings.nix
./style.nix
];
_module.args.scripts = scripts;
_module.args.osdPath = scripts.osdPath;
_module.args.networkScript = scripts.networkScript;
_module.args.bluetoothScript = scripts.bluetoothScript;
_module.args.volMuteScript = scripts.vol-mute;
_module.args.caffeineToggleScript = scripts.caffeine-toggle;
_module.args.osdStatusScript = scripts.waybar-osd-status;
_module.args.nightshiftToggleScript = scripts.nightshift-toggle;
programs.waybar.enable = true;
stylix.targets.waybar.enable = false;
@@ -26,35 +16,19 @@ in {
[
playerctl
pavucontrol
blueman
hyprsunset
]
++ (with scripts; [waybar-osd waybar-osd-status battery-monitor vol-up vol-down vol-mute mic-mute bright-up bright-down nightshift-toggle focus-toggle waybar-toggle wifi-toggle bluetooth-toggle dnd-toggle output-cycle input-cycle color-pick screenshot-edit record-toggle power-cycle airplane-toggle clipboard-menu emoji-picker caffeine-toggle mic-status]);
++ (with config.scripts; [waybar-osd waybar-osd-status vol-up vol-down vol-mute mic-mute bright-up bright-down waybar-toggle wifi-toggle bluetooth-toggle output-cycle input-cycle color-pick screenshot-edit record-toggle airplane-toggle mic-status]);
# Poll battery level every 30s; battery-monitor no-ops when no battery exists.
systemd.user.services.battery-monitor = {
Unit.Description = "Low-battery OSD and critical notification monitor";
Service = {
Type = "oneshot";
ExecStart = "${scripts.battery-monitor}/bin/battery-monitor";
};
xdg.desktopEntries.waybar-toggle = {
name = "Toggle Waybar";
exec = "${config.scripts.waybar-toggle}/bin/waybar-toggle";
icon = "panel-applets-symbolic";
comment = "Show or hide the status bar";
categories = ["System"];
terminal = false;
};
systemd.user.timers.battery-monitor = {
Unit.Description = "Poll battery level for low-battery alerts";
Timer = {
OnBootSec = "1min";
OnUnitActiveSec = "30s";
};
Install.WantedBy = ["timers.target"];
};
# Prevent blueman-applet from auto-starting via XDG autostart (we use custom/bluetooth module instead)
xdg.configFile."autostart/blueman.desktop".text = ''
[Desktop Entry]
Hidden=true
'';
wayland.windowManager.hyprland.settings.exec-once = [
"waybar"
];
-456
View File
@@ -1,456 +0,0 @@
{
pkgs,
config,
}: let
waybar-osd = pkgs.writeShellApplication {
name = "waybar-osd";
runtimeInputs = with pkgs; [procps coreutils];
text = ''
printf '%s' "$1" > "$XDG_RUNTIME_DIR/waybar-osd"
pkill -f -RTMIN+8 '^waybar$' 2>/dev/null || true
'';
};
waybar-osd-status = pkgs.writeShellApplication {
name = "waybar-osd-status";
runtimeInputs = with pkgs; [coreutils];
text = ''
file="$XDG_RUNTIME_DIR/waybar-osd"
[ -f "$file" ] || exit 1
mtime=$(stat -c %Y "$file" 2>/dev/null) || exit 1
age=$(( $(date +%s) - mtime ))
if [ "$age" -ge 3 ]; then
rm -f "$file"
exit 1
fi
'';
};
# battery-monitor: low-battery alerts, run by the systemd user timer.
battery-monitor = pkgs.writeShellApplication {
name = "battery-monitor";
runtimeInputs = with pkgs; [waybar-osd libnotify glib coreutils];
text = builtins.readFile ./battery-monitor.sh;
};
updateOsd = ''
${waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
volGetText = ''
VOL_RAW=$(${pkgs.wireplumber}/bin/wpctl get-volume @DEFAULT_SINK@)
VOL=$(printf '%s' "$VOL_RAW" | awk '{printf "%d", $2*100}')
if printf '%s' "$VOL_RAW" | grep -q MUTED; then
OSD_TEXT="󰝟 $VOL%"
elif [ "$VOL" -lt 33 ]; then
OSD_TEXT="󰕿 $VOL%"
elif [ "$VOL" -lt 66 ]; then
OSD_TEXT="󰖀 $VOL%"
else
OSD_TEXT="󰕾 $VOL%"
fi
'';
brightGetText = ''
BRIGHT=$(${pkgs.brightnessctl}/bin/brightnessctl -m | awk -F, '{print int($5)}')
if [ "$BRIGHT" -lt 33 ]; then
OSD_TEXT="󰃞 $BRIGHT%"
elif [ "$BRIGHT" -lt 66 ]; then
OSD_TEXT="󰃟 $BRIGHT%"
else
OSD_TEXT="󰃠 $BRIGHT%"
fi
'';
# Resolve $src: the default source, otherwise the first available source.
# Some machines have no default audio source (@DEFAULT_AUDIO_SOURCE@ unresolved).
micSource = ''
src=$(${pkgs.wireplumber}/bin/wpctl inspect @DEFAULT_AUDIO_SOURCE@ 2>/dev/null | sed -n 's/^id \([0-9]*\),.*/\1/p')
if [ -z "$src" ]; then
src=$(${pkgs.pipewire}/bin/pw-dump | ${pkgs.jq}/bin/jq -r \
'[.[] | select(.info.props."media.class"=="Audio/Source") | .id] | .[0] // empty')
fi
'';
# tofi in vertical dmenu mode: the global config is a 36px-tall horizontal
# launcher, unusable for a list, so override the geometry here.
tofiMenu = "${pkgs.tofi}/bin/tofi --horizontal false --anchor center --width 700 --height 500 --margin-top 0 --margin-left 0 --margin-right 0 --num-results 10";
# Name -> glyph table maintained by the nerd-fonts project itself, pinned to
# the same release as the nerd-fonts packages in nixos/fonts.nix.
nerdFontGlyphnames = pkgs.fetchurl {
url = "https://raw.githubusercontent.com/ryanoasis/nerd-fonts/v3.4.0/glyphnames.json";
sha256 = "sha256-4tENI/W/8L1vBnbpsB2XifzcZW3ntJiilVwncW6kQ5w=";
};
nerdFontIconList = pkgs.runCommand "nerd-font-icons.txt" {nativeBuildInputs = [pkgs.jq];} ''
jq -r 'to_entries[] | select(.key != "METADATA") | "\(.value.char) \(.key)"' \
${nerdFontGlyphnames} > "$out"
'';
osdPath = "$XDG_RUNTIME_DIR/waybar-osd";
in {
inherit waybar-osd waybar-osd-status battery-monitor osdPath;
bluetoothScript = pkgs.writeShellScript "waybar-bluetooth" ''
jq=${pkgs.jq}/bin/jq
nl=$'\n'
# bluetoothctl hangs without a controller by default → always bounded by timeout.
bt() { timeout 3 ${pkgs.bluez}/bin/bluetoothctl "$@" 2>/dev/null; }
powered=$(bt show | awk '/Powered:/ { print $2; exit }')
if [ "$powered" != "yes" ]; then
"$jq" -cn '{ text: "󰂲", class: "off", tooltip: "Bluetooth off" }'
exit 0
fi
tip=""
add_tip() { tip="''${tip:+$tip$nl}$1"; }
count=0
for mac in $(bt devices Connected | awk '{ print $2 }'); do
count=$((count + 1))
info=$(bt info "$mac")
name=$(printf '%s' "$info" | sed -n 's/^[[:space:]]*Name: //p' | head -1)
[ -z "$name" ] && name="$mac"
batt=$(printf '%s' "$info" | sed -n 's/.*Battery Percentage:.*(\([0-9][0-9]*\)).*/\1/p' | head -1)
add_tip "󰂱 $name''${batt:+ · $batt%}"
done
if [ "$count" -gt 0 ]; then
"$jq" -cn --arg tooltip "$tip" '{ text: "󰂰", class: "connected", tooltip: $tooltip }'
else
"$jq" -cn '{ text: "󰂰", class: "on", tooltip: "Bluetooth on" }'
fi
'';
networkScript = pkgs.writeShellScript "waybar-network" ''
nmcli=${pkgs.networkmanager}/bin/nmcli
ip=${pkgs.iproute2}/bin/ip
jq=${pkgs.jq}/bin/jq
nl=$'\n'
text=""
tip=""
class="disconnected"
add_seg() { text="''${text:+$text }$1"; }
add_tip() { tip="''${tip:+$tip$nl}$1"; }
# ── Ethernet ────────────────────────────────────────────────
eth=$("$nmcli" -t -f DEVICE,TYPE,STATE device status 2>/dev/null \
| awk -F: '$2 == "ethernet" && $3 == "connected" { print $1; exit }')
if [ -n "$eth" ]; then
eth_ip=$("$ip" -4 -br addr show "$eth" 2>/dev/null | awk '{ print $3 }' | cut -d/ -f1)
add_seg "󰛳"
class="ethernet"
add_tip "󰛳 Ethernet''${eth_ip:+ · $eth_ip}"
fi
# ── Wi-Fi ───────────────────────────────────────────────────
wifi=$("$nmcli" -t -f ACTIVE,SSID,SIGNAL device wifi 2>/dev/null | grep -m1 '^yes:')
if [ -n "$wifi" ]; then
ssid=$(printf '%s' "$wifi" | cut -d: -f2)
signal=$(printf '%s' "$wifi" | cut -d: -f3)
if [ "''${signal:-0}" -ge 80 ]; then icon="󰤨"
elif [ "''${signal:-0}" -ge 60 ]; then icon="󰤥"
elif [ "''${signal:-0}" -ge 40 ]; then icon="󰤢"
elif [ "''${signal:-0}" -ge 20 ]; then icon="󰤟"
else icon="󰤯"
fi
add_seg "$icon"
[ "$class" = "disconnected" ] && class="wifi"
add_tip "$icon ''${ssid:-Wi-Fi}''${signal:+ · $signal%}"
fi
# ── VPN (nmcli, fallback interfaces) ────────────────────────
vpn=$("$nmcli" -t -f TYPE,NAME connection show --active 2>/dev/null \
| awk -F: '$1 == "vpn" || $1 == "wireguard" { print $2; exit }')
if [ -z "$vpn" ]; then
vpn=$("$ip" -br link show 2>/dev/null \
| awk '($2 == "UP" || $2 == "UNKNOWN") && $1 ~ /^(tun|wg|proton|ppp)/ { sub(/@.*/, "", $1); print $1; exit }')
fi
if [ -n "$vpn" ]; then
add_seg "󰦝"
add_tip "󰦝 VPN · $vpn"
fi
if [ -z "$text" ]; then
text="󰤭"
tip="Disconnected"
fi
"$jq" -cn --arg text "$text" --arg class "$class" --arg tooltip "$tip" \
'{ text: $text, class: $class, tooltip: $tooltip }'
'';
vol-up = pkgs.writeShellScriptBin "vol-up" ''
${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_SINK@ 5%+ --limit 1.0
${volGetText}
${updateOsd}
'';
vol-down = pkgs.writeShellScriptBin "vol-down" ''
${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_SINK@ 5%-
${volGetText}
${updateOsd}
'';
vol-mute = pkgs.writeShellScriptBin "vol-mute" ''
${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_SINK@ toggle
${volGetText}
${updateOsd}
'';
mic-mute = pkgs.writeShellScriptBin "mic-mute" ''
wpctl=${pkgs.wireplumber}/bin/wpctl
${micSource}
[ -z "$src" ] && exit 0
"$wpctl" set-mute "$src" toggle
if "$wpctl" get-volume "$src" | grep -q MUTED; then
OSD_TEXT="󰍭 Muted"
else
OSD_TEXT="󰍬 Live"
fi
${updateOsd}
'';
mic-status = pkgs.writeShellScriptBin "mic-status" ''
${micSource}
if [ -n "$src" ] && ${pkgs.wireplumber}/bin/wpctl get-volume "$src" | grep -q MUTED; then
echo true
else
echo false
fi
'';
bright-up = pkgs.writeShellScriptBin "bright-up" ''
${pkgs.brightnessctl}/bin/brightnessctl set 5%+
${brightGetText}
${updateOsd}
'';
bright-down = pkgs.writeShellScriptBin "bright-down" ''
${pkgs.brightnessctl}/bin/brightnessctl set 5%-
${brightGetText}
${updateOsd}
'';
wifi-toggle = pkgs.writeShellScriptBin "wifi-toggle" ''
if ${pkgs.networkmanager}/bin/nmcli radio wifi | grep -q enabled; then
${pkgs.networkmanager}/bin/nmcli radio wifi off
else
${pkgs.networkmanager}/bin/nmcli radio wifi on
fi
${updateOsd}
'';
bluetooth-toggle = pkgs.writeShellScriptBin "bluetooth-toggle" ''
if ${pkgs.bluez}/bin/bluetoothctl show | grep -q "Powered: yes"; then
${pkgs.bluez}/bin/bluetoothctl power off
else
${pkgs.bluez}/bin/bluetoothctl power on
fi
${updateOsd}
'';
waybar-toggle = pkgs.writeShellScriptBin "waybar-toggle" ''
if pidof waybar > /dev/null; then
pkill waybar
else
hyprctl dispatch exec waybar
fi
'';
nightshift-toggle = pkgs.writeShellScriptBin "nightshift-toggle" ''
if ${pkgs.procps}/bin/pidof "hyprsunset" > /dev/null; then
pkill hyprsunset
OSD_TEXT="󰖔 Night Shift Off"
else
${pkgs.hyprsunset}/bin/hyprsunset -t 4500 &
OSD_TEXT="󰖔 Night Shift On"
fi
${updateOsd}
'';
focus-toggle = pkgs.writeShellScriptBin "focus-toggle" ''
if test -f "$XDG_RUNTIME_DIR/hypr-focus-mode"; then
rm "$XDG_RUNTIME_DIR/hypr-focus-mode"
OSD_TEXT="󰈈 Focus Off"
${updateOsd}
${pkgs.hyprland}/bin/hyprctl reload
${pkgs.hyprland}/bin/hyprctl dispatch exec waybar
else
touch "$XDG_RUNTIME_DIR/hypr-focus-mode"
OSD_TEXT="󰈈 Focus On"
${updateOsd}
${pkgs.procps}/bin/pkill waybar || true
${pkgs.hyprland}/bin/hyprctl keyword animations:enabled false
${pkgs.hyprland}/bin/hyprctl keyword general:gaps_in 0
${pkgs.hyprland}/bin/hyprctl keyword general:gaps_out 0
${pkgs.hyprland}/bin/hyprctl keyword decoration:active_opacity 1
${pkgs.hyprland}/bin/hyprctl keyword decoration:inactive_opacity 1
${pkgs.hyprland}/bin/hyprctl keyword decoration:rounding 0
fi
'';
dnd-toggle = pkgs.writeShellScriptBin "dnd-toggle" ''
state=$(${pkgs.swaynotificationcenter}/bin/swaync-client -d)
if [ "$state" = "true" ]; then
OSD_TEXT="󰂛 Do Not Disturb On"
else
OSD_TEXT="󰂚 Do Not Disturb Off"
fi
${updateOsd}
'';
output-cycle = pkgs.writeShellScriptBin "output-cycle" ''
wpctl=${pkgs.wireplumber}/bin/wpctl
jq=${pkgs.jq}/bin/jq
pwdump=${pkgs.pipewire}/bin/pw-dump
# Sink IDs in pw-dump order.
ids=($("$pwdump" | "$jq" -r '.[] | select(.info.props."media.class"=="Audio/Sink") | .id'))
n=''${#ids[@]}
[ "$n" -eq 0 ] && exit 0
cur=$("$wpctl" inspect @DEFAULT_AUDIO_SINK@ 2>/dev/null | sed -n 's/^id \([0-9]*\),.*/\1/p')
next_index=0
for i in "''${!ids[@]}"; do
if [ "''${ids[$i]}" = "$cur" ]; then
next_index=$(( (i + 1) % n ))
break
fi
done
next=''${ids[$next_index]}
"$wpctl" set-default "$next"
desc=$("$pwdump" | "$jq" -r --argjson id "$next" '.[] | select(.id==$id) | .info.props."node.description"')
OSD_TEXT="󰓃 ''${desc:-Output}"
${updateOsd}
'';
input-cycle = pkgs.writeShellScriptBin "input-cycle" ''
wpctl=${pkgs.wireplumber}/bin/wpctl
jq=${pkgs.jq}/bin/jq
pwdump=${pkgs.pipewire}/bin/pw-dump
# Source IDs in pw-dump order.
ids=($("$pwdump" | "$jq" -r '.[] | select(.info.props."media.class"=="Audio/Source") | .id'))
n=''${#ids[@]}
[ "$n" -eq 0 ] && exit 0
cur=$("$wpctl" inspect @DEFAULT_AUDIO_SOURCE@ 2>/dev/null | sed -n 's/^id \([0-9]*\),.*/\1/p')
next_index=0
for i in "''${!ids[@]}"; do
if [ "''${ids[$i]}" = "$cur" ]; then
next_index=$(( (i + 1) % n ))
break
fi
done
next=''${ids[$next_index]}
"$wpctl" set-default "$next"
desc=$("$pwdump" | "$jq" -r --argjson id "$next" '.[] | select(.id==$id) | .info.props."node.description"')
OSD_TEXT="󰍬 ''${desc:-Input}"
${updateOsd}
'';
color-pick = pkgs.writeShellScriptBin "color-pick" ''
# hyprpicker -a already copies the color to the clipboard.
color=$(${pkgs.hyprpicker}/bin/hyprpicker -a 2>/dev/null)
if [ -n "$color" ]; then
OSD_TEXT="󰈊 $color"
else
OSD_TEXT="󰈊 Cancelled"
fi
${updateOsd}
'';
screenshot-edit = pkgs.writeShellScriptBin "screenshot-edit" ''
${pkgs.hyprshot}/bin/hyprshot -m region --raw 2>/dev/null \
| ${pkgs.satty}/bin/satty --filename -
'';
record-toggle = pkgs.writeShellScriptBin "record-toggle" ''
if pgrep -x wf-recorder >/dev/null; then
# -INT lets wf-recorder finalize the file cleanly.
pkill -INT -x wf-recorder
OSD_TEXT="󰕧 Recording saved"
else
dir="$HOME/Videos"
mkdir -p "$dir"
file="$dir/rec-$(date +%Y%m%d-%H%M%S).mp4"
${pkgs.wf-recorder}/bin/wf-recorder -f "$file" >/dev/null 2>&1 &
OSD_TEXT="󰑊 Recording…"
fi
${updateOsd}
'';
power-cycle = pkgs.writeShellScriptBin "power-cycle" ''
ppd=${pkgs.power-profiles-daemon}/bin/powerprofilesctl
cur=$("$ppd" get)
case "$cur" in
power-saver) next=balanced; icon="󰾅" ;;
balanced) next=performance; icon="󰓅" ;;
performance) next=power-saver; icon="󰌪" ;;
*) next=balanced; icon="󰾅" ;;
esac
"$ppd" set "$next"
OSD_TEXT="$icon ''${next^}"
${updateOsd}
'';
airplane-toggle = pkgs.writeShellScriptBin "airplane-toggle" ''
# Reuse nmcli + bluetoothctl (unprivileged) instead of rfkill.
on=false
nmcli radio wifi 2>/dev/null | grep -q enabled && on=true
bluetoothctl show 2>/dev/null | grep -q "Powered: yes" && on=true
if $on; then
nmcli radio wifi off 2>/dev/null
bluetoothctl power off >/dev/null 2>&1 || true
OSD_TEXT="󰀝 Airplane On"
else
nmcli radio wifi on 2>/dev/null
bluetoothctl power on >/dev/null 2>&1 || true
OSD_TEXT="󰀞 Airplane Off"
fi
${updateOsd}
'';
clipboard-menu = pkgs.writeShellScriptBin "clipboard-menu" ''
list=$(${pkgs.cliphist}/bin/cliphist list)
selected=$(printf '%s\n' "$list" | cut -f2- | ${tofiMenu} --prompt-text "Clipboard: ")
[ -n "$selected" ] && printf '%s\n' "$list" \
| awk -F'\t' -v s="$selected" '$2 == s {print; exit}' \
| ${pkgs.cliphist}/bin/cliphist decode \
| ${pkgs.wl-clipboard}/bin/wl-copy
'';
emoji-picker = pkgs.writeShellScriptBin "emoji-picker" ''
export PATH="${pkgs.wtype}/bin:${pkgs.wl-clipboard}/bin:$PATH"
export BEMOJI_PICKER_CMD="${tofiMenu} --prompt-text 'emoji: '"
# -t types the emoji (wtype), -c also copies it.
exec ${pkgs.bemoji}/bin/bemoji -t -c
'';
icon-picker = pkgs.writeShellScriptBin "icon-picker" ''
selected=$(${tofiMenu} --font "${config.stylix.fonts.monospace.name}" --prompt-text 'icon: ' < ${nerdFontIconList})
[ -z "$selected" ] || icon=$(printf '%s\n' "$selected" | awk '{print $1}')
[ -n "''${icon:-}" ] || exit 0
${pkgs.wtype}/bin/wtype "$icon"
printf '%s' "$icon" | ${pkgs.wl-clipboard}/bin/wl-copy
'';
caffeine-toggle = pkgs.writeShellScriptBin "caffeine-toggle" ''
# Pause hypridle (stay awake) or resume it.
if systemctl --user is-active --quiet hypridle; then
systemctl --user stop hypridle
OSD_TEXT="󰅶 Keep Awake On"
else
systemctl --user start hypridle
OSD_TEXT="󰾫 Keep Awake Off"
fi
${updateOsd}
'';
}
+74
View File
@@ -0,0 +1,74 @@
{
pkgs,
scripts,
...
}: let
volGetText = ''
VOL_RAW=$(${pkgs.wireplumber}/bin/wpctl get-volume @DEFAULT_SINK@)
VOL=$(printf '%s' "$VOL_RAW" | awk '{printf "%d", $2*100}')
if printf '%s' "$VOL_RAW" | grep -q MUTED; then
OSD_TEXT="󰝟 $VOL%"
elif [ "$VOL" -lt 33 ]; then
OSD_TEXT="󰕿 $VOL%"
elif [ "$VOL" -lt 66 ]; then
OSD_TEXT="󰖀 $VOL%"
else
OSD_TEXT="󰕾 $VOL%"
fi
'';
# Resolve $src: the default source, otherwise the first available source.
# Some machines have no default audio source (@DEFAULT_AUDIO_SOURCE@ unresolved).
micSource = ''
src=$(${pkgs.wireplumber}/bin/wpctl inspect @DEFAULT_AUDIO_SOURCE@ 2>/dev/null | sed -n 's/^id \([0-9]*\),.*/\1/p')
if [ -z "$src" ]; then
src=$(${pkgs.pipewire}/bin/pw-dump | ${pkgs.jq}/bin/jq -r \
'[.[] | select(.info.props."media.class"=="Audio/Source") | .id] | .[0] // empty')
fi
'';
updateOsd = ''
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
in {
scripts.vol-up = pkgs.writeShellScriptBin "vol-up" ''
${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_SINK@ 5%+ --limit 1.0
${volGetText}
${updateOsd}
'';
scripts.vol-down = pkgs.writeShellScriptBin "vol-down" ''
${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_SINK@ 5%-
${volGetText}
${updateOsd}
'';
scripts.vol-mute = pkgs.writeShellScriptBin "vol-mute" ''
${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_SINK@ toggle
${volGetText}
${updateOsd}
'';
scripts.mic-mute = pkgs.writeShellScriptBin "mic-mute" ''
wpctl=${pkgs.wireplumber}/bin/wpctl
${micSource}
[ -z "$src" ] && exit 0
"$wpctl" set-mute "$src" toggle
if "$wpctl" get-volume "$src" | grep -q MUTED; then
OSD_TEXT="󰍭 Muted"
else
OSD_TEXT="󰍬 Live"
fi
${updateOsd}
'';
scripts.mic-status = pkgs.writeShellScriptBin "mic-status" ''
${micSource}
if [ -n "$src" ] && ${pkgs.wireplumber}/bin/wpctl get-volume "$src" | grep -q MUTED; then
echo true
else
echo false
fi
'';
}
+50
View File
@@ -0,0 +1,50 @@
{
pkgs,
scripts,
...
}: let
updateOsd = ''
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
in {
scripts.bluetoothScript = pkgs.writeShellScript "waybar-bluetooth" ''
jq=${pkgs.jq}/bin/jq
nl=$'\n'
# bluetoothctl hangs without a controller by default → always bounded by timeout.
bt() { timeout 3 ${pkgs.bluez}/bin/bluetoothctl "$@" 2>/dev/null; }
powered=$(bt show | awk '/Powered:/ { print $2; exit }')
if [ "$powered" != "yes" ]; then
"$jq" -cn '{ text: "󰂲", class: "off", tooltip: "Bluetooth off" }'
exit 0
fi
tip=""
add_tip() { tip="''${tip:+$tip$nl}$1"; }
count=0
for mac in $(bt devices Connected | awk '{ print $2 }'); do
count=$((count + 1))
info=$(bt info "$mac")
name=$(printf '%s' "$info" | sed -n 's/^[[:space:]]*Name: //p' | head -1)
[ -z "$name" ] && name="$mac"
batt=$(printf '%s' "$info" | sed -n 's/.*Battery Percentage:.*(\([0-9][0-9]*\)).*/\1/p' | head -1)
add_tip "󰂱 $name''${batt:+ · $batt%}"
done
if [ "$count" -gt 0 ]; then
"$jq" -cn --arg tooltip "$tip" '{ text: "󰂰", class: "connected", tooltip: $tooltip }'
else
"$jq" -cn '{ text: "󰂰", class: "on", tooltip: "Bluetooth on" }'
fi
'';
scripts.bluetooth-toggle = pkgs.writeShellScriptBin "bluetooth-toggle" ''
if ${pkgs.bluez}/bin/bluetoothctl show | grep -q "Powered: yes"; then
${pkgs.bluez}/bin/bluetoothctl power off
else
${pkgs.bluez}/bin/bluetoothctl power on
fi
${updateOsd}
'';
}
+32
View File
@@ -0,0 +1,32 @@
{
pkgs,
scripts,
...
}: let
brightGetText = ''
BRIGHT=$(${pkgs.brightnessctl}/bin/brightnessctl -m | awk -F, '{print int($5)}')
if [ "$BRIGHT" -lt 33 ]; then
OSD_TEXT="󰃞 $BRIGHT%"
elif [ "$BRIGHT" -lt 66 ]; then
OSD_TEXT="󰃟 $BRIGHT%"
else
OSD_TEXT="󰃠 $BRIGHT%"
fi
'';
updateOsd = ''
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
in {
scripts.bright-up = pkgs.writeShellScriptBin "bright-up" ''
${pkgs.brightnessctl}/bin/brightnessctl set 5%+
${brightGetText}
${updateOsd}
'';
scripts.bright-down = pkgs.writeShellScriptBin "bright-down" ''
${pkgs.brightnessctl}/bin/brightnessctl set 5%-
${brightGetText}
${updateOsd}
'';
}
+11
View File
@@ -0,0 +1,11 @@
{
imports = [
./osd.nix
./bluetooth.nix
./network.nix
./audio.nix
./brightness.nix
./devices.nix
./misc.nix
];
}
+63
View File
@@ -0,0 +1,63 @@
{
pkgs,
scripts,
...
}: let
updateOsd = ''
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
in {
scripts.output-cycle = pkgs.writeShellScriptBin "output-cycle" ''
wpctl=${pkgs.wireplumber}/bin/wpctl
jq=${pkgs.jq}/bin/jq
pwdump=${pkgs.pipewire}/bin/pw-dump
# Sink IDs in pw-dump order.
ids=($("$pwdump" | "$jq" -r '.[] | select(.info.props."media.class"=="Audio/Sink") | .id'))
n=''${#ids[@]}
[ "$n" -eq 0 ] && exit 0
cur=$("$wpctl" inspect @DEFAULT_AUDIO_SINK@ 2>/dev/null | sed -n 's/^id \([0-9]*\),.*/\1/p')
next_index=0
for i in "''${!ids[@]}"; do
if [ "''${ids[$i]}" = "$cur" ]; then
next_index=$(( (i + 1) % n ))
break
fi
done
next=''${ids[$next_index]}
"$wpctl" set-default "$next"
desc=$("$pwdump" | "$jq" -r --argjson id "$next" '.[] | select(.id==$id) | .info.props."node.description"')
OSD_TEXT="󰓃 ''${desc:-Output}"
${updateOsd}
'';
scripts.input-cycle = pkgs.writeShellScriptBin "input-cycle" ''
wpctl=${pkgs.wireplumber}/bin/wpctl
jq=${pkgs.jq}/bin/jq
pwdump=${pkgs.pipewire}/bin/pw-dump
# Source IDs in pw-dump order.
ids=($("$pwdump" | "$jq" -r '.[] | select(.info.props."media.class"=="Audio/Source") | .id'))
n=''${#ids[@]}
[ "$n" -eq 0 ] && exit 0
cur=$("$wpctl" inspect @DEFAULT_AUDIO_SOURCE@ 2>/dev/null | sed -n 's/^id \([0-9]*\),.*/\1/p')
next_index=0
for i in "''${!ids[@]}"; do
if [ "''${ids[$i]}" = "$cur" ]; then
next_index=$(( (i + 1) % n ))
break
fi
done
next=''${ids[$next_index]}
"$wpctl" set-default "$next"
desc=$("$pwdump" | "$jq" -r --argjson id "$next" '.[] | select(.id==$id) | .info.props."node.description"')
OSD_TEXT="󰍬 ''${desc:-Input}"
${updateOsd}
'';
}
+65
View File
@@ -0,0 +1,65 @@
{
pkgs,
scripts,
...
}: let
updateOsd = ''
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
in {
scripts.waybar-toggle = pkgs.writeShellScriptBin "waybar-toggle" ''
if pidof waybar > /dev/null; then
pkill waybar
else
hyprctl dispatch exec waybar
fi
'';
scripts.color-pick = pkgs.writeShellScriptBin "color-pick" ''
# hyprpicker -a already copies the color to the clipboard.
color=$(${pkgs.hyprpicker}/bin/hyprpicker -a 2>/dev/null)
if [ -n "$color" ]; then
OSD_TEXT="󰈊 $color"
else
OSD_TEXT="󰈊 Cancelled"
fi
${updateOsd}
'';
scripts.screenshot-edit = pkgs.writeShellScriptBin "screenshot-edit" ''
${pkgs.hyprshot}/bin/hyprshot -m region --raw 2>/dev/null \
| ${pkgs.satty}/bin/satty --filename -
'';
scripts.record-toggle = pkgs.writeShellScriptBin "record-toggle" ''
if pgrep -x wf-recorder >/dev/null; then
# -INT lets wf-recorder finalize the file cleanly.
pkill -INT -x wf-recorder
OSD_TEXT="󰕧 Recording saved"
else
dir="$HOME/Videos"
mkdir -p "$dir"
file="$dir/rec-$(date +%Y%m%d-%H%M%S).mp4"
${pkgs.wf-recorder}/bin/wf-recorder -f "$file" >/dev/null 2>&1 &
OSD_TEXT="󰑊 Recording…"
fi
${updateOsd}
'';
scripts.airplane-toggle = pkgs.writeShellScriptBin "airplane-toggle" ''
# Reuse nmcli + bluetoothctl (unprivileged) instead of rfkill.
on=false
nmcli radio wifi 2>/dev/null | grep -q enabled && on=true
bluetoothctl show 2>/dev/null | grep -q "Powered: yes" && on=true
if $on; then
nmcli radio wifi off 2>/dev/null
bluetoothctl power off >/dev/null 2>&1 || true
OSD_TEXT="󰀝 Airplane On"
else
nmcli radio wifi on 2>/dev/null
bluetoothctl power on >/dev/null 2>&1 || true
OSD_TEXT="󰀞 Airplane Off"
fi
${updateOsd}
'';
}
+77
View File
@@ -0,0 +1,77 @@
{
pkgs,
scripts,
...
}: let
updateOsd = ''
${scripts.waybar-osd}/bin/waybar-osd "$OSD_TEXT"
'';
in {
scripts.networkScript = pkgs.writeShellScript "waybar-network" ''
nmcli=${pkgs.networkmanager}/bin/nmcli
ip=${pkgs.iproute2}/bin/ip
jq=${pkgs.jq}/bin/jq
nl=$'\n'
text=""
tip=""
class="disconnected"
add_seg() { text="''${text:+$text }$1"; }
add_tip() { tip="''${tip:+$tip$nl}$1"; }
# ── Ethernet ────────────────────────────────────────────────
eth=$("$nmcli" -t -f DEVICE,TYPE,STATE device status 2>/dev/null \
| awk -F: '$2 == "ethernet" && $3 == "connected" { print $1; exit }')
if [ -n "$eth" ]; then
eth_ip=$("$ip" -4 -br addr show "$eth" 2>/dev/null | awk '{ print $3 }' | cut -d/ -f1)
add_seg "󰛳"
class="ethernet"
add_tip "󰛳 Ethernet''${eth_ip:+ · $eth_ip}"
fi
# ── Wi-Fi ───────────────────────────────────────────────────
wifi=$("$nmcli" -t -f ACTIVE,SSID,SIGNAL device wifi 2>/dev/null | grep -m1 '^yes:')
if [ -n "$wifi" ]; then
ssid=$(printf '%s' "$wifi" | cut -d: -f2)
signal=$(printf '%s' "$wifi" | cut -d: -f3)
if [ "''${signal:-0}" -ge 80 ]; then icon="󰤨"
elif [ "''${signal:-0}" -ge 60 ]; then icon="󰤥"
elif [ "''${signal:-0}" -ge 40 ]; then icon="󰤢"
elif [ "''${signal:-0}" -ge 20 ]; then icon="󰤟"
else icon="󰤯"
fi
add_seg "$icon"
[ "$class" = "disconnected" ] && class="wifi"
add_tip "$icon ''${ssid:-Wi-Fi}''${signal:+ · $signal%}"
fi
# ── VPN (nmcli, fallback interfaces) ────────────────────────
vpn=$("$nmcli" -t -f TYPE,NAME connection show --active 2>/dev/null \
| awk -F: '$1 == "vpn" || $1 == "wireguard" { print $2; exit }')
if [ -z "$vpn" ]; then
vpn=$("$ip" -br link show 2>/dev/null \
| awk '($2 == "UP" || $2 == "UNKNOWN") && $1 ~ /^(tun|wg|proton|ppp)/ { sub(/@.*/, "", $1); print $1; exit }')
fi
if [ -n "$vpn" ]; then
add_seg "󰦝"
add_tip "󰦝 VPN · $vpn"
fi
if [ -z "$text" ]; then
text="󰤭"
tip="Disconnected"
fi
"$jq" -cn --arg text "$text" --arg class "$class" --arg tooltip "$tip" \
'{ text: $text, class: $class, tooltip: $tooltip }'
'';
scripts.wifi-toggle = pkgs.writeShellScriptBin "wifi-toggle" ''
if ${pkgs.networkmanager}/bin/nmcli radio wifi | grep -q enabled; then
${pkgs.networkmanager}/bin/nmcli radio wifi off
else
${pkgs.networkmanager}/bin/nmcli radio wifi on
fi
${updateOsd}
'';
}
+27
View File
@@ -0,0 +1,27 @@
{pkgs, ...}: {
scripts.waybar-osd = pkgs.writeShellApplication {
name = "waybar-osd";
runtimeInputs = with pkgs; [procps coreutils];
text = ''
printf '%s' "$1" > "$XDG_RUNTIME_DIR/waybar-osd"
pkill -f -RTMIN+8 '^waybar$' 2>/dev/null || true
'';
};
scripts.waybar-osd-status = pkgs.writeShellApplication {
name = "waybar-osd-status";
runtimeInputs = with pkgs; [coreutils];
text = ''
file="$XDG_RUNTIME_DIR/waybar-osd"
[ -f "$file" ] || exit 1
mtime=$(stat -c %Y "$file" 2>/dev/null) || exit 1
age=$(( $(date +%s) - mtime ))
if [ "$age" -ge 3 ]; then
rm -f "$file"
exit 1
fi
'';
};
_module.args.osdPath = "$XDG_RUNTIME_DIR/waybar-osd";
}
+7 -14
View File
@@ -2,11 +2,6 @@
config,
pkgs,
osdPath,
networkScript,
bluetoothScript,
caffeineToggleScript,
osdStatusScript,
nightshiftToggleScript,
...
}: let
gaps-out = config.theme.gaps-out;
@@ -18,9 +13,7 @@ in {
position = "top";
height = config.theme.bar-height;
margin = "${toString gaps-out} ${toString gaps-out} 0";
modules-center = ["custom/osd" "custom/osd-sep" "clock" "tray" "hyprland/workspaces" "custom/network" "custom/bluetooth" "battery" "group/drawer"];
# ── Modules ─────────────────────────────────────────────────────────
modules-center = ["custom/osd" "custom/osd-sep" "clock" "tray" "hyprland/workspaces" "custom/network" "custom/bluetooth" "battery" "pulseaudio" "custom/nightshift" "custom/caffeine"];
"hyprland/workspaces" = {
format = "{id}";
@@ -55,7 +48,7 @@ in {
exec-if = "! systemctl --user is-active --quiet hypridle";
format = "{}";
interval = 5;
on-click = "${caffeineToggleScript}/bin/caffeine-toggle";
on-click = "${config.scripts.caffeine-toggle}/bin/caffeine-toggle";
tooltip = false;
};
@@ -64,12 +57,12 @@ in {
exec-if = "${pkgs.procps}/bin/pgrep -x hyprsunset";
format = "{}";
interval = 5;
on-click = "${nightshiftToggleScript}/bin/nightshift-toggle";
on-click = "${config.scripts.nightshift-toggle}/bin/nightshift-toggle";
tooltip = false;
};
"custom/bluetooth" = {
exec = "${bluetoothScript}";
exec = "${config.scripts.bluetoothScript}";
exec-if = "${pkgs.bluez}/bin/bluetoothctl list 2>/dev/null | grep -q Controller";
return-type = "json";
interval = 5;
@@ -78,7 +71,7 @@ in {
"custom/osd" = {
exec = "cat ${osdPath}";
exec-if = "${osdStatusScript}/bin/waybar-osd-status";
exec-if = "${config.scripts.waybar-osd-status}/bin/waybar-osd-status";
signal = 8;
interval = 1;
format = "{}";
@@ -86,7 +79,7 @@ in {
"custom/osd-sep" = {
exec = "echo '|'";
exec-if = "${osdStatusScript}/bin/waybar-osd-status";
exec-if = "${config.scripts.waybar-osd-status}/bin/waybar-osd-status";
signal = 8;
interval = 1;
format = "{}";
@@ -122,7 +115,7 @@ in {
};
"custom/network" = {
exec = "${networkScript}";
exec = "${config.scripts.networkScript}";
return-type = "json";
interval = 10;
on-click = "${pkgs.ghostty}/bin/ghostty +new-window -e ${pkgs.nur.repos.anotherhadi.settuings}/bin/settuings --page network";