├── .gitignore ├── LICENSE.txt ├── README.md ├── bin ├── find_project_root.sh ├── git-swc ├── tmux_attach.sh ├── tmux_update_window_name.sh ├── vim └── yank ├── docker ├── Dockerfile └── build.sh ├── ghostty ├── config └── themes │ ├── onedarkpro_onelight │ └── onedarkpro_snazzy ├── git ├── config ├── delta.txt ├── ignore └── signoff-aliases.txt ├── hammerspoon ├── Spoons │ └── EmmyLua.spoon │ │ ├── docs.json │ │ └── init.lua ├── browser_selector.lua ├── double_cmdq_to_quit.lua ├── init.lua ├── labels.lua ├── leader_key.lua ├── modifier_key_monitor.lua ├── utils.lua └── vim_mode.lua ├── lsd ├── colors.yaml └── config.yaml ├── nvim ├── init.lua └── lua │ ├── core │ ├── autocmds.lua │ ├── keymaps.lua │ ├── options.lua │ └── utils.lua │ └── plugins │ ├── basic.lua │ ├── blink.lua │ ├── copilot.lua │ ├── dap.lua │ ├── editor.lua │ ├── formatting.lua │ ├── git.lua │ ├── lsp.lua │ ├── mason.lua │ ├── snacks.lua │ ├── themes.lua │ ├── treesitter.lua │ └── ui.lua ├── setup.sh ├── tmux ├── tmux.conf └── tmux.reset.conf ├── vim ├── autoload │ └── plug.vim ├── basic.vim ├── less.vim ├── plugin_configs.vim ├── plugins.vim └── vimrc ├── wezterm ├── colors │ └── onedarkpro_snazzy.toml └── wezterm.lua └── zsh ├── alias.zsh ├── completions └── _git-swc ├── functions.zsh ├── fzf-git.sh ├── my-theme.zsh-theme └── zshrc /.gitignore: -------------------------------------------------------------------------------- 1 | *.local 2 | nvim/lazy-lock.json 3 | nvim/spell 4 | hammerspoon/local.lua 5 | hammerspoon/Spoons/EmmyLua.spoon/annotations 6 | zsh/my-theme.zsh-theme.antigen-compat 7 | tmux/plugins 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 raulchen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotfiles 2 | 3 | My config files for Zsh, Tmux, Neovim/Vim, and Hammerspoon. 4 | 5 | ## Installation 6 | 7 | 1. `git clone https://github.com/raulchen/dotfiles.git ~/dotfiles`. 8 | 1. `cd ~/dotfiles && ./setup.sh`. 9 | -------------------------------------------------------------------------------- /bin/find_project_root.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to find the project root based on specific files or directories 3 | 4 | # Define the files or directories that signify the project root 5 | special_paths=(".git") 6 | 7 | # Start at the current directory 8 | current_path="$1" 9 | 10 | # Traverse up to find the project root 11 | while [[ "$current_path" != "/" ]]; do 12 | for special_path in "${special_paths[@]}"; do 13 | if [[ -e "$current_path/$special_path" ]]; then 14 | echo "$current_path" 15 | exit 0 16 | fi 17 | done 18 | # Move up one directory 19 | current_path=$(dirname "$current_path") 20 | done 21 | 22 | # If no project root is found, default to the initial directory 23 | echo "$1" 24 | -------------------------------------------------------------------------------- /bin/git-swc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git switch -c "$1" 2>/dev/null || git switch "$1" 4 | -------------------------------------------------------------------------------- /bin/tmux_attach.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Attach to a tmux session+window or create it if it doesn't exist. 3 | # Usage: tmux_attach.sh 4 | 5 | SESSION_NAME="$1" 6 | WINDOW_NAME="$2" 7 | WORKING_DIR="$3" 8 | 9 | if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then 10 | # Create session if not exists 11 | tmux new-session -s "$SESSION_NAME" -n "$WINDOW_NAME" -c "$WORKING_DIR" 12 | else 13 | # Find the first window with the given name under the session. 14 | WINDOW_ID=$(tmux list-windows -t "$SESSION_NAME" -F "#{window_id} #{window_name}" | grep "$WINDOW_NAME$" | head -n 1 | awk '{print $1}') 15 | if [ -n "$WINDOW_ID" ]; then 16 | # If the window exists, attach to it. 17 | tmux attach-session -t "$SESSION_NAME":"$WINDOW_ID" 18 | else 19 | # Otherwise, create a new window under the session and attach to it. 20 | tmux new-window -d -t "$SESSION_NAME" -n "$WINDOW_NAME" -c "$WORKING_DIR" 21 | tmux attach-session -t "$SESSION_NAME":"$WINDOW_NAME" 22 | fi 23 | fi 24 | 25 | -------------------------------------------------------------------------------- /bin/tmux_update_window_name.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Update the current tmux window name to the project root directory name. 3 | if [ -z "$TMUX" ]; then 4 | exit 0 5 | fi 6 | 7 | tmux_update_window_name() { 8 | project_root="$(find_project_root.sh "$1")" 9 | window_name="$(basename "$project_root")" 10 | tmux rename-window -t "$(tmux display-message -p '#{window_id}')" "$window_name" 11 | } 12 | 13 | pwd="$1" 14 | if [ -z "$pwd" ]; then 15 | pwd="$(pwd)" 16 | fi 17 | 18 | tmux_update_window_name "$pwd" 19 | -------------------------------------------------------------------------------- /bin/vim: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if which nvim >/dev/null 2>&1; then 3 | nvim "$@" 4 | else 5 | # Find the original vim command. 6 | _vim="" 7 | for v in $(which -a vim); do 8 | if [[ "$v" != *"dotfiles/bin/vim"* ]]; then 9 | _vim="$v" 10 | break 11 | fi 12 | done 13 | $_vim "$@" 14 | fi 15 | 16 | -------------------------------------------------------------------------------- /bin/yank: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Usage: 4 | # - yank [FILE...] 5 | # - ... | yank 6 | # 7 | # Copied from https://github.com/sunaku/home/blob/master/bin/yank 8 | 9 | input=$( cat "$@" ) 10 | input() { printf %s "$input" ;} 11 | 12 | # copy to tmux 13 | test -n "$TMUX" && 14 | command -v tmux >/dev/null && input | tmux load-buffer - 15 | 16 | # copy via X11 17 | # test -n "$DISPLAY" && { 18 | # (command -v xsel >/dev/null && input | xsel -i -b || 19 | # command -v xclip >/dev/null && input | xclip -sel c) && exit 0 20 | # } 21 | 22 | # copy via pbcopy 23 | (command -v pbcopy >/dev/null && input | pbcopy) && exit 0 24 | 25 | # copy via OSC 52 26 | printf_escape() { 27 | esc=$1 28 | test -n "$TMUX" -o -z "${TERM##screen*}" && esc="\033Ptmux;\033$esc\033\\" 29 | printf "$esc" 30 | } 31 | len=$( input | wc -c ) max=74994 32 | test $len -gt $max && echo "$0: input is $(( len - max )) bytes too long" >&2 33 | printf_escape "\033]52;c;$( input | head -c $max | base64 | tr -d '\r\n' )\a" 34 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | WORKDIR /root 3 | ENV LC_ALL=C.UTF-8 4 | ENV LANG=C.UTF-8 5 | 6 | RUN yes | unminimize 7 | RUN apt update && \ 8 | apt install -y vim zsh tmux git fasd wget curl unzip ripgrep bc ssh htop && \ 9 | apt install -y locales dnsutils net-tools ca-certificates && \ 10 | apt install -y python3 python3-pip python-is-python3 && \ 11 | apt clean 12 | RUN locale-gen --no-purge en_US.UTF-8 13 | 14 | ARG git_user 15 | ARG git_email 16 | RUN git config --global user.name "$git_user" && git config --global user.email "$git_email" 17 | 18 | RUN git clone --recursive https://github.com/raulchen/dotfiles.git ~/dotfiles && yes n | ~/dotfiles/setup.sh 19 | 20 | # Install fzf 21 | RUN git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf && \ 22 | yes | ~/.fzf/install 23 | 24 | RUN mkdir -p ~/bin 25 | 26 | RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python 27 | 28 | # Install neovim 29 | RUN dpkgArch="$(dpkg --print-architecture)"; \ 30 | case "${dpkgArch##*-}" in \ 31 | arm64) echo "deb http://ports.ubuntu.com/ubuntu-ports/ kinetic main universe" >> /etc/apt/sources.list && \ 32 | apt update && apt install -y neovim && apt clean;; \ 33 | amd64) wget https://github.com/neovim/neovim/releases/download/v0.7.2/nvim-linux64.deb && \ 34 | apt install ./nvim-linux64.deb && rm ./nvim-linux64.deb;; \ 35 | esac; \ 36 | nvim +PlugInstall +qall; 37 | 38 | RUN pip install pynvim pyright 39 | 40 | # Install bazel 41 | RUN dpkgArch="$(dpkg --print-architecture)"; \ 42 | case "${dpkgArch##*-}" in \ 43 | arm64) export BAZEL_URL="https://github.com/bazelbuild/bazel/releases/download/5.3.0/bazel-5.3.0-linux-arm64";; \ 44 | amd64) export BAZEL_URL="https://github.com/bazelbuild/bazel/releases/download/5.3.0/bazel-5.3.0-linux-x86_64";; \ 45 | esac; \ 46 | wget -O ~/bin/bazel $BAZEL_URL 47 | 48 | RUN echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/dotfiles/zsh/zshrc.local 49 | 50 | # Install rust-analyzer 51 | RUN dpkgArch="$(dpkg --print-architecture)"; \ 52 | case "${dpkgArch##*-}" in \ 53 | arm64) export RUST_ANALYZER_URL="https://github.com/rust-lang/rust-analyzer/releases/download/2022-09-12/rust-analyzer-aarch64-unknown-linux-gnu.gz";; \ 54 | amd64) export RUST_ANALYZER_URL="https://github.com/rust-lang/rust-analyzer/releases/download/2022-09-12/rust-analyzer-x86_64-unknown-linux-gnu.gz";; \ 55 | esac; \ 56 | curl -L $RUST_ANALYZER_URL | gunzip -c - > ~/bin/rust-analyzer 57 | 58 | RUN apt install -y clangd nodejs && \ 59 | apt clean 60 | 61 | RUN go install golang.org/x/tools/gopls@latest 62 | 63 | CMD zsh 64 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | git_user=$(git config user.name) 3 | git_email=$(git config user.email) 4 | docker build --build-arg git_user="$git_user" --build-arg git_email="git_email" -t my/dev $@ . 5 | -------------------------------------------------------------------------------- /ghostty/config: -------------------------------------------------------------------------------- 1 | theme = onedarkpro_snazzy 2 | 3 | font-size = 16 4 | font-family = "Maple Mono NF CN" 5 | 6 | # Disable the confirmation dialog when closing Ghostty 7 | confirm-close-surface = false 8 | -------------------------------------------------------------------------------- /ghostty/themes/onedarkpro_onelight: -------------------------------------------------------------------------------- 1 | # Colors - https://github.com/olimorris/onedarkpro.nvim 2 | 3 | palette = 0=#6a6a6a 4 | palette = 1=#e05661 5 | palette = 2=#1da912 6 | palette = 3=#eea825 7 | palette = 4=#118dc3 8 | palette = 5=#9a77cf 9 | palette = 6=#56b6c2 10 | palette = 7=#fafafa 11 | palette = 8=#bebebe 12 | palette = 9=#e88189 13 | palette = 10=#25d717 14 | palette = 11=#f2bb54 15 | palette = 12=#1caceb 16 | palette = 13=#b69ddc 17 | palette = 14=#7bc6d0 18 | palette = 15=#ffffff 19 | background = #ededed 20 | foreground = #6a6a6a 21 | cursor-color = #9a77cf 22 | selection-background = #9a77cf 23 | selection-foreground = #6a6a6a 24 | -------------------------------------------------------------------------------- /ghostty/themes/onedarkpro_snazzy: -------------------------------------------------------------------------------- 1 | palette = 0=#000000 2 | palette = 1=#ff5c57 3 | palette = 2=#5af78e 4 | palette = 3=#f3f99d 5 | palette = 4=#57c7ff 6 | palette = 5=#ffb6e1 7 | palette = 6=#9aedfe 8 | palette = 7=#f1f1f0 9 | palette = 8=#686868 10 | palette = 9=#ff8d8a 11 | palette = 10=#8bf9af 12 | palette = 11=#f9fccd 13 | palette = 12=#8ad8ff 14 | palette = 13=#ffe9f6 15 | palette = 14=#cdf6ff 16 | palette = 15=#ffffff 17 | background = #282A36 18 | foreground = #b6becc 19 | # cursor-color = #ff9dd6 20 | selection-background = #92bcd0 21 | selection-foreground = #000000 22 | # selection-invert-fg-bg = true 23 | -------------------------------------------------------------------------------- /git/config: -------------------------------------------------------------------------------- 1 | # -*- mode: gitconfig; -*- 2 | # vim: set filetype=gitconfig: 3 | [user] 4 | name = Hao Chen 5 | email = chenh1024@gmail.com 6 | [core] 7 | excludesfile = ~/.config/git/ignore 8 | [merge] 9 | conflictstyle = diff3 10 | tool = vimdiff 11 | [alias] 12 | ad = "add" 13 | ada = "add --all" 14 | aliases = "!git config --get-regexp '^alias\\.' | cut -c 7- | sed 's/ / = /'" 15 | br = "branch" 16 | cl = "clean" 17 | cm = "commit" 18 | cma = "commit -a --verbose" 19 | cmam = "commit -am" 20 | cmd = "commit --amend --verbose" 21 | cmm = "commit -m" 22 | co = "checkout" 23 | cob = "!f() { git checkout -b \"$1\" 2>/dev/null || git checkout \"$1\"; }; f" 24 | com = "!git checkout $(git master-branch)" 25 | cp = "cherry-pick" 26 | cpa = "cherry-pick --abort" 27 | cpc = "cherry-pick --continue" 28 | df = "diff" 29 | dfc = "diff --cached" 30 | dfm = "!git diff --merge-base $1 $(git master-branch)" 31 | dfs = "!DELTA_FEATURES=+side-by-side git diff" 32 | fc = "fetch" 33 | lg = "log" 34 | lgl = "log --graph --pretty='%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ar) %C(bold blue)<%an>%Creset'" 35 | lgo = "log --oneline --decorate --graph" 36 | master-branch = "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'" 37 | mg = "merge" 38 | mga = "merge --abort" 39 | mgc = "merge --continue" 40 | mgm = "!git merge $(git master-branch)" 41 | pl = "pull" 42 | plb = "!git pull origin $(git branch --show-current)" 43 | ps = "push" 44 | psb = "!git push origin $(git branch --show-current)" 45 | psd = "push --delete origin" 46 | rb = "rebase" 47 | rba = "rebase --abort" 48 | rbc = "rebase --continue" 49 | rbi = "rebase -i" 50 | rbm = "!git rebase $(git master-branch)" 51 | rs = "reset" 52 | rsh = "reset --hard" 53 | rsho = "!git reset --hard origin/$(git branch --show-current)" 54 | rt = "restore" 55 | rv = "revert" 56 | sh = "show" 57 | ss = "stash" 58 | st = "status" 59 | sw = "switch" 60 | swc = "!git-swc" 61 | swm = "!git switch $(git master-branch)" 62 | swl = "switch -" -- switch to last branch 63 | wt = "worktree" 64 | # Create a new worktree named 'wt-' with a new branch '' 65 | wtc = "!f() { git worktree add wt-\"$1\" -b \"$1\"; }; f" 66 | # Remove worktree and delete associated branch. Accepts either branch name or worktree name with 'wt-' prefix 67 | wtd = "!f() { branch=\"${1#wt-}\"; git worktree remove wt-\"$branch\" && git branch -D \"$branch\"; }; f" 68 | [include] 69 | path = ~/.config/git/delta.txt 70 | [include] 71 | path = ~/.config/git/config.local 72 | -------------------------------------------------------------------------------- /git/delta.txt: -------------------------------------------------------------------------------- 1 | # -*- mode: gitconfig; -*- 2 | # vim: set filetype=gitconfig: 3 | 4 | [core] 5 | pager = (type delta >/dev/null && delta) || less 6 | [interactive] 7 | diffFilter = delta --color-only 8 | [delta] 9 | navigate = true # use n and N to move between diff sections 10 | [diff] 11 | colorMoved = default 12 | 13 | -------------------------------------------------------------------------------- /git/ignore: -------------------------------------------------------------------------------- 1 | # -*- mode: gitignore; -*- 2 | # vim: set filetype=gitignore: 3 | 4 | compile_commands.json 5 | pyrightconfig.json 6 | .clangd 7 | 8 | /_tmp/ 9 | # Ignore worktree directories 10 | /wt-*/ 11 | -------------------------------------------------------------------------------- /git/signoff-aliases.txt: -------------------------------------------------------------------------------- 1 | # -*- mode: gitconfig; -*- 2 | # vim: set filetype=gitconfig: 3 | 4 | [alias] 5 | cm = "commit --signoff" 6 | cma = "commit --signoff -a --verbose" 7 | cmam = "commit --signoff -am" 8 | cmd = "commit --signoff --amend --verbose" 9 | cmm = "commit --signoff -m" 10 | mg = "merge --signoff" 11 | mgm = "!git merge --signoff $(git master-branch)" 12 | rb = "rebase --signoff" 13 | rbi = "rebase --signoff -i" 14 | rbm = "!git rebase --signoff $(git master-branch)" 15 | rv = "revert --signoff" 16 | -------------------------------------------------------------------------------- /hammerspoon/Spoons/EmmyLua.spoon/docs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Command": [], 4 | "Constant": [], 5 | "Constructor": [], 6 | "Deprecated": [], 7 | "Field": [], 8 | "Function": [], 9 | "Method": [], 10 | "Variable": [], 11 | "desc": "Thie plugin generates EmmyLua annotations for Hammerspoon and any installed Spoons", 12 | "doc": "Thie plugin generates EmmyLua annotations for Hammerspoon and any installed Spoons\nunder ~/.hammerspoon/Spoons/EmmyLua.spoon/annotations.\nAnnotations will only be generated if they don't exist yet or are out of date.\n\nNote: Load this Spoon before any pathwatchers are defined to avoid unintended behaviour (for example multiple reloads when the annotions are created).\n\nIn order to get auto completion in your editor, you need to have one of the following LSP servers properly configured:\n* [lua-language-server](https://github.com/sumneko/lua-language-server) (recommended)\n* [EmmyLua-LanguageServer](https://github.com/EmmyLua/EmmyLua-LanguageServer)\n\nTo start using this annotations library, add the annotations folder to your workspace.\nfor lua-languag-server:\n\n```json\n{\n \"Lua.workspace.library\": [\"/Users/YOUR_USERNAME/.hammerspoon/Spoons/EmmyLua.spoon/annotations\"]\n}\n```\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/EmmyLua.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/EmmyLua.spoon.zip)", 13 | "items": [], 14 | "name": "EmmyLua", 15 | "stripped_doc": "under ~/.hammerspoon/Spoons/EmmyLua.spoon/annotations.\nAnnotations will only be generated if they don't exist yet or are out of date.\n\nNote: Load this Spoon before any pathwatchers are defined to avoid unintended behaviour (for example multiple reloads when the annotions are created).\n\nIn order to get auto completion in your editor, you need to have one of the following LSP servers properly configured:\n* [lua-language-server](https://github.com/sumneko/lua-language-server) (recommended)\n* [EmmyLua-LanguageServer](https://github.com/EmmyLua/EmmyLua-LanguageServer)\n\nTo start using this annotations library, add the annotations folder to your workspace.\nfor lua-languag-server:\n\n```json\n{\n \"Lua.workspace.library\": [\"/Users/YOUR_USERNAME/.hammerspoon/Spoons/EmmyLua.spoon/annotations\"]\n}\n```\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/EmmyLua.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/EmmyLua.spoon.zip)", 16 | "submodules": [], 17 | "type": "Module" 18 | } 19 | ] -------------------------------------------------------------------------------- /hammerspoon/Spoons/EmmyLua.spoon/init.lua: -------------------------------------------------------------------------------- 1 | --- === EmmyLua === 2 | --- 3 | --- Thie plugin generates EmmyLua annotations for Hammerspoon and any installed Spoons 4 | --- under ~/.hammerspoon/Spoons/EmmyLua.spoon/annotations. 5 | --- Annotations will only be generated if they don't exist yet or are out of date. 6 | --- 7 | --- Note: Load this Spoon before any pathwatchers are defined to avoid unintended behaviour (for example multiple reloads when the annotions are created). 8 | --- 9 | --- In order to get auto completion in your editor, you need to have one of the following LSP servers properly configured: 10 | --- * [lua-language-server](https://github.com/sumneko/lua-language-server) (recommended) 11 | --- * [EmmyLua-LanguageServer](https://github.com/EmmyLua/EmmyLua-LanguageServer) 12 | --- 13 | --- To start using this annotations library, add the annotations folder to your workspace. 14 | --- for lua-languag-server: 15 | --- 16 | --- ```json 17 | --- { 18 | --- "Lua.workspace.library": ["/Users/YOUR_USERNAME/.hammerspoon/Spoons/EmmyLua.spoon/annotations"] 19 | --- } 20 | --- ``` 21 | --- 22 | --- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/EmmyLua.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/EmmyLua.spoon.zip) 23 | 24 | local M = {} 25 | 26 | M.name = "EmmyLua" 27 | M.version = "1.0" 28 | M.author = "http://github.com/folke" 29 | M.license = "MIT - https://opensource.org/licenses/MIT" 30 | 31 | local options = { 32 | annotations = hs.spoons.resourcePath("annotations"), 33 | types = { 34 | bool = "boolean", 35 | boolean = "boolean", 36 | ["false"] = "boolean", 37 | ["true"] = "boolean", 38 | string = "string", 39 | number = "number", 40 | float = "number", 41 | integer = "number", 42 | app = "hs.application", 43 | hsminwebtable = "hs.httpserver.hsminweb", 44 | notificationobject = "hs.notify", 45 | point = "hs.geometry", 46 | rect = "hs.geometry", 47 | ["hs.geometry rect"] = "hs.geometry", 48 | size = "hs.geometry", 49 | }, 50 | } 51 | 52 | M.spoonPath = hs.spoons.scriptPath() 53 | 54 | function M.comment(str, commentStr) 55 | commentStr = commentStr or "--" 56 | return commentStr .. " " .. str:gsub("[\n]", "\n" .. commentStr .. " "):gsub("%s+\n", "\n") .. "\n" 57 | end 58 | 59 | function M.parseType(module, str) 60 | if not str then 61 | return 62 | end 63 | 64 | str = str:lower() 65 | 66 | if options.types[str] then 67 | return options.types[str] 68 | end 69 | 70 | local type = str:match("^(hs%.%S*)%s*object") 71 | if type then 72 | return type 73 | end 74 | 75 | type = str:match("^list of (hs%.%S*)%s*object") 76 | if type then 77 | return type .. "[]" 78 | end 79 | 80 | if module.name:find(str, 1, true) or str == "self" then 81 | return module.name 82 | end 83 | end 84 | 85 | function M.trim(str) 86 | str = str:gsub("^%s*", "") 87 | str = str:gsub("%s*$", "") 88 | return str 89 | end 90 | 91 | function M.parseArgs(str) 92 | local name, args = str:match("^(.*)%((.*)%)$") 93 | if name then 94 | args = args:gsub("%s*|%s*", "_or_") 95 | args = args:gsub("%s+or%s+", "_or_") 96 | args = args:gsub("[%[%]{}%(%)]", "") 97 | if args:find("...") then 98 | args = args:gsub(",?%s*%.%.%.", "") 99 | args = M.trim(args) 100 | if #args > 0 then 101 | args = args .. ", " 102 | end 103 | args = args .. "..." 104 | end 105 | args = hs.fnutils.split(args, "%s*,%s*") 106 | for a, arg in ipairs(args) do 107 | if arg == "false" then 108 | args[a] = "_false" 109 | elseif arg == "function" then 110 | args[a] = "fn" 111 | elseif arg == "end" then 112 | args[a] = "_end" 113 | end 114 | end 115 | return name, args 116 | end 117 | return str 118 | end 119 | 120 | function M.parseDef(module, el) 121 | el.def = el.def or "" 122 | el.def = module.prefix .. el.def 123 | local parts = hs.fnutils.split(el.def, "%s*%-+>%s*") 124 | local name, args = M.parseArgs(parts[1]) 125 | local ret = { name = name, args = args, type = M.parseType(module, parts[2]) } 126 | if name:match("%[.*%]$") then 127 | if not ret.type then 128 | ret.type = "table" 129 | end 130 | ret.name = ret.name:sub(1, ret.name:find("%[") - 1) 131 | end 132 | return ret 133 | end 134 | 135 | function M.processModule(module) 136 | io.write("--# selene: allow(unused_variable)\n") 137 | io.write("---@diagnostic disable: unused-local\n\n") 138 | 139 | if module.name == "hs" then 140 | io.write("--- global variable containing loaded spoons\n") 141 | io.write("spoon = {}\n\n") 142 | end 143 | 144 | io.write(M.comment(module.doc)) 145 | io.write("---@class " .. module.name .. "\n") 146 | io.write("local M = {}\n") 147 | io.write(module.name .. " = M\n\n") 148 | 149 | for _, item in ipairs(module.items) do 150 | local def = M.parseDef(module, item) 151 | -- io.write("-- " .. item.def) 152 | io.write(M.comment(item.doc)) 153 | local name = def.name 154 | if def.name:find(module.name, 1, true) == 1 then 155 | name = "M" .. def.name:sub(#module.name + 1) 156 | end 157 | if def.args then 158 | if def.type then 159 | io.write("---@return " .. def.type .. "\n") 160 | end 161 | io.write("function " .. name .. "(" .. table.concat(def.args, ", ") .. ") end\n") 162 | else 163 | if def.type then 164 | io.write("---@type " .. def.type .. "\n") 165 | end 166 | if def.type and (def.type:find("table") or def.type:find("%[%]")) then 167 | io.write(name .. " = {}\n") 168 | else 169 | io.write(name .. " = nil\n") 170 | end 171 | end 172 | io.write("\n") 173 | end 174 | end 175 | 176 | function M.create(jsonDocs, prefix) 177 | local mtime = hs.fs.attributes(jsonDocs, "modification") 178 | prefix = prefix or "" 179 | local data = hs.json.read(jsonDocs) 180 | for _, module in ipairs(data) do 181 | if module.type ~= "Module" then 182 | error("Expected a module, but found type=" .. module.type) 183 | end 184 | module.prefix = prefix 185 | module.name = prefix .. module.name 186 | local fname = options.annotations .. "/" .. module.name .. ".lua" 187 | local fmtime = hs.fs.attributes(fname, "modification") 188 | if fmtime == nil or mtime > fmtime then 189 | -- print("creating " .. fname) 190 | local fd = io.open(fname, "w+") 191 | io.output(fd) 192 | M.processModule(module) 193 | io.close(fd) 194 | end 195 | end 196 | end 197 | 198 | function M:init() 199 | hs.fs.mkdir(options.annotations) 200 | -- Load hammerspoon docs 201 | M.create(hs.docstrings_json_file) 202 | 203 | -- Load Spoons 204 | for _, spoon in ipairs(hs.spoons.list()) do 205 | local doc = hs.configdir .. "/Spoons/" .. spoon.name .. ".spoon/docs.json" 206 | if hs.fs.attributes(doc, "modification") then 207 | M.create(doc, "spoon.") 208 | end 209 | end 210 | end 211 | 212 | return M 213 | -------------------------------------------------------------------------------- /hammerspoon/browser_selector.lua: -------------------------------------------------------------------------------- 1 | if hs.urlevent.getDefaultHandler('http') ~= 'org.hammerspoon.Hammerspoon' then 2 | return 3 | end 4 | 5 | local browsers = { 6 | Safari = { bundleID = "com.apple.Safari" }, 7 | Arc = { bundleID = "company.thebrowser.Browser" }, 8 | } 9 | 10 | local text_style = { 11 | font = { size = 22 }, 12 | color = hs.drawing.color.definedCollections.hammerspoon.grey, 13 | paragraphSpacingBefore = 2, 14 | } 15 | 16 | for name, browser in pairs(browsers) do 17 | browser.name = name 18 | browser.text = hs.styledtext.new(name, text_style) 19 | browser.image = hs.image.imageFromAppBundle(browser.bundleID) 20 | browser.lastSelectedTime = 0 21 | end 22 | 23 | ---@diagnostic disable-next-line: unused-local 24 | local function selectBrowser(schema, host, params, fullUrl, senderPID) 25 | if not fullUrl then 26 | return 27 | end 28 | local choices = {} 29 | for _, browser in pairs(browsers) do 30 | table.insert(choices, browser) 31 | end 32 | -- Sort the choices by last selected time 33 | table.sort(choices, function(a, b) 34 | return a.lastSelectedTime >= b.lastSelectedTime 35 | end) 36 | 37 | hs.chooser.new(function(choice) 38 | if choice then 39 | browsers[choice.name].lastSelectedTime = hs.timer.secondsSinceEpoch() 40 | hs.urlevent.openURLWithBundle(fullUrl, choice.bundleID) 41 | end 42 | end) 43 | :choices(choices) 44 | :rows(#choices) 45 | :width(15) 46 | :show() 47 | end 48 | 49 | hs.urlevent.httpCallback = selectBrowser 50 | -------------------------------------------------------------------------------- /hammerspoon/double_cmdq_to_quit.lua: -------------------------------------------------------------------------------- 1 | -- Press Cmd+Q twice to quit 2 | 3 | local quit_modal = hs.hotkey.modal.new('cmd', 'q') 4 | local label = require("labels").new("Press Cmd+Q again to quit", "center") 5 | 6 | function quit_modal:entered() 7 | label:show(1) 8 | hs.timer.doAfter(1, function() quit_modal:exit() end) 9 | end 10 | 11 | local function do_quit() 12 | label:hide() 13 | local app = hs.application.frontmostApplication() 14 | app:kill() 15 | end 16 | 17 | quit_modal:bind('cmd', 'q', do_quit) 18 | 19 | quit_modal:bind('', 'escape', function() quit_modal:exit() end) 20 | -------------------------------------------------------------------------------- /hammerspoon/init.lua: -------------------------------------------------------------------------------- 1 | require("leader_key") 2 | require("double_cmdq_to_quit") 3 | require("modifier_key_monitor").start() 4 | require("vim_mode") 5 | require("browser_selector") 6 | ---@diagnostic disable-next-line: param-type-mismatch 7 | pcall(hs.fnutils.partial(require, "local")) 8 | 9 | require("utils").temp_notify(3, hs.notify.new({ 10 | title = "Config reloaded", 11 | })) 12 | 13 | if hs.fs.attributes("~/.hammerspoon/Spoons/EmmyLua.spoon/annotations", "size") == nil then 14 | hs.loadSpoon("EmmyLua") 15 | end 16 | -------------------------------------------------------------------------------- /hammerspoon/labels.lua: -------------------------------------------------------------------------------- 1 | -- Floating labels at the buttom right corner of screen 2 | 3 | local module = {} 4 | 5 | local drawing = hs.drawing 6 | 7 | local Label = {} 8 | Label.__index = Label 9 | 10 | local screen_margin = { x = 50, y = 10 } 11 | local bg_margin = { x = 7, y = 5 } 12 | 13 | function Label.new(message, position) 14 | local label = {} 15 | setmetatable(label, Label) 16 | label.message = message 17 | label.position = position or "center" 18 | local size = label.position == "center" and 22 or 17 19 | label.text_style = { 20 | size = size, 21 | color = { white = 1, alpha = 1 }, 22 | klignment = "center", 23 | lineBreak = "truncateTail", 24 | } 25 | 26 | label.bg_color = { white = 0.2, alpha = 0.9 } 27 | label.textFrame = drawing.getTextDrawingSize(message, label.text_style) 28 | return label 29 | end 30 | 31 | function Label:get_text_display_frame() 32 | local screen = hs.screen.mainScreen() 33 | local screen_frame = screen:fullFrame() 34 | 35 | if self.position == "bottom_left" then 36 | return { 37 | x = screen_margin.x, 38 | y = screen_frame.h - screen_margin.y - self.textFrame.h, 39 | w = self.textFrame.w, 40 | h = self.textFrame.h, 41 | } 42 | elseif self.position == "bottom_right" then 43 | return { 44 | x = screen_frame.w - self.textFrame.w - screen_margin.x, 45 | y = screen_frame.h - screen_margin.y - self.textFrame.h, 46 | w = self.textFrame.w, 47 | h = self.textFrame.h, 48 | } 49 | elseif self.position == "center" then 50 | return { 51 | x = (screen_frame.w - self.textFrame.w) / 2, 52 | y = (screen_frame.h - self.textFrame.h) / 2, 53 | w = self.textFrame.w, 54 | h = self.textFrame.h, 55 | } 56 | else 57 | assert(false, "Invalid position") 58 | end 59 | end 60 | 61 | function Label:text_display_frame() 62 | end 63 | 64 | function Label:show(duration) 65 | if self.text_obj then 66 | return 67 | end 68 | local text_display_frame = self:get_text_display_frame() 69 | 70 | local bg_display_frame = { 71 | x = text_display_frame.x - bg_margin.x, 72 | y = text_display_frame.y - bg_margin.y, 73 | w = text_display_frame.w + bg_margin.x * 2, 74 | h = text_display_frame.h + bg_margin.y * 2, 75 | } 76 | 77 | self.bg_obj = drawing.rectangle(bg_display_frame) 78 | :setStroke(true) 79 | :setFill(true) 80 | :setFillColor(self.bg_color) 81 | :setRoundedRectRadii(5, 5) 82 | :show(0.15) 83 | self.text_obj = drawing.text(text_display_frame, self.message) 84 | :setTextStyle(self.text_style) 85 | :show(0.15) 86 | 87 | if duration then 88 | hs.timer.doAfter(duration, function() self:hide() end) 89 | end 90 | end 91 | 92 | function Label:hide() 93 | if self.bg_obj then 94 | self.bg_obj:delete() 95 | self.bg_obj = nil 96 | end 97 | if self.text_obj then 98 | self.text_obj:delete() 99 | self.text_obj = nil 100 | end 101 | end 102 | 103 | module.new = function(message, position) 104 | return Label.new(message, position) 105 | end 106 | 107 | module.show = function(message, position, duration) 108 | Label.new(message, position):show(duration) 109 | end 110 | 111 | return module 112 | -------------------------------------------------------------------------------- /hammerspoon/leader_key.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | local TIMEOUT = 5 4 | 5 | local modal = hs.hotkey.modal.new() 6 | module.enabled = false 7 | 8 | module.toggle = function() 9 | if not module.enabled then 10 | modal:enter() 11 | else 12 | modal:exit() 13 | end 14 | end 15 | 16 | local label = require("labels").new("Leader Key", "center") 17 | local timer = nil 18 | 19 | local function cancel_timeout() 20 | if timer then 21 | timer:stop() 22 | end 23 | end 24 | 25 | function modal:entered() 26 | module.enabled = true 27 | label:show() 28 | timer = hs.timer.doAfter(TIMEOUT, function() modal:exit() end) 29 | end 30 | 31 | function modal:exited() 32 | module.enabled = false 33 | label:hide() 34 | cancel_timeout() 35 | end 36 | 37 | function module.exit() 38 | modal:exit() 39 | end 40 | 41 | function module.bind(mod, key, fn, can_repeat) 42 | if can_repeat ~= true then 43 | modal:bind(mod, key, nil, function() 44 | module.exit() 45 | fn() 46 | end, nil) 47 | else 48 | local pressed_fn = function() 49 | fn() 50 | cancel_timeout() 51 | end 52 | modal:bind(mod, key, pressed_fn, nil, fn) 53 | end 54 | end 55 | 56 | function module.bind_multiple(mod, key, pressed_fn, released_fn, repeat_fn) 57 | modal:bind(mod, key, pressed_fn, released_fn, repeat_fn) 58 | end 59 | 60 | module.bind('', 'escape', module.exit) 61 | 62 | module.bind('', 'd', hs.toggleConsole) 63 | module.bind('', 'r', hs.reload) 64 | 65 | module.bind({ 'shift' }, 'a', require("utils").toggle_caps_lock) 66 | 67 | local function switch_primary_monitor() 68 | hs.screen.primaryScreen():next():setPrimary() 69 | end 70 | 71 | module.bind('', 'm', switch_primary_monitor) 72 | 73 | local system_key_stroke_fn = require("utils").system_key_stroke_fn 74 | 75 | module.bind({}, 'p', system_key_stroke_fn('PLAY')) 76 | module.bind({}, '[', system_key_stroke_fn('REWIND')) 77 | module.bind({}, ']', system_key_stroke_fn('FAST')) 78 | 79 | -- Raycast shortcuts 80 | 81 | local raycast_shortcuts = { 82 | [{ {}, 'a' }] = "raycast://extensions/raycast/raycast-ai/ai-chat", 83 | [{ {}, 'c' }] = "raycast://extensions/raycast/clipboard-history/clipboard-history", 84 | [{ {}, 'e' }] = "raycast://extensions/raycast/emoji-symbols/search-emoji-symbols", 85 | [{ {}, 's' }] = "raycast://extensions/raycast/snippets/search-snippets", 86 | [{ {}, 'w' }] = "raycast://extensions/raycast/navigation/switch-windows", 87 | -- Window management 88 | [{ {}, 'h' }] = "raycast://extensions/raycast/window-management/left-half", 89 | [{ {}, 'j' }] = "raycast://extensions/raycast/window-management/almost-maximize", 90 | [{ {}, 'k' }] = "raycast://extensions/raycast/window-management/maximize", 91 | [{ {}, 'l' }] = "raycast://extensions/raycast/window-management/right-half", 92 | [{ { 'ctrl' }, 'h' }] = "raycast://extensions/raycast/window-management/top-left-quarter", 93 | [{ { 'ctrl' }, 'j' }] = "raycast://extensions/raycast/window-management/bottom-left-quarter", 94 | [{ { 'ctrl' }, 'k' }] = "raycast://extensions/raycast/window-management/top-right-quarter", 95 | [{ { 'ctrl' }, 'l' }] = "raycast://extensions/raycast/window-management/bottom-right-quarter", 96 | [{ { 'shift' }, 'h' }] = "raycast://extensions/raycast/window-management/previous-desktop", 97 | [{ { 'shift' }, 'l' }] = "raycast://extensions/raycast/window-management/next-desktop", 98 | [{ {}, ';' }] = "raycast://extensions/raycast/window-management/next-display", 99 | } 100 | 101 | for k, v in pairs(raycast_shortcuts) do 102 | module.bind(k[1], k[2], function() hs.execute("open -g " .. v) end) 103 | end 104 | 105 | return module 106 | -------------------------------------------------------------------------------- /hammerspoon/modifier_key_monitor.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | local inspect = hs.inspect.inspect 4 | 5 | local MIN_INTERVAL_S = 0.5 6 | 7 | local mod_keys = { "ctrl", "shift" } 8 | 9 | local key_states = {} 10 | 11 | local function reset_key_states() 12 | for _, key in ipairs(mod_keys) do 13 | key_states[key] = { 14 | repeats = 0, 15 | last_press_time = 0, 16 | } 17 | end 18 | end 19 | 20 | reset_key_states() 21 | 22 | local last_toggle_caps_lock_time = 0.0 23 | 24 | local mod_key_listener = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(e) 25 | local now = hs.timer.secondsSinceEpoch() or 0 26 | 27 | local modifiers = e:getFlags() 28 | 29 | local pressed_keys = {} 30 | for key, _ in pairs(modifiers) do 31 | table.insert(pressed_keys, key) 32 | end 33 | 34 | if #pressed_keys > 1 then 35 | -- If multiple modifier keys are pressed, reset the states of all keys. 36 | reset_key_states() 37 | elseif #pressed_keys == 1 then 38 | local key = pressed_keys[1] 39 | for _, k in ipairs(mod_keys) do 40 | if k == key then 41 | if now - key_states[k].last_press_time < MIN_INTERVAL_S then 42 | key_states[k].repeats = key_states[k].repeats + 1 43 | else 44 | key_states[k].repeats = 1 45 | end 46 | key_states[k].last_press_time = now 47 | else 48 | key_states[k].repeats = 0 49 | end 50 | end 51 | else 52 | -- If shift is pressed twice, toggle caps lock. 53 | if key_states["shift"].repeats == 2 then 54 | -- If the second shift press was released after the interval, 55 | -- do not trigger caps lock. 56 | if now - key_states["shift"].last_press_time < MIN_INTERVAL_S then 57 | -- `hs.hid.capslock.toggle()` will also trigger this callback, 58 | -- need the following check to avoid infinite callbacks. 59 | if now - last_toggle_caps_lock_time > MIN_INTERVAL_S then 60 | require("utils").toggle_caps_lock() 61 | last_toggle_caps_lock_time = now 62 | end 63 | end 64 | end 65 | if key_states["ctrl"].repeats == 2 then 66 | if now - key_states["ctrl"].last_press_time < MIN_INTERVAL_S then 67 | require("leader_key").toggle() 68 | end 69 | end 70 | end 71 | 72 | return false, nil 73 | end) 74 | 75 | local normal_key_listener = hs.eventtap.new({ hs.eventtap.event.types.keyDown }, function(_) 76 | -- If a non-modifier key is pressed, reset key states. 77 | reset_key_states() 78 | end) 79 | 80 | module.start = function() 81 | mod_key_listener:start() 82 | normal_key_listener:start() 83 | end 84 | 85 | return module 86 | -------------------------------------------------------------------------------- /hammerspoon/utils.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | function module.temp_notify(timeout, notif) 4 | notif:send() 5 | hs.timer.doAfter(timeout, function() notif:withdraw() end) 6 | end 7 | 8 | function module.split_str(str, sep) 9 | if sep == nil then 10 | sep = "%s" 11 | end 12 | local t = {} 13 | local i = 1 14 | for s in string.gmatch(str, "([^" .. sep .. "]+)") do 15 | t[i] = s 16 | i = i + 1 17 | end 18 | return t 19 | end 20 | 21 | function module.str_to_table(str) 22 | local t = {} 23 | for i = 1, #str do 24 | t[i] = str:sub(i, i) 25 | end 26 | return t 27 | end 28 | 29 | local caps_lock_on_label = require("labels").new("Caps lock on", "bottom_left") 30 | local caps_lock_off_label = require("labels").new("Caps lock off", "bottom_left") 31 | 32 | function module.toggle_caps_lock() 33 | hs.hid.capslock.toggle() 34 | local msg = "Caps lock" 35 | if hs.hid.capslock.get() then 36 | msg = msg .. " on" 37 | caps_lock_off_label:hide() 38 | caps_lock_on_label:show(1) 39 | else 40 | msg = msg .. " off" 41 | caps_lock_on_label:hide() 42 | caps_lock_off_label:show(1) 43 | end 44 | end 45 | 46 | module.key_stroke_fn = function(mod, key, delay) 47 | delay = delay or (10 * 1000) 48 | return function() 49 | hs.eventtap.keyStroke(mod, key, delay) 50 | end 51 | end 52 | 53 | module.system_key_stroke_fn = function(key, delay) 54 | delay = delay or (10 * 1000) 55 | return function() 56 | hs.eventtap.event.newSystemKeyEvent(key, true):post() 57 | hs.timer.usleep(delay) 58 | hs.eventtap.event.newSystemKeyEvent(key, false):post() 59 | end 60 | end 61 | 62 | return module 63 | -------------------------------------------------------------------------------- /hammerspoon/vim_mode.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | local logger = hs.logger.new('vim_mode', 'info') 4 | 5 | local current_mode = nil 6 | 7 | local mode_entered = function(mode) 8 | logger.d("Entered " .. mode.name) 9 | current_mode = mode.name 10 | if mode.name ~= "off" then 11 | mode.label:show() 12 | end 13 | end 14 | 15 | local mode_exited = function(mode) 16 | logger.d("Exited " .. mode.name) 17 | current_mode = nil 18 | mode.label:hide() 19 | end 20 | 21 | local function new_mode(name) 22 | local mode = { 23 | name = name, 24 | modal = hs.hotkey.modal.new(), 25 | label = require("labels").new(name, "bottom_right"), 26 | } 27 | 28 | mode.modal.entered = function() 29 | mode_entered(mode) 30 | end 31 | mode.modal.exited = function() 32 | mode_exited(mode) 33 | end 34 | 35 | return mode 36 | end 37 | 38 | local off = new_mode("off") 39 | local insert = new_mode("insert") 40 | local normal = new_mode("normal") 41 | local normal_g = new_mode("normal:g") 42 | local normal_c = new_mode("normal:c") 43 | local normal_d = new_mode("normal:d") 44 | local visual = new_mode("visual") 45 | local visual_g = new_mode("visual:g") 46 | 47 | local modes = { 48 | off = off, 49 | insert = insert, 50 | normal = normal, 51 | ["normal:g"] = normal_g, 52 | ["normal:c"] = normal_c, 53 | ["normal:d"] = normal_d, 54 | visual = visual, 55 | ["visual:g"] = visual_g, 56 | } 57 | 58 | local function switch_to_mode(mode) 59 | if current_mode then 60 | modes[current_mode].modal:exit() 61 | end 62 | mode.modal:enter() 63 | end 64 | 65 | module.toggle = function() 66 | if current_mode == "off" then 67 | switch_to_mode(normal) 68 | else 69 | switch_to_mode(off) 70 | end 71 | end 72 | 73 | local leader_key = require("leader_key") 74 | leader_key.bind({}, "v", module.toggle) 75 | 76 | local key_stroke_fn = require("utils").key_stroke_fn 77 | local system_key_stroke_fn = require("utils").system_key_stroke_fn 78 | 79 | local bind_fn = function(mode, mod, key, fn, can_repeat) 80 | local pressed_fn = nil 81 | local released_fn = nil 82 | local repeat_fn = nil 83 | 84 | if not can_repeat then 85 | released_fn = fn 86 | else 87 | pressed_fn = fn 88 | repeat_fn = fn 89 | end 90 | mode.modal:bind(mod, key, pressed_fn, released_fn, repeat_fn) 91 | end 92 | 93 | local bind_key = function(mode, source_mod, source_key, target_mod, target_key, can_repeat) 94 | local fn = key_stroke_fn(target_mod, target_key) 95 | bind_fn(mode, source_mod, source_key, fn, can_repeat) 96 | end 97 | 98 | -- === Normal mode === 99 | 100 | -- hjkl movements 101 | bind_key(normal, {}, 'h', {}, 'left', true) 102 | bind_key(normal, {}, 'j', {}, 'down', true) 103 | bind_key(normal, {}, 'k', {}, 'up', true) 104 | bind_key(normal, {}, 'l', {}, 'right', true) 105 | 106 | -- w/e -> move forward by word 107 | bind_key(normal, {}, 'w', { 'alt' }, 'right', true) 108 | bind_key(normal, {}, 'e', { 'alt' }, 'right', true) 109 | -- b -> move backward by word 110 | bind_key(normal, {}, 'b', { 'alt' }, 'left', true) 111 | 112 | -- 0/$ -> move to the beginning/end of the line 113 | bind_key(normal, {}, '0', { 'cmd' }, 'left', false) 114 | bind_key(normal, { 'shift' }, '4', { 'cmd' }, 'right', false) 115 | 116 | -- gg -> move to the beginning of the file 117 | bind_fn(normal, {}, 'g', function() 118 | switch_to_mode(normal_g) 119 | end, false) 120 | bind_fn(normal_g, {}, 'g', function() 121 | key_stroke_fn({ 'cmd' }, 'up')() 122 | switch_to_mode(normal) 123 | end, false) 124 | 125 | -- G -> move to the end of the file 126 | bind_key(normal, { 'shift' }, 'g', { 'cmd' }, 'down', false) 127 | 128 | -- ctrl + u/d -> move up/down N lines 129 | local CTRL_UD_NUM_LINES = 20 130 | local CTRL_UD_KEY_PRESS_DELAY = 500 131 | bind_fn(normal, { 'ctrl' }, 'u', function() 132 | for _ = 1, CTRL_UD_NUM_LINES do 133 | key_stroke_fn({}, 'up', CTRL_UD_KEY_PRESS_DELAY)() 134 | end 135 | end, true) 136 | bind_fn(normal, { 'ctrl' }, 'd', function() 137 | for _ = 1, CTRL_UD_NUM_LINES do 138 | key_stroke_fn({}, 'down', CTRL_UD_KEY_PRESS_DELAY)() 139 | end 140 | end, true) 141 | 142 | -- p -> paste 143 | bind_key(normal, {}, 'p', { 'cmd' }, 'v', false) 144 | 145 | -- x -> delete character forward 146 | bind_key(normal, {}, 'x', {}, 'forwarddelete', true) 147 | 148 | -- Implement c_ d_ commands 149 | bind_fn(normal, {}, 'c', function() 150 | switch_to_mode(normal_c) 151 | end, false) 152 | bind_fn(normal, {}, 'd', function() 153 | switch_to_mode(normal_d) 154 | end, false) 155 | for _, op in ipairs({ 'c', 'd' }) do 156 | local mode = op == 'c' and normal_c or normal_d 157 | local target_mode = op == 'c' and insert or normal 158 | -- w/e -> delete word forward 159 | bind_fn(mode, {}, 'w', function() 160 | key_stroke_fn({ 'alt' }, 'forwarddelete')() 161 | switch_to_mode(target_mode) 162 | end, false) 163 | bind_fn(mode, {}, 'e', function() 164 | key_stroke_fn({ 'alt' }, 'forwarddelete')() 165 | switch_to_mode(target_mode) 166 | end, false) 167 | -- b -> delete word backwards 168 | bind_fn(mode, {}, 'b', function() 169 | key_stroke_fn({ 'alt' }, 'delete')() 170 | switch_to_mode(target_mode) 171 | end, false) 172 | -- 0/$ -> delete to the beginning/end of the line 173 | bind_fn(mode, {}, '0', function() 174 | key_stroke_fn({ 'cmd' }, 'delete')() 175 | switch_to_mode(target_mode) 176 | end, false) 177 | bind_fn(mode, { 'shift' }, '4', function() 178 | key_stroke_fn({ 'ctrl' }, 'k')() 179 | switch_to_mode(target_mode) 180 | end, false) 181 | -- cc/dd -> delete the whole line 182 | bind_fn(mode, {}, op, function() 183 | key_stroke_fn({ 'cmd' }, 'right')() 184 | key_stroke_fn({ 'cmd' }, 'delete')() 185 | if op == 'd' then 186 | key_stroke_fn({ '' }, 'forwarddelete')() 187 | end 188 | switch_to_mode(target_mode) 189 | end, false) 190 | -- C/D -> delete to the end of the line 191 | bind_fn(normal, { 'shift' }, op, function() 192 | key_stroke_fn({ 'ctrl' }, 'k')() 193 | switch_to_mode(target_mode) 194 | end, false) 195 | end 196 | 197 | -- u -> undo 198 | bind_key(normal, {}, 'u', { 'cmd' }, 'z', true) 199 | -- ctrl + r -> redo 200 | bind_key(normal, { 'ctrl' }, 'r', { 'shift', 'cmd' }, 'z', true) 201 | 202 | -- i/I/a/A/o/O -> switch to insert mode 203 | bind_fn(normal, {}, 'i', function() 204 | switch_to_mode(insert) 205 | end, false) 206 | bind_fn(normal, { 'shift' }, 'i', function() 207 | key_stroke_fn({ 'cmd' }, 'left')() 208 | switch_to_mode(insert) 209 | end, false) 210 | bind_fn(normal, {}, 'a', function() 211 | key_stroke_fn({}, 'right')() 212 | switch_to_mode(insert) 213 | end, false) 214 | bind_fn(normal, { 'shift' }, 'a', function() 215 | key_stroke_fn({ 'cmd' }, 'right')() 216 | switch_to_mode(insert) 217 | end, false) 218 | bind_fn(normal, {}, 'o', function() 219 | key_stroke_fn({ 'cmd' }, 'right')() 220 | key_stroke_fn({ '' }, 'return')() 221 | switch_to_mode(insert) 222 | end, false) 223 | bind_fn(normal, { 'shift' }, 'o', function() 224 | key_stroke_fn({ 'cmd' }, 'left')() 225 | key_stroke_fn({ '' }, 'return')() 226 | key_stroke_fn({ '' }, 'up')() 227 | switch_to_mode(insert) 228 | end, false) 229 | 230 | -- v -> switch to visual mode 231 | bind_fn(normal, {}, 'v', function() 232 | switch_to_mode(visual) 233 | end, false) 234 | -- V -> select current line and switch to visual mode 235 | -- TODO: implement real line-wise visual mode. 236 | bind_fn(normal, { 'shift' }, 'v', function() 237 | key_stroke_fn({ 'cmd' }, 'left')() 238 | key_stroke_fn({ 'shift', 'cmd' }, 'right')() 239 | switch_to_mode(visual) 240 | visual.is_cursor_right_to_start = true 241 | end, false) 242 | 243 | -- ==== Visual mode ==== 244 | 245 | -- Whether the cursor is on the right side of 246 | -- the visual selection start. 247 | visual.is_cursor_right_to_start = nil 248 | 249 | visual.modal.entered = function() 250 | mode_entered(visual) 251 | visual.is_cursor_right_to_start = nil 252 | end 253 | 254 | local visual_bind_key = function(source_mod, source_key, target_mod, target_key, right) 255 | table.insert(target_mod, 'shift') 256 | local fn = function() 257 | key_stroke_fn(target_mod, target_key)() 258 | visual.is_cursor_right_to_start = right 259 | end 260 | bind_fn(visual, source_mod, source_key, fn, true) 261 | end 262 | 263 | -- hjkl movements 264 | visual_bind_key({}, 'h', {}, 'left', false) 265 | visual_bind_key({}, 'j', {}, 'down', true) 266 | visual_bind_key({}, 'k', {}, 'up', false) 267 | visual_bind_key({}, 'l', {}, 'right', true) 268 | 269 | -- w/e -> move forward by word 270 | visual_bind_key({}, 'w', { 'alt' }, 'right', true) 271 | visual_bind_key({}, 'e', { 'alt' }, 'right', true) 272 | -- b -> move backward by word 273 | visual_bind_key({}, 'b', { 'alt' }, 'left', false) 274 | 275 | -- 0/$ -> move to the beginning/end of the line 276 | visual_bind_key({}, '0', { 'cmd' }, 'left', false) 277 | visual_bind_key({ 'shift' }, '4', { 'cmd' }, 'right', true) 278 | 279 | -- ctrl-u/d -> move up/down N lines 280 | bind_fn(visual, { 'ctrl' }, 'u', function() 281 | for _ = 1, CTRL_UD_NUM_LINES do 282 | key_stroke_fn({ 'shift' }, 'up', CTRL_UD_KEY_PRESS_DELAY)() 283 | end 284 | end, true) 285 | bind_fn(visual, { 'ctrl' }, 'd', function() 286 | for _ = 1, CTRL_UD_NUM_LINES do 287 | key_stroke_fn({ 'shift' }, 'down', CTRL_UD_KEY_PRESS_DELAY)() 288 | end 289 | end, true) 290 | 291 | -- gg -> move to the beginning of the file 292 | bind_fn(visual, {}, 'g', function() 293 | switch_to_mode(visual_g) 294 | end, false) 295 | bind_fn(visual_g, {}, 'g', function() 296 | key_stroke_fn({ 'shift', 'cmd' }, 'up')() 297 | visual.is_cursor_right_to_start = false 298 | switch_to_mode(visual) 299 | end, false) 300 | -- G -> move to the end of the file 301 | visual_bind_key({ 'shift' }, 'g', { 'cmd' }, 'down', true) 302 | 303 | local visual_to_normal = function(clear_selection) 304 | if clear_selection == nil then 305 | clear_selection = true 306 | end 307 | if clear_selection then 308 | -- Clear visual selection and move the cursor to 309 | -- the visual selection start position. 310 | if visual.is_cursor_right_to_start ~= nil then 311 | if visual.is_cursor_right_to_start then 312 | key_stroke_fn({}, 'right')() 313 | else 314 | key_stroke_fn({}, 'left')() 315 | end 316 | end 317 | end 318 | switch_to_mode(normal) 319 | end 320 | 321 | -- y -> copy 322 | bind_fn(visual, {}, 'y', function() 323 | key_stroke_fn({ 'cmd' }, 'c')() 324 | visual_to_normal() 325 | end, false) 326 | -- p -> paste 327 | bind_fn(visual, {}, 'p', function() 328 | key_stroke_fn({ 'cmd' }, 'v')() 329 | visual_to_normal() 330 | end, false) 331 | 332 | -- x/d -> delete visual selection 333 | bind_fn(visual, {}, 'x', function() 334 | key_stroke_fn({ '' }, 'forwarddelete')() 335 | visual_to_normal(false) 336 | end, false) 337 | bind_fn(visual, {}, 'd', function() 338 | key_stroke_fn({ '' }, 'forwarddelete')() 339 | visual_to_normal(false) 340 | end, false) 341 | 342 | -- c -> delete visual selection and switch to insert mode 343 | bind_fn(visual, {}, 'c', function() 344 | key_stroke_fn({ '' }, 'forwarddelete')() 345 | switch_to_mode(insert) 346 | end, false) 347 | 348 | -- v/esc/ctrl-[ -> switch to normal mode 349 | bind_fn(visual, {}, 'v', visual_to_normal, false) 350 | bind_fn(visual, {}, 'escape', visual_to_normal, false) 351 | bind_fn(visual, { 'ctrl' }, '[', visual_to_normal, false) 352 | 353 | -- ==== Insert mode ==== 354 | 355 | bind_fn(insert, {}, 'escape', function() switch_to_mode(normal) end, false) 356 | bind_fn(insert, { 'ctrl' }, '[', function() switch_to_mode(normal) end, false) 357 | 358 | -- ==== Addtional bindings for both off/insert modes ==== 359 | 360 | for _, mode in ipairs({ off, insert }) do 361 | -- alt-hjkl -> arrow keys 362 | bind_key(mode, { 'alt' }, 'h', {}, 'left', true) 363 | bind_key(mode, { 'alt' }, 'j', {}, 'down', true) 364 | bind_key(mode, { 'alt' }, 'k', {}, 'up', true) 365 | bind_key(mode, { 'alt' }, 'l', {}, 'right', true) 366 | 367 | -- alt-m/n -> ctrl-tab and ctrl-shift-tab 368 | bind_key(mode, { 'alt' }, 'm', { 'ctrl' }, 'tab', true) 369 | bind_key(mode, { 'alt' }, 'n', { 'ctrl', 'shift' }, 'tab', true) 370 | 371 | bind_fn(mode, { 'alt' }, ',', system_key_stroke_fn('SOUND_DOWN'), true) 372 | bind_fn(mode, { 'alt' }, '.', system_key_stroke_fn('SOUND_UP'), true) 373 | bind_fn(mode, { 'alt' }, '/', system_key_stroke_fn('MUTE'), false) 374 | end 375 | 376 | switch_to_mode(off) 377 | return module 378 | -------------------------------------------------------------------------------- /lsd/colors.yaml: -------------------------------------------------------------------------------- 1 | user: dark_yellow 2 | group: dark_yellow 3 | permission: 4 | read: dark_green 5 | write: dark_yellow 6 | exec: dark_red 7 | exec-sticky: 5 8 | no-access: 245 9 | octal: 6 10 | acl: dark_cyan 11 | context: cyan 12 | date: 13 | hour-old: green 14 | day-old: green 15 | older: dark_green 16 | size: 17 | none: blue 18 | small: blue 19 | medium: dark_blue 20 | large: dark_blue 21 | inode: 22 | valid: 13 23 | invalid: 245 24 | links: 25 | valid: 13 26 | invalid: 245 27 | tree-edge: 245 28 | git-status: 29 | default: 245 30 | unmodified: 245 31 | ignored: 245 32 | new-in-index: dark_green 33 | new-in-workdir: dark_green 34 | typechange: dark_yellow 35 | deleted: dark_red 36 | renamed: dark_green 37 | modified: dark_yellow 38 | conflicted: dark_red 39 | -------------------------------------------------------------------------------- /lsd/config.yaml: -------------------------------------------------------------------------------- 1 | color: 2 | when: auto 3 | theme: custom 4 | date: +%Y-%m-%d %H:%M:%S 5 | size: short 6 | sorting: 7 | dir-grouping: first 8 | -------------------------------------------------------------------------------- /nvim/init.lua: -------------------------------------------------------------------------------- 1 | require("core.options") 2 | require("core.keymaps") 3 | require("core.autocmds") 4 | 5 | local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" 6 | ---@diagnostic disable-next-line: undefined-field 7 | if not vim.uv.fs_stat(lazypath) then 8 | vim.fn.system({ 9 | "git", 10 | "clone", 11 | "--filter=blob:none", 12 | "https://github.com/folke/lazy.nvim.git", 13 | "--branch=stable", -- latest stable release 14 | lazypath, 15 | }) 16 | end 17 | ---@diagnostic disable-next-line: undefined-field 18 | vim.opt.rtp:prepend(lazypath) 19 | 20 | require("lazy").setup("plugins") 21 | -------------------------------------------------------------------------------- /nvim/lua/core/autocmds.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | 3 | local general_group = api.nvim_create_augroup('GeneralSettings', { clear = true }) 4 | 5 | api.nvim_create_autocmd('BufReadPost', { 6 | desc = 'Return to last cursor position', 7 | group = general_group, 8 | pattern = '*', 9 | callback = function() 10 | -- Skip `.git/COMMIT_EDITMSG` 11 | if vim.fn.expand('%:t') == 'COMMIT_EDITMSG' then return end 12 | local mark = vim.api.nvim_buf_get_mark(0, '"') 13 | local lcount = vim.api.nvim_buf_line_count(0) 14 | if mark[1] > 0 and mark[1] <= lcount then 15 | pcall(vim.api.nvim_win_set_cursor, 0, mark) 16 | end 17 | end, 18 | }) 19 | 20 | api.nvim_create_autocmd('BufWritePre', { 21 | desc = 'Trim trailing whitespaces on save', 22 | group = general_group, 23 | pattern = '*', 24 | callback = function() 25 | local save = vim.fn.winsaveview() 26 | vim.cmd([[keepjumps keeppatterns %s/\s\+$//e]]) 27 | vim.fn.winrestview(save) 28 | end, 29 | }) 30 | 31 | vim.api.nvim_create_autocmd("TextYankPost", { 32 | desc = "Highlight on yank", 33 | group = general_group, 34 | callback = function() 35 | vim.highlight.on_yank({ timeout = 300 }) 36 | end, 37 | }) 38 | 39 | vim.api.nvim_create_autocmd({ "BufWritePre" }, { 40 | desc = "Auto create parent dir when saving a file", 41 | group = general_group, 42 | callback = function(event) 43 | if event.match:match("^%w%w+://") then 44 | return 45 | end 46 | local file = vim.loop.fs_realpath(event.match) or event.match 47 | local dir = vim.fn.fnamemodify(file, ":p:h") 48 | if vim.fn.isdirectory(dir) == 0 then 49 | if vim.fn.confirm("Create directory: " .. dir .. "?", "&Yes\n&No") == 1 then 50 | vim.fn.mkdir(dir, "p") 51 | end 52 | end 53 | end, 54 | }) 55 | 56 | vim.api.nvim_create_autocmd({ "VimResized" }, { 57 | desc = "Equalize window sizes when vim is resized", 58 | group = general_group, 59 | callback = function() 60 | vim.cmd("tabdo wincmd =") 61 | end, 62 | }) 63 | 64 | vim.api.nvim_create_autocmd("BufReadPost", { 65 | desc = "Prompt to resolve symlinks when opening files", 66 | group = general_group, 67 | callback = function(event) 68 | local file = vim.api.nvim_buf_get_name(event.buf) 69 | local real_file = vim.fn.resolve(file) 70 | if real_file ~= file then 71 | if vim.fn.confirm("File is a symlink. Resolve to original file?\n" .. real_file, "&Yes\n&No") == 1 then 72 | vim.schedule(function() 73 | vim.cmd("ResolveSymlink") 74 | end 75 | ) 76 | end 77 | end 78 | end, 79 | }) 80 | 81 | -- Filetype specific settings 82 | 83 | local filetype_group = api.nvim_create_augroup('FileTypeSettings', { clear = true }) 84 | 85 | -- Update formatoptions for all filetypes 86 | vim.api.nvim_create_autocmd("FileType", { 87 | group = filetype_group, 88 | pattern = "*", 89 | callback = function() 90 | -- Use vim.schedule to ensure that the autocmd runs after the default 91 | -- formatoptions are set 92 | vim.schedule(function() 93 | vim.opt_local.formatoptions = vim.opt_local.formatoptions 94 | - "t" -- Do not auto-wrap text using textwidth 95 | - "c" -- Do not auto-wrap comments using textwidth 96 | - "o" -- Do not insert comment leader after hitting o/O 97 | + "r" -- Automatically insert the comment leader after hitting Enter 98 | end) 99 | end, 100 | }) 101 | 102 | vim.api.nvim_create_autocmd("FileType", { 103 | desc = "Close some filetypes with ", 104 | group = filetype_group, 105 | pattern = { 106 | "help", 107 | "lspinfo", 108 | "man", 109 | "notify", 110 | "qf", 111 | "startuptime", 112 | "checkhealth", 113 | "gitsigns-blame", 114 | }, 115 | callback = function(event) 116 | vim.bo[event.buf].buflisted = false 117 | vim.keymap.set("n", "q", "close", { buffer = event.buf, silent = true }) 118 | end, 119 | }) 120 | 121 | api.nvim_create_autocmd('FileType', { 122 | desc = 'Python autocmd', 123 | group = filetype_group, 124 | pattern = { 'python', 'pyrex' }, 125 | callback = function() 126 | -- Fold based on indentation 127 | vim.opt_local.foldmethod = 'indent' 128 | vim.opt_local.foldlevel = 99 129 | -- Set macro 'p' to print a debug message 130 | local esc = vim.api.nvim_replace_termcodes('', true, true, true) 131 | vim.fn.setreg('p', 'yoprint("=== "' .. esc .. 'PA, )' .. esc .. 'P') 132 | end 133 | }) 134 | 135 | api.nvim_create_autocmd('FileType', { 136 | desc = 'C/C++: Use //-style comments', 137 | group = filetype_group, 138 | pattern = { 'c', 'cpp' }, 139 | callback = function() 140 | vim.opt_local.commentstring = '// %s' 141 | end 142 | }) 143 | -------------------------------------------------------------------------------- /nvim/lua/core/keymaps.lua: -------------------------------------------------------------------------------- 1 | local map = vim.keymap.set 2 | 3 | -- Leader configuration 4 | vim.g.mapleader = ' ' 5 | vim.g.maplocalleader = '\\' 6 | 7 | -- Buffer management 8 | map('n', 'be', 'enew', { desc = 'Create new buffer' }) 9 | map('n', 'x', 'bd', { desc = 'Delete current buffer' }) 10 | map('n', 'bw', 'w', { desc = 'Save current buffer' }) 11 | map({ 'n', 'x' }, '', 'bn', { desc = 'Next buffer' }) 12 | map({ 'n', 'x' }, ']b', 'bn', { desc = 'Next buffer' }) 13 | map({ 'n', 'x' }, '', 'bp', { desc = 'Previous buffer' }) 14 | map({ 'n', 'x' }, '[b', 'bp', { desc = 'Previous buffer' }) 15 | map('n', 'bl', '', { desc = 'Switch to last buffer' }) 16 | 17 | -- Tab management 18 | map('n', 'te', 'tabnew', { desc = 'Create new tab' }) 19 | map({ 'n', 'x' }, 'tn', 'tabn', { desc = 'Next tab' }) 20 | map({ 'n', 'x' }, ']t', 'tabn', { desc = 'Next tab' }) 21 | map({ 'n', 'x' }, 'tp', 'tabp', { desc = 'Previous tab' }) 22 | map({ 'n', 'x' }, '[t', 'tabp', { desc = 'Previous tab' }) 23 | map('n', 'tx', 'tabclose', { desc = 'Close current tab' }) 24 | 25 | -- Window management 26 | map('n', 'wv', 'v', { desc = 'Vertical split window' }) 27 | map('n', 'wh', 's', { desc = 'Horizontal split window' }) 28 | map('n', 'we', '=', { desc = 'Equalize window sizes' }) 29 | map('n', 'wx', 'c', { desc = 'Close current window' }) 30 | map('n', 'ww', 'p', { desc = 'Previous window' }) 31 | map('n', '', 'h', { desc = 'Move to left window' }) 32 | map('n', '', 'l', { desc = 'Move to right window' }) 33 | map('n', '', 'j', { desc = 'Move to lower window' }) 34 | map('n', '', 'k', { desc = 'Move to upper window' }) 35 | 36 | -- Switch quickfix 37 | map("n", "[q", vim.cmd.cprev, { desc = "Previous quickfix" }) 38 | map("n", "]q", vim.cmd.cnext, { desc = "Next quickfix" }) 39 | 40 | -- UI toggles 41 | map('n', 'uh', 'set hlsearch!', { desc = 'Toggle search highlight' }) 42 | map('n', 'un', function() 43 | -- Toggle between number/relativenumber/none 44 | if vim.wo.relativenumber then 45 | vim.wo.relativenumber = false 46 | vim.wo.number = true 47 | elseif vim.wo.number then 48 | vim.wo.number = false 49 | else 50 | vim.wo.relativenumber = true 51 | end 52 | end, { desc = 'Toggle line number mode' }) 53 | 54 | map('n', 'uw', 'set wrap!', { desc = 'Toggle line wrapping' }) 55 | map('n', 'us', 'set spell!', { desc = 'Toggle spell checking' }) 56 | 57 | -- Text manipulation 58 | map('x', '<', '', '>gv', { remap = true, desc = 'Indent right (keep selection)' }) 60 | map('v', '', ":m '>+1gv=gv", { desc = 'Move line(s) down' }) 61 | map('v', '', ":m '<-2gv=gv", { desc = 'Move line(s) up' }) 62 | 63 | -- System clipboard 64 | map({ 'n', 'v' }, 'y', '"+y', { desc = 'Yank to system clipboard' }) 65 | 66 | -- Make n/N direction consistent regardless of / or ? search 67 | map("n", "n", "'Nn'[v:searchforward].'zv'", { expr = true, desc = "Next Search Result" }) 68 | map("x", "n", "'Nn'[v:searchforward]", { expr = true, desc = "Next Search Result" }) 69 | map("o", "n", "'Nn'[v:searchforward]", { expr = true, desc = "Next Search Result" }) 70 | map("n", "N", "'nN'[v:searchforward].'zv'", { expr = true, desc = "Prev Search Result" }) 71 | map("x", "N", "'nN'[v:searchforward]", { expr = true, desc = "Prev Search Result" }) 72 | map("o", "N", "'nN'[v:searchforward]", { expr = true, desc = "Prev Search Result" }) 73 | 74 | -- Command line mappings 75 | -- Ctrl-Y to edit command line 76 | vim.cmd([[set cedit=\]]) 77 | -- Emacs-style navigation 78 | map('c', '', '', { desc = 'Start of line' }) 79 | map('c', '', '', { desc = 'Move left' }) 80 | map('c', '', '', { desc = 'Delete character' }) 81 | map('c', '', '', { desc = 'End of line' }) 82 | map('c', '', '', { desc = 'Move right' }) 83 | map('c', '', '', { desc = 'Next command' }) 84 | map('c', '', '', { desc = 'Previous command' }) 85 | map('c', '', '', { desc = 'Back one word' }) 86 | map('c', '', '', { desc = 'Forward one word' }) 87 | map('c', '', '=expand("%:p:h")."/"', { desc = 'Insert directory path' }) 88 | map('c', '', '=expand("%:p")', { desc = 'Insert file path' }) 89 | 90 | -- Terminal Mappings 91 | map("t", "", "", { desc = "Enter Normal Mode" }) 92 | map("t", "", "wincmd h", { desc = "Go to left window" }) 93 | map("t", "", "wincmd j", { desc = "Go to lower window" }) 94 | map("t", "", "wincmd k", { desc = "Go to upper window" }) 95 | map("t", "", "wincmd l", { desc = "Go to right window" }) 96 | 97 | -- Esc to clear search highlights 98 | map({ "i", "n", "s" }, "", function() 99 | vim.cmd("noh") 100 | return "" 101 | end, { expr = true, desc = "Escape and clear hlsearch" }) 102 | 103 | -- Clear search, diff update and redraw 104 | map( 105 | "n", 106 | "ur", 107 | "nohlsearchdiffupdatenormal! ", 108 | { desc = "Redraw / clear hlsearch / diff update" } 109 | ) 110 | 111 | -- Custom commands 112 | vim.api.nvim_create_user_command('W', 'w !sudo tee % > /dev/null', { 113 | desc = 'Write file with sudo privileges' 114 | }) 115 | 116 | -- Command to resolve symlinks 117 | vim.api.nvim_create_user_command('ResolveSymlink', function() 118 | local bufnr = vim.api.nvim_get_current_buf() 119 | local file = vim.api.nvim_buf_get_name(bufnr) 120 | local real_file = vim.fn.resolve(file) 121 | 122 | if real_file ~= file then 123 | -- Use absolute paths and completely wipe the buffer 124 | real_file = vim.fn.fnamemodify(real_file, ':p') 125 | vim.cmd('bwipeout! ' .. bufnr) 126 | -- Force reload from disk 127 | vim.cmd('edit! ' .. vim.fn.fnameescape(real_file)) 128 | end 129 | end, { desc = 'Close symlinked buffer and open the original file' }) 130 | -------------------------------------------------------------------------------- /nvim/lua/core/options.lua: -------------------------------------------------------------------------------- 1 | local opt = vim.opt 2 | 3 | ------------------- 4 | -- General settings 5 | ------------------- 6 | 7 | -- Show line numbers 8 | opt.number = true 9 | 10 | -- Show relative line numbers 11 | opt.relativenumber = true 12 | 13 | -- Show incomplete commands at bottom 14 | opt.showcmd = true 15 | 16 | -- Time to wait for mapped sequence 17 | opt.timeoutlen = 500 18 | 19 | -- Disable mouse by default 20 | opt.mouse = '' 21 | 22 | -- Enable spell checking 23 | opt.spell = true 24 | 25 | -- Set spellcheck language 26 | opt.spelllang = 'en_us' 27 | 28 | -- Confirm to save changes before exiting modified buffer 29 | opt.confirm = true 30 | 31 | -- Decrease update time to make features like git signs, code diagnostics, 32 | -- and swap file updates more responsive 33 | opt.updatetime = 1000 34 | 35 | -------------------------- 36 | -- User Interface settings 37 | -------------------------- 38 | 39 | -- Set 7 lines to the cursor - when moving vertically using j/k 40 | opt.scrolloff = 7 41 | 42 | -- Turn on the wild menu 43 | opt.wildmenu = true 44 | 45 | -- Completion behavior: longest:full,full 46 | opt.wildmode = 'longest:full,full' 47 | 48 | -- Ignore compiled files 49 | opt.wildignore = { 50 | '*.o', '*~', '*.pyc', '*.pyo', '*.class', '*.swp', 51 | vim.fn.has('win32') == 1 and '.git\\*,.hg\\*,.svn\\*' or '*/.git/*,*/.hg/*,*/.svn/*,*/.DS_Store' 52 | } 53 | 54 | -- Always show current position 55 | opt.ruler = true 56 | 57 | -- Highlight current line 58 | opt.cursorline = true 59 | 60 | -- Hide cmdline unless needed 61 | opt.cmdheight = 0 62 | 63 | -- Configure backspace so it acts as it should act 64 | opt.backspace = 'eol,start,indent' 65 | 66 | -- Allow cursor to wrap lines 67 | opt.whichwrap:append('<,>,h,l') 68 | 69 | -- Ignore case when searching 70 | opt.ignorecase = true 71 | 72 | -- When searching try to be smart about cases 73 | opt.smartcase = true 74 | 75 | -- Highlight search results 76 | opt.hlsearch = true 77 | 78 | -- Makes search act like search in modern browsers 79 | opt.incsearch = true 80 | 81 | -- Enable true color support 82 | opt.termguicolors = true 83 | 84 | -- Always show one status line across all windows 85 | opt.laststatus = 3 86 | 87 | -- Always show tabline 88 | opt.showtabline = 2 89 | 90 | -- New horizontal splits below current 91 | opt.splitbelow = true 92 | 93 | -- New vertical splits right of current 94 | opt.splitright = true 95 | 96 | -- Merge signcolumn and number column 97 | opt.signcolumn = 'number' 98 | 99 | -- Preview substitutions in a split window 100 | opt.inccommand = 'split' 101 | 102 | -- W hides “written” messages when saving a file (“filename written”). 103 | -- I hides the intro screen that appears on startup. 104 | -- c hides completion messages like “match 1 of 2”. 105 | -- C hides messages while scanning for completion items. 106 | opt.shortmess:append({ W = true, I = true, c = true, C = true }) 107 | 108 | -- Override special fill chars 109 | opt.fillchars = { 110 | diff = "╱", 111 | } 112 | 113 | ------------------------- 114 | -- Files/backups settings 115 | ------------------------- 116 | 117 | -- Persistent undo history 118 | opt.undofile = true 119 | 120 | --------------------- 121 | -- Text/tabs settings 122 | --------------------- 123 | 124 | -- Use spaces instead of tabs 125 | opt.expandtab = true 126 | 127 | -- Smart tab handling 128 | opt.smarttab = true 129 | 130 | -- 1 tab == 4 spaces 131 | opt.shiftwidth = 4 132 | opt.tabstop = 4 133 | 134 | -- round indentation with `>`/`<` to shiftwidth 135 | opt.shiftround = true 136 | -- Number of space inserted for indentation, 137 | -- when zero the 'tabstop' value will be used 138 | opt.shiftwidth = 0 139 | 140 | -- Wrap indent to match line start 141 | opt.breakindent = true 142 | 143 | -- Automatically adjusts indentation for new lines based on programming syntax 144 | opt.smartindent = true 145 | 146 | -- Ensure lines break only at specific characters (like spaces or hyphens) 147 | opt.linebreak = true 148 | 149 | -- Wrap long lines 150 | opt.wrap = true 151 | 152 | -- Show line wrap indicator 153 | opt.showbreak = '↪ ' 154 | 155 | -- Use histogram algorithm and line matching for more accurate diffs 156 | opt.diffopt = vim.list_extend( 157 | opt.diffopt:get(), 158 | { "algorithm:histogram", "linematch:60" } 159 | ) 160 | -------------------------------------------------------------------------------- /nvim/lua/core/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local _last_executed_fn = nil 4 | 5 | _G.repeat_last_fn = function() 6 | if _last_executed_fn ~= nil then 7 | _last_executed_fn() 8 | end 9 | end 10 | 11 | -- Make the input function dot-repeatable. 12 | -- Reference: https://gist.github.com/kylechui/a5c1258cd2d86755f97b10fc921315c3 13 | M.dot_repeatable_fn = function(input_fn) 14 | local function wrapper_fn() 15 | _last_executed_fn = input_fn 16 | vim.go.operatorfunc = "v:lua.repeat_last_fn" 17 | return "g@l" 18 | end 19 | return wrapper_fn 20 | end 21 | 22 | -- Make the given lazy.nvim-style keymap options dot-repeatable 23 | M.dot_repeatable_keymap = function(keymap_opts) 24 | assert(type(keymap_opts[2]) == "function", "The rhs must be a function") 25 | keymap_opts[2] = M.dot_repeatable_fn(keymap_opts[2]) 26 | keymap_opts.expr = true 27 | if keymap_opts.desc ~= nil then 28 | keymap_opts.desc = keymap_opts.desc .. " (repeatable)" 29 | end 30 | return keymap_opts 31 | end 32 | 33 | return M 34 | -------------------------------------------------------------------------------- /nvim/lua/plugins/basic.lua: -------------------------------------------------------------------------------- 1 | local session_manaer_keys = { 2 | { "sl", function() require("session_manager").load_current_dir_session() end, desc = "Load CWD session" }, 3 | { "sL", function() require("session_manager").load_session(false) end, desc = "Load session" }, 4 | { "sr", function() require("session_manager").load_last_session() end, desc = "Load most recent session" }, 5 | { "sd", function() require("session_manager").delete_current_dir_session() end, desc = "Delete CWD session" }, 6 | { "sD", function() require("session_manager").delete_session() end, desc = "Delete session" }, 7 | } 8 | 9 | local function session_manager_opts() 10 | local config = require('session_manager.config') 11 | return { 12 | autoload_mode = config.AutoloadMode.Disabled, 13 | autosave_ignore_dirs = { "~" }, 14 | } 15 | end 16 | 17 | return { 18 | { 19 | "nvim-lua/plenary.nvim", 20 | }, 21 | { 22 | "Shatur/neovim-session-manager", 23 | event = "VeryLazy", 24 | commands = { "SessionManager" }, 25 | keys = session_manaer_keys, 26 | opts = session_manager_opts, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /nvim/lua/plugins/blink.lua: -------------------------------------------------------------------------------- 1 | ---@module 'blink.cmp' 2 | ---@type blink.cmp.Config 3 | local blink_opts = { 4 | keymap = { 5 | preset = 'default', 6 | [''] = { 'show_and_insert', 'select_prev', 'fallback_to_mappings' }, 7 | [''] = { 'show_and_insert', 'select_next', 'fallback_to_mappings' }, 8 | [''] = { 'accept', 'fallback' }, 9 | [''] = { 10 | function() return require("plugins.copilot").copilot_accept() end, 11 | 'snippet_forward', 12 | 'fallback', 13 | }, 14 | [''] = { 15 | function() return require("plugins.copilot").copilot_accept() end, 16 | 'select_and_accept', 17 | 'fallback', 18 | }, 19 | }, 20 | cmdline = { 21 | keymap = { 22 | preset = 'cmdline', 23 | [''] = { 'show_and_insert', 'select_prev', 'fallback_to_mappings' }, 24 | [''] = { 'show_and_insert', 'select_next', 'fallback_to_mappings' }, 25 | [''] = { 'select_and_accept', 'fallback' }, 26 | }, 27 | sources = function() 28 | local type = vim.fn.getcmdtype() 29 | -- Search forward and backward 30 | if type == '/' or type == '?' then return { 'buffer' } end 31 | -- Command line 32 | if type == ':' then return { 'cmdline', 'buffer' } end 33 | -- Input (vim.fn.input()) 34 | if type == '@' then return { 'buffer' } end 35 | return {} 36 | end, 37 | completion = { 38 | menu = { auto_show = false }, 39 | ghost_text = { enabled = true }, 40 | }, 41 | }, 42 | appearance = { 43 | nerd_font_variant = 'mono' 44 | }, 45 | completion = { 46 | accept = { 47 | auto_brackets = { 48 | -- Whether to auto-insert brackets for functions 49 | enabled = false, 50 | } 51 | }, 52 | documentation = { 53 | auto_show = true, 54 | window = { border = 'single' } 55 | }, 56 | menu = { 57 | max_height = 15, 58 | border = 'single', 59 | draw = { 60 | columns = { { 'kind_icon' }, { 'label', 'label_description' }, { 'source_name' } }, 61 | }, 62 | }, 63 | }, 64 | signature = { 65 | enabled = true, 66 | window = { 67 | border = 'single', 68 | show_documentation = true, 69 | } 70 | }, 71 | sources = { 72 | default = { 73 | 'lsp', 74 | 'path', 75 | 'snippets', 76 | 'buffer', 77 | 'spell', 78 | "avante_commands", 79 | "avante_mentions", 80 | "avante_files", 81 | }, 82 | providers = { 83 | buffer = { 84 | opts = { 85 | -- Make the buffer source include all normal buffers. 86 | get_bufnrs = function() 87 | return vim.tbl_filter(function(bufnr) 88 | return vim.bo[bufnr].buftype == '' 89 | end, vim.api.nvim_list_bufs()) 90 | end 91 | } 92 | }, 93 | path = { 94 | opts = { 95 | -- Make the path source relative to the current working directory, 96 | -- instead of the buffer directory. 97 | get_cwd = function(_) 98 | return vim.fn.getcwd() 99 | end, 100 | }, 101 | }, 102 | spell = { 103 | name = 'Spell', 104 | module = 'blink-cmp-spell', 105 | score_offset = -10, 106 | }, 107 | avante_commands = { 108 | name = "avante_commands", 109 | module = "blink.compat.source", 110 | score_offset = 90, 111 | opts = {}, 112 | }, 113 | avante_files = { 114 | name = "avante_files", 115 | module = "blink.compat.source", 116 | score_offset = 100, 117 | opts = {}, 118 | }, 119 | avante_mentions = { 120 | name = "avante_mentions", 121 | module = "blink.compat.source", 122 | score_offset = 1000, 123 | opts = {}, 124 | }, 125 | } 126 | }, 127 | fuzzy = { implementation = "prefer_rust_with_warning" } 128 | } 129 | 130 | return { 131 | { 132 | 'saghen/blink.cmp', 133 | event = { 134 | 'InsertEnter', 135 | 'CmdlineEnter', 136 | }, 137 | version = '*', 138 | opts = blink_opts, 139 | opts_extend = { 'sources.default' }, 140 | dependencies = { 141 | 'rafamadriz/friendly-snippets', 142 | 'ribru17/blink-cmp-spell', 143 | { 144 | 'saghen/blink.compat', 145 | version = '*', 146 | lazy = true, 147 | opts = {}, 148 | config = function() 149 | -- monkeypatch cmp.ConfirmBehavior for Avante 150 | require("cmp").ConfirmBehavior = { 151 | Insert = "insert", 152 | Replace = "replace", 153 | } 154 | end, 155 | }, 156 | }, 157 | }, 158 | } 159 | -------------------------------------------------------------------------------- /nvim/lua/plugins/copilot.lua: -------------------------------------------------------------------------------- 1 | local copilot_keys = { 2 | { 3 | "as", 4 | function() require("copilot.suggestion").toggle_auto_trigger() end, 5 | desc = "Copilot: Toggle auto suggestion", 6 | }, 7 | } 8 | 9 | local copilot_opts = { 10 | copilot_model = "gpt-4o-copilot", 11 | suggestion = { 12 | keymap = { 13 | accept = false, 14 | next = "", 15 | prev = "", 16 | dismiss = "", 17 | }, 18 | }, 19 | filetypes = { 20 | gitcommit = true, 21 | } 22 | } 23 | 24 | local function copilot_accept() 25 | if require("copilot.suggestion").is_visible() then 26 | require("copilot.suggestion").accept() 27 | return true 28 | else 29 | return false 30 | end 31 | end 32 | 33 | local copilot = { 34 | "zbirenbaum/copilot.lua", 35 | cmd = "Copilot", 36 | event = "InsertEnter", 37 | keys = copilot_keys, 38 | opts = copilot_opts, 39 | } 40 | 41 | local function save_copilot_chat(name) 42 | -- Save current chat 43 | require("CopilotChat").save(name) 44 | 45 | -- Cleanup old saves 46 | local max_saves = 20 47 | local save_dir = vim.fn.stdpath("data") .. "/copilotchat_history/" 48 | 49 | local files = vim.fn.glob(save_dir .. "chat_*", false, true) 50 | table.sort(files, function(a, b) 51 | return vim.fn.getftime(a) > vim.fn.getftime(b) 52 | end) 53 | 54 | -- Remove oldest files if exceeding max_saves 55 | for i = max_saves + 1, #files do 56 | vim.fn.delete(files[i]) 57 | end 58 | end 59 | 60 | local CHAT_TITLE_PROMPT = [[ 61 | Generate a short title (maximum 10 words) for the following chat. 62 | Use a filepath-friendly format, replace all spaces with underscores. 63 | Output only the title and nothing else in your response. 64 | 65 | ``` 66 | %s 67 | ``` 68 | ]] 69 | 70 | local function auto_save_copilot_chat(response, source) 71 | if vim.g.copilot_chat_title then 72 | save_copilot_chat(vim.g.copilot_chat_title) 73 | else 74 | -- use AI to generate chat title based on first AI response to user question 75 | require("CopilotChat").ask(vim.trim(CHAT_TITLE_PROMPT:format(response)), { 76 | callback = function(gen_response) 77 | vim.print("Generated chat title: " .. gen_response) 78 | -- Prefix the title with timestamp in format YYYYMMDD_HHMMSS 79 | local timestamp = os.date("%Y%m%d_%H%M%S") 80 | vim.g.copilot_chat_title = timestamp .. "_" .. vim.trim(gen_response) 81 | save_copilot_chat(vim.g.copilot_chat_title) 82 | end, 83 | -- disable updating chat buffer and history with this question 84 | headless = true, 85 | }) 86 | end 87 | end 88 | 89 | local function load_copilot_chat() 90 | require("snacks").picker({ 91 | title = "Copilot: Load chat", 92 | finder = function() 93 | -- Get list of json files from the chat save directory 94 | local save_dir = vim.fn.stdpath("data") .. "/copilotchat_history" 95 | local files = vim.fn.glob(save_dir .. "/*.json", false, true) 96 | -- Sort files by modification time 97 | table.sort(files, function(a, b) 98 | return vim.fn.getftime(a) > vim.fn.getftime(b) 99 | end) 100 | 101 | local choices = {} 102 | for _, file in ipairs(files) do 103 | local name = vim.fn.fnamemodify(file, ":t:r") 104 | table.insert(choices, { 105 | text = name, 106 | file = file, 107 | }) 108 | end 109 | return choices 110 | end, 111 | format = "text", 112 | confirm = function(picker, item) 113 | picker:close() 114 | local chat = require("CopilotChat") 115 | chat.load(item.text) 116 | chat.open() 117 | vim.g.copilot_chat_title = item.text 118 | end, 119 | preview = function(ctx) 120 | local file = io.open(ctx.item.file, "r") 121 | if not file then 122 | ctx.preview:set_lines({ "Unable to read file" }) 123 | return 124 | end 125 | 126 | local content = file:read("*a") 127 | file:close() 128 | 129 | local ok, messages = pcall(vim.json.decode, content, { 130 | luanil = { 131 | object = true, 132 | array = true, 133 | }, 134 | }) 135 | 136 | if not ok then 137 | ctx.preview:set_lines({ "vim.fn.json_decode error" }) 138 | return 139 | end 140 | 141 | local config = require("CopilotChat.config") 142 | local preview = { ctx.item.text, "", } 143 | for _, message in ipairs(messages or {}) do 144 | local header = message.role == "user" and config.question_header or config.answer_header 145 | table.insert(preview, header .. config.separator .. "\n") 146 | table.insert(preview, message.content .. "\n") 147 | end 148 | 149 | ctx.preview:highlight({ ft = "copilot-chat" }) 150 | ctx.preview:set_lines(preview) 151 | end, 152 | }) 153 | end 154 | 155 | local copilot_chat_keys = { 156 | { "", "", ft = "copilot-chat", desc = "Submit prompt", remap = true }, 157 | { 158 | "aa", 159 | function() 160 | -- If not in visual mode, clear the marks. 161 | -- So the chat selection will be empty. 162 | local mode = vim.api.nvim_get_mode().mode 163 | if not (mode:sub(1, 1) == "v" or mode:sub(1, 1) == "V" or mode == "\22") then 164 | local bufnr = vim.api.nvim_get_current_buf() 165 | vim.api.nvim_buf_set_mark(bufnr, '<', 0, 0, {}) 166 | vim.api.nvim_buf_set_mark(bufnr, '>', 0, 0, {}) 167 | end 168 | -- Toggle CopilotChat 169 | local chat = require("CopilotChat") 170 | chat.toggle() 171 | end, 172 | desc = "CopilotChat: Toggle chat window", 173 | mode = { "n", "x" }, 174 | }, 175 | { 176 | "aq", 177 | function() 178 | vim.ui.input({ prompt = "Ask AI: " }, function(input) 179 | if input ~= nil and input ~= "" then 180 | require("CopilotChat").ask(input) 181 | end 182 | end) 183 | end, 184 | desc = "CopilotChat: Quick chat", 185 | mode = { "n", "x" }, 186 | }, 187 | { 188 | "ax", 189 | function() 190 | vim.g.copilot_chat_title = nil 191 | return require("CopilotChat").reset() 192 | end, 193 | desc = "CopilotChat: Reset", 194 | mode = { "n", "v" }, 195 | }, 196 | { 197 | "ap", 198 | function() 199 | require("CopilotChat").select_prompt() 200 | end, 201 | desc = "CopilotChat: Prompt actions", 202 | mode = { "n", "x" }, 203 | }, 204 | { 205 | "aL", 206 | load_copilot_chat, 207 | desc = "CopilotChat: Load chat", 208 | ft = "copilot-chat", 209 | }, 210 | { 211 | "", 212 | function() 213 | require("CopilotChat").stop() 214 | end, 215 | desc = "CopilotChat: stop", 216 | ft = "copilot-chat", 217 | }, 218 | } 219 | 220 | local function setup_copilot_chat() 221 | require("CopilotChat").setup({ 222 | model = "claude-3.7-sonnet-thought", 223 | question_header = " User ", 224 | answer_header = " Copilot ", 225 | error_header = "  Error ", 226 | selection = require("CopilotChat.select").visual, 227 | mappings = { 228 | close = { 229 | insert = '', -- disable to close window 230 | }, 231 | reset = { -- disable to reset chat. 232 | normal = '', 233 | insert = '' 234 | }, 235 | }, 236 | prompts = { 237 | BetterNamings = "Please provide better names for the following variables and functions.", 238 | Summarize = "Please summarize the following text.", 239 | Spelling = "Please correct any grammar and spelling errors in the following text.", 240 | Wording = "Please improve the grammar and wording of the following text.", 241 | Concise = "Please rewrite the following text to make it more concise.", 242 | }, 243 | callback = auto_save_copilot_chat, 244 | }) 245 | 246 | vim.api.nvim_create_autocmd("BufEnter", { 247 | pattern = "copilot-chat", 248 | callback = function() 249 | vim.opt_local.relativenumber = false 250 | vim.opt_local.number = false 251 | end, 252 | }) 253 | end 254 | 255 | 256 | local copilot_chat = { 257 | "CopilotC-Nvim/CopilotChat.nvim", 258 | version = "*", 259 | keys = copilot_chat_keys, 260 | cmd = { 261 | "CopilotChat", 262 | }, 263 | config = setup_copilot_chat, 264 | } 265 | 266 | local avante = { 267 | "yetone/avante.nvim", 268 | event = "VeryLazy", 269 | version = false, 270 | opts = { 271 | provider = "copilot", 272 | -- auto_suggestions_provider = "copilot", 273 | file_selector = { 274 | provider = "snacks", 275 | }, 276 | copilot = { 277 | model = "claude-3.7-sonnet", 278 | }, 279 | windows = { 280 | width = 40, 281 | sidebar_header = { 282 | rounded = false, 283 | }, 284 | ask = { 285 | start_insert = false, 286 | }, 287 | }, 288 | hints = { 289 | enabled = false, 290 | }, 291 | }, 292 | build = "make", 293 | dependencies = { 294 | "nvim-treesitter/nvim-treesitter", 295 | "stevearc/dressing.nvim", 296 | "nvim-lua/plenary.nvim", 297 | "MunifTanjim/nui.nvim", 298 | "nvim-tree/nvim-web-devicons", 299 | "zbirenbaum/copilot.lua", 300 | }, 301 | } 302 | 303 | local use_copilot = os.getenv("NVIM_USE_COPILOT") ~= "0" 304 | if not use_copilot then 305 | return {} 306 | end 307 | 308 | local use_avante = os.getenv("NVIM_USE_AVANTE") ~= "0" 309 | 310 | if use_avante then 311 | return { 312 | copilot, 313 | avante, 314 | copilot_accept = copilot_accept, 315 | } 316 | else 317 | return { 318 | copilot, 319 | copilot_chat, 320 | copilot_accept = copilot_accept, 321 | } 322 | end 323 | -------------------------------------------------------------------------------- /nvim/lua/plugins/dap.lua: -------------------------------------------------------------------------------- 1 | local function Debug(opts) 2 | local args = vim.fn.split(opts.args, " ", true) 3 | -- remove empty strings from args 4 | for i = #args, 1, -1 do 5 | if args[i] == "" then 6 | table.remove(args, i) 7 | end 8 | end 9 | local dap = require("dap") 10 | if #args == 0 then 11 | dap.continue() 12 | return 13 | end 14 | local program = args[1] 15 | local program_args = { unpack(args, 2) } 16 | 17 | local ft = vim.bo.filetype 18 | local configs = dap.configurations[ft] 19 | if configs == nil then 20 | print("Filetype \"" .. ft .. "\" has no dap configs") 21 | return 22 | end 23 | local dap_config = configs[1] 24 | if #configs > 1 then 25 | vim.ui.select( 26 | configs, 27 | { 28 | prompt = "Select config to run: ", 29 | format_item = function(config) 30 | return config.name 31 | end 32 | }, 33 | function(config) 34 | dap_config = config 35 | end 36 | ) 37 | end 38 | dap_config = vim.deepcopy(dap_config) 39 | ---@diagnostic disable-next-line 40 | dap_config.program = program 41 | ---@diagnostic disable-next-line 42 | dap_config.args = program_args 43 | dap.run(dap_config) 44 | end 45 | 46 | local function setup_dap(_, _) 47 | local dap = require('dap') 48 | dap.adapters.codelldb = { 49 | type = 'server', 50 | port = "${port}", 51 | executable = { 52 | command = 'codelldb', 53 | args = { "--port", "${port}" }, 54 | } 55 | } 56 | dap.configurations.cpp = { 57 | { 58 | name = "Launch file", 59 | type = "codelldb", 60 | request = "launch", 61 | program = function() 62 | return vim.fn.input('Path to executable: ', vim.fn.getcwd() .. '/', 'file') 63 | end, 64 | args = function() 65 | local args = vim.fn.input('Arguments: ') 66 | return vim.fn.split(args, " ", true) 67 | end, 68 | cwd = '${workspaceFolder}', 69 | stopOnEntry = false, 70 | }, 71 | } 72 | 73 | vim.api.nvim_create_user_command('Debug', Debug, { nargs = '?' }) 74 | end 75 | 76 | local function dap_keys() 77 | local _repeatable = require("core.utils").dot_repeatable_keymap 78 | local _dap = function() return require("dap") end 79 | return { 80 | { "dd", function() _dap().continue() end, desc = "Start/conintue debugger" }, 81 | { "dt", function() _dap().terminate() end, desc = "Terminate debugger" }, 82 | { "db", function() _dap().toggle_breakpoint() end, desc = "Toggle breakpoint" }, 83 | { "dB", function() _dap().list_breakpoints(true) end, desc = "List breakpoints" }, 84 | { "dC", function() _dap().set_breakpoint(vim.fn.input('Breakpoint condition: ')) end, desc = "Conditional breakpoint" }, 85 | { "dE", function() _dap().set_exception_breakpoints() end, desc = "Exception breakpoint" }, 86 | { "dl", function() _dap().run_last() end, desc = "Debug last" }, 87 | _repeatable({ "ds", function() _dap().step_over() end, desc = "Step over" }), 88 | _repeatable({ "di", function() _dap().step_into() end, desc = "Step into" }), 89 | _repeatable({ "do", function() _dap().step_out() end, desc = "Step out" }), 90 | { "dc", function() _dap().run_to_cursor() end, desc = "Continue execution until current cursor" }, 91 | { "dF", function() _dap().restart_frame() end, desc = "Restart curent frame" }, 92 | _repeatable({ "dU", function() _dap().up() end, desc = "Go up in stacktrace" }), 93 | _repeatable({ "dD", function() _dap().down() end, desc = "Go down in stacktrace" }), 94 | { "dp", function() _dap().pause() end, desc = "Pause" }, 95 | } 96 | end 97 | 98 | local function setup_dapui(_, _) 99 | local dapui = require("dapui") 100 | dapui.setup() 101 | local dap = require("dap") 102 | dap.listeners.after.event_initialized["dapui_config"] = function() 103 | dapui.open({ reset = true }) 104 | end 105 | end 106 | 107 | local dapui_keys = { 108 | { "du", function() require('dapui').toggle() end, desc = "Toggle dap-ui" }, 109 | { "de", function() require('dapui').eval() end, desc = "Evaluate" }, 110 | } 111 | 112 | local function setup_dap_python() 113 | local dap_python = require("dap-python") 114 | dap_python.setup(vim.fn.stdpath('data') .. "/mason/packages/debugpy/venv/bin/python") 115 | dap_python.resolve_python = function() 116 | return 'python' 117 | end 118 | end 119 | 120 | local dap_python_keys = { 121 | { "d", "", desc = "+debug", ft = "python" }, 122 | { "dm", function() require('dap-python').test_method() end, desc = "Debug current method", ft = "python" }, 123 | { "dc", function() require('dap-python').test_class() end, desc = "Debug current class", ft = "python" }, 124 | } 125 | 126 | return { 127 | { 128 | "mfussenegger/nvim-dap", 129 | config = setup_dap, 130 | keys = dap_keys, 131 | dependencies = { 132 | { 133 | "mfussenegger/nvim-dap-python", 134 | config = setup_dap_python, 135 | keys = dap_python_keys, 136 | }, 137 | { 138 | "rcarriga/nvim-dap-ui", 139 | config = setup_dapui, 140 | keys = dapui_keys, 141 | dependencies = { "mfussenegger/nvim-dap", "nvim-neotest/nvim-nio" }, 142 | }, 143 | }, 144 | }, 145 | } 146 | -------------------------------------------------------------------------------- /nvim/lua/plugins/editor.lua: -------------------------------------------------------------------------------- 1 | local tmux_navigator_keys = { 2 | { "", "TmuxNavigateLeft", desc = "TmuxNavigateLeft" }, 3 | { "", "TmuxNavigateDown", desc = "TmuxNavigateDown" }, 4 | { "", "TmuxNavigateUp", desc = "TmuxNavigateUp" }, 5 | { "", "TmuxNavigateRight", desc = "TmuxNavigateRight" }, 6 | { "", "TmuxNavigateLeft", mode = "t", desc = "TmuxNavigateLeft" }, 7 | { "", "TmuxNavigateDown", mode = "t", desc = "TmuxNavigateDown" }, 8 | { "", "TmuxNavigateUp", mode = "t", desc = "TmuxNavigateUp" }, 9 | { "", "TmuxNavigateRight", mode = "t", desc = "TmuxNavigateRight" }, 10 | } 11 | 12 | local function setup_whichkey(_, _) 13 | local wk = require("which-key") 14 | ---@diagnostic disable-next-line: missing-fields 15 | wk.setup({ 16 | preset = "modern", 17 | }) 18 | wk.add({ 19 | { "a", group = "ai" }, 20 | { "b", group = "buffers" }, 21 | { "c", group = "code" }, 22 | { "d", group = "debug" }, 23 | { "f", group = "find" }, 24 | { "fg", group = "git" }, 25 | { "g", group = "git" }, 26 | { "os", group = "search" }, 27 | { "s", group = "sessions" }, 28 | { "t", group = "tabs" }, 29 | { "u", group = "ui" }, 30 | { "w", group = "windows" }, 31 | }) 32 | end 33 | 34 | local flash_keys = { 35 | { "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash" }, 36 | { "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash Treesitter" }, 37 | { "r", mode = "o", function() require("flash").remote() end, desc = "Remote Flash" }, 38 | { "R", mode = { "o", "x" }, function() require("flash").treesitter_search() end, desc = "Treesitter Search" }, 39 | { "", mode = { "c" }, function() require("flash").toggle() end, desc = "Toggle Flash Search" }, 40 | } 41 | 42 | local mini_surround_opts = { 43 | mappings = { 44 | add = "gsa", -- Add surrounding in Normal and Visual modes 45 | delete = "gsd", -- Delete surrounding 46 | find = "gsf", -- Find surrounding (to the right) 47 | find_left = "gsF", -- Find surrounding (to the left) 48 | highlight = "gsh", -- Highlight surrounding 49 | replace = "gsr", -- Replace surrounding 50 | update_n_lines = "gsn", -- Update `n_lines` 51 | }, 52 | } 53 | 54 | return { 55 | { 56 | "christoomey/vim-tmux-navigator", 57 | keys = tmux_navigator_keys, 58 | init = function() 59 | vim.cmd([[ 60 | let g:tmux_navigator_no_mappings = 1 61 | ]]) 62 | end, 63 | }, 64 | { 65 | "tpope/vim-sleuth", 66 | event = "VeryLazy", 67 | }, 68 | { 69 | "ojroques/vim-oscyank", 70 | event = "VeryLazy", 71 | init = function() 72 | vim.cmd([[ 73 | let g:oscyank_silent = 1 74 | " Automatically copy text that was yanked to register +. 75 | autocmd TextYankPost * 76 | \ if v:event.operator is 'y' && v:event.regname is '+' | 77 | \ execute 'OSCYankRegister +' | 78 | \ endif 79 | ]]) 80 | end, 81 | }, 82 | { 83 | "folke/which-key.nvim", 84 | event = "VeryLazy", 85 | config = setup_whichkey, 86 | keys = { 87 | { 88 | "?", 89 | function() 90 | require("which-key").show({ global = false }) 91 | end, 92 | desc = "List buffer local keymaps", 93 | }, 94 | }, 95 | }, 96 | { 97 | "folke/flash.nvim", 98 | event = "VeryLazy", 99 | ---@type Flash.Config 100 | opts = {}, 101 | keys = flash_keys, 102 | }, 103 | { 104 | 'echasnovski/mini.cursorword', 105 | version = '*', 106 | event = "VeryLazy", 107 | opts = {}, 108 | }, 109 | { 110 | 'echasnovski/mini.splitjoin', 111 | version = '*', 112 | event = "VeryLazy", 113 | opts = {}, 114 | }, 115 | { 116 | "echasnovski/mini.surround", 117 | version = "*", 118 | event = "VeryLazy", 119 | opts = mini_surround_opts, 120 | keys = { 121 | { "gs", "+surround", mode = { "n", "x" } }, 122 | } 123 | }, 124 | } 125 | -------------------------------------------------------------------------------- /nvim/lua/plugins/formatting.lua: -------------------------------------------------------------------------------- 1 | return { 2 | "stevearc/conform.nvim", 3 | dependencies = { 4 | "WhoIsSethDaniel/mason-tool-installer.nvim", 5 | }, 6 | event = { "BufWritePre" }, 7 | cmd = { "ConformInfo" }, 8 | keys = { 9 | { 10 | "cf", 11 | function() 12 | require("conform").format({ async = true, lsp_fallback = true }) 13 | end, 14 | mode = "", 15 | desc = "Format file or range", 16 | }, 17 | { 18 | "cF", 19 | function() 20 | if vim.g.enable_auto_format then 21 | vim.g.enable_auto_format = false 22 | print("Auto-format off") 23 | else 24 | vim.g.enable_auto_format = true 25 | print("Auto-format on") 26 | end 27 | end, 28 | mode = "", 29 | desc = "Toogle auto-format", 30 | } 31 | }, 32 | opts = { 33 | formatters_by_ft = { 34 | python = { "black", "ruff" }, 35 | }, 36 | ---@diagnostic disable-next-line 37 | format_on_save = function(bufnr) 38 | if not vim.g.enable_auto_format then 39 | return 40 | end 41 | return { timeout_ms = 1000, lsp_fallback = true } 42 | end, 43 | log_level = vim.log.levels.DEBUG, 44 | }, 45 | init = function() 46 | vim.g.enable_auto_format = true 47 | vim.o.formatexpr = "v:lua.require'conform'.formatexpr()" 48 | end, 49 | } 50 | -------------------------------------------------------------------------------- /nvim/lua/plugins/git.lua: -------------------------------------------------------------------------------- 1 | local fugitive_keys = { 2 | { "gg", "Git", desc = "Git status" }, 3 | } 4 | 5 | local function setup_gitsigns() 6 | require('gitsigns').setup { 7 | signcolumn = false, 8 | numhl = true, 9 | on_attach = function(bufnr) 10 | local gs = package.loaded.gitsigns 11 | 12 | local function map(mode, l, r, desc, opts) 13 | opts = opts or {} 14 | opts.desc = desc 15 | opts.buffer = bufnr 16 | vim.keymap.set(mode, l, r, opts) 17 | end 18 | 19 | -- Navigation 20 | map('n', ']c', function() 21 | if vim.wo.diff then 22 | vim.cmd.normal({ ']c', bang = true }) 23 | else 24 | gs.nav_hunk('next') 25 | end 26 | end, "Next change/hunk", { expr = true }) 27 | 28 | map('n', '[c', function() 29 | if vim.wo.diff then 30 | vim.cmd.normal({ '[c', bang = true }) 31 | else 32 | gs.nav_hunk('prev') 33 | end 34 | end, "Previous change/hunk", { expr = true }) 35 | 36 | -- Actions 37 | map('n', 'gs', gs.stage_hunk, "Stage hunk") 38 | map('n', 'gu', gs.undo_stage_hunk, "Undo stage hunk") 39 | map('n', 'gr', gs.reset_hunk, "Reset hunk") 40 | map('v', 'gs', function() gs.stage_hunk { vim.fn.line('.'), vim.fn.line('v') } end, "Stage hunk") 41 | map('v', 'gr', function() gs.reset_hunk { vim.fn.line('.'), vim.fn.line('v') } end, "Reset hunk") 42 | map('n', 'gS', gs.stage_buffer, "Stage buffer") 43 | map('n', 'gR', gs.reset_buffer, "Reset buffer") 44 | map('n', 'gp', gs.preview_hunk, "Preview hunk") 45 | map('n', 'gb', function() gs.blame_line { full = true } end, "Blame line") 46 | map('n', 'gB', gs.blame, "Blame buffer") 47 | map('n', 'gq', gs.setqflist, "Show buffer hunks in quickfix") 48 | map('n', 'gQ', function() gs.setqflist('all') end, "Show all hunks in quickfix") 49 | 50 | -- Text object 51 | map({ 'o', 'x' }, 'ih', ':Gitsigns select_hunk', "Git hunk") 52 | end 53 | } 54 | end 55 | 56 | local function toggle_diffview() 57 | if vim.g.diffview_open then 58 | vim.cmd("DiffviewClose") 59 | return 60 | end 61 | 62 | -- Define the diff options with their corresponding action functions 63 | local diff_options = { 64 | { 65 | name = "Index (Working tree)", 66 | action = function() vim.cmd("DiffviewOpen") end 67 | }, 68 | { 69 | name = "Merge-base with master", 70 | action = function() 71 | vim.fn.jobstart({ 'git', 'merge-base', 'HEAD', 'master' }, { 72 | stdout_buffered = true, 73 | on_stdout = function(_, data) 74 | if data and data[1] and data[1] ~= "" then 75 | local merge_base = data[1]:gsub("%s+$", "") -- Trim whitespace 76 | vim.schedule(function() 77 | vim.cmd("DiffviewOpen " .. merge_base) 78 | end) 79 | end 80 | end, 81 | on_stderr = function(_, data) 82 | if data and data[1] and data[1] ~= "" then 83 | vim.schedule(function() 84 | vim.notify("Error getting merge-base: " .. table.concat(data, "\n"), vim.log.levels.ERROR) 85 | end) 86 | end 87 | end 88 | }) 89 | end 90 | }, 91 | { 92 | name = "Pick a commit", 93 | action = function() 94 | require("snacks.picker").git_log({ 95 | title = "Select commit for diff", 96 | confirm = function(picker, item) 97 | picker:close() 98 | vim.cmd("DiffviewOpen " .. item.commit) 99 | end, 100 | }) 101 | end 102 | }, 103 | { 104 | name = "Manually specify", 105 | action = function() vim.api.nvim_feedkeys(":DiffviewOpen ", "c", true) end 106 | } 107 | } 108 | 109 | -- Show the selector 110 | vim.ui.select(diff_options, { 111 | prompt = "Select git revision for diff:", 112 | format_item = function(item) return item.name end, 113 | }, function(choice, idx) 114 | if not choice then return end 115 | 116 | -- Execute the action function 117 | choice.action() 118 | end) 119 | end 120 | 121 | local diffview_keys = { 122 | { 'gd', mode = 'n', toggle_diffview, desc = "Toggle diff view" }, 123 | { 'gh', 'DiffviewFileHistory %', mode = 'n', desc = 'Current file history' }, 124 | { 'gh', ':DiffviewFileHistory', mode = 'x', desc = 'File range history' }, 125 | { 'gl', 'DiffviewFileHistory', mode = 'n', desc = 'Git log' }, 126 | } 127 | 128 | local diffview_opts = { 129 | file_panel = { 130 | win_config = { 131 | type = "split", 132 | position = "bottom", 133 | height = 12, 134 | }, 135 | }, 136 | hooks = { 137 | diff_buf_read = function(bufnr) 138 | -- Disable snacks.scroll 139 | vim.b[bufnr].snacks_scroll = false 140 | end, 141 | ---@diagnostic disable-next-line 142 | view_enter = function(view) 143 | -- Save the current view 144 | vim.g.diffview_open = true 145 | end, 146 | ---@diagnostic disable-next-line 147 | view_leave = function(view) 148 | vim.g.diffview_open = false 149 | end, 150 | } 151 | } 152 | 153 | local function setup_octo() 154 | local mappings = {} 155 | 156 | for _, buf_type in pairs({ "issue", "pull_request" }) do 157 | mappings[buf_type] = { 158 | reload = { lhs = "or" }, 159 | open_in_browser = { lhs = "ob" }, 160 | copy_url = { lhs = "oy" }, 161 | } 162 | end 163 | 164 | for _, buf_type in pairs({ "review_thread", "review_diff", "file_panel" }) do 165 | mappings[buf_type] = { 166 | -- Disable closing review tabs with ctrl-c. 167 | close_review_tab = { lhs = "" }, 168 | -- Use "tab" and "s-tab" to navigate between files. 169 | select_next_entry = { lhs = "" }, 170 | select_prev_entry = { lhs = "" }, 171 | } 172 | end 173 | 174 | require("octo").setup({ 175 | enable_builtin = true, 176 | picker = "snacks", 177 | default_merge_method = "squash", 178 | mappings = mappings, 179 | suppress_missing_scope = { 180 | projects_v2 = true, 181 | }, 182 | }) 183 | vim.treesitter.language.register('markdown', 'octo') 184 | 185 | local function set_which_key(buf) 186 | require('which-key').add({ 187 | buffer = buf, 188 | { "a", group = "assignee", icon = { icon = " ", color = "blue" }, }, 189 | { "c", group = "comment", icon = { icon = " ", color = "blue" }, }, 190 | { "g", group = "goto", icon = { icon = " ", color = "blue" }, }, 191 | { "i", group = "issue", icon = { icon = " ", color = "blue" }, }, 192 | { "l", group = "label", icon = { icon = "󰌕 ", color = "blue" }, }, 193 | { "o", group = "operation", icon = { icon = " ", color = "blue" }, }, 194 | { "p", group = "pr", icon = { icon = " ", color = "blue" }, }, 195 | { "r", group = "react", icon = { icon = "󰞅 ", color = "blue" }, }, 196 | { "s", group = "suggest", icon = { icon = "󱀡 ", color = "blue" }, }, 197 | { "v", group = "review", icon = { icon = " ", color = "blue" }, }, 198 | }) 199 | end 200 | 201 | -- For PR review buffers ("octo") and file panel buffers ("octo_panel"). 202 | -- NOTE: "octo://*" file name pattern can also match the PR review buffers. 203 | -- But that will lead to a weird behavior where the mappings 204 | -- are not usable until is pressed once. 205 | vim.api.nvim_create_autocmd("FileType", { 206 | pattern = { "octo", "octo_panel" }, 207 | callback = function(ev) set_which_key(ev.buf) end, 208 | }) 209 | 210 | -- For file review buffers. 211 | vim.api.nvim_create_autocmd("BufEnter", { 212 | pattern = { "octo://*/review/*" }, 213 | callback = function(ev) 214 | if not vim.b[ev.buf].octo_setup_done then 215 | vim.b[ev.buf].octo_setup_done = true 216 | --- Disable snacks.scroll as it conflicts with the comment buffers 217 | vim.b[ev.buf].snacks_scroll = false 218 | set_which_key(ev.buf) 219 | end 220 | end, 221 | }) 222 | end 223 | 224 | local octo_keys = { 225 | { 226 | "gO", 227 | "", 228 | desc = "+octo", 229 | }, 230 | { 231 | "gOp", 232 | "Octo pr search author:@me", 233 | desc = "Search repo: my PRs", 234 | }, 235 | { 236 | "gOr", 237 | "Octo pr search is:open assignee:@me", 238 | desc = "Search repo: PRs to review", 239 | }, 240 | { 241 | "gOi", 242 | "Octo issue search author:@me", 243 | desc = "Search repo: issues created by me", 244 | }, 245 | { 246 | "gOa", 247 | "Octo issue search is:open assignee:@me", 248 | desc = "Search repo: open issues assigned to me", 249 | }, 250 | { 251 | "gOP", 252 | "Octo search is:pr author:@me", 253 | desc = "Search global: my PRs", 254 | }, 255 | { 256 | "gOR", 257 | "Octo search is:pr is:open assignee:@me", 258 | desc = "Search global: PRs to review", 259 | }, 260 | { 261 | "gOI", 262 | "Octo search is:issue author:@me", 263 | desc = "Search global: issues created by me", 264 | }, 265 | { 266 | "gOA", 267 | "Octo search is:issue is:open assignee:@me", 268 | desc = "Search global: open issues assigned to me", 269 | }, 270 | } 271 | 272 | return { 273 | { 274 | "tpope/vim-fugitive", 275 | cmd = { "Git", "G" }, 276 | keys = fugitive_keys, 277 | }, 278 | { 279 | "lewis6991/gitsigns.nvim", 280 | event = { "BufReadPre", "BufNewFile" }, 281 | config = setup_gitsigns, 282 | }, 283 | { 284 | "sindrets/diffview.nvim", 285 | cmd = { 286 | "DiffviewOpen", 287 | "DiffviewFileHistory", 288 | }, 289 | keys = diffview_keys, 290 | opts = diffview_opts, 291 | }, 292 | { 293 | 'pwntester/octo.nvim', 294 | dependencies = { 295 | 'nvim-lua/plenary.nvim', 296 | 'nvim-tree/nvim-web-devicons', 297 | }, 298 | cmd = { "Octo" }, 299 | keys = octo_keys, 300 | config = setup_octo, 301 | }, 302 | } 303 | -------------------------------------------------------------------------------- /nvim/lua/plugins/lsp.lua: -------------------------------------------------------------------------------- 1 | -- Settings for each LSP server. 2 | local server_settings = { 3 | basedpyright = { 4 | -- Use ruff to organize imports. 5 | disableOrganizeImports = true, 6 | basedpyright = { 7 | analysis = { 8 | -- NOTE: `typeCheckingMode` and `diagnosticSeverityOverrides` doesn't seem to 9 | -- work on projects with a local pyrightconfig.json file 10 | -- 11 | -- Use a less-strict type checking mode. 12 | typeCheckingMode = "standard", 13 | autoSearchPaths = true, 14 | useLibraryCodeForTypes = true, 15 | diagnosticMode = "openFilesOnly", 16 | diagnosticSeverityOverrides = { 17 | -- Ignores "Variable not allowed in type expression". 18 | reportInvalidTypeForm = false, 19 | }, 20 | }, 21 | }, 22 | }, 23 | lua_ls = { 24 | Lua = { 25 | format = { 26 | enable = true, 27 | defaultConfig = { 28 | align_array_table = "false", 29 | align_continuous_assign_statement = "false", 30 | align_continuous_rect_table_field = "false", 31 | align_if_branch = "false", 32 | }, 33 | }, 34 | }, 35 | }, 36 | } 37 | 38 | 39 | local function setup_lspconfig(_, _) 40 | vim.lsp.set_log_level("warn") 41 | 42 | local float_win_opts = { border = 'single' } 43 | 44 | -- Global mappings. 45 | -- See `:help vim.diagnostic.*` for documentation on any of the below functions 46 | local keymap = vim.keymap.set 47 | keymap('n', 'cx', function() vim.diagnostic.open_float(float_win_opts) end, 48 | { desc = "Show diagnostics in a floating window." }) 49 | keymap('n', 'cX', vim.diagnostic.setqflist, { desc = "Show all diagnostics" }) 50 | 51 | keymap('n', '[d', function() vim.diagnostic.jump({ count = 1, float = false }) end, 52 | { desc = "Go to previous diagnostic" }) 53 | keymap('n', ']d', function() vim.diagnostic.jump({ count = -1, float = false }) end, { desc = "Go to next diagnostic" }) 54 | 55 | -- Buffer local mappings. 56 | local on_attach = function(ev) 57 | local client = vim.lsp.get_client_by_id(ev.data.client_id) 58 | if client ~= nil and client.name == "clangd" then 59 | -- Like "a.vim", use command "A" for switching between source/header files. 60 | vim.api.nvim_buf_create_user_command(ev.buf, 'A', "LspClangdSwitchSourceHeader", { nargs = 0 }) 61 | end 62 | 63 | -- Enable completion triggered by 64 | vim.bo[ev.buf].omnifunc = 'v:lua.vim.lsp.omnifunc' 65 | 66 | -- Mappings. 67 | local function map(mode, key, cmd, desc) 68 | local opts = { buffer = ev.buf, desc = desc } 69 | vim.keymap.set(mode, key, cmd, opts) 70 | end 71 | 72 | local wk = require("which-key") 73 | local gp = require('goto-preview') 74 | 75 | wk.add({ 76 | buffer = ev.buf, 77 | { 'gr', name = 'LSP' }, 78 | }) 79 | 80 | local has_picker, picker = pcall(require, "snacks.picker") 81 | 82 | local lsp_references = function() 83 | vim.notify("Finding references…", vim.log.levels.INFO, { 84 | title = "LSP", 85 | icon = "", 86 | }) 87 | if picker then 88 | picker.lsp_references() 89 | else 90 | vim.lsp.buf.references() 91 | end 92 | end 93 | 94 | -- See `:help vim.lsp.*` for documentation on any of the below functions 95 | if has_picker then 96 | map('n', 'gd', picker.lsp_definitions, "Go to definition") 97 | map('n', 'grr', lsp_references, "Search references") 98 | map('n', 'cd', picker.lsp_declarations, "Go to declaration") 99 | map('n', 'ci', picker.lsp_implementations, "Go to implementation") 100 | map('n', 'gri', picker.lsp_implementations, "Go to implementation") 101 | map('n', 'ct', picker.lsp_type_definitions, "Go to type definition") 102 | map('n', 'cs', picker.lsp_symbols, "Search LSP symbols") 103 | map('n', 'cS', picker.lsp_workspace_symbols, "Search LSP symbols in workspace") 104 | else 105 | map('n', 'gd', vim.lsp.buf.definition, "Go to definition") 106 | map('n', 'grr', lsp_references, "Search references") 107 | map('n', 'cd', vim.lsp.buf.declaration, "Go to declaration") 108 | map('n', 'ci', vim.lsp.buf.implementation, "Go to implementation") 109 | map('n', 'gri', vim.lsp.buf.implementation, "Go to implementation") 110 | map('n', 'ct', vim.lsp.buf.type_definition, "Go to type definition") 111 | map('n', 'cs', vim.lsp.buf.document_symbol, "Search LSP symbols") 112 | map('n', 'cS', vim.lsp.buf.workspace_symbol, "Search LSP symbols in workspace") 113 | end 114 | map('n', 'gD', gp.goto_preview_definition, "Preview definition") 115 | map('n', 'cD', gp.goto_preview_declaration, "Preview declaration") 116 | map('n', 'cI', gp.goto_preview_implementation, "Preview implementation") 117 | map('n', 'cT', gp.goto_preview_type_definition, "Preview type definition") 118 | 119 | map('n', 'K', function() vim.lsp.buf.hover(float_win_opts) end, "Display hover information") 120 | map('i', '', function() vim.lsp.buf.signature_help(float_win_opts) end, "Show signature") 121 | 122 | map('n', 'cr', vim.lsp.buf.rename, "Rename symbol under cursor") 123 | map('n', 'grn', vim.lsp.buf.rename, "Rename symbol under cursor") 124 | map('n', 'ca', vim.lsp.buf.code_action, "Code action") 125 | map('n', 'gra', vim.lsp.buf.code_action, "Code action") 126 | 127 | wk.add({ 128 | buffer = ev.buf, 129 | { "cw", name = "workspace" }, 130 | }) 131 | map('n', 'cwa', vim.lsp.buf.add_workspace_folder, "Add workspace folder") 132 | map('n', 'cwr', vim.lsp.buf.remove_workspace_folder, "Remove workspace folder") 133 | map('n', 'cwl', function() 134 | print(vim.inspect(vim.lsp.buf.list_workspace_folders())) 135 | end, "List workspace folders") 136 | 137 | -- Inlay hint 138 | if client and client:supports_method(vim.lsp.protocol.Methods.textDocument_inlayHint, ev.buf) then 139 | vim.lsp.inlay_hint.enable(true, { bufnr = ev.buf }) 140 | map('n', 'cH', function() 141 | local enabled = vim.lsp.inlay_hint.is_enabled({ bufnr = ev.buf }) 142 | if enabled then 143 | vim.lsp.inlay_hint.enable(false, { bufnr = ev.buf }) 144 | vim.notify("Inlay hints disabled") 145 | else 146 | vim.lsp.inlay_hint.enable(true, { bufnr = ev.buf }) 147 | vim.notify("Inlay hints enabled") 148 | end 149 | end, 'Toggle inlay hints') 150 | end 151 | end 152 | vim.api.nvim_create_autocmd('LspAttach', { 153 | group = vim.api.nvim_create_augroup('UserLspConfig', {}), 154 | callback = on_attach, 155 | }) 156 | end 157 | 158 | 159 | local function setup_goto_preview() 160 | require('goto-preview').setup({ 161 | default_mappings = false, 162 | height = 30, 163 | post_open_hook = function(_, win) 164 | -- Close the current preview window with or 'q'. 165 | local function close_window() 166 | vim.api.nvim_win_close(win, true) 167 | end 168 | vim.keymap.set('n', '', close_window, { buffer = true }) 169 | vim.keymap.set('n', 'q', close_window, { buffer = true }) 170 | end, 171 | }) 172 | end 173 | 174 | local function setup_tiny_inline_diagnostic() 175 | vim.diagnostic.config({ 176 | -- Disable signs. 177 | signs = false, 178 | virtual_text = { 179 | -- Disable messages, only show markers. 180 | format = function(_) return "" end, 181 | prefix = "󰊠", 182 | spacing = 0, 183 | }, 184 | }) 185 | require('tiny-inline-diagnostic').setup({ 186 | preset = "ghost", 187 | options = { 188 | multiple_diag_under_cursor = true, 189 | virt_texts = { 190 | priority = 9999, 191 | }, 192 | }, 193 | }) 194 | end 195 | 196 | local function setup_mason_lspconfig() 197 | local opts = { 198 | automatic_enable = false, 199 | ensure_installed = { 200 | "bashls", 201 | "clangd", 202 | "lua_ls", 203 | "basedpyright", 204 | "vimls", 205 | }, 206 | } 207 | local mason_lspconfig = require("mason-lspconfig") 208 | mason_lspconfig.setup(opts) 209 | 210 | local installed_servers = mason_lspconfig.get_installed_servers() 211 | local capabilities = require('blink.cmp').get_lsp_capabilities() 212 | for _, server in ipairs(installed_servers) do 213 | local config = { 214 | capabilities = capabilities, 215 | settings = server_settings[server], 216 | restart = 'off', 217 | } 218 | vim.lsp.config(server, config) 219 | vim.lsp.enable(server) 220 | end 221 | end 222 | 223 | return { 224 | { 225 | 'neovim/nvim-lspconfig', 226 | event = { "BufReadPre", "BufNewFile" }, 227 | config = setup_lspconfig, 228 | dependencies = { 229 | { 230 | 'rmagatti/goto-preview', 231 | config = setup_goto_preview, 232 | }, 233 | }, 234 | }, 235 | { 236 | "mason-org/mason-lspconfig.nvim", 237 | event = { "BufReadPre", "BufNewFile" }, 238 | dependencies = { 239 | "mason-org/mason.nvim", 240 | "saghen/blink.cmp", 241 | }, 242 | config = setup_mason_lspconfig, 243 | }, 244 | { 245 | "folke/lazydev.nvim", 246 | ft = "lua", 247 | opts = { 248 | library = { 249 | "~/.hammerspoon/Spoons/EmmyLua.spoon/annotations", 250 | }, 251 | }, 252 | }, 253 | { 254 | "rachartier/tiny-inline-diagnostic.nvim", 255 | event = "LspAttach", 256 | priority = 1000, -- needs to be loaded in first 257 | config = setup_tiny_inline_diagnostic, 258 | }, 259 | } 260 | -------------------------------------------------------------------------------- /nvim/lua/plugins/mason.lua: -------------------------------------------------------------------------------- 1 | local function setup_mason_tool_installer() 2 | require("mason-tool-installer").setup({ 3 | ensure_installed = { 4 | { "black", version = "22.10.0" }, 5 | "ruff", 6 | "debugpy", 7 | }, 8 | }) 9 | -- Manually call the check_install function, 10 | -- because mason-tool-installer doesn't automatically install packages 11 | -- when it's lazily loaded. 12 | require("mason-tool-installer").check_install(false, false) 13 | end 14 | 15 | return { 16 | { 17 | "WhoIsSethDaniel/mason-tool-installer.nvim", 18 | lazy = true, 19 | dependencies = { 20 | "williamboman/mason.nvim", 21 | }, 22 | config = setup_mason_tool_installer, 23 | }, 24 | { 25 | "mason-org/mason.nvim", 26 | cmd = "Mason", 27 | build = ":MasonUpdate", -- Update registry 28 | opts = {}, 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /nvim/lua/plugins/snacks.lua: -------------------------------------------------------------------------------- 1 | local snacks_keys = {} 2 | 3 | -- snacks.picker configurations -- 4 | 5 | local function oil_current_dir() 6 | local exists, oil = pcall(require, "oil") 7 | if not exists then 8 | return nil 9 | end 10 | return oil.get_current_dir() 11 | end 12 | 13 | local function prompt_for_search_dir(callback) 14 | -- If buffer is not under cwd, prompt for the search directory. 15 | local buffer_dir = vim.fn.expand("%:p:h") 16 | local cwd = vim.fn.getcwd() 17 | if vim.startswith(buffer_dir, "/") and not vim.startswith(buffer_dir, cwd) then 18 | vim.ui.input({ 19 | prompt = "Search directory: ", 20 | default = buffer_dir .. "/", 21 | completion = "dir", 22 | }, function(dir) 23 | if not dir then 24 | return 25 | end 26 | callback(dir) 27 | end) 28 | return true 29 | else 30 | return false 31 | end 32 | end 33 | 34 | local function picker_smart_files(opts) 35 | opts = opts or {} 36 | if not opts.cwd then 37 | local oil_dir = oil_current_dir() 38 | if oil_dir then 39 | opts.cwd = oil_dir 40 | else 41 | local callback = function(dir) 42 | opts.cwd = dir 43 | picker_smart_files(opts) 44 | end 45 | if prompt_for_search_dir(callback) then 46 | return 47 | else 48 | opts.cwd = vim.fn.getcwd() 49 | end 50 | end 51 | end 52 | if not opts.filter then 53 | opts.filter = { cwd = true, } 54 | end 55 | opts.title = opts.cwd 56 | -- Search dirs to cycle through. 57 | local search_dirs = { 58 | dirs = { 59 | opts.cwd, 60 | }, 61 | current_index = 0 62 | } 63 | local buffer_dir = vim.fn.expand("%:p:h") 64 | if vim.startswith(buffer_dir, "/") and buffer_dir ~= opts.cwd then 65 | table.insert(search_dirs.dirs, buffer_dir) 66 | end 67 | 68 | opts.win = { 69 | input = { 70 | keys = { 71 | [""] = { "cycle_search_dirs", mode = { "i", "n" } }, 72 | }, 73 | }, 74 | } 75 | opts.actions = { 76 | cycle_search_dirs = function(picker) 77 | if #search_dirs.dirs == 1 then 78 | return 79 | end 80 | search_dirs.current_index = (search_dirs.current_index + 1) % #search_dirs.dirs 81 | picker.opts.cwd = search_dirs.dirs[search_dirs.current_index + 1] 82 | vim.notify("Searching " .. picker.opts.cwd) 83 | picker:find() 84 | end, 85 | } 86 | 87 | ---@diagnostic disable-next-line: undefined-field 88 | require("snacks.picker").smart(opts) 89 | end 90 | 91 | local function picker_recent(opts) 92 | opts = opts or {} 93 | if not opts.filter then 94 | opts.filter = { cwd = false, } 95 | end 96 | if not opts.matcher then 97 | opts.matcher = { cwd_bonus = true, sort_empty = true } 98 | end 99 | opts.win = { 100 | input = { 101 | keys = { 102 | [""] = { "toggle_cwd", mode = { "i", "n" } }, 103 | }, 104 | }, 105 | } 106 | opts.actions = { 107 | toggle_cwd = function(picker) 108 | picker.opts.filter.cwd = not picker.opts.filter.cwd 109 | if picker.opts.filter.cwd then 110 | vim.notify("Searching " .. vim.fn.getcwd()) 111 | else 112 | vim.notify("Searching global") 113 | end 114 | picker:find() 115 | end, 116 | } 117 | require("snacks.picker").recent(opts) 118 | end 119 | 120 | local function picker_grep(opts) 121 | opts = opts or {} 122 | if not opts.dirs then 123 | local oil_dir = oil_current_dir() 124 | if oil_dir then 125 | opts.cwd = oil_dir 126 | else 127 | local callback = function(dir) 128 | opts.dirs = { dir } 129 | picker_grep(opts) 130 | end 131 | if prompt_for_search_dir(callback) then 132 | return 133 | end 134 | end 135 | end 136 | require("snacks.picker").grep(opts) 137 | end 138 | 139 | local function picker_grep_word(opts) 140 | opts = opts or {} 141 | if not opts.dirs then 142 | local callback = function(dir) 143 | opts.dirs = { dir } 144 | picker_grep_word(opts) 145 | end 146 | if prompt_for_search_dir(callback) then 147 | return 148 | end 149 | end 150 | require("snacks.picker").grep_word(opts) 151 | end 152 | 153 | local function picker_keys() 154 | local function p() 155 | return require("snacks.picker") 156 | end 157 | return { 158 | { "fa", function() p().pickers() end, desc = "Search all snacks.picker commands" }, 159 | { "fR", function() p().resume() end, desc = "Resume last snacks.picker command" }, 160 | -- Buffers and files. 161 | { "ff", function() picker_smart_files() end, desc = "Smart find files" }, 162 | { "fF", function() p().files() end, desc = "Find files" }, 163 | { "fr", function() picker_recent() end, desc = "Find recent files" }, 164 | { "fb", function() p().buffers() end, desc = "Find buffers" }, 165 | -- Search 166 | { "fS", function() picker_grep() end, desc = "Search" }, 167 | { "fs", function() picker_grep_word() end, desc = "Search word or visual selection", mode = { "n", "x" } }, 168 | -- Lines 169 | { "fl", function() p().lines() end, desc = "Search lines" }, 170 | { "fL", function() p().grep_buffers() end, desc = "Search lines from all buffers" }, 171 | -- Misc 172 | { "fu", function() p().undo() end, desc = "Find undo history" }, 173 | { "f:", function() p().command_history() end, desc = "Find command history" }, 174 | { "f/", function() p().search_history() end, desc = "Find search history" }, 175 | { "f\"", function() p().registers() end, desc = "Find registers" }, 176 | { "f'", function() p().marks() end, desc = "Find marks" }, 177 | { "fj", function() p().jumps() end, desc = "Find jump list" }, 178 | -- git 179 | { "fga", function() p().git_stash() end, desc = "Git stash" }, 180 | { "fgb", function() p().git_branches() end, desc = "Git branches" }, 181 | { "fgf", function() p().git_files() end, desc = "Git files" }, 182 | { "fgh", function() p().git_log_file() end, desc = "Git file history" }, 183 | { "fgH", function() p().git_log_line() end, desc = "Git line history" }, 184 | { "fgl", function() p().git_log() end, desc = "Git log" }, 185 | { "fgs", function() p().git_status() end, desc = "Git status" }, 186 | { "fgd", function() p().git_diff() end, desc = "Git diffs" }, 187 | -- quickfix 188 | { "ff", function() p().qflist() end, desc = "Search quickfix", ft = "qf" }, 189 | -- LSP 190 | { "ft", function() p().lsp_symbols() end, desc = "Search LSP symbols" }, 191 | { "fT", function() p().lsp_workspace_symbols() end, desc = "Search LSP symbols in workspace" }, 192 | } 193 | end 194 | 195 | for _, key in ipairs(picker_keys()) do 196 | table.insert(snacks_keys, key) 197 | end 198 | 199 | vim.api.nvim_create_user_command( 200 | "Rg", 201 | function(opts) 202 | picker_grep({ 203 | search = opts.args, 204 | live = false, 205 | supports_live = true, 206 | }) 207 | end, 208 | { nargs = "?" } 209 | ) 210 | 211 | ---@class snacks.picker.Config 212 | local picker_opts = { 213 | layouts = { 214 | default = { 215 | layout = { 216 | width = 0.9, 217 | height = 0.9, 218 | }, 219 | }, 220 | }, 221 | win = { 222 | input = { 223 | keys = { 224 | [""] = { 225 | "go_to_beginning_or_select_all", 226 | mode = { "n", "i" }, 227 | }, 228 | [""] = { 229 | "go_to_end", 230 | mode = { "i" }, 231 | }, 232 | }, 233 | }, 234 | }, 235 | actions = { 236 | go_to_beginning_or_select_all = function(picker) 237 | -- If in insert mode, go to beginning of line. 238 | -- Otherwise, select all. 239 | local m = vim.api.nvim_get_mode().mode 240 | if m == "i" then 241 | vim.api.nvim_feedkeys( 242 | vim.api.nvim_replace_termcodes("", true, false, true), 243 | "i", 244 | true 245 | ) 246 | else 247 | picker:action("select_all") 248 | end 249 | end, 250 | go_to_end = function(picker) 251 | vim.api.nvim_feedkeys( 252 | vim.api.nvim_replace_termcodes("", true, false, true), 253 | "i", 254 | true 255 | ) 256 | end, 257 | }, 258 | formatters = { 259 | file = { 260 | truncate = 80, 261 | }, 262 | }, 263 | } 264 | 265 | -- End of snacks.picker configurations -- 266 | 267 | local gitbrowse_keys = { 268 | { "go", function() Snacks.gitbrowse.open() end, desc = "Git browse", mode = { "n", "x" }, }, 269 | } 270 | 271 | for _, key in ipairs(gitbrowse_keys) do 272 | table.insert(snacks_keys, key) 273 | end 274 | 275 | local explorer_keys = { 276 | { "ut", function() Snacks.explorer() end, desc = "Toggle file explorer" }, 277 | } 278 | 279 | for _, key in ipairs(explorer_keys) do 280 | table.insert(snacks_keys, key) 281 | end 282 | 283 | --@type snacks.dashboard.Config 284 | local dashboard_opts = { 285 | preset = { 286 | keys = { 287 | { icon = " ", key = "f", desc = "Find File", action = function() picker_smart_files() end }, 288 | { icon = " ", key = "r", desc = "Recent Files", action = function() picker_recent() end }, 289 | { icon = " ", key = "n", desc = "New File", action = ":ene | startinsert" }, 290 | { icon = " ", key = "S", desc = "Search Text", action = function() picker_grep() end }, 291 | { icon = " ", key = "s", desc = "Restore Session", section = "session" }, 292 | { icon = "󰒲 ", key = "L", desc = "Lazy", action = ":Lazy", enabled = package.loaded.lazy ~= nil }, 293 | { icon = " ", key = "q", desc = "Quit", action = ":qa" }, 294 | }, 295 | header = table.concat({ 296 | [[ __ ]], 297 | [[ ___ ___ ___ __ __ /\_\ ___ ___ ]], 298 | [[ / _ `\ / __`\ / __`\/\ \/\ \\/\ \ / __` __`\ ]], 299 | [[/\ \/\ \/\ __//\ \_\ \ \ \_/ |\ \ \/\ \/\ \/\ \ ]], 300 | [[\ \_\ \_\ \____\ \____/\ \___/ \ \_\ \_\ \_\ \_\]], 301 | [[ \/_/\/_/\/____/\/___/ \/__/ \/_/\/_/\/_/\/_/]], 302 | }, "\n"), 303 | }, 304 | sections = { 305 | { section = "header" }, 306 | { icon = " ", title = "Keymaps", section = "keys", indent = 2, padding = 1 }, 307 | { 308 | icon = " ", 309 | pane = 2, 310 | title = "Recent Files", 311 | section = "recent_files", 312 | indent = 2, 313 | padding = 1, 314 | cwd = true, 315 | limit = 10, 316 | }, 317 | { 318 | icon = " ", 319 | pane = 2, 320 | title = "Projects", 321 | section = "projects", 322 | indent = 2, 323 | padding = 1, 324 | }, 325 | 326 | { 327 | icon = " ", 328 | pane = 2, 329 | title = "Git Status", 330 | section = "terminal", 331 | enabled = function() 332 | return Snacks.git.get_root() ~= nil 333 | end, 334 | cmd = "git status --short --branch --renames", 335 | indent = 2, 336 | padding = 1, 337 | height = 5, 338 | ttl = 10, 339 | }, 340 | { section = "startup" }, 341 | }, 342 | } 343 | 344 | local terminal_keys = { 345 | { "", function() Snacks.terminal.toggle() end, desc = "Toggle terminal", mode = { "n", "t" } }, 346 | { "", function() Snacks.terminal.toggle() end, desc = "Toggle terminal", mode = { "n", "t" } }, 347 | -- : return to normal mode, z: zoom, i: enter insert mode 348 | { "", [[zi]], desc = "Zoom", mode = "t", remap = true }, 349 | { "bt", function() Snacks.terminal.colorize() end, desc = "Parse terminal color codes" }, 350 | } 351 | 352 | for _, key in ipairs(terminal_keys) do 353 | table.insert(snacks_keys, key) 354 | end 355 | 356 | return { 357 | "folke/snacks.nvim", 358 | priority = 1000, 359 | lazy = false, 360 | ---@type snacks.Config 361 | ---@diagnostic disable-next-line: missing-fields 362 | opts = { 363 | picker = picker_opts, 364 | indent = {}, 365 | notifier = {}, 366 | bigfile = {}, 367 | scroll = {}, 368 | gitbrowse = { 369 | what = "permalink", 370 | }, 371 | explorer = {}, 372 | dashboard = dashboard_opts, 373 | scope = {}, 374 | terminal = { 375 | win = { 376 | wo = { winhighlight = "NormalFloat:Normal" }, 377 | }, 378 | }, 379 | image = { 380 | doc = { 381 | inline = false, 382 | float = true, 383 | max_width = 200, 384 | max_height = 200, 385 | } 386 | }, 387 | zen = { 388 | win = { 389 | width = 0.9, 390 | border = "hpad", 391 | backdrop = { 392 | transparent = false, 393 | }, 394 | }, 395 | }, 396 | }, 397 | keys = snacks_keys, 398 | init = function() 399 | vim.api.nvim_create_autocmd("User", { 400 | pattern = "VeryLazy", 401 | callback = function() 402 | -- Setup some globals for debugging (lazy-loaded) 403 | _G.dd = function(...) 404 | Snacks.debug.inspect(...) 405 | end 406 | _G.bt = function() 407 | Snacks.debug.backtrace() 408 | end 409 | vim.print = _G.dd -- Override print to use snacks for `:=` command 410 | 411 | Snacks.toggle.zoom():map("wz"):map("z"):map("", { mode = { "n", "v", "i" } }) 412 | Snacks.toggle.zen():map("uz") 413 | Snacks.toggle.option("spell", { name = "spelling" }):map("us") 414 | Snacks.toggle.option("wrap", { name = "wrap" }):map("uw") 415 | Snacks.toggle.new({ 416 | id = "dark_theme", 417 | name = "dark theme", 418 | get = function() 419 | return vim.g.colors_name == "onedark" 420 | end, 421 | set = function(state) 422 | vim.cmd(state and "colorscheme onedark" or "colorscheme onelight") 423 | -- laststatus gets reset when changing colorscheme, 424 | -- set it to 3 again 425 | vim.opt.laststatus = 3 426 | end, 427 | }):map( 428 | "uT") 429 | end, 430 | }) 431 | end, 432 | } 433 | -------------------------------------------------------------------------------- /nvim/lua/plugins/themes.lua: -------------------------------------------------------------------------------- 1 | local function setup_onedarkpro(_, _) 2 | local color = require("onedarkpro.helpers") 3 | local dark_colors = { 4 | black = '#000000', 5 | white = '#f1f1f0', 6 | gray = '#686868', 7 | red = '#ff5c57', 8 | green = '#5af78e', 9 | yellow = '#f3f99d', 10 | blue = '#57c7ff', 11 | purple = color.lighten('#ff6ac1', 15), 12 | cyan = '#9aedfe', 13 | orange = color.brighten("orange", 15, "onedark"), 14 | bg = '#282A36', 15 | fg = color.brighten("fg", 5, "onedark"), 16 | comment = color.lighten("comment", 5, "onedark"), 17 | cursorline = color.lighten("#282A36", 10), 18 | float_bg = '#323540', 19 | } 20 | 21 | local light_colors = { 22 | bg = color.darken("bg", 5, "onelight"), 23 | } 24 | 25 | -- highlights only for the dark theme 26 | local dark_highlights = { 27 | Identifier = { fg = "${cyan}" }, 28 | ["@property"] = { fg = "${cyan}" }, 29 | String = { fg = "${yellow}" }, 30 | Character = { fg = "${yellow}" }, 31 | ["@string"] = { fg = "${yellow}" }, 32 | Constant = { fg = "${green}" }, 33 | ["@constant"] = { fg = "${green}" }, 34 | } 35 | 36 | for _, highlight in pairs(dark_highlights) do 37 | for k, v in pairs(highlight) do 38 | ---@diagnostic disable-next-line: assign-type-mismatch 39 | highlight[k] = { 40 | onedark = v, 41 | } 42 | end 43 | end 44 | 45 | -- common highlights for both dark and light themes 46 | local highlights = { 47 | ["@variable"] = { link = "Identifier" }, 48 | ["@variable.parameter"] = { link = "Identifier" }, 49 | ["@variable.member"] = { link = "Identifier" }, 50 | ["@odp.interpolation.python"] = { link = "Identifier" }, -- Variables in f-strings. 51 | ["@constant.builtin"] = { link = "Constant" }, 52 | pythonString = { link = "String" }, 53 | SpellBad = { undercurl = true, sp = "${red}" }, 54 | DiagnosticUnderlineError = { undercurl = true, sp = "${red}" }, 55 | Title = { fg = "${cyan}", extend = true }, 56 | PmenuSel = { bg = "#4d505c", extend = true }, 57 | -- Plug-ins 58 | -- which-key.nvim 59 | WhichKeyBorder = { bg = "${bg}", extend = true }, 60 | WhichKeyNormal = { bg = "${bg}", extend = true }, 61 | -- snacks.nvim 62 | SnacksPicker = { bg = "${bg}", extend = true }, 63 | SnacksPickerBorder = { bg = "${bg}", extend = true }, 64 | } 65 | 66 | for k, v in pairs(dark_highlights) do 67 | highlights[k] = v 68 | end 69 | 70 | local styles = { 71 | comments = "italic", 72 | } 73 | 74 | require("onedarkpro").setup({ 75 | colors = { 76 | onedark = dark_colors, 77 | onelight = light_colors, 78 | }, 79 | highlights = highlights, 80 | styles = styles, 81 | options = { 82 | cursorline = true, 83 | highlight_inactive_windows = false, 84 | lualine_transparency = true, 85 | } 86 | }) 87 | vim.cmd [[colorscheme onedark]] 88 | end 89 | 90 | return { 91 | { 92 | "olimorris/onedarkpro.nvim", 93 | priority = 1000, 94 | config = setup_onedarkpro, 95 | }, 96 | } 97 | -------------------------------------------------------------------------------- /nvim/lua/plugins/treesitter.lua: -------------------------------------------------------------------------------- 1 | local function setup_treesitter(_, _) 2 | local treesitter_filetypes = { 3 | "bash", 4 | "c", 5 | "cpp", 6 | "json", 7 | "lua", 8 | "luadoc", 9 | "luap", 10 | "markdown", 11 | "markdown_inline", 12 | "python", 13 | "vim", 14 | "vimdoc", 15 | "yaml", 16 | } 17 | local opts = { 18 | highlight = { enable = true }, 19 | indent = { enable = true }, 20 | ensure_installed = treesitter_filetypes, 21 | } 22 | opts.textobjects = { 23 | select = { 24 | enable = true, 25 | -- Automatically jump forward to textobj, similar to targets.vim 26 | lookahead = true, 27 | keymaps = { 28 | ["ak"] = { query = "@block.outer", desc = "around block" }, 29 | ["ik"] = { query = "@block.inner", desc = "inside block" }, 30 | ["ac"] = { query = "@class.outer", desc = "around class" }, 31 | ["ic"] = { query = "@class.inner", desc = "inside class" }, 32 | ["a?"] = { query = "@conditional.outer", desc = "around conditional" }, 33 | ["i?"] = { query = "@conditional.inner", desc = "inside conditional" }, 34 | ["af"] = { query = "@function.outer", desc = "around function " }, 35 | ["if"] = { query = "@function.inner", desc = "inside function " }, 36 | ["al"] = { query = "@loop.outer", desc = "around loop" }, 37 | ["il"] = { query = "@loop.inner", desc = "inside loop" }, 38 | ["aa"] = { query = "@parameter.outer", desc = "around argument" }, 39 | ["ia"] = { query = "@parameter.inner", desc = "inside argument" }, 40 | }, 41 | }, 42 | move = { 43 | enable = true, 44 | set_jumps = true, 45 | goto_next_start = { 46 | ["]k"] = { query = "@block.outer", desc = "Next block start" }, 47 | ["]f"] = { query = "@function.outer", desc = "Next function start" }, 48 | ["]a"] = { query = "@parameter.inner", desc = "Next parameter start" }, 49 | ["]C"] = { query = "@class.outer", desc = "Next class start" }, 50 | }, 51 | goto_next_end = { 52 | ["]K"] = { query = "@block.outer", desc = "Next block end" }, 53 | ["]F"] = { query = "@function.outer", desc = "Next function end" }, 54 | ["]A"] = { query = "@parameter.inner", desc = "Next parameter end" }, 55 | }, 56 | goto_previous_start = { 57 | ["[k"] = { query = "@block.outer", desc = "Previous block start" }, 58 | ["[f"] = { query = "@function.outer", desc = "Previous function start" }, 59 | ["[a"] = { query = "@parameter.inner", desc = "Previous parameter start" }, 60 | }, 61 | ["[C"] = { query = "@class.outer", desc = "Previous class start" }, 62 | goto_previous_end = { 63 | ["[K"] = { query = "@block.outer", desc = "Previous block end" }, 64 | ["[F"] = { query = "@function.outer", desc = "Previous function end" }, 65 | ["[A"] = { query = "@parameter.inner", desc = "Previous parameter end" }, 66 | }, 67 | }, 68 | } 69 | require("nvim-treesitter.configs").setup(opts) 70 | -- Tree-sitter based folding. 71 | for _, filetype in ipairs(treesitter_filetypes) do 72 | vim.api.nvim_create_autocmd( 73 | 'FileType', 74 | { 75 | pattern = filetype, 76 | command = 'setlocal foldmethod=expr foldexpr=nvim_treesitter#foldexpr() nofoldenable', 77 | } 78 | ) 79 | end 80 | end 81 | 82 | local function setup_treesitter_context(_, _) 83 | require("treesitter-context").setup { 84 | multiline_threshold = 1, 85 | } 86 | vim.cmd([[ 87 | hi TreesitterContextBottom gui=underline guisp=Grey 88 | hi TreesitterContextLineNumberBottom gui=underline guisp=Grey 89 | ]]) 90 | vim.keymap.set("n", "[x", function() 91 | require("treesitter-context").go_to_context() 92 | end, { desc = "Go to context beginning" }) 93 | end 94 | 95 | return { 96 | { 97 | "nvim-treesitter/nvim-treesitter", 98 | event = { "BufReadPre", "BufNewFile" }, 99 | build = ":TSUpdate", 100 | config = setup_treesitter, 101 | dependencies = { 102 | "nvim-treesitter/nvim-treesitter-textobjects", 103 | { 104 | "nvim-treesitter/nvim-treesitter-context", 105 | config = setup_treesitter_context, 106 | } 107 | }, 108 | }, 109 | } 110 | -------------------------------------------------------------------------------- /nvim/lua/plugins/ui.lua: -------------------------------------------------------------------------------- 1 | local function setup_oil() 2 | require("oil").setup({ 3 | columns = { 4 | "icon", 5 | "permissions", 6 | "size", 7 | "mtime", 8 | }, 9 | view_options = { 10 | show_hidden = true, 11 | }, 12 | constrain_cursor = "name", 13 | use_default_keymaps = false, 14 | keymaps = { 15 | ["g?"] = "actions.show_help", 16 | [""] = "actions.select", 17 | ["L"] = "actions.select", 18 | [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" }, 19 | [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" }, 20 | [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" }, 21 | [""] = "actions.preview", 22 | [""] = "actions.close", 23 | ["q"] = "actions.close", 24 | ["r"] = "actions.refresh", 25 | ["-"] = "actions.parent", 26 | ["H"] = "actions.parent", 27 | ["_"] = "actions.open_cwd", 28 | ["`"] = "actions.cd", 29 | ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" }, 30 | ["gs"] = "actions.change_sort", 31 | ["gx"] = "actions.open_external", 32 | ["g."] = "actions.toggle_hidden", 33 | ["g\\"] = "actions.toggle_trash", 34 | }, 35 | float = { 36 | max_width = 160, 37 | }, 38 | }) 39 | 40 | vim.api.nvim_create_autocmd("User", { 41 | pattern = "OilActionsPost", 42 | callback = function(event) 43 | if event.data.actions.type == "move" then 44 | Snacks.rename.on_rename_file(event.data.actions.src_url, event.data.actions.dest_url) 45 | end 46 | end, 47 | }) 48 | end 49 | 50 | local function toggle_oil(prompt_for_dir) 51 | local oil = require("oil") 52 | if not prompt_for_dir then 53 | oil.toggle_float() 54 | else 55 | ---@diagnostic disable-next-line: undefined-field 56 | local cwd = vim.uv.cwd() .. "/" 57 | vim.ui.input({ 58 | prompt = "Toggle file explorer: ", 59 | default = cwd, 60 | completion = "dir", 61 | }, function(dir) 62 | if not dir then 63 | return 64 | end 65 | oil.toggle_float(dir) 66 | end) 67 | end 68 | end 69 | 70 | local oil_keys = { 71 | { "uf", function() toggle_oil(false) end, desc = "Toggle file explorer on buffer dir" }, 72 | { "uF", function() toggle_oil(true) end, desc = "Toggle file explorer on selected dir" }, 73 | } 74 | 75 | local lualine_opts = { 76 | options = { 77 | section_separators = "", 78 | component_separators = "|", 79 | }, 80 | sections = { 81 | lualine_c = { 82 | { 83 | "filename", 84 | path = 1, 85 | }, 86 | { 87 | "navic", 88 | }, 89 | }, 90 | lualine_x = { 91 | { 92 | -- Show macro recording message 93 | function() 94 | ---@diagnostic disable-next-line 95 | local mode = require("noice").api.statusline.mode.get() 96 | local substr_idx = mode:find("recording") 97 | return mode:sub(substr_idx) 98 | end, 99 | cond = function() 100 | local exist, noice = pcall(require, "noice") 101 | if not exist then 102 | return false 103 | end 104 | ---@diagnostic disable-next-line 105 | local mode = noice.api.statusline.mode.get() 106 | return mode ~= nil and mode:find("recording") ~= nil 107 | end, 108 | color = { fg = "#ff9e64" }, 109 | }, 110 | -- Show search count 111 | { 112 | function() 113 | ---@diagnostic disable-next-line 114 | local search = require("noice").api.status.search.get() 115 | -- replace multiple spaces with one space 116 | return search:gsub("%s+", " ") 117 | end, 118 | cond = function() 119 | local exist, noice = pcall(require, "noice") 120 | ---@diagnostic disable-next-line 121 | return exist and noice.api.status.search.has() 122 | end, 123 | color = { fg = "#ff9e64" }, 124 | }, 125 | { 126 | "copilot", 127 | show_colors = true, 128 | }, 129 | { 130 | "filetype", 131 | }, 132 | }, 133 | }, 134 | } 135 | 136 | local function setup_noice() 137 | local opts = {} 138 | opts.lsp = { 139 | hover = { enabled = false, }, 140 | signature = { enabled = false, }, 141 | } 142 | -- Use mini view for the following verbose messages. 143 | local verbose_messages = { 144 | "lines yanked", 145 | -- lazy.nvim config change message 146 | "Config Change Detected", 147 | -- undo message 148 | "; before #", 149 | -- redo message 150 | "; after #", 151 | } 152 | opts.routes = { 153 | { 154 | filter = { 155 | any = {}, 156 | }, 157 | view = "mini", 158 | }, 159 | } 160 | for _, msg in ipairs(verbose_messages) do 161 | table.insert(opts.routes[1].filter.any, { 162 | find = msg, 163 | }) 164 | end 165 | require("noice").setup(opts) 166 | end 167 | 168 | local dot_repeatable_keymap = require("core.utils").dot_repeatable_keymap 169 | 170 | local barbar_keys = { 171 | { "", "BufferNext", desc = "Next buffer" }, 172 | { "", "BufferPrevious", desc = "Previous buffer" }, 173 | { "x", "BufferClose", desc = "Close buffer" }, 174 | { "br", "BufferRestore", desc = "Restore buffer" }, 175 | { "bo", "BufferCloseAllButCurrent", desc = "Only keep current buffer" }, 176 | { "bb", "BufferPick", desc = "Pick buffer" }, 177 | { "bx", "BufferPickDelete", desc = "Pick buffer to delete" }, 178 | dot_repeatable_keymap({ "bn", function() vim.cmd("BufferMoveNext") end, desc = "Move buffer to next" }), 179 | dot_repeatable_keymap({ "bp", function() vim.cmd("BufferMovePrevious") end, desc = "Move buffer to previous" }), 180 | { 181 | "bs", 182 | function() 183 | vim.ui.select( 184 | { 185 | "BufferNumber", 186 | "Directory", 187 | "Language", 188 | "Name", 189 | "WindowNumber", 190 | }, 191 | { 192 | prompt = 'Sort buffers by:', 193 | }, 194 | function(selected) 195 | if selected == nil then 196 | return 197 | end 198 | local cmd = "BufferOrderBy" .. selected 199 | vim.cmd(cmd) 200 | end 201 | ) 202 | end, 203 | desc = "Sort buffers", 204 | } 205 | } 206 | 207 | return { 208 | { 209 | 'stevearc/oil.nvim', 210 | dependencies = { "nvim-tree/nvim-web-devicons" }, 211 | -- Disable lazy loading so that `vim ` and `:e ` will use oil. 212 | lazy = false, 213 | keys = oil_keys, 214 | config = setup_oil, 215 | }, 216 | { 217 | "nvim-lualine/lualine.nvim", 218 | event = "VeryLazy", 219 | dependencies = { 220 | "nvim-tree/nvim-web-devicons", 221 | { 222 | -- Show current code context. 223 | "SmiteshP/nvim-navic", 224 | opts = { 225 | lsp = { 226 | auto_attach = true, 227 | }, 228 | }, 229 | }, 230 | { 231 | 'AndreM222/copilot-lualine', 232 | }, 233 | }, 234 | config = function() 235 | require("lualine").setup(lualine_opts) 236 | -- Lualine will reset laststatus when it's lazy loaded, 237 | -- so we need to set it again. 238 | vim.opt.laststatus = 3 239 | end, 240 | }, 241 | { 242 | 'romgrk/barbar.nvim', 243 | version = '*', 244 | event = "VeryLazy", 245 | dependencies = { 246 | 'nvim-tree/nvim-web-devicons', 247 | }, 248 | init = function() vim.g.barbar_auto_setup = false end, 249 | opts = { 250 | focus_on_close = 'right', 251 | icons = { 252 | buffer_index = true, 253 | } 254 | }, 255 | keys = barbar_keys, 256 | }, 257 | { 258 | "folke/noice.nvim", 259 | event = "VeryLazy", 260 | config = setup_noice, 261 | dependencies = { 262 | "MunifTanjim/nui.nvim", 263 | } 264 | }, 265 | } 266 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | base_dir=$(cd "$(dirname "$0")" && pwd) 3 | backup_dir="$base_dir/.backups.local" 4 | backup_prefix="$backup_dir/$(date '+%Y%m%d%H%M%S')" 5 | 6 | link_file() { 7 | local src=$1 dst=$2 8 | local backup_dst=false delete_dst=false link_dst=true 9 | if [[ -e $dst ]]; then 10 | current_link=$(readlink "$dst") 11 | if [[ "$current_link" != "$src" ]]; then 12 | while true; do 13 | printf "File already exists: %s. What do you want?\n" "$dst" 14 | printf "[r]eplace; [b]ack up; [s]kip: " 15 | read -r op 16 | case $op in 17 | r ) 18 | delete_dst=true 19 | link_dst=true 20 | break;; 21 | b ) 22 | backup_dst=true 23 | delete_dst=true 24 | link_dst=true 25 | break;; 26 | s ) 27 | link_dst=false 28 | break;; 29 | * ) 30 | echo "Unrecognized option: $op";; 31 | esac 32 | done 33 | else 34 | echo "$dst is already linked to $src" 35 | link_dst=false 36 | fi 37 | fi 38 | if [[ "$backup_dst" == "true" ]]; then 39 | mkdir -p "$backup_dir" 40 | local backup_file 41 | backup_file="$backup_prefix$(basename "$dst")" 42 | mv "$dst" "$backup_file" 43 | echo "$dst was backed up to $backup_file" 44 | fi 45 | if [[ "$delete_dst" == "true" ]]; then 46 | rm -rf "$dst" 47 | fi 48 | if [[ "$link_dst" == "true" ]]; then 49 | ln -s "$src" "$dst" 50 | echo "Linked $dst to $src" 51 | return 0 52 | fi 53 | return 1 54 | } 55 | 56 | echo "Setting up zsh..." 57 | link_file "$base_dir/zsh/zshrc" ~/.zshrc 58 | 59 | echo "Setting up neovim..." 60 | mkdir -p ~/.config 61 | link_file "$base_dir/nvim" ~/.config/nvim 62 | 63 | echo "Setting up vim..." 64 | if link_file "$base_dir/vim/vimrc" ~/.vimrc ; then 65 | vim +PlugInstall +qall 66 | fi 67 | 68 | echo "Setting up tmux..." 69 | link_file "$base_dir/tmux" ~/.config/tmux 70 | 71 | if [[ `uname` == "Darwin" ]]; then 72 | echo "Setting up Hammerspoon..." 73 | link_file "$base_dir/hammerspoon" ~/.hammerspoon 74 | fi 75 | 76 | echo "Setting up git..." 77 | link_file "$base_dir/git" ~/.config/git 78 | 79 | echo "Setting up lsd..." 80 | link_file "$base_dir/lsd" ~/.config/lsd 81 | 82 | echo "Setting up WezTerm..." 83 | link_file "$base_dir/wezterm" ~/.config/wezterm 84 | 85 | echo "Setting up Ghostty..." 86 | link_file "$base_dir/ghostty" ~/.config/ghostty 87 | 88 | echo "Done" 89 | -------------------------------------------------------------------------------- /tmux/tmux.conf: -------------------------------------------------------------------------------- 1 | #============== 2 | # basic 3 | #============== 4 | set -g prefix C-b 5 | 6 | # Scroll History 7 | set -g history-limit 30000 8 | 9 | set -s escape-time 0 10 | 11 | set-window-option -g xterm-keys on 12 | 13 | if "infocmp tmux-256color" { 14 | set -g default-terminal tmux-256color 15 | } { 16 | set -g default-terminal screen-256color 17 | } 18 | 19 | # Enable RGB color if running in xterm 20 | # NOTE: without this, Neovim will have a weird background color. 21 | set-option -sa terminal-overrides ",xterm*:Tc" 22 | 23 | # Use vi key bindings in copy and choice modes 24 | setw -g mode-keys vi 25 | 26 | # emacs key bindings in tmux command prompt (prefix + :) are better than 27 | # vi keys, even for vim users 28 | set -g status-keys emacs 29 | 30 | # Set ability to capture on start and restore on exit window data when running an application 31 | setw -g alternate-screen on 32 | 33 | # super useful when using "grouped sessions" and multi-monitor setup 34 | setw -g aggressive-resize on 35 | 36 | # enable mouse 37 | set -g mouse off 38 | 39 | # focus events enabled for terminals that support them 40 | set -g focus-events on 41 | 42 | set-option -g set-titles on 43 | set-option -g set-titles-string "#W" 44 | 45 | set -s set-clipboard on 46 | 47 | # Update window name when pane focus changes. 48 | set-hook -g pane-focus-in "run-shell \"tmux_update_window_name.sh '#{pane_current_path}'\"" 49 | 50 | # Start windows and panes index at 1, not 0. 51 | set -g base-index 1 52 | setw -g pane-base-index 1 53 | 54 | # Ensure window index numbers get reordered on delete. 55 | set-option -g renumber-windows on 56 | 57 | # Allow passthrough to support showing images. 58 | set-option -g allow-passthrough on 59 | 60 | #============== 61 | # key bindings 62 | #============== 63 | 64 | bind r source-file ~/.config/tmux/tmux.conf\; display 'config reloaded' 65 | 66 | bind | split-window -h -c "#{pane_current_path}" 67 | bind '\' split-window -h -c "#{pane_current_path}" 68 | bind - split-window -v -c "#{pane_current_path}" 69 | 70 | # Smart pane switching with awareness of Vim splits. 71 | # See: https://github.com/christoomey/vim-tmux-navigator 72 | is_vim="ps -o state= -o comm= -t '#{pane_tty}' \ 73 | | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|l?n?vim?x?)(diff)?$'" 74 | bind-key -n 'C-M-h' if-shell "$is_vim" 'send-keys C-M-h' 'select-pane -L' 75 | bind-key -n 'C-M-j' if-shell "$is_vim" 'send-keys C-M-j' 'select-pane -D' 76 | bind-key -n 'C-M-k' if-shell "$is_vim" 'send-keys C-M-k' 'select-pane -U' 77 | bind-key -n 'C-M-l' if-shell "$is_vim" 'send-keys C-M-l' 'select-pane -R' 78 | 79 | bind-key -T copy-mode-vi 'C-M-h' select-pane -L 80 | bind-key -T copy-mode-vi 'C-M-j' select-pane -D 81 | bind-key -T copy-mode-vi 'C-M-k' select-pane -U 82 | bind-key -T copy-mode-vi 'C-M-l' select-pane -R 83 | 84 | # map Vi movement keys as pane movement keys 85 | bind h select-pane -L 86 | bind j select-pane -D 87 | bind k select-pane -U 88 | bind l select-pane -R 89 | 90 | # resize panes using PREFIX ctrl-hjkl 91 | bind c-h resize-pane -L 5 92 | bind c-j resize-pane -D 5 93 | bind c-k resize-pane -U 5 94 | bind c-l resize-pane -R 5 95 | 96 | # easier and faster switching between next/prev window 97 | bind C-p previous-window 98 | bind C-n next-window 99 | 100 | # new windows are created next to the current 101 | bind c new-window -a 102 | 103 | # close window 104 | bind c-x confirm-before -p "kill-window #W? (y/n)" kill-window 105 | 106 | # copy mode 107 | bind c-u copy-mode -u 108 | bind c-[ copy-mode 109 | bind c-] paste-buffer 110 | 111 | if-shell -b "tmux -V | awk '{if($2<2.4){exit 0}else{exit 1}}'" \ 112 | "bind-key -t vi-copy Escape cancel; \ 113 | bind-key -t vi-copy v begin-selection; \ 114 | bind-key -t vi-copy V select-line; \ 115 | bind-key -t vi-copy r rectangle-toggle; \ 116 | bind-key -t vi-copy y copy-selection " 117 | 118 | if-shell -b "tmux -V | awk '{if($2>=2.4){exit 0}else{exit 1}}'" \ 119 | "bind-key -T copy-mode-vi Escape send -X cancel; \ 120 | bind-key -T copy-mode-vi 'v' send -X begin-selection; \ 121 | bind-key -T copy-mode-vi 'V' send -X select-line; \ 122 | bind-key -T copy-mode-vi 'r' send -X rectangle-toggle; \ 123 | bind-key -T copy-mode-vi 'y' send -X copy-selection-and-cancel" 124 | 125 | if-shell -b "test $(uname) = 'Darwin' && \ 126 | tmux -V | awk '{if($2>=2.4){exit 0}else{exit 1}}'" \ 127 | "bind-key -T copy-mode-vi 'y' send -X copy-pipe-and-cancel pbcopy" 128 | 129 | # Toggle mouse 130 | bind-key M if-shell "tmux show -g -v mouse | grep -q 'on$'" "set-option -g mouse off \; display-message 'Mouse: OFF'" "set-option -g mouse on \; display-message 'Mouse: ON'" 131 | 132 | # Capture last output command in vim. 133 | # `vim +$` moves the cursor to the last line. 134 | bind-key c-e capture-pane -J -S - \; save-buffer /tmp/tmux-capture.txt \; new-window 'grep -E "^❯" /tmp/tmux-capture.txt -n | tail -2 | head -1 | cut -d: -f1 | xargs -I {} tail -n +{} /tmp/tmux-capture.txt | vim +$ -' 135 | # Capture entire pane history in vim. 136 | bind-key e capture-pane -J -S - \; new-window 'tmux show-buffer | vim +$ -' 137 | # Clear pane history 138 | bind-key K clear-history 139 | 140 | # Find the last prompt. 141 | bind-key b copy-mode\;\ 142 | send-keys -X start-of-line\;\ 143 | send-keys -X search-backward "❯" 144 | 145 | # Pop up or detach from the floating session. 146 | bind c-f if-shell -F '#{==:#{session_name},floating}' { 147 | detach-client 148 | } { 149 | set -gF '@last_session_name' '#S' 150 | set -gF '@window_name' '#{window_name}' 151 | set -gF '@pane_current_path' '#{pane_current_path}' 152 | # Temporarily disable status bar updates for the outer session. 153 | # Because frequent updates break mouse selection in the popup window. 154 | set status-interval 100 155 | popup -d -xC -yC -w95% -h90% -E 'tmux_attach.sh "floating" "$(tmux show -gvq '@window_name')" "$(tmux show -gvq '@pane_current_path')"' 156 | set status-interval 1 157 | } 158 | 159 | # Break the current pane into a new window. 160 | bind ! if-shell -F '#{!=:#{session_name},floating}' { 161 | break-pane 162 | } { 163 | run-shell 'bash -c "tmux break-pane -s floating -t \"$(tmux show -gvq '@last_session_name'):\""' 164 | } 165 | 166 | # Break the current pane into a new window in the background. 167 | bind @ if-shell -F '#{!=:#{session_name},floating}' { 168 | break-pane -d 169 | } { 170 | run-shell 'bash -c "tmux break-pane -d -s floating -t \"$(tmux show -gvq '@last_session_name'):\""' 171 | } 172 | 173 | #============== 174 | # UI 175 | #============== 176 | 177 | # Border colors 178 | set-option -g pane-border-style fg=colour240 179 | set-option -g pane-active-border-style fg=colour250 180 | set-option -g popup-border-lines rounded 181 | 182 | #------------- 183 | # Status bar 184 | #------------- 185 | set-option -g status on # turn the status bar on 186 | set -g status-interval 1 # set update frequency (default 15 seconds) 187 | set -g status-justify left # window list on left side 188 | 189 | # visual notification of activity in other windows 190 | setw -g monitor-activity on 191 | set -g visual-activity on 192 | 193 | # set color for status bar 194 | set-option -g status-style bg=terminal 195 | set-option -g status-style fg=white 196 | 197 | # window list 198 | set-window-option -g window-status-format '#[fg=white]  #I:#W#F ' 199 | set-window-option -g window-status-current-format '#[bg=blue]#[fg=black]  #I:#W#F ' 200 | set-window-option -g window-status-separator '' 201 | 202 | set -g status-left "#[fg=blue]  #S:#I:#P " 203 | set -g status-left-length 40 204 | 205 | # Right side of status bar: 206 | # prefix indicator, 207 | # session:window:pane number, 208 | # date time. 209 | set -g status-right "#{prefix_highlight} \ 210 | #[fg=yellow] #{=/-40/…:#{s|^$HOME|~:pane_current_path}} \ 211 | #[fg=green]󰃰 %a %m/%d %H:%M:%S" 212 | set -g status-right-length 70 213 | 214 | # Install tpm if not already installed 215 | run-shell "if [ ! -d ~/.config/tmux/plugins/tpm ]; then mkdir -p ~/.config/tmux/plugins; git clone --depth 1 https://github.com/tmux-plugins/tpm ~/.config/tmux/plugins/tpm; fi" 216 | 217 | set -g @plugin 'tmux-plugins/tmux-prefix-highlight' 218 | set -g @plugin 'jaclu/tmux-menus' 219 | set -g @menus_trigger 'Space' 220 | set -g @plugin 'laktak/extrakto' 221 | set -g @extrakto_filter_order 'path url word line all' 222 | set -g @plugin 'schasse/tmux-jump' 223 | set -g @jump-key 'J' 224 | set -g @jump-bg-color '\e[0m\e[90m' 225 | set -g @jump-fg-color '\e[1m\e[31m' 226 | 227 | run '~/.config/tmux/plugins/tpm/tpm' 228 | -------------------------------------------------------------------------------- /tmux/tmux.reset.conf: -------------------------------------------------------------------------------- 1 | # First remove *all* keybindings 2 | unbind-key -a 3 | # Now reinsert all the regular tmux keys 4 | bind-key C-b send-prefix 5 | bind-key C-o rotate-window 6 | bind-key C-z suspend-client 7 | bind-key Space next-layout 8 | bind-key ! break-pane 9 | bind-key '"' split-window 10 | bind-key '#' list-buffers 11 | bind-key '$' command-prompt -I "#S" "rename-session '%%'" 12 | bind-key % split-window -h 13 | bind-key & confirm-before -p "kill-window #W? (y/n)" kill-window 14 | bind-key "'" command-prompt -p index "select-window -t ':%%'" 15 | bind-key ( switch-client -p 16 | bind-key ) switch-client -n 17 | bind-key , command-prompt -I "#W" "rename-window '%%'" 18 | bind-key - delete-buffer 19 | bind-key . command-prompt "move-window -t '%%'" 20 | bind-key 0 select-window -t :0 21 | bind-key 1 select-window -t :1 22 | bind-key 2 select-window -t :2 23 | bind-key 3 select-window -t :3 24 | bind-key 4 select-window -t :4 25 | bind-key 5 select-window -t :5 26 | bind-key 6 select-window -t :6 27 | bind-key 7 select-window -t :7 28 | bind-key 8 select-window -t :8 29 | bind-key 9 select-window -t :9 30 | bind-key : command-prompt 31 | bind-key \; last-pane 32 | bind-key = choose-buffer 33 | bind-key ? list-keys 34 | bind-key D choose-client 35 | bind-key L switch-client -l 36 | bind-key [ copy-mode 37 | bind-key ] paste-buffer 38 | bind-key c new-window 39 | bind-key d detach-client 40 | bind-key f command-prompt "find-window '%%'" 41 | bind-key i display-message 42 | bind-key l last-window 43 | bind-key n next-window 44 | bind-key o select-pane -t :.+ 45 | bind-key p previous-window 46 | bind-key q display-panes 47 | bind-key r refresh-client 48 | bind-key s choose-tree 49 | bind-key t clock-mode 50 | bind-key w choose-window 51 | bind-key x confirm-before -p "kill-pane #P? (y/n)" kill-pane 52 | bind-key z resize-pane -Z 53 | bind-key { swap-pane -U 54 | bind-key } swap-pane -D 55 | bind-key '~' show-messages 56 | bind-key PPage copy-mode -u 57 | bind-key -r Up select-pane -U 58 | bind-key -r Down select-pane -D 59 | bind-key -r Left select-pane -L 60 | bind-key -r Right select-pane -R 61 | bind-key M-1 select-layout even-horizontal 62 | bind-key M-2 select-layout even-vertical 63 | bind-key M-3 select-layout main-horizontal 64 | bind-key M-4 select-layout main-vertical 65 | bind-key M-5 select-layout tiled 66 | bind-key M-n next-window -a 67 | bind-key M-o rotate-window -D 68 | bind-key M-p previous-window -a 69 | bind-key -r M-Up resize-pane -U 5 70 | bind-key -r M-Down resize-pane -D 5 71 | bind-key -r M-Left resize-pane -L 5 72 | bind-key -r M-Right resize-pane -R 5 73 | bind-key -r C-Up resize-pane -U 74 | bind-key -r C-Down resize-pane -D 75 | bind-key -r C-Left resize-pane -L 76 | bind-key -r C-Right resize-pane -R 77 | 78 | -------------------------------------------------------------------------------- /vim/basic.vim: -------------------------------------------------------------------------------- 1 | """ General 2 | set nocompatible 3 | 4 | set number 5 | set relativenumber 6 | 7 | " Show incomplete cmds down the bottom 8 | set showcmd 9 | 10 | " Sets how many lines of history VIM has to remember 11 | set history=1000 12 | 13 | " Enable filetype plugins 14 | filetype plugin on 15 | filetype indent on 16 | 17 | " Set to auto read when a file is changed from the outside 18 | "set autoread 19 | 20 | set timeoutlen=500 21 | set ttimeoutlen=10 22 | 23 | " Disable mouse by default. 24 | set mouse= 25 | 26 | " Enable spell checking 27 | set spell 28 | set spelllang=en_us 29 | 30 | """ User Interface 31 | 32 | " Set 7 lines to the cursor - when moving vertically using j/k 33 | set scrolloff=7 34 | 35 | " Turn on the wild menu 36 | set wildmenu 37 | set wildmode=longest:full,full 38 | 39 | " Ignore compiled files 40 | set wildignore=*.o,*~,*.pyc,*.pyo,*.class,*.swp 41 | if has("win16") || has("win32") 42 | set wildignore+=.git\*,.hg\*,.svn\* 43 | else 44 | set wildignore+=*/.git/*,*/.hg/*,*/.svn/*,*/.DS_Store 45 | endif 46 | 47 | " Always show current position 48 | set ruler 49 | 50 | " Highlight current line 51 | set cursorline 52 | 53 | " Height of the command bar 54 | set cmdheight=1 55 | 56 | " A buffer becomes hidden when it is abandoned 57 | set hidden 58 | 59 | " Configure backspace so it acts as it should act 60 | set backspace=eol,start,indent 61 | set whichwrap+=<,>,h,l 62 | 63 | " Ignore case when searching 64 | set ignorecase 65 | 66 | " When searching try to be smart about cases 67 | set smartcase 68 | 69 | " Highlight search results 70 | set hlsearch 71 | 72 | " Makes search act like search in modern browsers 73 | set incsearch 74 | 75 | " Don't redraw while executing macros (good performance config) 76 | " TODO: this seems to cause rendering issues. 77 | " set lazyredraw 78 | 79 | " For regular expressions turn magic on 80 | set magic 81 | 82 | " Show matching brackets when text indicator is over them 83 | set showmatch 84 | " How many tenths of a second to blink when matching brackets 85 | set matchtime=2 86 | 87 | " No annoying sound on errors 88 | set noerrorbells 89 | set novisualbell 90 | set t_vb= 91 | 92 | " Enable syntax highlighting 93 | syntax enable 94 | 95 | set background=dark 96 | 97 | set encoding=utf8 98 | 99 | " Use Unix as the standard file type 100 | set fileformats=unix,dos,mac 101 | 102 | " Support italic 103 | set t_ZH= 104 | set t_ZR= 105 | 106 | " Always show status line and tabline 107 | set laststatus=2 108 | set showtabline=2 109 | 110 | if !has("nvim") && exists('+termguicolors') 111 | set termguicolors 112 | endif 113 | 114 | set splitbelow 115 | set splitright 116 | 117 | if has("patch-8.1.1564") 118 | " Recently vim can merge signcolumn and number column into one 119 | set signcolumn=number 120 | endif 121 | 122 | """ Files, backups and undo 123 | 124 | set undofile 125 | if !has("nvim") 126 | let g:vim_data_dir = '~/.local/share/vim/' 127 | " Create temp dirs if not exist 128 | for d in ['undo', 'swap'] 129 | let p = g:vim_data_dir.d 130 | if !isdirectory(p) 131 | execute 'silent !mkdir -p '.p.' > /dev/null 2>&1' 132 | endif 133 | endfor 134 | exec "set undodir=".g:vim_data_dir."/undo//" 135 | exec "set directory=".g:vim_data_dir."/swap//" 136 | endif 137 | 138 | """ Text, tab and indent related 139 | 140 | " Use spaces instead of tabs 141 | set expandtab 142 | 143 | " Be smart when using tabs 144 | set smarttab 145 | 146 | " 1 tab == 4 spaces 147 | set shiftwidth=4 148 | set tabstop=4 149 | 150 | " Linebreak on 500 characters 151 | set linebreak 152 | set textwidth=500 153 | 154 | set autoindent 155 | set wrap " Wrap lines 156 | let &showbreak = "↪ " " Show line wrap indicator 157 | 158 | """ Custom commands 159 | 160 | " :W sudo saves the file 161 | command W w !sudo tee % > /dev/null 162 | 163 | """ Auto Commands 164 | 165 | " Parse syntax from this many lines backwards. 166 | " If syntax is still incorrect, manually reparse syntax with 167 | " ':syntax sync fromstart'. 168 | autocmd BufEnter * syntax sync minlines=5000 169 | 170 | " Return to last edit position when opening files 171 | autocmd BufReadPost * if line("'\"") > 1 && line("'\"") <= line("$") | exe "normal! g'\"" | endif 172 | 173 | " Delete trailing white space on save 174 | func! DeleteTrailingWhitespaces() 175 | exe "normal mz" 176 | %s/\s\+$//ge 177 | exe "normal `z" 178 | endfunc 179 | autocmd BufWrite * :call DeleteTrailingWhitespaces() 180 | 181 | " Python 182 | " Fold files based on indentation 183 | autocmd FileType python,pyrex set foldmethod=indent 184 | autocmd FileType python,pyrex set foldlevel=99 185 | 186 | " C/C++ 187 | " Use "//"-style comments 188 | autocmd FileType c,cpp setlocal commentstring=//\ %s 189 | 190 | """ Key mappings 191 | 192 | let g:mapleader = " " 193 | let g:maplocalleader = "\\" 194 | 195 | function! GetSelection(one_line) range 196 | let [lnum1, col1] = getpos("'<")[1:2] 197 | let [lnum2, col2] = getpos("'>")[1:2] 198 | let lines = getline(lnum1, lnum2) 199 | let lines[-1] = lines[-1][: col2 - (&selection == 'inclusive' ? 1 : 2)] 200 | let lines[0] = lines[0][col1 - 1:] 201 | let res = join(lines, a:one_line ? '\n' : "\n") 202 | return res 203 | endfunction 204 | 205 | function! ReplaceSelection() range 206 | let cmd = '":\%sno/".GetSelection(1)."/"' 207 | return ":\call feedkeys(". cmd. ", 'n')\" 208 | endfunction 209 | 210 | function! SearchSelection(forward) range 211 | let cmd = a:forward ? '"/' : '"?' 212 | let cmd .= '\".GetSelection(1)."\"' 213 | return ":\call feedkeys(". cmd . ", 'n')\" 214 | endfunction 215 | 216 | function! SwitchNumber() 217 | if(&relativenumber) 218 | set norelativenumber 219 | set number 220 | elseif(&number) 221 | set norelativenumber 222 | set nonumber 223 | else 224 | set relativenumber 225 | set nonumber 226 | endif 227 | endfunc 228 | 229 | "" Override builtins 230 | 231 | " Make * and # work in visual mode as well 232 | vnoremap * SearchSelection(1) 233 | vnoremap # SearchSelection(0) 234 | 235 | " use register z for x and s 236 | nnoremap x "zx 237 | nnoremap X "zX 238 | 239 | " Don't lose selection when indenting 240 | xnoremap < >gv 242 | 243 | " Fast quit 244 | nnoremap q :q 245 | nnoremap Q :q! 246 | 247 | " Copy to system clipboard 248 | map y "+y 249 | 250 | "" UI 251 | 252 | " toggle highlight search 253 | noremap uh :set hlsearch! hlsearch? 254 | " switch between number, relative_number, no_number 255 | noremap un :call SwitchNumber() 256 | " toggle wrap 257 | noremap uw :set wrap! wrap? 258 | " toggle spell checking 259 | noremap us :set spell! spell? 260 | 261 | "" Buffers 262 | 263 | " open new buffer 264 | noremap be :enew 265 | " delete buffer 266 | noremap x :bd 267 | " write buffer 268 | noremap bw :w 269 | " switch buffers 270 | nnoremap :bn 271 | nnoremap ]b :bn 272 | nnoremap :bp 273 | nnoremap [b :bp 274 | " switch to last edited buffer 275 | noremap bl 276 | " Only keep the current buffer, close all others. 277 | " %bd = delete all buffers; e# = edit the last buffer; bd# = delete the last buffer with "[No Name]". 278 | command! BufOnly silent! execute "%bd|e#|bd#" 279 | noremap bo :BufOnly 280 | 281 | """ Tabs 282 | " Open new tab 283 | noremap te :tabnew 284 | " Next tab 285 | noremap tn :tabn 286 | noremap ]t :tabn 287 | " Previous tab 288 | noremap tp :tabp 289 | noremap [t :tabp 290 | " Close tab 291 | noremap tx :tabclose 292 | 293 | "" Windows 294 | 295 | " split window vertically 296 | noremap wv v 297 | " split window horizontally 298 | noremap wh s 299 | " make split windows equal size 300 | noremap we = 301 | " close current window 302 | noremap wx c 303 | " increase window height 304 | nnoremap w= + 305 | " decrease window height 306 | nnoremap w- - 307 | " increase window width 308 | nnoremap w. > 309 | " decrease window width 310 | nnoremap w, < 311 | " switch windows 312 | nnoremap h 313 | nnoremap l 314 | nnoremap j 315 | nnoremap k 316 | 317 | " Switch quicklist 318 | nnoremap [q :cprev 319 | nnoremap ]q :cnext 320 | 321 | " Move a line of text 322 | vnoremap :m'>+`mzgv`yo`z 323 | vnoremap :m'<-2`>my`r to replace selected text 326 | vnoremap r ReplaceSelection() 327 | 328 | "" Command-line mode. 329 | 330 | set cedit=\ 331 | 332 | " Emacs-style key mappings for the command line (`:help emacs-keys`). 333 | " start of line 334 | cnoremap 335 | " back one character 336 | cnoremap 337 | " also bind to because is tmux prefix 338 | cnoremap 339 | " delete character under cursor 340 | cnoremap 341 | " end of line 342 | cnoremap 343 | " forward one character 344 | cnoremap 345 | " recall newer command-line 346 | cnoremap 347 | " recall previous (older) command-line 348 | cnoremap 349 | " back one word 350 | cnoremap 351 | " forward one word 352 | cnoremap 353 | 354 | " Insert the path of the current file's directory. 355 | cnoremap =expand('%:p:h')."/" 356 | " Insert the path of the current file. 357 | cnoremap =expand('%:p') 358 | -------------------------------------------------------------------------------- /vim/less.vim: -------------------------------------------------------------------------------- 1 | " Vim script to work like "less" 2 | " Maintainer: Bram Moolenaar 3 | " Last Change: 2006 Dec 05 4 | 5 | " Avoid loading this file twice, allow the user to define his own script. 6 | if exists("loaded_less") 7 | finish 8 | endif 9 | let loaded_less = 1 10 | 11 | " If not reading from stdin, skip files that can't be read. 12 | " Exit if there is no file at all. 13 | if argc() > 0 14 | let s:i = 0 15 | while 1 16 | if filereadable(argv(s:i)) 17 | if s:i != 0 18 | sleep 3 19 | endif 20 | break 21 | endif 22 | if isdirectory(argv(s:i)) 23 | echomsg "Skipping directory " . argv(s:i) 24 | elseif getftime(argv(s:i)) < 0 25 | echomsg "Skipping non-existing file " . argv(s:i) 26 | else 27 | echomsg "Skipping unreadable file " . argv(s:i) 28 | endif 29 | echo "\n" 30 | let s:i = s:i + 1 31 | if s:i == argc() 32 | quit 33 | endif 34 | next 35 | endwhile 36 | endif 37 | 38 | set nocp 39 | syntax on 40 | set so=0 41 | set hlsearch 42 | set incsearch 43 | nohlsearch 44 | " Don't remember file names and positions 45 | set viminfo= 46 | set nows 47 | " Inhibit screen updates while searching 48 | let s:lz = &lz 49 | set lz 50 | 51 | " When reading from stdin don't consider the file modified. 52 | au VimEnter * set nomod 53 | 54 | " Can't modify the text 55 | set noma 56 | 57 | " Give help 58 | noremap h :call Help() 59 | map H h 60 | fun! s:Help() 61 | echo " One page forward b One page backward" 62 | echo "d Half a page forward u Half a page backward" 63 | echo " One line forward k One line backward" 64 | echo "G End of file g Start of file" 65 | echo "N% percentage in file" 66 | echo "\n" 67 | echo "/pattern Search for pattern ?pattern Search backward for pattern" 68 | echo "n next pattern match N Previous pattern match" 69 | echo "\n" 70 | echo ":n Next file :p Previous file" 71 | echo "\n" 72 | echo "q Quit v Edit file" 73 | let i = input("Hit Enter to continue") 74 | endfun 75 | 76 | " Scroll one page forward 77 | noremap