flake/modules/home/desktop/i3.nix
2026-03-08 14:16:19 +00:00

253 lines
9 KiB
Nix

{
pkgs,
lib,
config,
osConfig,
...
}: let
# Bootstrap script that runs as the first program inside a new wezterm window.
# It sets up the three tabs synchronously before the shell prompt appears,
# so by the time the window is visible the layout is already complete.
# Receives the project path as $1.
wezterm-zmenu-init = pkgs.writeShellApplication {
name = "wezterm-zmenu-init";
runtimeInputs = [pkgs.wezterm pkgs.zsh];
text = ''
ZPATH=$1
WIN=$(wezterm cli list --format json 2>/dev/null \
| tr ',' '\n' \
| awk -v pane="$WEZTERM_PANE" '
/window_id/ { gsub(/.*: */,""); w=int($0) }
/pane_id/ { gsub(/.*: */,""); if (int($0)==pane) { print w; exit } }')
wezterm cli set-tab-title project 2>/dev/null
P2=$(wezterm cli spawn --window-id "$WIN" --cwd "$ZPATH")
wezterm cli set-tab-title --pane-id "$P2" test 2>/dev/null
P3=$(wezterm cli spawn --window-id "$WIN" --cwd "$ZPATH" -- lazygit)
wezterm cli set-tab-title --pane-id "$P3" git 2>/dev/null
wezterm cli activate-tab --tab-index 0 2>/dev/null
exec zsh -c "direnv exec . nvim"
'';
};
zmenu = with pkgs;
writeShellApplication {
name = "zmenu";
runtimeInputs =
[zoxide wmctrl i3 rofi zsh]
++ lib.optionals config.mods.terminal.wezterm.enable [wezterm]
++ lib.optionals (!config.mods.terminal.wezterm.enable) [zellij alacritty];
text =
if config.mods.terminal.wezterm.enable
then ''
ZPATH=$(zoxide query -l | sed -e "s|$HOME/||g" | rofi -dmenu)
[[ -z "$ZPATH" ]] && exit
ZSESH=$(echo "$ZPATH" | tr / -)
# Per-workspace socket registry so we can focus existing workspaces.
SOCKDIR="$XDG_RUNTIME_DIR/zmenu-wez"
mkdir -p "$SOCKDIR"
SOCKFILE="$SOCKDIR/$ZSESH"
SOCK=""
if [[ -f "$SOCKFILE" ]]; then
SOCK=$(cat "$SOCKFILE")
# Extract the PID from the socket path (gui-sock-<pid>) and check
# if the process is still alive. Dead sockets linger on disk.
SOCK_PID=''${SOCK##*-}
if ! kill -0 "$SOCK_PID" 2>/dev/null; then
rm -f "$SOCKFILE"
SOCK=""
fi
fi
if [[ -n "$SOCK" ]]; then
# Workspace open: raise its window by matching the wezterm PID in
# wmctrl's process list, then activate a pane to ensure focus.
SOCK_PID=''${SOCK##*-}
WIN_HEX=$(wmctrl -lp | awk -v pid="$SOCK_PID" '$3==pid {print $1; exit}')
wmctrl -i -a "$WIN_HEX"
PANE=$(WEZTERM_UNIX_SOCKET="$SOCK" wezterm cli list --format json 2>/dev/null \
| tr ',' '\n' \
| awk '/pane_id/{gsub(/.*: */,""); print int($0); exit}')
WEZTERM_UNIX_SOCKET="$SOCK" wezterm cli activate-pane --pane-id "$PANE" 2>/dev/null
else
# Not open: the init script runs as the first program inside the new
# window. It sets up all tabs synchronously before the shell prompt
# appears, so the layout is complete before anything is visible.
wezterm start --always-new-process \
--workspace "$ZSESH" --cwd "$HOME/$ZPATH" \
-- ${lib.getExe wezterm-zmenu-init} "$HOME/$ZPATH" &
WEZ_PID=$!
# Record the socket for future focus calls.
for _ in $(seq 50); do
sleep 0.1
[[ -S /run/user/$UID/wezterm/gui-sock-$WEZ_PID ]] && break
done
echo "/run/user/$UID/wezterm/gui-sock-$WEZ_PID" > "$SOCKFILE"
fi
''
else ''
ZPATH=$(zoxide query -l | sed -e "s|$HOME/||g" | rofi -dmenu)
[[ -z "$ZPATH" ]] && exit
ZSESH=$(echo "$ZPATH" | tr / -)
ZWIND=$(wmctrl -l | grep "$ZSESH$" || echo "")
cd "$HOME/$ZPATH"
if [[ -z "$ZWIND" ]]; then
alacritty -T "$ZSESH" -e zsh -c "zellij -s $ZSESH -n dev || zellij a $ZSESH"
else
wmctrl -a "$ZSESH"
fi
'';
};
in
with lib; {
options.mods.i3.enable = mkEnableOption "enables i3";
options.mods.battery.enable = mkEnableOption "enables battery";
config = mkIf config.mods.i3.enable {
services.unclutter.enable = true;
programs.rofi.enable = true;
programs.i3status-rust = {
enable = true;
bars.default = lib.mkMerge [
{
blocks = lib.mkBefore [
{
block = "privacy";
driver = [{name = "v4l";} {name = "pipewire";}];
}
# {
# block = "net";
# format = " $icon ";
# inactive_format = " $icon ";
# }
];
}
(lib.mkIf config.mods.battery.enable {
blocks = [
{
block = "battery";
format = " $icon $percentage ";
# format_alt =
# " $icon $percentage {$time_remaining.dur(hms:true, min_unit:m) |}";
}
];
})
{
settings.theme.overrides = lib.mkMerge [
{separator = "";}
config.lib.stylix.i3status-rust.bar
];
icons = "awesome5";
blocks = lib.mkAfter [
{
block = "sound";
format = " $icon {$volume |} ";
click = [
{
button = "left";
cmd = "${lib.getExe pkgs.pavucontrol}";
}
];
}
{
block = "time";
format = "$icon $timestamp.datetime(f:'%Y-%m-%d %a %H:%M:%S') ";
# format_alt = "$icon $timestamp.datetime(f:'%Y-%m-%d %H:%M') ";
interval = 1;
}
];
}
];
};
xsession.windowManager.i3 = let
modifier = "Mod4";
wsAmount = 10;
genSpaces = f: map builtins.toString (builtins.genList f wsAmount);
workspaces = genSpaces (x: x + 1);
in {
enable = true;
config = {
modifier = modifier;
terminal =
if config.mods.terminal.wezterm.enable
then "wezterm"
else "alacritty";
menu = "rofi -show drun";
window = {
titlebar = false;
hideEdgeBorders = "smart";
};
floating.criteria = [
{title = "^Cheat$";}
];
defaultWorkspace = "workspace number 1";
workspaceAutoBackAndForth = true;
focus.wrapping = "yes";
# startup = [ ] ++ lib.optionals config.mods.social.enable [{
# command = "exec vesktop";
# }];
assigns = {
"2" = [{class = "Vesktop";}];
"3" = [{class = "^Steam$";}];
};
keybindings = let
# modKeyComb: string -> valueAction: string
# -> keys: [string] -> values: [string]
# -> keybindings: attrSet { string -> string }
zipBinds = comb: action: keys: values: (builtins.listToAttrs (zipListsWith (k: v: {
name = "${modifier}${comb}${k}";
value = "${action} ${v}";
})
keys
values));
moveKeys = ["h" "j" "k" "l"];
moveDirs = ["left" "down" "up" "right"];
workspKeys = genSpaces (x: mod (x + 1) wsAmount);
in
mkOptionDefault ((zipBinds "+" "focus" moveKeys moveDirs)
// (zipBinds "+Shift+" "move" moveKeys moveDirs)
// (zipBinds "+" "workspace number" workspKeys workspaces)
// (zipBinds "+Shift+" "move container to workspace number" workspKeys
workspaces)
// {
"XF86AudioRaiseVolume" = "exec --no-startup-id pactl set-sink-volume 0 +2%";
"XF86AudioLowerVolume" = "exec --no-startup-id pactl set-sink-volume 0 -2%";
"Print" = "exec flameshot gui -c -s";
"${modifier}+z" = "exec ${getExe zmenu}";
"${modifier}+p" = "exec clipmenu";
"${modifier}+b" = "exec ${getExe pkgs.rofi-rbw-x11}";
"${modifier}+y" = "sticky toggle";
"${modifier}+g" = "floating toggle";
});
bars = let
status_conf = "${config.xdg.configHome}/i3status-rust/config-default.toml";
stylix_bar =
{
mode = "dock";
hiddenState = "hide";
position = "bottom";
statusCommand = "${getExe pkgs.i3status-rust} ${status_conf}";
command = "${pkgs.i3}/bin/i3bar";
workspaceButtons = true;
workspaceNumbers = true;
trayOutput = "primary";
}
// config.stylix.targets.i3.exportedBarConfig;
in [stylix_bar];
};
};
};
}