This repository has no description
14 kB
377 lines
1{ self, inputs, ... }:
2{
3 perSystem =
4 { pkgs, ... }:
5 let
6 zshrc = ''
7 autoload -U compinit && compinit
8
9 # Setup custom interactive shell init stuff.
10 # Bind gpg-agent to this TTY if gpg commands are used.
11 export GPG_TTY=$(tty)
12
13 # show how long a command takes
14 ZSH_COMMAND_TIME_MIN_SECONDS=3
15 ZSH_COMMAND_TIME_EXCLUDE=(vim)
16 ZSH_COMMAND_TIME_MSG="took: %s sec"
17 source ${
18 pkgs.fetchFromGitHub {
19 owner = "popstas";
20 repo = "zsh-command-time";
21 rev = "803d26eef526bff1494d1a584e46a6e08d25d918";
22 hash = "sha256-ndHVFcz+XmUW0zwFq7pBXygdRKyPLjDZNmTelhd5bv8=";
23 }
24 }/command-time.plugin.zsh
25
26 unsetopt nomatch # no matches found urls
27 setopt autocd extendedglob
28 bindkey -e
29
30 #C-x C-e open line in editor
31 autoload -z edit-command-line
32 zle -N edit-command-line
33 bindkey "^X^E" edit-command-line
34
35 #fzf inclusion
36 source ${pkgs.fzf}/share/fzf/completion.zsh
37 source ${pkgs.fzf}/share/fzf/key-bindings.zsh
38
39 # atuin distributed shell history
40 if type -p atuin >/dev/null; then
41 export ATUIN_CONFIG_DIR=${pkgs.writeTextDir "/config.toml" ''
42 auto_sync = true
43 update_check = false
44 sync_address = "http://green.r:8888"
45 sync_frequency = 0
46 style = "compact"
47 ''};
48 export ATUIN_NOBIND="true" # disable all keybdinings of atuin
49 eval "$(${pkgs.atuin}/bin/atuin init zsh)" # TODO make this optional?
50 bindkey '^r' _atuin_search_widget # bind ctrl+r to atuin
51 # use zsh only session history
52 fc -p
53 fi
54
55 #completion magic
56 autoload -Uz compinit
57 compinit
58 zstyle ':completion:*' menu select
59
60 #enable automatic rehashing of $PATH
61 zstyle ':completion:*' rehash true
62
63 # Lazy load completions for commands based on their installation path
64 _lazy_completion_loader() {
65 local cmd="$words[1]"
66 local basename_cmd="$cmd"
67 local cmd_path=""
68
69 [[ -n "$ZSH_COMP_DEBUG" ]] && echo "DEBUG: _lazy_completion_loader called for: $cmd" >&2
70
71 # Get command path and basename
72 if [[ "$cmd" == /* ]] && [[ -x "$cmd" ]]; then
73 cmd_path="$cmd"
74 basename_cmd="$(basename "$cmd")"
75 elif command -v "$cmd" >/dev/null 2>&1; then
76 cmd_path="$(which "$cmd" 2>/dev/null)"
77 basename_cmd="$(basename "$cmd_path")"
78 fi
79
80 [[ -n "$ZSH_COMP_DEBUG" ]] && echo "DEBUG: cmd_path=$cmd_path, basename_cmd=$basename_cmd" >&2
81
82 # Find and load completion file
83 local current="$cmd_path"
84 while [[ -n "$current" ]]; do
85 local share_dir="$(dirname "$current")/../share"
86 for subdir in zsh/site-functions zsh/vendor-completions; do
87 local dir="$share_dir/$subdir"
88 if [[ -f "$dir/_$basename_cmd" ]]; then
89 [[ -n "$ZSH_COMP_DEBUG" ]] && echo "DEBUG: found completion: $dir/_$basename_cmd" >&2
90 [[ ! " ''${fpath[@]} " =~ " $dir " ]] && fpath=("$dir" $fpath)
91 source "$dir/_$basename_cmd"
92 [[ -n "$ZSH_COMP_DEBUG" ]] && echo "DEBUG: after sourcing, _comps[$basename_cmd]=''${_comps[$basename_cmd]:-not set}" >&2
93 break 2
94 fi
95 done
96 # Follow absolute symlinks only
97 [[ -L "$current" ]] || break
98 current="$(readlink "$current" 2>/dev/null)"
99 [[ "$current" == /* ]] || break
100 done
101
102 # Register completion for the original command if needed
103 if [[ -n "''${_comps[$basename_cmd]:-}" ]]; then
104 if [[ "$cmd" != "$basename_cmd" ]]; then
105 _comps[$cmd]="''${_comps[$basename_cmd]}"
106 [[ -n "$ZSH_COMP_DEBUG" ]] && echo "DEBUG: registered ''${_comps[$basename_cmd]} for $cmd" >&2
107 fi
108 fi
109
110 # Remove lazy loader only if it's still set
111 if [[ "''${_comps[$cmd]:-}" == "_lazy_completion_loader" ]]; then
112 compdef -d "$cmd" 2>/dev/null
113 fi
114
115 # Run the appropriate completion
116 local comp="''${_comps[$cmd]:-}"
117 [[ -z "$comp" ]] && comp="''${_comps[$basename_cmd]:-_normal}"
118 [[ -n "$ZSH_COMP_DEBUG" ]] && echo "DEBUG: using completion: $comp" >&2
119 $comp "$@"
120 }
121
122 # Set as default completer for commands without completions
123 zstyle -e ':completion:*' completer '
124 if [[ -z $_comps[$words[1]] ]]; then
125 reply=(_lazy_completion_loader _complete)
126 else
127 reply=(_complete)
128 fi
129 '
130
131 # fancy mv which interactively gets the second argument if not given
132 function mv() {
133 if [[ "$#" -ne 1 ]] || [[ ! -e "$1" ]]; then
134 command mv -v "$@"
135 return
136 fi
137
138 newfilename="$1"
139 vared newfilename
140 command mv -v -- "$1" "$newfilename"
141 }
142
143 #emacs bindings
144 bindkey "[7~" beginning-of-line
145 bindkey "[8~" end-of-line
146 bindkey "Oc" emacs-forward-word
147 bindkey "Od" emacs-backward-word
148
149 # direnv integration
150 eval "$(${pkgs.direnv}/bin/direnv hook zsh)"
151
152 # Use nvim as MANPAGER if available and output is not piped
153 if command -v nvim >/dev/null 2>&1; then
154 export MANPAGER='sh -c "if [ -t 1 ]; then nvim +Man!; else cat; fi"'
155 fi
156
157 # This function is called whenever a command is not found.
158 command_not_found_handler() {
159 local p='${pkgs.nix-index}/etc/profile.d/command-not-found.sh'
160 if [ -x "$p" ] && [ -f '/nix/var/nix/profiles/per-user/root/channels/nixos/programs.sqlite' ]; then
161 # Run the helper program.
162 "$p" "$@"
163
164 # Retry the command if we just installed it.
165 if [ $? = 126 ]; then
166 "$@"
167 else
168 return 127
169 fi
170 else
171 # Indicate than there was an error so ZSH falls back to its default handler
172 echo "$1: command not found" >&2
173 return 127
174 fi
175 }
176
177 download_nixpkgs_cache_index () {
178 filename="index-$(uname -m | sed 's/^arm64$/aarch64/')-$(uname | tr A-Z a-z)"
179 mkdir -p ~/.cache/nix-index && cd ~/.cache/nix-index
180 # -N will only download a new version if there is an update.
181 , wget -q -N https://github.com/nix-community/nix-index-database/releases/latest/download/$filename
182 ln -f $filename files
183 }
184
185 # Setup aliases.
186 alias -- grep='grep --color=auto'
187 alias -- ip='ip -color=auto'
188 alias -- l='ls -alh'
189 alias -- la='ls -la'
190 alias -- ll='ls -l'
191 alias -- ls='ls --color'
192 alias -- ns='nix-shell --command zsh'
193 alias -- nb='nix build --no-link --print-out-paths -L'
194 alias -- ne='nix eval --json'
195 alias -- nloc='nix-locate --top-level --whole-name'
196 # a child process cannot change this shell's env, so eval its export line
197 alias -- ssh-agent-pick='eval "$(${
198 self.packages.${pkgs.system}.ssh-agent-pick
199 }/bin/ssh-agent-pick)"'
200
201 # Setup prompt.
202 autoload -U promptinit
203 promptinit
204
205 p_error='%(?..%F{red}%?%f )'
206 t_error='%(?..%? )'
207
208 case $UID in
209 0)
210 p_username='%F{red}root%f'
211 t_username='root'
212 ;;
213 1337)
214 p_username=""
215 t_username=""
216 ;;
217 *)
218 p_username='%F{blue}%n%f'
219 t_username='%n'
220 ;;
221 esac
222
223 if test -n "$SSH_CLIENT"; then
224 p_hostname='@%F{magenta}%M%f '
225 t_hostname='@%M '
226 else
227 p_hostname=""
228 t_hostname=""
229 fi
230
231 #check if in nix shell
232 if test -n "$IN_NIX_SHELL"; then
233 p_nixshell='%F{green}[s]%f '
234 t_nixshell='[s] '
235 else
236 p_nixshell=""
237 t_nixshell=""
238 fi
239
240 PROMPT="$p_error$p_username$p_hostname$p_nixshell%~ "
241 TITLE="$t_error$t_username$t_hostname$t_nixshell%~"
242 case $TERM in
243 (*xterm* | *rxvt*)
244 function precmd {
245 PROMPT_EVALED=$(print -P "$TITLE")
246 echo -ne "\033]0;$$ $PROMPT_EVALED\007"
247 }
248 # This seems broken for some reason
249 # # This is seen while the shell waits for a command to complete.
250 # function preexec {
251 # PROMPT_EVALED=$(print -P "$TITLE")
252 # echo -ne "\033]0;$$ $PROMPT_EVALED $1\007"
253 # }
254 ;;
255 esac
256
257
258 # Auto-start tmux for new sessions
259 # Check if we're already in tmux (even through sudo)
260 # Only run in interactive shells — non-interactive SSH commands must not exec tmux
261 if [[ -o interactive && "$TERM" != "linux" && -z "$TMUX" && "$TERM" != "dumb" ]]; then
262 # Check if we're inside tmux by looking for tmux in the process tree
263 in_tmux=false
264 pid=$$
265 while [[ $pid -ne 1 ]]; do
266 if ps -p $pid -o comm= 2>/dev/null | grep -q '^tmux'; then
267 in_tmux=true
268 break
269 fi
270 # Get parent PID
271 pid=$(ps -p $pid -o ppid= 2>/dev/null | tr -d ' ') || break
272 done
273
274 if [[ "$in_tmux" == "false" ]]; then
275 # Preserve SSH_AUTH_SOCK for tmux
276 if [[ -n "$SSH_AUTH_SOCK" ]]; then
277 tmux set-environment -g SSH_AUTH_SOCK "$SSH_AUTH_SOCK" 2>/dev/null
278 fi
279
280 # SSH session chooser: show existing sessions via fzf
281 # Delete key kills the highlighted session, then refreshes the list
282 if [[ -n "$SSH_CLIENT" ]]; then
283 while true; do
284 existing=$(tmux list-sessions -F '#{session_name}: #{session_windows} windows (created #{session_created_string})#{?session_attached, (attached),}' 2>/dev/null)
285 [[ -z "$existing" ]] && break
286 chosen=$(echo ">>> new session <<<\n$existing" \
287 | fzf --height=80% --reverse \
288 --header="Pick a tmux session (DEL to kill)" \
289 --expect=delete \
290 --preview='s={1}; s=''${s%:}; [ "$s" = ">>>" ] && echo "Create a new session" || tmux capture-pane -t "$s" -p -e 2>/dev/null' \
291 --preview-window=right:60%)
292 key=$(echo "$chosen" | head -1)
293 selection=$(echo "$chosen" | tail -1)
294 chosen_name=$(echo "$selection" | cut -d: -f1)
295 if [[ "$key" == "delete" && -n "$chosen_name" && "$chosen_name" != ">>> new session <<<" ]]; then
296 tmux kill-session -t "$chosen_name" 2>/dev/null
297 continue
298 fi
299 if [[ -n "$chosen_name" && "$chosen_name" != ">>> new session <<<" ]]; then
300 exec tmux -u attach -t "$chosen_name"
301 fi
302 break
303 done
304 fi
305
306 # Generate a readable session name using name-generator
307 session_name=$(${
308 self.packages.${pkgs.system}.name-generator
309 }/bin/name-generator 2>/dev/null || echo "session-$$")
310 # Prefix with workspace name in graphical sessions
311 if [[ -n "$WAYLAND_DISPLAY" ]]; then
312 _ws_name=$(${
313 self.packages.${pkgs.system}.workspace-manager
314 }/bin/workspace-manager workspace 2>/dev/null)
315 [[ -n "$_ws_name" ]] && session_name="''${_ws_name}-''${session_name}"
316 fi
317 # Start tmux with the generated session name
318 exec tmux -u new-session -s "$session_name"
319 fi
320 fi
321
322 # Set tmux status bar color based on hostname for SSH sessions
323 if [[ -n "$TMUX" && "$__host__" != "$HOST" ]]; then
324 # Generate a color based on hostname hash
325 color=$(( $(echo -n "$HOST" | od -An -N4 -tu4) % 255 ))
326 tmux set -g status-bg "colour$color" 2>/dev/null || true
327 export __host__="$HOST"
328 fi
329
330 # Configure tmux for better mouse support
331 if [[ -n "$TMUX" ]]; then
332 # Enable mouse mode for scrolling
333 tmux set -g mouse on 2>/dev/null || true
334 fi
335
336 # Workspace manager integration: cd to workspace directory on new terminal.
337 # Only for shells that start in $HOME: a pane spawned with an explicit
338 # directory (tmux split in cwd) must keep it.
339 if [[ "$PWD" == "$HOME" ]]; then
340 _ws_dir=$(${self.packages.${pkgs.system}.workspace-manager}/bin/workspace-manager dir 2>/dev/null)
341 [[ -n "$_ws_dir" && -d "$_ws_dir" ]] && cd "$_ws_dir"
342 fi
343
344 # Disable some features to support TRAMP.
345 if [ "$TERM" = dumb ]; then
346 unsetopt zle prompt_cr prompt_subst
347 unset RPS1 RPROMPT
348 PS1='$ '
349 PROMPT='$ '
350 fi
351 '';
352 in
353 {
354 packages.zsh =
355 (inputs.wrappers.lib.wrapPackage {
356 pkgs = pkgs;
357 package = pkgs.zsh;
358 runtimeInputs = with pkgs; [
359 gnugrep
360 tmux
361 fzf
362 direnv
363 atuin
364 nix-index
365 self.packages.${pkgs.system}.name-generator
366 ];
367 env = {
368 ZDOTDIR = pkgs.writeTextDir "/.zshrc" zshrc;
369 };
370 }).overrideAttrs
371 (old: {
372 passthru = (old.passthru or { }) // {
373 inherit zshrc;
374 };
375 });
376 };
377}