From: Gabriel Dunne Date: Sat, 19 Sep 2026 19:36:13 +0000 (-0700) Subject: Update dotfiles X-Git-Url: https://git.quilime.com/?a=commitdiff_plain;h=e3f5e41884963f4c705e2f3572d779fdd87d91a9;p=cfg.git Update dotfiles --- diff --git a/.Xresources b/.Xresources deleted file mode 100644 index 118b0a2..0000000 --- a/.Xresources +++ /dev/null @@ -1,238 +0,0 @@ -Xft.dpi: 96 -Xft.antialias: true -Xft.hinting: true -Xft.rgba: rgb -Xft.autohint: false -Xft.hintstyle: hintslight -Xft.lcdfilter: lcddefault - -XTerm*background: #222D31 -XTerm*foreground: #d8d8d8 -XTerm*pointerColor: #1ABB9B -XTerm*faceName: Fixed -XTerm*faceSize: 11 -XTerm*reverseVideo: on -XTerm*selectToClipboard: true - -*background: #222D31 -*foreground: #d8d8d8 -*fading: 8 -*fadeColor: black -*cursorColor: #1ABB9B -*pointerColorBackground: #2B2C2B -*pointerColorForeground: #16A085 - -!! black dark/light -*color0: #222D31 -*color8: #585858 - -!! red dark/light -*color1: #ab4642 -*color9: #ab4642 - -!! green dark/light -*color2: #7E807E -*color10: #8D8F8D - -!! yellow dark/light -*color3: #f7ca88 -*color11: #f7ca88 - -!! blue dark/light -*color4: #7cafc2 -*color12: #7cafc2 - -!! magenta dark/light -*color5: #ba8baf -*color13: #ba8baf - -!! cyan dark/light -*color6: #1ABB9B -*color14: #1ABB9B - -!! white dark/light -*color7: #d8d8d8 -*color15: #f8f8f8 - -Xcursor.theme: xcursor-breeze -Xcursor.size: 0 - -URxvt*font: xft:DejaVuSansMono:size=10 -! URxvt.font: xft:TerminessTTFNerdFontMono:size:11 -! URxvt.font: 9x15,xft:TerminessTTFNerdFontMono - -! alternative font settings with 'terminus': -! URxvt.font: -xos4-terminus-medium-r-normal--16-160-72-72-c-80-iso10646-1 -! URxvt.bold.font: -xos4-terminus-bold-r-normal--16-160-72-72-c-80-iso10646-1 -!! terminus names see end of file! - -URxvt.depth: 32 -URxvt.background: [100]#222D31 -URxvt*scrollBar: false -URxvt*mouseWheelScrollPage: false -URxvt*cursorBlink: true -URxvt*background: black -URxvt*foreground: grey -URxvt*saveLines: 5000 - -! for 'fake' transparency (without Compton) uncomment the following three lines -! URxvt*inheritPixmap: true -! URxvt*transparent: true -! URxvt*shading: 138 - -! Normal copy-paste keybindings without perls -URxvt.iso14755: false -URxvt.keysym.Shift-Control-V: eval:paste_clipboard -URxvt.keysym.Shift-Control-C: eval:selection_to_clipboard -!Xterm escape codes, word by word movement -URxvt.keysym.Control-Left: \033[1;5D -URxvt.keysym.Shift-Control-Left: \033[1;6D -URxvt.keysym.Control-Right: \033[1;5C -URxvt.keysym.Shift-Control-Right: \033[1;6C -URxvt.keysym.Control-Up: \033[1;5A -URxvt.keysym.Shift-Control-Up: \033[1;6A -URxvt.keysym.Control-Down: \033[1;5B -URxvt.keysym.Shift-Control-Down: \033[1;6B -! Rxvt.perl-ext-common: ...,clipboard -! URxvt.keysym.M-C-c: perl:clipboard:copy -! URxvt.keysym.M-v: perl:clipboard:paste -! URxvt.keysym.M-C-v: perl:clipboard:paste_escaped -! URxvt*termName: string -! URxvt*geometry: geometry -! URxvt*chdir: string -! URxvt*loginShell: boolean -! URxvt*multiClickTime: number -! URxvt*jumpScroll: boolean -! URxvt*skipScroll: boolean -! URxvt*pastableTabs: boolean -! URxvt*scrollstyle: plain -! URxvt*scrollBar_right: boolean -! URxvt*scrollBar_floating: true -! URxvt*scrollBar_align: mode -! URxvt*thickness: number -! URxvt*scrollTtyOutput: boolean -! URxvt*scrollTtyKeypress: boolean -! URxvt*scrollWithBuffer: boolean -! URxvt*tintColor: !7DA55 -! URxvt*blurRadius: HxV -! URxvt*fading: number -! URxvt*fadeColor: color -! URxvt*utmpInhibit: boolean -! URxvt*urgentOnBell: boolean -! URxvt*visualBell: boolean -! URxvt*mapAlert: boolean -! URxvt*meta8: boolean -! URxvt*tripleclickwords: boolean -! URxvt*insecure: boolean -! URxvt*cursorUnderline: boolean -! URxvt*pointerBlank: boolean -! URxvt*color0: color -! URxvt*color1: color -! URxvt*color2: color -! URxvt*color3: color -! URxvt*color4: color -! URxvt*color5: color -! URxvt*color6: color -! URxvt*color7: color -! URxvt*color8: color -! URxvt*color9: color -! URxvt*color10: color -! URxvt*color11: color -! URxvt*color12: color -! URxvt*color13: color -! URxvt*color14: color -! URxvt*color15: color -! URxvt*colorBD: color -! URxvt*colorIT: color -! URxvt*colorUL: color -! URxvt*colorRV: color -! URxvt*underlineColor: color -! URxvt*scrollColor: color -! URxvt*troughColor: color -! URxvt*highlightColor: color -! URxvt*highlightTextColor: color -! URxvt*cursorColor: color -! URxvt*cursorColor2: color -! URxvt*pointerColor: color -! URxvt*pointerColor2: color -! URxvt*borderColor: color -! URxvt*iconFile: file -! URxvt*font: fontname -! URxvt*boldFont: fontname -! URxvt*italicFont: fontname -! URxvt*boldItalicFont: fontname -! URxvt*intensityStyles: boolean -! URxvt*inputMethod: name -! URxvt*preeditType: style -! URxvt*imLocale: string -! URxvt*imFont: fontname -! URxvt*title: string -! URxvt*iconName: string -! URxvt*buffered: boolean -! URxvt*depth: number -! URxvt*visual: number -! URxvt*transient-for: windowid -! URxvt*override-redirect: boolean -! URxvt*hold: boolean -! URxvt*externalBorder: number -! URxvt*internalBorder: number -! URxvt*borderLess: true -! URxvt*lineSpace: number -! URxvt*letterSpace: number -! URxvt*skipBuiltinGlyphs: boolean -! URxvt*pointerBlankDelay: number -! URxvt*backspacekey: string -! URxvt*deletekey: string -! URxvt*print-pipe: string -! URxvt*modifier: modifier -! URxvt*cutchars: string -! URxvt*answerbackString: string -! URxvt*secondaryScreen: boolean -! URxvt*secondaryScroll: boolean -! URxvt*perl-lib: string -! URxvt*perl-eval: perl-eval -! URxvt*perl-ext-common: string -! URxvt*perl-ext: string -! URxvt*iso14755: boolean -! URxvt*iso14755_52: boolean -! URxvt*xrm: string -! URxvt*keysym.sym: keysym -! URxvt*background.border: boolean -! URxvt*background.expr: string -! URxvt*background.interval: seconds -! URxvt*bell-command: string -! URxvt*kuake.hotkey: string -! URxvt*matcher.button: string -! URxvt*matcher.launcher: string -! URxvt*matcher.launcher.*: string -! URxvt*matcher.pattern.*: string -! URxvt*matcher.rend.*: string -! URxvt*remote-clipboard.fetch: string -! URxvt*remote-clipboard.store: string -! URxvt*searchable-scrollback: string -! URxvt*selection-autotransform.*: string -! URxvt*selection-pastebin.cmd: string -! URxvt*selection-pastebin.url: string -! URxvt*selection.pattern-0: string -! URxvt*tab-bg: colour -! URxvt*tab-fg: colour -! URxvt*tabbar-bg: colour -! URxvt*tabbar-fg: colour -! URxvt*url-launcher: string - -! The Terminus font uses the following X-names: -! -xos4-terminus-medium-r-normal--12-120-72-72-c-60-iso10646-1 -! -xos4-terminus-medium-r-normal--14-140-72-72-c-80-iso10646-1 -! -xos4-terminus-medium-r-normal--16-160-72-72-c-80-iso10646-1 -! -xos4-terminus-medium-r-normal--20-200-72-72-c-100-iso10646-1 -! -xos4-terminus-medium-r-normal--22-220-72-72-c-110-iso10646-1 -! -xos4-terminus-medium-r-normal--24-240-72-72-c-120-iso10646-1 -! -xos4-terminus-medium-r-normal--28-280-72-72-c-140-iso10646-1 -! -xos4-terminus-medium-r-normal--32-320-72-72-c-160-iso10646-1 -! -xos4-terminus-bold-r-normal--12-120-72-72-c-60-iso10646-1 -! -xos4-terminus-bold-r-normal--14-140-72-72-c-80-iso10646-1 -! -xos4-terminus-bold-r-normal--16-160-72-72-c-80-iso10646-1 -! -xos4-terminus-bold-r-normal--20-200-72-72-c-100-iso10646-1 -! -xos4-terminus-bold-r-normal--24-240-72-72-c-120-iso10646-1 -! -xos4-terminus-bold-r-normal--28-280-72-72-c-140-iso10646-1 -! -xos4-terminus-bold-r-normal--32-320-72-72-c-160-iso10646-1 diff --git a/.tmux.conf b/.tmux.conf index f7f1f36..9126e9d 100644 --- a/.tmux.conf +++ b/.tmux.conf @@ -6,6 +6,9 @@ set -g mouse on # Allows for faster key repetition set -s escape-time 0 +# multi-line shift+enter +bind -n S-Enter send-keys C-j + # bind new keys for splits bind | split-window -hc "#{pane_current_path}" bind - split-window -vc "#{pane_current_path}" @@ -16,6 +19,11 @@ bind r source-file ~/.tmux.conf \; display "Reloaded .tmux.conf" # constrain window size to the maximum size of any client connected to that window setw -g aggressive-resize on +# Start window and pane numbering at 1. +set -g base-index 1 +setw -g pane-base-index 1 +set -g renumber-windows on + # Set status bar set -g status-bg colour0 set -g status-fg white @@ -24,3 +32,29 @@ set -g status-right-length 40 set -g status-left "#[bg=colour0]#[fg=colour4] #h [#[fg=colour15]#I#[fg=colour4]] " # set -g status-right "#[bg=colour33]#[fg=colour255]#[fg=colour232] #[fg=colour255]#(uptime | awk {'print $8, $9, $10'})#[fg=colour232] #[bg=colour232] #[bg=colour33] #[fg=colour255]#(date '+%H:%M %d-%m-%Y')#[fg=colour232] " set -g status-right "#[bg=colour0]#[fg=colour255]#[fg=colour232]#[bg=colour0]#[fg=colour149]#(date '+%Y-%m-%d %a %H:%M')" + +# Highlight the active tab and show activity/bell notifications. +set -g status-justify left +setw -g window-status-separator '' +setw -g window-status-format '#[fg=colour244,bg=colour0] #I #[fg=colour250]#W #[fg=colour238]|' +setw -g window-status-current-format '#[fg=colour232,bg=colour39,bold] #I #[fg=colour255]#W #[fg=colour39] ' +setw -g window-status-last-style 'fg=colour0,bg=colour245' +setw -g window-status-activity-style 'fg=colour232,bg=colour214,bold' +setw -g window-status-bell-style 'fg=colour255,bg=colour160,bold' + +# Switch tabs and panes with Ctrl+Shift+Arrow. +bind -n C-S-Left previous-window +bind -n C-S-Right next-window +bind -n C-S-Up select-pane -U +bind -n C-S-Down select-pane -D + +# Jump directly to tabs 1–9. +bind -n C-1 select-window -t 1 +bind -n C-2 select-window -t 2 +bind -n C-3 select-window -t 3 +bind -n C-4 select-window -t 4 +bind -n C-5 select-window -t 5 +bind -n C-6 select-window -t 6 +bind -n C-7 select-window -t 7 +bind -n C-8 select-window -t 8 +bind -n C-9 select-window -t 9 diff --git a/.zsh-config b/.zsh-config index d1513c2..d1e6bfb 100644 --- a/.zsh-config +++ b/.zsh-config @@ -78,9 +78,9 @@ export LESS=-r ## Plugins section: Enable fish style features # Use syntax highlighting -source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh +source /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh # Use history substring search -source /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh +source /opt/homebrew/share/zsh-history-substring-search/zsh-history-substring-search.zsh # bind UP and DOWN arrow keys to history substring search zmodload zsh/terminfo bindkey "$terminfo[kcuu1]" history-substring-search-up diff --git a/.zsh-darwin b/.zsh-darwin index 8b1b63a..0b133bb 100644 --- a/.zsh-darwin +++ b/.zsh-darwin @@ -14,6 +14,31 @@ zstyle ':vcs_info:git:*' formats '%b' PROMPT='%F{cyan}%n@%m%f %F{240}%1~ %f%# ' RPROMPT='%F{green}${vcs_info_msg_0_} %F{240}%*%f' + + +# ZSH completion +if type brew &>/dev/null; then + FPATH="$(brew --prefix)/share/zsh/site-functions:${FPATH}" +fi +autoload -Uz compinit +# only rebuild the completion dump once a day +if [[ -n ~/.zcompdump(#qN.mh+24) ]]; then compinit; else compinit -C; fi + +export LS_COLORS='di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43' + +zstyle ':completion:*' menu select +zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=*' 'l:|=* r:|=*' +zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" +zstyle ':completion:*' group-name '' +zstyle ':completion:*:descriptions' format '%F{yellow}-- %d --%f' +zstyle ':completion:*:messages' format '%F{purple}-- %d --%f' +zstyle ':completion:*:warnings' format '%F{red}-- no matches --%f' +zstyle ':completion:*' verbose yes +zstyle ':completion:*' use-cache on +zstyle ':completion:*' cache-path ~/.zsh/cache +setopt complete_in_word auto_menu + + # aliases alias ls='ls -G' alias cats='highlight -O ansi --force' @@ -23,6 +48,12 @@ alias hf='defaults write com.apple.finder AppleShowAllFiles false; killall Finde alias shf='defaults write com.apple.finder AppleShowAllFiles true; killall Finder' # show hidden # alias cat='bat' alias grep='grep --color' +alias ff='fastfetch' +#alias ncdu='ncdu --color=dark' + +# scripts +export PATH="/Users/gld/bin:$PATH" + # Lima alias archlinux='limactl shell archlinux' @@ -52,13 +83,16 @@ export NVM_DIR="$HOME/.nvm" export PATH="/etc/paths.d/TeX:/Library/TeX/texbin:$PATH" # ruby -#export PATH="/Users/gld/.gem/ruby/3.1.1/bin:/opt/homebrew/opt/ruby/bin:/opt/homebrew/lib/ruby/gems/3.1.1/bin:$PATH" -export PATH="/Users/gld/.gem/ruby/3.1.0/bin:/opt/homebrew/opt/ruby/bin:/opt/homebrew/lib/ruby/gems/3.1.0/bin:$PATH" -source /opt/homebrew/opt/chruby/share/chruby/chruby.sh -source /opt/homebrew/opt/chruby/share/chruby/auto.sh -chruby ruby-3.1.2 # run chruby to see actual version +export PATH="/Users/gld/.gem/ruby/3.1.1/bin:/opt/homebrew/opt/ruby/bin:/opt/homebrew/lib/ruby/gems/3.1.1/bin:$PATH" +#export PATH="/Users/gld/.gem/ruby/3.1.0/bin:/opt/homebrew/opt/ruby/bin:/opt/homebrew/lib/ruby/gems/3.1.0/bin:$PATH" source /opt/homebrew/opt/chruby/share/chruby/chruby.sh source /opt/homebrew/opt/chruby/share/chruby/auto.sh +# chruby ruby-3.1.2 # run chruby to see actual version +#source /opt/homebrew/opt/chruby/share/chruby/chruby.sh +#source /opt/homebrew/opt/chruby/share/chruby/auto.sh + +# mysql +export PATH="/opt/homebrew/opt/mysql/bin:$PATH" # dotfiles alias cfg='/opt/homebrew/bin/git --git-dir=/Users/gld/.cfg/ --work-tree=/Users/gld' @@ -66,7 +100,14 @@ alias cfg='/opt/homebrew/bin/git --git-dir=/Users/gld/.cfg/ --work-tree=/Users/g # more path export PATH="$HOME/.local/bin:$PATH" +# fzf and fuzzy tab completion +source <(fzf --zsh) +source "$(brew --prefix)/opt/fzf-tab/share/fzf-tab/fzf-tab.zsh" +zstyle ':fzf-tab:*' fzf-flags --height=40% +# plugins -- syntax highlighting must stay last +source "$(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh" +source "$(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" diff --git a/.zsh-prompt b/.zsh-prompt index 00b1b42..4c6f146 100644 --- a/.zsh-prompt +++ b/.zsh-prompt @@ -99,14 +99,14 @@ case $(basename "$(cat "/proc/$PPID/comm")") in # #BASE16_SHELL="/usr/share/zsh/scripts/base16-shell/base16-$theme.$shade.sh" # #[[ -s $BASE16_SHELL ]] && source $BASE16_SHELL # # Use autosuggestion -# source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh +# source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh # ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=20 # ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8' # ;; *) RPROMPT='$(git_prompt_string)' # Use autosuggestion - source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh + source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=20 ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8' ;; diff --git a/.zsh/cd-smart.README.md b/.zsh/cd-smart.README.md new file mode 100644 index 0000000..d931a65 --- /dev/null +++ b/.zsh/cd-smart.README.md @@ -0,0 +1,125 @@ +# cd-smart + +A `cd` that learns where you go. About 250 lines of zsh, one plain-text +database, no external processes on the `cd` path, and no dependencies beyond +zsh itself (fzf is used by `cdi` when present). + +## Install + +```zsh +# ~/.zshrc, after compinit +source ~/.zsh/cd-smart.zsh +``` + +Files live next to the script: `cd-smart.tsv` (learned directories) and +`cd-smart.aliases` (shortcuts defined at the prompt). + +## Use + +```zsh +cd ~/Projects/notes # ordinary cd; the directory is learned +cd agents # jump to the best learned match for "agents" +cd open ag # words must appear in order: .../Projects/notes +cd ag # normal completion plus learned matches and shortcuts +cdi ag # pick from matches in fzf; ctrl-/ toggles a preview +cdi # browse everything learned +``` + +Precedence inside the wrapper, first match wins: + +1. No argument, or anything with options, slashes, or a leading `.` or `~`, + goes straight to the builtin. `cd ..`, `cd -`, `cd ~/x` are untouched. +2. A single word that exists as a directory here. +3. A single word that is a shortcut. +4. Two words that work as the builtin's `cd old new` substitution. +5. The best-scored learned directory containing the words in order. +6. The builtin's normal error. + +### Shortcuts + +```zsh +cdalias osa ~/Projects/notes # "cd osa" always goes there +cdalias dl # current directory becomes "dl" +cdalias # list +cdunalias dl # remove +``` + +Shortcuts can also be declared in `.zshrc` before sourcing, and those win over +saved ones: + +```zsh +typeset -gA CDS_ALIASES=(osa ~/Projects/notes) +``` + +### Maintenance + +```zsh +cdlearn PATH # learn a directory without visiting it +cdforget PATH # remove one +cdlist # learned directories with scores +cdsmart-help # short reminder of all of this +``` + +### Configuration + +Set before sourcing. + +| variable | default | meaning | +|---------------|------------------------------|-------------------------------------------| +| `CDS_DB` | `cd-smart.tsv` beside script | learned-directory database | +| `CDS_ROOTS` | `$HOME` (and `/zpool` if present) | only directories under these are learned | +| `CDS_MAXAGE` | `1000` | aging threshold, see below | +| `CDS_ALIASES` | empty | shortcuts declared in `.zshrc` | + +## How ranking works + +Each learned directory has a score and a last-visit time. A visit adds 1 to +the score. At lookup time a candidate's rank is its score plus a bonus for an +exact basename match (+100) or basename prefix (+50) on the last query word, +plus a recency bonus (20 within an hour, 15 within a day, 10 within a week, +5 within a month). + +Growth is bounded by aging. When the sum of all scores exceeds `CDS_MAXAGE`, +every score is multiplied by `0.9 × CDS_MAXAGE / total`, entries that fall +below 1 are dropped, and directories that no longer exist are pruned. Places +you keep returning to stay; one-off visits fade out. + +## Tests + +```zsh +zsh ~/.zsh/cd-smart.test.zsh +``` + +Runs in a throwaway sandbox and never touches your real database. + +## Compared with zoxide + +[zoxide](https://github.com/ajeetdsouza/zoxide) solves the same problem and +is the obvious alternative. The ranking model here is deliberately close to +its design. The differences are mostly about scope. + +| | cd-smart | zoxide | +|---|---|---| +| Implementation | ~250 lines of zsh, one file | ~2,000 lines of Rust plus a shell template per shell | +| Shells | zsh only | bash, zsh, fish, nushell, PowerShell, and others | +| Database | plain TSV you can read and edit | binary, with file locking | +| Command | wraps `cd` itself | separate `z` and `zi`; `--cmd cd` can replace `cd` | +| Real directories vs. guesses | an existing local directory always wins, then shortcuts, then learned matches | with `--cmd cd`, an existing path also wins; otherwise `z` always guesses | +| Tab completion | learned matches and shortcuts appear inside normal `cd ` | completion on the `z` command only | +| Shortcuts | built in (`cdalias`) | none; use shell aliases | +| Where learning happens | only under `CDS_ROOTS` | everywhere, minus `_ZO_EXCLUDE_DIRS` | +| Multi-word queries | words in order anywhere in the path; last word gets basename bonuses | words in order; last word must match the last path component | +| Recency | additive bonus by age bucket | multiplier by age bucket (4×, 2×, 0.5×, 0.25×) | +| Aging | same scheme: scale by 0.9 × cap / total, drop below 1, prune dead paths | same, default cap 10,000 | +| Concurrency | last writer wins if two shells `cd` at the same instant | locked writes | +| External processes | none on `cd` or lookup | one binary launch per `cd` and per query | +| Speed at 300 entries (typical after aging) | ~3.5 ms per `cd`, ~1 ms per lookup | a few ms, dominated by the launch | +| Speed at 2,000 entries | ~17 ms per `cd`, ~7 ms per lookup | a few ms | +| Tests | 45 smoke tests in one script | full Rust test suite | + +In short: zoxide is safer under concurrent shells and portable, and stays +fast at any database size. +cd-smart is smaller, fully inspectable, restricted to the directories you +care about, and integrates with `cd` and its completion rather than adding a +second command. In practice aging keeps the database in the low hundreds of +entries, where neither tool is perceptibly slower than a plain `cd`. diff --git a/.zsh/cd-smart.tsv b/.zsh/cd-smart.tsv new file mode 100644 index 0000000..61d7670 --- /dev/null +++ b/.zsh/cd-smart.tsv @@ -0,0 +1 @@ +1.00 1789845937 /Users/gld diff --git a/.zsh/cd-smart.zsh b/.zsh/cd-smart.zsh new file mode 100644 index 0000000..0a69120 --- /dev/null +++ b/.zsh/cd-smart.zsh @@ -0,0 +1,326 @@ +# cd-smart.zsh +# +# A small, self-contained Zsh "smart cd" layer. +# +# Normal: +# cd /Users/gabrieldunne/Code +# cd ~/Downloads +# cd ../foo +# +# Smart: +# cd ph -> best learned directory matching "ph" +# cd pro notes -> words must appear in order: .../Projects/notes +# cd ph -> best learned matches +# cdi ph -> pick from matches in fzf +# +# Learning happens automatically after successful directory changes. + +[[ -o interactive ]] || return 0 + +zmodload zsh/datetime +zmodload zsh/files + +: ${CDS_DB:="${${(%):-%x}:A:h}/cd-smart.tsv"} +: ${CDS_MAXAGE:=1000} +: ${CDS_ALIAS_FILE:="${CDS_DB:h}/cd-smart.aliases"} + +(( ${+CDS_ALIASES} )) || typeset -gA CDS_ALIASES +typeset -gA _cds_zshrc_aliases=("${(@kv)CDS_ALIASES}") + +_cds_load_aliases() { + local name target + [[ -r "$CDS_ALIAS_FILE" ]] || return 0 + while IFS=$'\t' read -r name target; do + [[ -n "$name" && -n "$target" ]] || continue + (( ${+_cds_zshrc_aliases[$name]} )) || CDS_ALIASES[$name]="$target" + done < "$CDS_ALIAS_FILE" +} +_cds_load_aliases + +_cds_valid_alias_name() { + [[ -n "$1" && "$1" != -* && "$1" != */* && "$1" != .* && "$1" != '~'* && "$1" != *[[:space:]]* ]] +} + +if (( ! ${+CDS_ROOTS} )); then + typeset -ga CDS_ROOTS=("$HOME") + [[ -d /zpool ]] && CDS_ROOTS+=("/zpool") +fi +CDS_ROOTS=("${(@)CDS_ROOTS%/}") + +_cds_init() { + [[ -d "${CDS_DB:h}" ]] || zf_mkdir -p -- "${CDS_DB:h}" 2>/dev/null + [[ -f "$CDS_DB" ]] || : >| "$CDS_DB" 2>/dev/null +} + +_cds_save() { + local tmp="${CDS_DB}.tmp.$$" line dir sc + local -F total=0 factor score + local -a lines=("${(@)@:#}") scores + + scores=("${(@)lines%%$'\t'*}") + for sc in "${scores[@]}"; do (( total += ${sc:-0} )); done + + if (( total > CDS_MAXAGE )); then + (( factor = 0.9 * CDS_MAXAGE / total )) + { + for line in "${lines[@]}"; do + dir="${line##*$'\t'}" + [[ -n "$dir" && -d "$dir" ]] || continue + (( score = ${${line%%$'\t'*}:-0} * factor )) + (( score >= 1 )) || continue + printf '%.2f\t%s\n' "$score" "${line#*$'\t'}" + done + } >| "$tmp" 2>/dev/null || { zf_rm -f -- "$tmp"; return 1 } + else + { (( ${#lines} == 0 )) || print -rl -- "${lines[@]}" } >| "$tmp" 2>/dev/null || + { zf_rm -f -- "$tmp"; return 1 } + fi + + zf_mv -f -- "$tmp" "$CDS_DB" 2>/dev/null || { zf_rm -f -- "$tmp"; return 1 } +} + +_cds_allowed() { + local p="$1" root + for root in "${CDS_ROOTS[@]}"; do + [[ "$p" == "$root" || "$p" == "$root"/* ]] && return 0 + done + return 1 +} + +_cds_record() { + local p="${1:-$PWD}" i pat + local -F score + local -a lines + + _cds_allowed "$p" || return 0 + _cds_init || return 0 + lines=("${(@f)$(<"$CDS_DB")}") + lines=("${(@)lines:#}") + pat="*"$'\t'"${(b)p}" + i=${lines[(i)$pat]} + if (( i <= ${#lines} )); then + (( score = ${${lines[i]%%$'\t'*}:-0} + 1 )) + lines[i]="$(printf '%.2f' "$score")"$'\t'"$EPOCHSECONDS"$'\t'"$p" + else + lines+=("1.00"$'\t'"$EPOCHSECONDS"$'\t'"$p") + fi + _cds_save "${lines[@]}" +} + +autoload -Uz add-zsh-hook +add-zsh-hook chpwd _cds_record + +_cds_matches() { + setopt localoptions extendedglob + local now stored last dir base age recency pat w line key + local -F score + local -a words=("${(@L)@}") lines ranked + local q="${words[-1]}" + + pat='(#i)*' + for w in "${words[@]}"; do pat+="${(b)w}*"; done + + _cds_init || return 0 + now=$EPOCHSECONDS + lines=("${(@f)$(<"$CDS_DB")}") + lines=("${(@M)lines:#${~pat}}") + + for line in "${lines[@]}"; do + stored="${line%%$'\t'*}" + dir="${line##*$'\t'}" + last="${${line#*$'\t'}%%$'\t'*}" + [[ -n "$dir" && -d "$dir" ]] || continue + base="${dir:t:l}" + score=${stored:-0} + + if [[ -n "$q" && "$base" == "$q" ]]; then + (( score += 100 )) + elif [[ -n "$q" && "$base" == "$q"* ]]; then + (( score += 50 )) + fi + + age=$(( now - ${last:-now} )) + if (( age < 3600 )); then recency=20 + elif (( age < 86400 )); then recency=15 + elif (( age < 604800 )); then recency=10 + elif (( age < 2592000 )); then recency=5 + else recency=0 + fi + + (( score += recency )) + printf -v key '%012.2f' "$score" + ranked+=("$key"$'\t'"$dir") + done + + (( ${#ranked} )) && print -rl -- "${(@)${(@O)ranked}#*$'\t'}" +} + +_cds_simple_words() { + local w + (( $# )) || return 1 + for w in "$@"; do + [[ "$w" == -* || "$w" == */* || "$w" == .* || "$w" == '~'* ]] && return 1 + done + return 0 +} + +cd() { + local -a matches + (( $# == 0 )) && { builtin cd; return; } + + if (( $# > 1 )) && [[ "${@[-1]}" == /* && -d "${@[-1]}" ]]; then + builtin cd -- "${@[-1]}" + return + fi + + if ! _cds_simple_words "$@"; then + builtin cd "$@" + return + fi + + if (( $# == 1 )) && [[ -d "$1" ]]; then + builtin cd -- "$1" + return + fi + + if (( $# == 1 && ${+CDS_ALIASES[$1]} )); then + local target="${CDS_ALIASES[$1]}" + [[ -d "$target" ]] || { print -u2 "cd-smart: alias $1 -> $target: not a directory"; return 1; } + builtin cd -- "$target" + return + fi + + if (( $# == 2 )) && builtin cd "$@" 2>/dev/null; then + return + fi + + matches=(${(f)"$(_cds_matches "$@")"}) + if (( ${#matches} )); then + builtin cd -- "${matches[1]}" + return + fi + + if (( $# > 1 )); then + print -u2 "cd-smart: no learned match for ${(q)*}" + return 1 + fi + builtin cd "$@" +} + +_cds_cd_complete() { + (( $+functions[_cd] )) && _cd "$@" + local -a query learned + query=("${(@)words[2,CURRENT]}") + _cds_simple_words "${query[@]}" || return 0 + + if (( CURRENT == 2 && ${#CDS_ALIASES} )); then + local name + local -a names descs + for name in "${(@ko)CDS_ALIASES}"; do + names+=("$name") + descs+=("$name -- ${CDS_ALIASES[$name]/#$HOME/~}") + done + compadd -J shortcuts -X 'shortcuts' -d descs -- "${names[@]}" + fi + + learned=(${(f)"$(_cds_matches "${query[@]}")"}) + (( ${#learned} )) || return 0 + compadd -U -Q -J learned -X 'learned directories' -- "${learned[@]}" +} + +compdef _cds_cd_complete cd 2>/dev/null + +cdi() { + local q="$*" choice + local -a paths + paths=(${(f)"$(_cds_matches "$@")"}) + if (( ! ${#paths[@]} )); then + print -u2 "cd-smart: no learned match for ${(q)q}" + return 1 + fi + + if (( $+commands[fzf] )); then + choice="$(print -rl -- "${paths[@]}" | fzf --height=~40% --layout=reverse --no-sort --select-1 --exit-0 --prompt='cd › ' --query="$q" --preview='CLICOLOR_FORCE=1 ls -1G {}' --preview-window=right:50%:hidden --bind=ctrl-/:toggle-preview)" || return 1 + elif (( ${#paths[@]} == 1 )); then + choice="${paths[1]}" + else + print + select choice in "${paths[@]}" "cancel"; do + [[ "$choice" == cancel || -z "$choice" ]] && return 1 + break + done + fi + + [[ -n "$choice" ]] && builtin cd -- "$choice" +} + +cdlearn() { + local p + (( $# == 1 )) || { print -u2 "usage: cdlearn PATH"; return 2; } + p="${1:A}" + [[ -d "$p" ]] || { print -u2 "cd-smart: not a directory: $1"; return 1; } + _cds_allowed "$p" || { print -u2 "cd-smart: outside CDS_ROOTS, not learned: $p"; return 1; } + _cds_record "$p" +} + +cdforget() { + local target="${1:A}" score last dir + local -a lines + (( $# == 1 )) || { print -u2 "usage: cdforget PATH"; return 2; } + _cds_init || return 1 + while IFS=$'\t' read -r score last dir; do + [[ -n "$dir" && "$dir" != "$target" ]] || continue + lines+=("$score"$'\t'"$last"$'\t'"$dir") + done < "$CDS_DB" + _cds_save "${lines[@]}" +} + +cdlist() { + local score last dir + _cds_init || return 1 + while IFS=$'\t' read -r score last dir; do + [[ -n "$dir" ]] && printf '%9.2f %s\n' "$score" "$dir" + done < "$CDS_DB" | sort -rn +} + +_cds_save_aliases() { + local name tmp="${CDS_ALIAS_FILE}.tmp.$$" + { + for name in "${(@ko)CDS_ALIASES}"; do + (( ${+_cds_zshrc_aliases[$name]} )) && continue + print -r -- "$name"$'\t'"${CDS_ALIASES[$name]}" + done + } >| "$tmp" 2>/dev/null || { zf_rm -f -- "$tmp"; return 1; } + zf_mv -f -- "$tmp" "$CDS_ALIAS_FILE" 2>/dev/null || { zf_rm -f -- "$tmp"; return 1; } +} + +cdalias() { + local name="$1" target="${2:-$PWD}" + if (( $# == 0 )); then + for name in "${(@ko)CDS_ALIASES}"; do + printf '%-12s %s\n' "$name" "${CDS_ALIASES[$name]/#$HOME/~}" + done + return 0 + fi + (( $# <= 2 )) || { print -u2 "usage: cdalias [NAME [PATH]]"; return 2; } + _cds_valid_alias_name "$name" || { print -u2 "cd-smart: invalid alias name: ${(q)name}"; return 2; } + target="${target:A}" + [[ -d "$target" ]] || { print -u2 "cd-smart: not a directory: $2"; return 1; } + (( ${+_cds_zshrc_aliases[$name]} )) && { print -u2 "cd-smart: $name is defined in your zshrc; edit it there"; return 1; } + CDS_ALIASES[$name]="$target" + _cds_save_aliases +} + +cdunalias() { + local name="$1" + (( $# == 1 )) || { print -u2 "usage: cdunalias NAME"; return 2; } + (( ${+CDS_ALIASES[$name]} )) || { print -u2 "cd-smart: no such alias: $name"; return 1; } + (( ${+_cds_zshrc_aliases[$name]} )) && { print -u2 "cd-smart: $name is defined in your zshrc; remove it there"; return 1; } + unset "CDS_ALIASES[$name]" + _cds_save_aliases +} + +cdsmart-help() { + print 'cd-smart: cd learns visited directories; cdi searches them with fzf.' + print 'Commands: cdlearn PATH, cdforget PATH, cdlist, cdalias [NAME [PATH]], cdunalias NAME' +} diff --git a/.zsh/common.zsh b/.zsh/common.zsh new file mode 100644 index 0000000..7295d54 --- /dev/null +++ b/.zsh/common.zsh @@ -0,0 +1,55 @@ +# Shared Zsh settings + +setopt correct extendedglob nocaseglob rcexpandparam nocheckjobs +setopt numericglobsort nobeep appendhistory histignorealldups autocd prompt_subst + +HISTFILE="$HOME/.zsh_history" +HISTSIZE=100000 +SAVEHIST=100000 +setopt share_history hist_ignore_all_dups hist_ignore_space hist_reduce_blanks extended_history + +export LS_COLORS='di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43' + +# Completion +if command -v brew >/dev/null 2>&1; then + FPATH="$(brew --prefix)/share/zsh/site-functions:${FPATH}" +fi +autoload -Uz compinit +if [[ -n ~/.zcompdump(#qN.mh+24) ]]; then compinit; else compinit -C; fi +zstyle ':completion:*' menu no +zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=*' 'l:|=* r:|=*' +zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" +zstyle ':completion:*' group-name '' +zstyle ':completion:*:descriptions' format '[%d]' +zstyle ':completion:*:messages' format '[%d]' +zstyle ':completion:*:warnings' format '[no matches]' +zstyle ':completion:*' verbose yes use-cache on +zstyle ':completion:*' cache-path "$HOME/.zsh/cache" +setopt complete_in_word auto_menu + +# Prompt +autoload -Uz vcs_info +precmd_vcs_info() { vcs_info } +precmd_functions+=(precmd_vcs_info) +zstyle ':vcs_info:git:*' formats '%b' +PROMPT='%F{cyan}%n@%m%f %F{240}%1~%f %F{cyan}❯%f ' +RPROMPT='%F{green}${vcs_info_msg_0_} %F{240}%*%f' + +bindkey -e +WORDCHARS=${WORDCHARS//\/[&.;]} + +# Smart cd; its database remains local to this machine. +export CDS_DB="$HOME/.local/state/cd-smart.tsv" +source "$HOME/.zsh/cd-smart.zsh" + +alias grep='grep --color' +alias cfg='git --git-dir="$HOME/.cfg" --work-tree="$HOME"' +export PATH="$HOME/bin:$HOME/.local/bin:$PATH" + +# fzf, when installed +if command -v fzf >/dev/null 2>&1; then + export FZF_CTRL_R_OPTS="--height=~15 --layout=reverse --no-info --no-separator --no-scrollbar --prompt='history › ' --scheme=history --with-nth 2.." + source <(fzf --zsh) +fi + +# fzf-tab and optional plugins are loaded by the platform file when available. diff --git a/.zsh/fzf-tab.zsh b/.zsh/fzf-tab.zsh new file mode 100644 index 0000000..b77e1d0 --- /dev/null +++ b/.zsh/fzf-tab.zsh @@ -0,0 +1,29 @@ +# fzf-tab: fuzzy tab completion; preview hidden by default, Ctrl-/ toggles it +zstyle ':fzf-tab:*' fzf-flags --height=50% --preview-window=right:55%:wrap:hidden --bind=ctrl-/:toggle-preview +zstyle ':fzf-tab:*' switch-group '<' '>' + +# Preview commands run in a separate shell. Quote the fzf-tab variables there, +# rather than expanding them while this configuration is sourced. +zstyle ':fzf-tab:complete:cd:*' fzf-preview 'CLICOLOR_FORCE=1 ls -1G -- "$realpath"' +zstyle ':fzf-tab:complete:(cat|bat|less|vim|subl|code|cp|mv|rm|open):*' fzf-preview \ + 'if [[ -d "$realpath" ]]; then + CLICOLOR_FORCE=1 ls -1G -- "$realpath" + elif grep -qI . -- "$realpath" 2>/dev/null; then + bat --color=always --style=numbers --line-range=:200 -- "$realpath" + else + file -b -- "$realpath" + fi' +zstyle ':fzf-tab:complete:git-(add|diff|restore):*' fzf-preview 'git diff --color=always -- $word | head -200' +zstyle ':fzf-tab:complete:git-(checkout|switch|merge|rebase|log):*' fzf-preview 'git log --oneline --color=always -20 $word 2>/dev/null' +zstyle ':fzf-tab:complete:(-parameter-|-brace-parameter-|export|unset):*' fzf-preview 'echo ${(P)word}' +unset _ls + +# Complete full Git file paths instead of one directory at a time. +autoload -Uz +X _multi_parts +functions -c _multi_parts _multi_parts_orig +_multi_parts() { + [[ $curcontext == *:git-* ]] || { _multi_parts_orig "$@"; return; } + local -a opts + while [[ $1 != - ]]; do opts+=("$1"); shift; done + compadd "${opts[@]}" -- "${(@P)3}" +} diff --git a/.zsh/linux.zsh b/.zsh/linux.zsh new file mode 100644 index 0000000..aa2186d --- /dev/null +++ b/.zsh/linux.zsh @@ -0,0 +1,19 @@ +# Linux-specific Zsh settings + +if command -v ls >/dev/null 2>&1; then + alias ls='ls --color=auto' +fi + +if command -v fzf-tab >/dev/null 2>&1; then + source "$(dirname "$(command -v fzf-tab)")/../share/fzf-tab/fzf-tab.zsh" 2>/dev/null +fi + +[[ -r "$HOME/.zsh/fzf-tab.zsh" ]] && source "$HOME/.zsh/fzf-tab.zsh" + +for plugin in \ + "$HOME/.local/share/zsh-autosuggestions/zsh-autosuggestions.zsh" \ + /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh \ + "$HOME/.local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" \ + /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh; do + [[ -r "$plugin" ]] && source "$plugin" +done diff --git a/.zsh/local.zsh.example b/.zsh/local.zsh.example new file mode 100644 index 0000000..10c5c1c --- /dev/null +++ b/.zsh/local.zsh.example @@ -0,0 +1,10 @@ +# Copy to ~/.zsh/local.zsh for machine- or work-specific settings. +# Keep this file out of shared/public dotfiles. + +# Examples: +# AWS configuration is usually best managed in ~/.aws/config. +# If a machine-wide default is needed, set it here with generic values: +# export AWS_PROFILE=your-profile +# export AWS_REGION=your-region +# +# Put any other machine-specific paths or environment variables here. diff --git a/.zsh/macos.zsh b/.zsh/macos.zsh new file mode 100644 index 0000000..6361021 --- /dev/null +++ b/.zsh/macos.zsh @@ -0,0 +1,18 @@ +# macOS-specific Zsh settings + +alias ls='ls -G' +alias x86='arch -x86_64 zsh' + +if command -v brew >/dev/null 2>&1; then + eval "$(brew shellenv)" + if [[ -r "$(brew --prefix)/opt/fzf-tab/share/fzf-tab/fzf-tab.zsh" ]]; then + source "$(brew --prefix)/opt/fzf-tab/share/fzf-tab/fzf-tab.zsh" + [[ -r "$HOME/.zsh/fzf-tab.zsh" ]] && source "$HOME/.zsh/fzf-tab.zsh" + fi + [[ -r "$(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh" ]] && \ + source "$(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh" + [[ -r "$(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" ]] && \ + source "$(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" +fi + +[[ -r "$HOME/.docker/init-zsh.sh" ]] && source "$HOME/.docker/init-zsh.sh" diff --git a/.zshrc b/.zshrc index 9fcca49..63fc81e 100644 --- a/.zshrc +++ b/.zshrc @@ -1,20 +1,11 @@ -if [[ "$OSTYPE" == "darwin"* ]]; then +# Shared entry point +source "$HOME/.zsh/common.zsh" - # OSX - if [[ -e ~/.zsh-darwin ]]; then - source ~/.zsh-darwin - fi +if [[ "$OSTYPE" == darwin* ]]; then + [[ -r "$HOME/.zsh/macos.zsh" ]] && source "$HOME/.zsh/macos.zsh" +elif [[ "$OSTYPE" == linux* ]]; then + [[ -r "$HOME/.zsh/linux.zsh" ]] && source "$HOME/.zsh/linux.zsh" +fi -else - - # Linux or other - if [[ -e ~/.zsh-config ]]; then - source ~/.zsh-config - fi - - # Use zsh prompt - if [[ -e ~/.zsh-prompt ]]; then - source ~/.zsh-prompt - fi - -fi \ No newline at end of file +# Optional machine- or work-specific settings. +[[ -r "$HOME/.zsh/local.zsh" ]] && source "$HOME/.zsh/local.zsh"