From 73a14218a836c3ec44882a09a816152f34437e44 Mon Sep 17 00:00:00 2001 From: Andrejus Date: Thu, 12 Feb 2026 15:44:56 +0000 Subject: [PATCH] feat: tmux and widgets --- .gitignore | 1 + files/dot-ssh/config | 2 + files/home/.aliases | 37 +++++++++++ files/home/.tmux.conf | 66 +++++++++++++++++++ files/home/.zshrc | 125 ++++++++++++++++++++++++++++++++---- script/install.d/24-tmux.sh | 26 ++++++++ tests/test_binaries.py | 1 + 7 files changed, 245 insertions(+), 13 deletions(-) create mode 100644 files/home/.tmux.conf create mode 100644 script/install.d/24-tmux.sh diff --git a/.gitignore b/.gitignore index e008966..9eedaab 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ temp # ssh env **/id_* **/known_hosts* +**/config.d # setup files **/autoload diff --git a/files/dot-ssh/config b/files/dot-ssh/config index 5e421eb..9d8cab8 100644 --- a/files/dot-ssh/config +++ b/files/dot-ssh/config @@ -1,3 +1,5 @@ +Include config.d/* + Host * IgnoreUnknown UseKeychain UseKeychain yes diff --git a/files/home/.aliases b/files/home/.aliases index e5e8d9e..2270974 100644 --- a/files/home/.aliases +++ b/files/home/.aliases @@ -19,3 +19,40 @@ alias gl='git ld' alias ga='git a' alias gr='git r' +# fzf workflows +gb() { local b; b="$(git branch --all --sort=-committerdate --format='%(refname:short)' | fzf --preview 'git log --oneline --color -20 {}')" && git checkout "${b#origin/}"; } +glo() { git log --oneline --color --decorate | fzf --ansi --preview 'git show --color --stat {1}' --bind 'enter:become(echo {1})'; } +f() { local f; f="$(fzf --preview 'head -100 {}')" && ${EDITOR:-vim} "$f"; } + +ssh() { + local ssh_target="${@: -1}" + if [[ -n "$TMUX" ]]; then + tmux rename-window "ssh:$ssh_target" + fi + + command ssh "$@" + local rc=$? + + if (( rc == 0 )); then + local host="$ssh_target" + local ssh_log="${XDG_DATA_HOME:-$HOME/.local/share}/ssh/log" + mkdir -p "$(dirname "$ssh_log")" + + if [[ "$ssh_target" == *@* ]]; then + local user="${ssh_target%%@*}" + host="${ssh_target#*@}" + local config_file="$HOME/.ssh/config.d/auto" + mkdir -p "$HOME/.ssh/config.d" + if ! grep -q "^Host $host$" "$config_file" 2>/dev/null; then + printf '\nHost %s\n HostName %s\n User %s\n' "$host" "$host" "$user" >> "$config_file" + fi + fi + + printf '%s %s\n' "$(date +%s)" "$host" >> "$ssh_log" + fi + + [[ -n "$TMUX" ]] && tmux set automatic-rename on + return $rc +} +s() { local h; h="$(_dots_ssh_hosts | fzf)" && ssh "$h"; } + diff --git a/files/home/.tmux.conf b/files/home/.tmux.conf new file mode 100644 index 0000000..5a55a6d --- /dev/null +++ b/files/home/.tmux.conf @@ -0,0 +1,66 @@ +# Prefix: Ctrl+A +unbind C-b +set -g prefix C-a +bind C-a send-prefix + +# True color +set -g default-terminal "tmux-256color" +set -ag terminal-overrides ",xterm-256color:RGB" + +# Shell +set -g default-shell "$SHELL" + +# Windows and panes start at 1 +set -g base-index 1 +setw -g pane-base-index 1 +set -g renumber-windows on +set -g history-limit 50000 +set -sg escape-time 10 +set -g focus-events on + +# Copy mode +setw -g mode-keys vi +bind -T copy-mode-vi v send -X begin-selection +bind -T copy-mode-vi y send -X copy-pipe-and-cancel "pbcopy 2>/dev/null || xclip -selection clipboard 2>/dev/null || xsel --clipboard" + +# Splits +bind | split-window -h -c "#{pane_current_path}" +bind - split-window -v -c "#{pane_current_path}" +unbind '"' +unbind % + +bind c new-window -c "#{pane_current_path}" + +# Pane navigation +bind h select-pane -L +bind j select-pane -D +bind k select-pane -U +bind l select-pane -R + +# Pane resize +bind -r C-h resize-pane -L 5 +bind -r C-j resize-pane -D 5 +bind -r C-k resize-pane -U 5 +bind -r C-l resize-pane -R 5 + +# Reload +bind r source-file ~/.tmux.conf \; display "Config reloaded" + +# Status bar +set -g status-position bottom +set -g status-interval 5 +set -g status-style "fg=#728cb8,bg=default" +set -g status-left "" +set -g status-right "" +set -g window-status-format " #I:#W " +set -g window-status-current-format "#[fg=#2cb494,bold] #I:#W " +set -g window-status-bell-style "fg=#f88c14,bold" +set -g pane-border-style "fg=#728cb8" +set -g pane-active-border-style "fg=#2cb494" +set -g message-style "fg=#2cb494,bg=default" +set -g message-command-style "fg=#f88c14,bg=default" +set -g mode-style "fg=default,bg=#728cb8,bold" + +# Auto-rename +set -g automatic-rename on +set -g automatic-rename-format '#{?#{==:#{pane_current_command},zsh},zsh:#{b:pane_current_path},#{pane_current_command}}' diff --git a/files/home/.zshrc b/files/home/.zshrc index b467c81..93cf81f 100644 --- a/files/home/.zshrc +++ b/files/home/.zshrc @@ -32,15 +32,21 @@ _dots_cache_ls_colors [[ -f ~/.aliases ]] && source ~/.aliases _dots_init_completion() { - # Add custom completions to fpath local comp_dir="${XDG_DATA_HOME:-$HOME/.local/share}/zsh/completions" [[ -d "$comp_dir" ]] && fpath=("$comp_dir" $fpath) autoload -Uz compinit - # Rebuild zcompdump at most once per day + # Daily cache invalidation local dump="$HOME/.zcompdump" - if [[ -f "$dump" && $(date +'%j') == $(stat -f '%Sm' -t '%j' "$dump" 2>/dev/null || date -r "$dump" +'%j' 2>/dev/null) ]]; then - compinit -C + if [[ -f "$dump" ]]; then + zmodload -F zsh/stat b:zstat 2>/dev/null + local -a dump_stat + zstat -A dump_stat +mtime "$dump" 2>/dev/null + if (( dump_stat[1] > EPOCHSECONDS - 86400 )); then + compinit -C + else + compinit + fi else compinit fi @@ -49,10 +55,9 @@ _dots_init_completion _dots_load_plugins() { local plugin_dir="${XDG_DATA_HOME:-$HOME/.local/share}/zsh/plugins" - # autosuggestions first local f="$plugin_dir/zsh-autosuggestions/zsh-autosuggestions.zsh" [[ -f "$f" ]] && source "$f" - # syntax-highlighting must be last + # syntax-highlighting must be sourced last f="$plugin_dir/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" [[ -f "$f" ]] && source "$f" } @@ -62,7 +67,7 @@ _dots_load_history() { HISTFILE="${XDG_DATA_HOME:-$HOME/.local/share}/zsh/history" HISTSIZE=50000 SAVEHIST=50000 - mkdir -p "$(dirname "$HISTFILE")" + [[ -d "${HISTFILE:h}" ]] || mkdir -p "${HISTFILE:h}" setopt HIST_IGNORE_DUPS HIST_IGNORE_SPACE SHARE_HISTORY } _dots_load_history @@ -70,10 +75,7 @@ _dots_load_history _dots_load_keybindings() { bindkey -e - bindkey '^[[H' beginning-of-line - bindkey '^[[F' end-of-line - - # Ctrl+J: interactive zoxide jump (zi) + # Ctrl+J: zoxide jump _dots_zoxide_widget() { local result result="$(zoxide query -i -- 2>&1)" && cd "$result" @@ -81,6 +83,104 @@ _dots_load_keybindings() { } zle -N _dots_zoxide_widget bindkey '^J' _dots_zoxide_widget + + # Ctrl+B: git branch checkout + _dots_git_branch_widget() { + local branch + branch="$(git branch --all --sort=-committerdate --format='%(refname:short)' 2>/dev/null \ + | fzf --preview 'git log --oneline --color -20 {}')" || { zle reset-prompt; return; } + branch="${branch#origin/}" + git checkout "$branch" 2>&1 + zle reset-prompt + } + zle -N _dots_git_branch_widget + bindkey '^B' _dots_git_branch_widget + + # Ctrl+E: edit file + _dots_edit_widget() { + local file + file="$(fzf --preview 'head -100 {}')" || { zle reset-prompt; return; } + ${EDITOR:-vim} "$file" /dev/null + awk '{print $1}' ~/.ssh/known_hosts 2>/dev/null | tr ',' '\n' | sed 's/\[//;s/\]:.*//' + } | awk '!seen[$0]++' + } + _dots_ssh_widget() { + local host + host="$(_dots_ssh_hosts | fzf)" || { zle reset-prompt; return; } + BUFFER="ssh $host" + zle accept-line + } + zle -N _dots_ssh_widget + bindkey '^G' _dots_ssh_widget + + # Ctrl+F: find in files + _dots_find_in_files_widget() { + local selection + selection="$(rg --color=always --line-number --no-heading '' 2>/dev/null \ + | fzf --ansi --delimiter=: \ + --preview 'head -n $((({2}+30))) {1} | tail -n 60' \ + --preview-window='right:60%')" || { zle reset-prompt; return; } + local file="${selection%%:*}" + local line="${${selection#*:}%%:*}" + ${EDITOR:-vim} "+$line" "$file" /dev/null ;; + pr) gh pr view --web 2>/dev/null ;; + issues) gh browse --issues 2>/dev/null ;; + actions) gh browse --actions 2>/dev/null ;; + esac + zle reset-prompt + } + zle -N _dots_open_widget + bindkey '^O' _dots_open_widget + + # Ctrl+P: project switch + _dots_project_widget() { + local result + result="$(zoxide query -l 2>/dev/null | grep "${WORKSPACE:-$HOME/Workspace}" \ + | fzf --preview 'ls -1 {}')" && cd "$result" + zle reset-prompt + } + zle -N _dots_project_widget + bindkey '^P' _dots_project_widget } _dots_load_keybindings @@ -89,11 +189,10 @@ _dots_load_fzf() { export FZF_DEFAULT_COMMAND='rg --files --hidden --glob "!.git"' export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" export FZF_DEFAULT_OPTS='--layout=reverse --height=40% --prompt="> " --info=inline-right --no-separator' - # Modern fzf (v0.48+) provides --zsh + # fzf --zsh requires v0.48+ if fzf --zsh &>/dev/null; then source <(fzf --zsh) else - # Fallback paths for shell integration local -a fzf_paths=( "${HOMEBREW_PREFIX:-/opt/homebrew}/opt/fzf/shell" "/usr/share/fzf" diff --git a/script/install.d/24-tmux.sh b/script/install.d/24-tmux.sh new file mode 100644 index 0000000..dc1e1d8 --- /dev/null +++ b/script/install.d/24-tmux.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------- +# Description: +# Install and configure tmux. +# + +if ! command -v tmux &> /dev/null; then + case "$DOTS_PKG" in + apt) + sudo apt-get install -qq tmux + ;; + pacman) + sudo pacman -S --noconfirm tmux + ;; + brew) + brew install tmux + ;; + *) + log_warn "Skipping tmux install: no supported package manager found" + return 0 + ;; + esac +fi + +tmux -V diff --git a/tests/test_binaries.py b/tests/test_binaries.py index 11fb01b..b74736a 100644 --- a/tests/test_binaries.py +++ b/tests/test_binaries.py @@ -69,6 +69,7 @@ binaries: List[Text] = [ "fzf", "zoxide", "rg", + "tmux", ] binaries = [binary for binary in binaries if binary is not None]