diff options
Diffstat (limited to 'common')
112 files changed, 24300 insertions, 0 deletions
diff --git a/common/.bash_profile b/common/.bash_profile new file mode 100644 index 0000000..f6c3ee4 --- /dev/null +++ b/common/.bash_profile @@ -0,0 +1,11 @@ +# ~/.bash_profile + +# Source ~/.profile if it exists (environment variables) +if [ -f "$HOME/.profile" ]; then + . "$HOME/.profile" +fi + +# Source ~/.bashrc for interactive settings (aliases, prompt, etc.) +if [ -f "$HOME/.bashrc" ]; then + . "$HOME/.bashrc" +fi diff --git a/common/.bashrc b/common/.bashrc new file mode 100644 index 0000000..fbc86fe --- /dev/null +++ b/common/.bashrc @@ -0,0 +1,371 @@ +# shellcheck shell=bash +# +#██████╗ █████╗ ███████╗██╗ ██╗██████╗ ██████╗ +#██╔══██╗██╔══██╗██╔════╝██║ ██║██╔══██╗██╔════╝ +#██████╔╝███████║███████╗███████║██████╔╝██║ +#██╔══██╗██╔══██║╚════██║██╔══██║██╔══██╗██║ +#██████╔╝██║ ██║███████║██║ ██║██║ ██║╚██████╗ +#╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ +# +# ~/.bashrc +# + +if [[ $- != *i* ]]; then + . ~/.profile + return +fi + +# Get the current active terminal +term="$(cat /proc/"$PPID"/comm)" + +# Set a default prompt +p='\[\033[01;37m\]┌─[\[\033[01;32m\]srdusr\[\033[01;37m\]]-[\[\033[01;36m\]archlinux\[\033[01;37m\]]-[\[\033[01;33m\]\W\]\[\033[00;37m\]\[\033 +\[\033[01;37m\]└─[\[\033[05;33m\]$\[\033[00;37m\]\[\033[01;37m\]]\[\033[00;37m\] ' + +# Set transparency and prompt while using st +if [[ $term = "st" ]]; then + transset-df "0.65" --id "$WINDOWID" >/dev/null + + # [Your_Name]-----| |=======|------[Your_Distro] + # [Color]--------| | [Color]------| | + # [Style]------------| | | [Style]---------| | | + # V V V V V V + p='\[\033[01;37m\]┌─[\[\033[01;32m\]srdusr\[\033[01;37m\]]-[\[\033[01;36m\]archlinux\[\033[01;37m\]]-[\[\033[01;33m\]\W\[\033[00;37m\]\[\033[01;37m\]] +\[\033[01;37m\]└─[\[\033[05;33m\]$\[\033[00;37m\]\[\033[01;37m\]]\[\033[00;37m\] ' +# A A A +# [Style]----| | |-------- [Your_Choice] +# [Color]------------| + +fi + +# If not running interactively, dont do anything +[[ $- != *i* ]] && return + +# My alias commands +alias ls='ls --color=auto -1' +alias shred='shred -uzvn3' +alias wallset='feh --bg-fill' + +# Dotfiles Management System +if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then + # Core git wrapper with repository as work-tree + _config() { + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" "$@" + } + + # Detect OS + case "$(uname -s)" in + Linux) CFG_OS="linux" ;; + Darwin) CFG_OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) CFG_OS="windows" ;; + *) CFG_OS="other" ;; + esac + + # Map system path to repository path + _repo_path() { + local f="$1" + + # If it's an absolute path that's not in HOME, handle it specially + if [[ "$f" == /* && "$f" != "$HOME/"* ]]; then + echo "$CFG_OS/${f#/}" + return + fi + + # Check for paths that should go to the repository root + case "$f" in + common/*|linux/*|macos/*|windows/*|profile/*|README.md) + echo "$f" + return + ;; + "$HOME/"*) + f="${f#$HOME/}" + ;; + esac + + # Default: put under OS-specific home + echo "$CFG_OS/home/$f" + } + + _sys_path() { + local repo_path="$1" + local os_path_pattern="$CFG_OS/" + + # Handle OS-specific files that are not in the home subdirectory + if [[ "$repo_path" == "$os_path_pattern"* && "$repo_path" != */home/* ]]; then + echo "/${repo_path#$os_path_pattern}" + return + fi + + case "$repo_path" in + # Common configs → OS-specific config dirs + common/config/*) + case "$CFG_OS" in + linux) + local base="${XDG_CONFIG_HOME:-$HOME/.config}" + echo "$base/${repo_path#common/config/}" + ;; + macos) + echo "$HOME/Library/Application Support/${repo_path#common/config/}" + ;; + windows) + echo "$LOCALAPPDATA\\${repo_path#common/config/}" + ;; + *) + echo "$HOME/.config/${repo_path#common/config/}" + ;; + esac + ;; + + # Common assets → stay in repo + common/assets/*) + echo "$HOME/.cfg/$repo_path" + ;; + + # Other common files (dotfiles like .bashrc, .gitconfig, etc.) → $HOME + common/*) + echo "$HOME/${repo_path#common/}" + ;; + + # OS-specific home + */home/*) + echo "$HOME/${repo_path#*/home/}" + ;; + + # Profile configs and README → stay in repo + profile/*|README.md) + echo "$HOME/.cfg/$repo_path" + ;; + + # Default fallback + *) + echo "$HOME/.cfg/$repo_path" + ;; + + esac + } + + # Prompts for sudo if needed and runs the command + _sudo_prompt() { + if [[ $EUID -eq 0 ]]; then + "$@" + else + if command -v sudo >/dev/null; then + sudo "$@" + elif command -v doas >/dev/null; then + doas "$@" + elif command -v pkexec >/dev/null; then + pkexec "$@" + else + echo "Error: No privilege escalation tool found." + return 1 + fi + fi + } + + # Main config command + config() { + local cmd="$1"; shift + local target_dir="" + # Parse optional --target flag for add + if [[ "$cmd" == "add" ]]; then + while [[ "$1" == --* ]]; do + case "$1" in + --target|-t) + target_dir="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + return 1 + ;; + esac + done + fi + + case "$cmd" in + add) + local file_path + for file_path in "$@"; do + local repo_path + if [[ -n "$target_dir" ]]; then + local rel_path + if [[ "$file_path" == /* ]]; then + rel_path="$(basename "$file_path")" + else + rel_path="$file_path" + fi + repo_path="$target_dir/$rel_path" + else + repo_path="$(_repo_path "$file_path")" + fi + + local full_repo_path="$HOME/.cfg/$repo_path" + mkdir -p "$(dirname "$full_repo_path")" + cp -a "$file_path" "$full_repo_path" + + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" add "$repo_path" + + echo "Added: $file_path -> $repo_path" + done + ;; + rm) + local rm_opts="" + local file_path_list=() + + for arg in "$@"; do + if [[ "$arg" == "-"* ]]; then + rm_opts+=" $arg" + else + file_path_list+=("$arg") + fi + done + + for file_path in "${file_path_list[@]}"; do + local repo_path="$(_repo_path "$file_path")" + + if [[ "$rm_opts" == *"-r"* ]]; then + _config rm --cached -r "$repo_path" + else + _config rm --cached "$repo_path" + fi + + eval "rm $rm_opts \"$file_path\"" + echo "Removed: $file_path" + done + ;; + sync) + local direction="${1:-to-repo}"; shift + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ "$direction" == "to-repo" ]]; then + if [[ -e "$sys_file" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + cp -a "$sys_file" "$full_repo_path" + echo "Synced to repo: $sys_file" + fi + elif [[ "$direction" == "from-repo" ]]; then + if [[ -e "$full_repo_path" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + local dest_dir="$(dirname "$sys_file")" + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + echo "Synced from repo: $sys_file" + fi + fi + done + ;; + status) + local auto_synced=() + while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ -e "$sys_file" && -e "$full_repo_path" ]]; then + if ! diff -q "$full_repo_path" "$sys_file" >/dev/null 2>&1; then + cp -fa "$sys_file" "$full_repo_path" + auto_synced+=("$repo_file") + fi + fi + done < <(_config ls-files) + if [[ ${#auto_synced[@]} -gt 0 ]]; then + echo "=== Auto-synced Files ===" + for repo_file in "${auto_synced[@]}"; do + echo "synced: $(_sys_path "$repo_file") -> $repo_file" + done + echo + fi + _config status + echo + ;; + deploy) + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" # destination only + + # Only continue if the source exists + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if needed + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Deployed: $repo_file -> $sys_file" + fi + done + ;; + checkout) + echo "Checking out dotfiles from .cfg..." + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" + + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if it doesn't exist + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Checked out: $repo_file -> $sys_file" + fi + done + ;; + backup) + local timestamp=$(date +%Y%m%d%H%M%S) + local backup_dir="$HOME/.dotfiles_backup/$timestamp" + echo "Backing up existing dotfiles to $backup_dir..." + + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + if [[ -e "$sys_file" ]]; then + local dest_dir_full="$backup_dir/$(dirname "$repo_file")" + mkdir -p "$dest_dir_full" + cp -a "$sys_file" "$backup_dir/$repo_file" + fi + done + echo "Backup complete. To restore, copy files from $backup_dir to their original locations." + ;; + *) + _config "$cmd" "$@" + ;; + esac + } +fi + +PS1=$p + +bind -m vi-command 'Control-l: clear-screen' +bind -m vi-insert 'Control-l: clear-screen' + +export EDITOR="nvim" + +#export NVM_DIR="$HOME/.local/share/nvm" +#[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +#[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + +export PROMPT_COMMAND="resize &>/dev/null ; $PROMPT_COMMAND" + +# Rust environment (silent if not installed) +export RUSTUP_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/rustup" +export CARGO_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/cargo" +export PATH="$CARGO_HOME/bin:$RUSTUP_HOME/bin:$PATH" + +if command -v rustc >/dev/null 2>&1; then + export RUST_BACKTRACE=1 +fi diff --git a/common/.editorconfig b/common/.editorconfig new file mode 100644 index 0000000..22f30d4 --- /dev/null +++ b/common/.editorconfig @@ -0,0 +1,80 @@ + +root = true + + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 +max_line_length = 10000 + +# Makefile-specific settings +[Makefile] +indent_style = tab +indent_size = 4 + +# C/C++ source files +[*.c] +indent_style = tab +indent_size = 4 + +# TypeScript/JavaScript config +[**.{ts,js}] +indent_size = 2 + +# Json config +[**.json] +indent_size = 2 + +# Lua config +[*.lua] +indent_size = 2 +tab_width = 2 +# [none/single/double] +quote_style = double +# [line break] +break_all_list_when_line_exceed = false +auto_collapse_lines = false +break_before_braces = false +# [preference] +ignore_space_after_colon = false +remove_call_expression_list_finish_comma = false +end_statement_with_semicolon = keep + +# Python config +[*.py] +indent_size = 4 + +# Shell config +[*.sh] +indent_size = 4 + +# Bash config +[*.bash] +indent_size = 4 + +# Yaml config +[*.yml] +indent_size = 2 + +# Latex config +[**.tex] +trim_trailing_whitespace = false + +# Markdown config +[**.md] +indent_size = 2 +trim_trailing_whitespace = false + +# Textfile config +[**.txt] +trim_trailing_whitespace = false +insert_final_newline = false + +# Snippets config +[**.snippets] +indent_style = tab + diff --git a/common/.face b/common/.face Binary files differnew file mode 100644 index 0000000..3aafd06 --- /dev/null +++ b/common/.face diff --git a/common/.gitconfig b/common/.gitconfig new file mode 100644 index 0000000..2c0d5ef --- /dev/null +++ b/common/.gitconfig @@ -0,0 +1,34 @@ +[user] + name = + email = + +[init] + defaultBranch = main + +[color] + ui = true + +[alias] + graph = log --oneline --graph --decorate + ls = log --pretty=format:"%C(yellow)%h%Cred%d\\ %Creset%s%Cblue\\ [%cn]" --decorate + ll = log --pretty=format:"%C(yellow)%h%Cred%d\\ %Creset%s%Cblue\\ [%cn]" --decorate --numstat + lds = log --pretty=format:"%C(yellow)%h\\ %ad%Cred%d\\ %Creset%s%Cblue\\ [%cn]" --decorate --date=short + conflicts = diff --name-only --diff-filter=U + local-branches = !git branch -vv | cut -c 3- | awk '$3 !~/\\[/ { print $1 }' + recent-branches = !git branch --sort=-committerdate | head + authors = !git log --format='%aN <%aE>' | grep -v 'users.noreply.github.com' | sort -u --ignore-case + sba ="!f() { git subtree add --prefix $2 $1 main; }; f" + sbu ="!f() { git subtree pull --prefix $2 $1 main; }; f" + stashrebase = "!f() { if [ \"$(git symbolic-ref --short HEAD)\" = \"main\" ]; then git stash save && git fetch && git rebase origin main && git stash apply; else git stash save && git fetch && git rebase origin master && git stash apply; fi; }; f" + dotfiles = "!f() { git --git-dir=$HOME/.cfg --work-tree=$HOME stashrebase; }; f" + + +[credential "https://github.com"] + helper = + helper = !sh -c 'command -v gh >/dev/null 2>&1 && exec gh auth git-credential || exit 0' + +[credential "https://gist.github.com"] + helper = + helper = !sh -c 'command -v gh >/dev/null 2>&1 && exec gh auth git-credential || exit 0' +[credential] + helper = cache diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 0000000..9c9bef7 --- /dev/null +++ b/common/.gitignore @@ -0,0 +1,44 @@ +# Ignore .git directory +.git/ + +# Ignore git config +.gitconfig + +# Ignore Packer's compiled files +packer_compiled.lua + +# Ignore Zsh plugins directory +~/.config/zsh/plugins + +# Ignore zcompdump files +zcompdump + +# Ignore .DS_Store files (macOS) +*.DS_Store + +# Ignore .spl files +*.spl + +# Ignore node_modules directory +node_modules/ + +# Ignore .zip files +*.zip + +# Ignore .pxd files +*.pxd + +# Ignore .cache directory +^.cache/ + +# Ignore normal directories +^downloads/ +^music/ +^images/ +^pictures/ +^videos/ +^virt/ + +# Ignore dotfiles dir +~/.cfg +.cfg diff --git a/common/.gitmodules b/common/.gitmodules new file mode 100644 index 0000000..37da2b9 --- /dev/null +++ b/common/.gitmodules @@ -0,0 +1,12 @@ +[submodule ".vim/pack/plugins/start/vim-tmux-navigator"] + path = .vim/pack/plugins/start/vim-tmux-navigator + url = https://github.com/christoomey/vim-tmux-navigator.git +[submodule "zsh/plugins/zsh-you-should-use"] + path = .config/zsh/plugins/zsh-you-should-use + url = git@github.com:MichaelAquilina/zsh-you-should-use.git +[submodule "zsh/plugins/zsh-syntax-highlighting"] + path = .config/zsh/plugins/zsh-syntax-highlighting + url = git@github.com:zsh-users/zsh-syntax-highlighting.git +[submodule "zsh/plugins/zsh-syntax-highlighting"] + path = .config/zsh/plugins/zsh-autosuggestions + url = git@github.com:zsh-users/zsh-autosuggestions.git diff --git a/common/.gitsubtrees b/common/.gitsubtrees new file mode 100644 index 0000000..d8a7f29 --- /dev/null +++ b/common/.gitsubtrees @@ -0,0 +1,7 @@ +[subtree "common/config/nvim"] + path = common/config/nvim + url = git@github.com:srdusr/nvim.git + +[subtree "common/scripts"] + path = common/scripts + url = git@github.com:srdusr/scripts.git diff --git a/common/.prettierrc.yml b/common/.prettierrc.yml new file mode 100644 index 0000000..c5d13e2 --- /dev/null +++ b/common/.prettierrc.yml @@ -0,0 +1,5 @@ +semi: true +singleQuote: true +jsxSingleQuote: true +trailingComma: all +arrowParens: avoid diff --git a/common/.profile b/common/.profile new file mode 100644 index 0000000..c63f761 --- /dev/null +++ b/common/.profile @@ -0,0 +1,99 @@ +#!/bin/bash + +# ====================================== +# Basic environment setup +# ====================================== + +export EDITOR="$(command -v nvim || command -v vim || echo nano)" + +# Load zsh env if running zsh +if [ -n "$ZSH_VERSION" ] && [ -f "$HOME/.config/zsh/.zshenv" ]; then + . "$HOME/.config/zsh/.zshenv" +fi + +cd "$HOME" || exit 1 + +# ====================================== +# Session launcher +# ====================================== + +# Detect graphical DE session +if [ -n "$DISPLAY" ]; then + #echo "Graphical session detected ($XDG_SESSION_DESKTOP). Skipping auto TTY session launch." + return +fi + +# Only run on first virtual terminal +if [ -z "$XDG_VTNR" ] || [ "$XDG_VTNR" -ne 1 ]; then + return +fi + +# Clean environment +unset DISPLAY XAUTHORITY DBUS_SESSION_BUS_ADDRESS + +# Priority-ordered list of sessions (WM/DE) +sessions=( + "Hyprland" + "bspwm" + "sway" + "gnome-session" + "startplasma-x11" + "startxfce4" + "openbox" + "i3" +) + +# Handle saved session +if [ -f "$HOME/.session" ]; then + chosen_session=$(<"$HOME/.session") + rm -f "$HOME/.session" +fi + +# Start a session +start_session() { + local s="$1" + case "$s" in + bspwm) + export XDG_SESSION_TYPE="x11" + exec startx /usr/bin/bspwm + ;; + Hyprland|sway) + exec dbus-launch --sh-syntax --exit-with-session "$s" + ;; + gnome-session|startplasma-x11|startxfce4|openbox|i3) + exec "$s" + ;; + *) + return 1 + ;; + esac +} + +# Try saved session first +if [ -n "$chosen_session" ]; then + if start_session "$chosen_session"; then + exit + else + echo "Saved session '$chosen_session' not found. Falling back..." + fi +fi + +# Try default sessions in priority +for wm in "${sessions[@]}"; do + if command -v "$wm" >/dev/null 2>&1; then + echo "Starting session: $wm" + start_session "$wm" + exit + fi +done + +# Fallback: Check for common display managers (GDM/LightDM/SDDM) +for dm in gdm lightdm sddm; do + if command -v "$dm" >/dev/null 2>&1; then + echo "Launching display manager: $dm" + exec "$dm" + fi +done + +echo "No suitable window manager or display manager found." +exit 1 diff --git a/common/.zprofile b/common/.zprofile new file mode 100644 index 0000000..dafce71 --- /dev/null +++ b/common/.zprofile @@ -0,0 +1,3 @@ +emulate sh -c '. ~/.profile' +#[[ -f ~/.config/zsh/.zshenv ]] && . ~/.config/zsh/.zshenv +#[[ -f ~/.profile ]] && . ~/.profile diff --git a/common/.zshrc b/common/.zshrc new file mode 100644 index 0000000..bd22e32 --- /dev/null +++ b/common/.zshrc @@ -0,0 +1,10 @@ +# ~/.zshrc +[[ -f ~/.config/zsh/.zshrc ]] && source ~/.config/zsh/.zshrc + +# Point all zsh startup files to ~/.config/zsh +export ZDOTDIR="$HOME/.config/zsh" + +# If you want, you can still source your real zshenv from there: +if [[ -f "$ZDOTDIR/.zshenv" ]]; then + source "$ZDOTDIR/.zshenv" +fi diff --git a/common/assets/desktop.jpg b/common/assets/desktop.jpg Binary files differnew file mode 100644 index 0000000..e85b2ff --- /dev/null +++ b/common/assets/desktop.jpg diff --git a/common/assets/old_desktop.jpg b/common/assets/old_desktop.jpg Binary files differnew file mode 100644 index 0000000..3ecb22e --- /dev/null +++ b/common/assets/old_desktop.jpg diff --git a/common/config/alacritty/alacritty.yml b/common/config/alacritty/alacritty.yml new file mode 100644 index 0000000..21e70c7 --- /dev/null +++ b/common/config/alacritty/alacritty.yml @@ -0,0 +1,106 @@ +# ~/.config/alacritty/alacritty.yml + + +live_config_reload: true + +window: + opacity: 0.6 + dynamic_title: true + dimensions: + columns: 2 + lines: 2 + + + # startup_mode: Maximized + # position: + # x: 0 + # y: 0 + + padding: + x: 9 + y: 9 + + dynamic_padding: false + decorations: none + +scrolling: + history: 50000 + multiplier: 3 + + + +font: + normal: + family: JetBrains Mono Medium + #family: Fira Mono Regular + #family: UbuntuMono Nerd Font Regular + #family: monospace + size: 8.5 + offset: + x: 0 + y: 0 + glyph_offset: + x: 0 + y: 0 + builtin_box_drawing: true + #size: 8.5 + + # Glyph offset determines the locations of the glyphs within their cells with + # the default being at the bottom. Increase the x offset to move the glyph to + # the right, increase the y offset to move the glyph upward. + +key_bindings: +- { key: V, mods: Control, action: Paste } +- { key: C, mods: Control, action: Copy } +- { key: C, mods: Control|Shift, chars: "\x03" } +- { key: N, mods: Control|Shift, action: SpawnNewInstance } +- { key: O, mods: Control|Shift, command: { program: "opacity-change.sh", args: ["-"] } } +- { key: P, mods: Control|Shift, command: { program: "opacity-change.sh", args: ["+"] } } +save_to_clipboard: true + +colors: + primary: + background: '#000000' + foreground: '#FFFACD' + normal: + black: '#313539' + red: '#b02626' + green: '#40a62f' + yellow: '#f2e635' + blue: '#314ad0' + magenta: '#b30ad0' + cyan: '#32d0fc' + white: '#acadb1' + bright: + black: '#676f78' + red: '#b55454' + green: '#78a670' + yellow: '#faf380' + blue: '#707fd0' + magenta: '#c583d0' + cyan: '#8adaf1' + white: '#e0e3e7' + + +#colors: +# primary: +# background: '#0F111A' +# foreground: '#8F93A2' +# normal: +# black: '#0F111A' +# red: '#FF5370' +# green: '#99C794' +# yellow: '#C4E88D' +# blue: '#82AAFF' +# magenta: '#C792EA' +# cyan: '#89DDFF' +# white: '#464B5D' +# bright: +# black: '#0F111A' +# red: '#FF5370' +# green: '#99C794' +# yellow: '#C4E88D' +# blue: '#82AAFF' +# magenta: '#C792EA' + # cyan: '#89DDFF' + # white: '#8F93A2' diff --git a/common/config/nvim/.gitignore b/common/config/nvim/.gitignore new file mode 100755 index 0000000..f6f2fa2 --- /dev/null +++ b/common/config/nvim/.gitignore @@ -0,0 +1,4 @@ +plugin/packer_compiled.lua +startup.log +tmp +*.log diff --git a/common/config/nvim/.luacheckrc b/common/config/nvim/.luacheckrc new file mode 100755 index 0000000..26f9f67 --- /dev/null +++ b/common/config/nvim/.luacheckrc @@ -0,0 +1,5 @@ +-- .luacheckrc +globals = { + "vim", + -- Add other Neovim globals like 'require', 'rawset', etc., if needed +} diff --git a/common/config/nvim/after/ftplugin/c.lua b/common/config/nvim/after/ftplugin/c.lua new file mode 100755 index 0000000..6af8a5c --- /dev/null +++ b/common/config/nvim/after/ftplugin/c.lua @@ -0,0 +1,9 @@ +-- Fix C filetype comments +vim.api.nvim_create_autocmd("Filetype", { + pattern = "c", + callback = function() + vim.bo.commentstring = "//%s" + end, + group = comment_augroup, +}) + diff --git a/common/config/nvim/after/ftplugin/lua.lua.bak b/common/config/nvim/after/ftplugin/lua.lua.bak new file mode 100755 index 0000000..fe9587b --- /dev/null +++ b/common/config/nvim/after/ftplugin/lua.lua.bak @@ -0,0 +1,35 @@ +local lspconfig = require("lspconfig") + +if lspconfig.lua_ls then + lspconfig.lua_ls.setup({ + settings = { + Lua = { + diagnostics = { + -- This is the Lua table for diagnostics settings + globals = { "vim", "use", "_G", "packer_plugins", "P" }, + disable = { + "undefined-global", + "lowercase-global", + "unused-local", + "unused-vararg", + "trailing-space" + }, + }, + workspace = { + -- Points the language server to Neovim's runtime files for auto-completion + library = { + --vim.api.nvim_get_runtime_path(), + --checkThirdParty = false, + vim.env.VIMRUNTIME, + -- Depending on the usage, you might want to add additional paths here. + "${3rd}/luv/library", + "${3rd}/busted/library", + }, + }, + telemetry = { + enable = false, + }, + }, + }, + }) +end diff --git a/common/config/nvim/after/ftplugin/markdown.lua b/common/config/nvim/after/ftplugin/markdown.lua new file mode 100755 index 0000000..5941402 --- /dev/null +++ b/common/config/nvim/after/ftplugin/markdown.lua @@ -0,0 +1,37 @@ +vim.wo.spell = true +vim.bo.spelllang = "en" +vim.wo.wrap = true +vim.wo.linebreak = true +vim.wo.breakindent = true +vim.wo.colorcolumn = "0" +--vim.wo.conceallevel = 3 +vim.opt.softtabstop = 2 -- Tab key indents by 2 spaces. +vim.opt.shiftwidth = 2 -- >> indents by 2 spaces. +-- vim.g.markdown_recommended_style = 0 -- prevents markdown from changing tabs to 4 spaces + +vim.b[0].undo_ftplugin = "setlocal nospell nowrap nolinebreak nobreakindent conceallevel=0" + +vim.cmd([[ + autocmd FileType markdown iabbrev <buffer> `` `` +]]) + +require("nvim-surround").buffer_setup({ + surrounds = { + -- ["e"] = { + -- add = function() + -- local env = require("nvim-surround.config").get_input ("Environment: ") + -- return { { "\\begin{" .. env .. "}" }, { "\\end{" .. env .. "}" } } + -- end, + -- }, + ["b"] = { + add = { "**", "**" }, + find = "**.-**", + delete = "^(**)().-(**)()$", + }, + ["i"] = { + add = { "_", "_" }, + find = "_.-_", + delete = "^(_)().-(_)()$", + }, + }, +}) diff --git a/common/config/nvim/after/ftplugin/vim.lua b/common/config/nvim/after/ftplugin/vim.lua new file mode 100755 index 0000000..7823f73 --- /dev/null +++ b/common/config/nvim/after/ftplugin/vim.lua @@ -0,0 +1,18 @@ +vim.opt_local.tabstop = 4 +vim.opt_local.shiftwidth = 4 +vim.opt_local.softtabstop = 4 +vim.opt_local.expandtab = true +vim.opt_local.autoindent = true +vim.opt_local.smartindent = true + +--vim.api.nvim_create_autocmd("FileType", { +-- pattern = "vim", +-- callback = function() +-- vim.opt_local.tabstop = 4 +-- vim.opt_local.shiftwidth = 4 +-- vim.opt_local.softtabstop = 4 +-- vim.opt_local.expandtab = true +-- vim.opt_local.autoindent = true +-- vim.opt_local.smartindent = true +-- end, +--}) diff --git a/common/config/nvim/autoload/statusline.vim b/common/config/nvim/autoload/statusline.vim new file mode 100755 index 0000000..bf5f972 --- /dev/null +++ b/common/config/nvim/autoload/statusline.vim @@ -0,0 +1,267 @@ +" statusline.vim + +if exists('g:loaded_statusline') | finish | endif +let g:loaded_statusline = 1 + +" --- Detect Nerd Fonts --- +function! s:HasNerdFonts() + if exists('g:statusline_nerd_fonts') + return g:statusline_nerd_fonts + endif + + if executable('fc-list') + let l:output = system('fc-list | grep -i nerd') + if len(split(l:output, '\n')) > 0 + return 1 + endif + endif + + return 0 +endfunction + +let g:statusline_has_nerd_fonts = s:HasNerdFonts() + +" --- Color Palette --- +let g:StslineColorGreen = '#2BBB4F' +let g:StslineColorBlue = '#4799EB' +let g:StslineColorViolet = '#986FEC' +let g:StslineColorYellow = '#D7A542' +let g:StslineColorOrange = '#EB754D' +let g:StslineColorLight = '#C0C0C0' +let g:StslineColorDark = '#080808' +let g:StslineColorDark1 = '#181818' +let g:StslineColorDark2 = 'NONE' +let g:StslineColorDark3 = '#303030' + +let g:StslineBackColor = g:StslineColorDark2 +let g:StslineOnBackColor = g:StslineColorLight +let g:StslinePriColor = g:StslineColorGreen +let g:StslineOnPriColor = g:StslineColorDark +let g:StslineSecColor = g:StslineColorDark3 +let g:StslineOnSecColor = g:StslineColorLight + +" --- Highlight Groups --- +" Initial setup of highlight groups (will be updated by UpdateStslineColors) +execute 'highlight StslinePriColorBG guifg=' . g:StslineOnPriColor . ' guibg=' . g:StslinePriColor +execute 'highlight StslineSecColorFG guifg=' . g:StslineSecColor . ' guibg=' . g:StslineBackColor +execute 'highlight StslineSecColorBG guifg=' . g:StslineColorLight . ' guibg=' . g:StslineSecColor +execute 'highlight StslineBackColorBG guifg=' . g:StslineColorLight . ' guibg=' . g:StslineBackColor +execute 'highlight StslineBackColorFGSecColorBG guifg=' . g:StslineBackColor . ' guibg=' . g:StslineSecColor +execute 'highlight StslineSecColorFGBackColorBG guifg=' . g:StslineSecColor . ' guibg=' . g:StslineBackColor +execute 'highlight StslineModColorFG guifg=' . g:StslineColorYellow . ' guibg=' . g:StslineBackColor +execute 'highlight StslinePriColorBG_SecColorBG guifg=' . g:StslinePriColor . ' guibg=' . g:StslineSecColor +execute 'highlight StslineModeSep guifg=' . g:StslinePriColor . ' guibg=' . g:StslineSecColor +execute 'highlight StslineGitSep guifg=' . g:StslineSecColor . ' guibg=' . g:StslineColorDark2 + +" --- Statusline Settings --- +if has('nvim') + set laststatus=3 +else + set laststatus=2 +endif + +"set noshowmode +"set termguicolors + +let space = '' + +" Get Statusline mode & also set primary color for that mode +function! autoload#statusline#StslineMode() abort + let l:CurrentMode = mode() + + if l:CurrentMode ==# 'n' + let g:StslinePriColor = g:StslineColorGreen + let b:CurrentMode = 'NORMAL ' + elseif l:CurrentMode ==# 'i' + let g:StslinePriColor = g:StslineColorViolet + let b:CurrentMode = 'INSERT ' + elseif l:CurrentMode ==# 'c' + let g:StslinePriColor = g:StslineColorYellow + let b:CurrentMode = 'COMMAND' + elseif l:CurrentMode ==# 'v' + let g:StslinePriColor = g:StslineColorBlue + let b:CurrentMode = 'VISUAL ' + elseif l:CurrentMode ==# '\<C-v>' + let g:StslinePriColor = g:StslineColorBlue + let b:CurrentMode = 'V-BLOCK' + elseif l:CurrentMode ==# 'V' + let g:StslinePriColor = g:StslineColorBlue + let b:CurrentMode = 'V-LINE ' + elseif l:CurrentMode ==# 'R' + let g:StslinePriColor = g:StslineColorViolet + let b:CurrentMode = 'REPLACE' + elseif l:CurrentMode ==# 's' + let g:StslinePriColor = g:StslineColorBlue + let b:CurrentMode = 'SELECT ' + elseif l:CurrentMode ==# 't' + let g:StslinePriColor = g:StslineColorYellow + let b:CurrentMode = 'TERM ' + elseif l:CurrentMode ==# '!' + let g:StslinePriColor = g:StslineColorYellow + let b:CurrentMode = 'SHELL ' + else + let g:StslinePriColor = g:StslineColorGreen + endif + + call autoload#statusline#UpdateStslineColors() + + return b:CurrentMode +endfunction + +function! autoload#statusline#UpdateStslineColors() abort + execute 'highlight StslinePriColorBG guifg=' . g:StslineOnPriColor . ' guibg=' . g:StslinePriColor + execute 'highlight StslinePriColorBGBold guifg=' . g:StslineOnPriColor . ' guibg=' . g:StslinePriColor . ' gui=bold' + execute 'highlight StslinePriColorFG guifg=' . g:StslinePriColor . ' guibg=' . g:StslineBackColor + execute 'highlight StslinePriColorFGSecColorBG guifg=' . g:StslinePriColor . ' guibg=' . g:StslineSecColor + execute 'highlight StslineModeSep guifg=' . g:StslinePriColor . ' guibg=' . g:StslineSecColor + execute 'highlight StslineGitSep guifg=' . g:StslineSecColor . ' guibg=' . g:StslineColorDark2 + execute 'highlight StslineSecColorBG guifg=' . g:StslineColorLight . ' guibg=' . g:StslineSecColor + execute 'highlight StslineBackColorBG guifg=' . g:StslineColorLight . ' guibg=' . g:StslineBackColor + execute 'highlight StslineBackColorFGSecColorBG guifg=' . g:StslineBackColor . ' guibg=' . g:StslineSecColor + execute 'highlight StslineSecColorFGBackColorBG guifg=' . g:StslineSecColor . ' guibg=' . g:StslineBackColor + execute 'highlight StslineModColorFG guifg=' . g:StslineColorYellow . ' guibg=' . g:StslineBackColor + execute 'highlight StslinePriColorBG_SecColorBG guifg=' . g:StslinePriColor . ' guibg=' . g:StslineSecColor + execute 'highlight StslineSecColorFG guifg=' . g:StslineSecColor . ' guibg=' . g:StslineBackColor +endfunction + +function! autoload#statusline#GetGitBranch() abort + let b:GitBranch = '' + try + let l:dir = expand('%:p:h') + let l:gitrevparse = system("git -C ".l:dir." rev-parse --abbrev-ref HEAD") + if !v:shell_error + let icon = g:statusline_has_nerd_fonts ? ' ' : ' [git] ' + let b:GitBranch = icon . substitute(l:gitrevparse, '\n', '', 'g') . ' ' + endif + catch + endtry +endfunction + +function! autoload#statusline#GetFileType() abort + if !g:statusline_has_nerd_fonts + let b:FiletypeIcon = '' + return + endif + if &filetype ==# 'typescript' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'html' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'scss' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'css' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'javascript' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'javascriptreact' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'markdown' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'sh' || &filetype ==# 'zsh' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'vim' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'rust' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'ruby' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'cpp' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'c' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'go' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'lua' | let b:FiletypeIcon = ' ' + elseif &filetype ==# 'haskell' | let b:FiletypeIcon = ' ' + else | let b:FiletypeIcon = ' ' + endif +endfunction + +function! autoload#statusline#ActivateStatusline() abort + call autoload#statusline#GetFileType() + call autoload#statusline#GetGitBranch() " Ensure git branch is updated + + let current_mode_str = autoload#statusline#StslineMode() + call autoload#statusline#UpdateStslineColors() + + let readonly_icon = g:statusline_has_nerd_fonts ? ' ' : '[RO] ' + let modified_icon = g:statusline_has_nerd_fonts ? ' ' : '[+] ' + let git_sep = g:statusline_has_nerd_fonts ? '' : ' ' + let file_sep1 = g:statusline_has_nerd_fonts ? ' ' : ' ' + let file_sep2 = g:statusline_has_nerd_fonts ? '' : '' + + " Get dynamic parts as simple strings + let git_status_str = get(b:, "coc_git_status", get(b:, "GitBranch", "")) + let git_blame_str = get(b:, "coc_git_blame", "") + let filetype_icon_str = get(b:, "FiletypeIcon", "") + let file_encoding_str = '' + if &fenc != "utf-8" + let file_encoding_str = &fenc . ' ' + endif + + " Build the statusline as a static string + let l:statusline = '' + + let l:statusline .= '%#StslinePriColorBG# ' . current_mode_str . '' + let l:statusline .= '%#StslineModeSep#' . git_sep + let l:statusline .= '%#StslineSecColorBG#' . git_status_str . git_blame_str + let l:statusline .= '%#StslineGitSep#' . git_sep + + " File info (Readonly, Modified, Filename) + let l:statusline .= '%#StslinePriColorFG#' + if &readonly + let l:statusline .= readonly_icon + endif + let l:statusline .= ' %F ' + if &modified + let l:statusline .= modified_icon + endif + + " Right align everything after this + let l:statusline .= '%=' + + " Right side (Filetype, Encoding, Position) + let l:statusline .= '%#StslinePriColorFG# ' . filetype_icon_str . '%y' + let l:statusline .= '%#StslineSecColorFG#' . file_sep1 + "let l:statusline .= '%#StslineSecColorBG# ' . file_encoding_str + let l:statusline .= '%#StslinePriColorFGSecColorBG#' . file_sep2 + let l:statusline .= '%#StslinePriColorBG# %p%% %#StslinePriColorBGBold#%l%#StslinePriColorBG#/%L :%c ' + let l:statusline .= '%#StslineBackColorBG#' + + " Set the statusline for the current buffer + let &l:statusline = l:statusline +endfunction + +function! autoload#statusline#DeactivateStatusline() abort + let git_sep = g:statusline_has_nerd_fonts ? '' : '' + let readonly_icon = g:statusline_has_nerd_fonts ? ' ' : '[RO] ' + let modified_icon = g:statusline_has_nerd_fonts ? ' ' : '[+] ' + + " NOTE: This DeactivateStatusline function still uses %{} for dynamic parts. + " If you encounter general E518 or other issues related to %{} expressions, + " you will need to refactor this function to build a static string + " similar to how ActivateStatusline now does it. + if !exists("b:GitBranch") || b:GitBranch == '' + let statusline = + \ '%#StslineSecColorBG# INACTIVE ' . + \ '%{get(b:,"coc_git_statusline",b:GitBranch)}%{get(b:,"coc_git_blame","")}' . + \ '%#StslineBackColorFGSecColorBG#' . git_sep . + \ '%#StslineBackColorBG# %{&readonly?"' . readonly_icon . '":""}%F ' . + \ '%#StslineModColorFG#%{&modified?"' . modified_icon . '":""}' . + \ '%=%#StslineBackColorBG# %{b:FiletypeIcon}%{&filetype}' . + \ '%#StslineSecColorFGBackColorBG# | %p%% %l/%L :%c' + else + let statusline = + \ '%#StslineSecColorBG# %{get(b:,"coc_git_statusline",b:GitBranch)}%{get(b:,"coc_git_blame","")}' . + \ '%#StslineBackColorFGSecColorBG#' . git_sep . + \ '%#StslineBackColorBG# %{&readonly?"' . readonly_icon . '":""}%F ' . + \ '%#StslineModColorFG#%{&modified?"' . modified_icon . '":""}' . + \ '%=%#StslineBackColorBG# %{b:FiletypeIcon}%{&filetype}' . + \ '%#StslineSecColorFGBackColorBG# | %p%% %l/%L :%c' + endif + + execute 'setlocal statusline=' . substitute(statusline, '"', '\\"', 'g') +endfunction + +augroup StatuslineGit + autocmd! + autocmd BufEnter * call autoload#statusline#GetGitBranch() +augroup END + +augroup SetStsline + autocmd! + autocmd BufEnter,WinEnter * call autoload#statusline#ActivateStatusline() + autocmd ModeChanged * call autoload#statusline#ActivateStatusline() +augroup END + +augroup StatuslineAutoReload + autocmd! + autocmd BufWritePost statusline.vim source <afile> | call autoload#statusline#ActivateStatusline() +augroup END + +"call autoload#statusline#ActivateStatusline() diff --git a/common/config/nvim/autoload/utils.vim b/common/config/nvim/autoload/utils.vim new file mode 100755 index 0000000..d92b771 --- /dev/null +++ b/common/config/nvim/autoload/utils.vim @@ -0,0 +1,238 @@ +" Toggle Zoom +function! utils#ZoomToggle() + if exists('t:zoomed') && t:zoomed + execute t:zoom_winrestcmd + let t:zoomed = 0 + else + let t:zoom_winrestcmd = winrestcmd() + resize + vertical resize + let t:zoomed = 1 + endif +endfunction +"command! ZoomToggle call ZoomToggle() + + +"------------------------------------------------- + +" Toggle DiagnosticsOpenFloat +" Enable DiagnosticsOpenFloat by default +"let g:DiagnosticsOpenFloat = 1 +" +"" Define the autocmd group on startup +"augroup OpenFloat +" autocmd! +" autocmd CursorHold * lua if vim.g.DiagnosticsOpenFloat then vim.diagnostic.open_float(nil, { focusable = false }) end +"augroup END + +" Toggle function +function! utils#ToggleDiagnosticsOpenFloat() + " Flip the toggle + let g:DiagnosticsOpenFloat = !get(g:, 'DiagnosticsOpenFloat', 1) +endfunction + +" Command to toggle +command! ToggleDiagnosticsOpenFloat call utils#ToggleDiagnosticsOpenFloat() + +"------------------------------------------------- + +" Toggle transparency +let t:is_transparent = 0 +function! utils#Toggle_transparent_background() + if t:is_transparent == 0 + hi Normal guibg=#111111 ctermbg=black + let t:is_transparent = 1 + else + hi Normal guibg=NONE ctermbg=NONE + let t:is_transparent = 0 + endif +endfunction +"nnoremap <leader>tb :call Toggle_transparent_background()<CR> + + +"------------------------------------------------- + +" Toggle statusline +let s:hidden_all = 0 +function! utils#ToggleHiddenAll() + if s:hidden_all == 0 + let s:hidden_all = 1 + set noshowmode + set noruler + set laststatus=0 + set noshowcmd + else + let s:hidden_all = 0 + set showmode + set ruler + set laststatus=2 + set showcmd + endif +endfunction +"nnoremap <S-h> :call ToggleHiddenAll()<CR> + + +"------------------------------------------------- + +" Open last closed buffer +function! utils#OpenLastClosed() + let last_buf = bufname('#') + if empty(last_buf) + echo "No recently closed buffer found" + return + endif + let result = input("Open ". last_buf . " in (n)ormal (v)split, (t)ab or (s)plit ? (n/v/t/s) : ") + if empty(result) || (result !=# 'v' && result !=# 't' && result !=# 's' && result !=# 'n') + return + endif + if result ==# 't' + execute 'tabnew' + elseif result ==# 'v' + execute "vsplit" + elseif result ==# 's' + execute "split" + endif + execute 'b ' . last_buf +endfunction + + +"------------------------------------------------- + +" Toggle Diff +let g:diff_is_open = 0 + +function! utils#ToggleDiff() + if g:diff_is_open + windo diffoff + let g:diff_is_open = 0 + else + windo diffthis + let g:diff_is_open = 1 + endif +endfunction + + +"------------------------------------------------- + +" Verbose Toggle +function! utils#VerboseToggle() + if !&verbose + set verbosefile=~/.config/nvim/verbose.log + set verbose=15 + else + set verbose=0 + set verbosefile= + endif +endfunction + + +"------------------------------------------------- + +" Jump List +function! utils#GotoJump() + jumps + let j = input("Please select your jump: ") + if j != '' + let pattern = '\v\c^\+' + if j =~ pattern + let j = substitute(j, pattern, '', 'g') + execute "normal " . j . "\<c-i>" + else + execute "normal " . j . "\<c-o>" + endif + endif +endfunction + + +"------------------------------------------------- + +" Disable annoying auto line break +fu! utils#DisableBr() + set wrap + set linebreak + set nolist " list disables linebreak + set textwidth=0 + set wrapmargin=0 + set formatoptions-=t +endfu + +" Disable line breaks for all file types +autocmd! BufNewFile,BufRead *.* call utils#DisableBr() + + +"------------------------------------------------- + +" Annoying timestamp issue on write (The file has been changed since reading it...) +"function! utils#ProcessFileChangedShell() +" if v:fcs_reason == 'mode' || v:fcs_reason == 'time' +" let v:fcs_choice = '' +" else +" let v:fcs_choice = 'ask' +" endif +"endfunction +"autocmd FileChangedShell <buffer> call utils#ProcessFileChangedShell() +" +"let lastline = line('$') +"let bufcontents = getline(1, lastline) +"edit! +"call setline(1, bufcontents) +"if line('$') > lastline +" execute lastline+1.',$:d _' +"endif + +" Annoying timestamp issue on write (The file has been changed since reading it...) +function! utils#ProcessFileChangedShell() + if v:fcs_reason == 'mode' || v:fcs_reason == 'time' + let v:fcs_choice = '' + else + let v:fcs_choice = 'ask' + endif +endfunction + +" Triggered when the file is changed externally +autocmd FileChangedShell <buffer> call utils#ProcessFileChangedShell() + +" Triggered before writing the buffer to the file +autocmd BufWritePre <buffer> call utils#BeforeWrite() + +function! utils#BeforeWrite() + let lastline = line('$') + let bufcontents = getline(1, lastline) + edit! + call setline(1, bufcontents) + if line('$') > lastline + execute lastline+1.',$:d _' + endif +endfunction + + +"------------------------------------------------- + +" On The Fly Table mode +function! s:isAtStartOfLine(mapping) + let text_before_cursor = getline('.')[0 : col('.')-1] + let mapping_pattern = '\V' . escape(a:mapping, '\') + let comment_pattern = '\V' . escape(substitute(&l:commentstring, '%s.*$', '', ''), '\') + return (text_before_cursor =~? '^' . ('\v(' . comment_pattern . '\v)?') . '\s*\v' . mapping_pattern . '\v$') +endfunction + + +"------------------------------------------------- + +" :Rename {newname} +function! utils#RenameFile() + let old_name = expand('%') + let new_name = input('New file name: ', expand('%'), 'file') + if new_name != '' && new_name != old_name + exec ':saveas ' . new_name + exec ':silent !rm ' . old_name + redraw! + endif +endfunction + +augroup obsidian + autocmd! + autocmd Filetype markdown set conceallevel=2 +augroup END + +"------------------------------------------------- diff --git a/common/config/nvim/colors/colorscheme.vim b/common/config/nvim/colors/colorscheme.vim new file mode 100755 index 0000000..ce0526e --- /dev/null +++ b/common/config/nvim/colors/colorscheme.vim @@ -0,0 +1,247 @@ +" Vim Colorscheme +" Name: cherryblossom.vim +" Author: Luo Boming +" Version: 0.3 +" License: The MIT Licence + +"{{{ Pre-setting +let g:colors_name = expand('<sfile>:t:r') + +"hi clear +"if exists("syntax_on") +" syntax reset +"endif + +if ! exists("g:terminal_italics") + let g:terminal_italics = 0 +endif + +"if ! exists("g:switch_statusline_bg_in_insert") +" let g:switch_statusline_bg_in_insert = 0 +"endif + +if ! exists("g:spell_undercurl") + let g:spell_undercurl = 0 +endif + +"}}} +"{{{ Color Palette +" Color Entity +let s:black = { "gui": "#171717", "cterm": "16" } +let s:white = { "gui": "#EAE8E7", "cterm": "231" } + +let s:gray = { "gui": "#3a3f52", "cterm": "247" } + +let s:green = { "gui": "#30B536", "cterm": "34" } +let s:pink = { "gui": "#D36DD3", "cterm": "170" } +let s:orange = { "gui": "#FC923F", "cterm": "208" } +let s:purple = { "gui": "#B586E7", "cterm": "141" } +let s:light_cyan = { "gui": "#D7FFFF", "cterm": "195" } +let s:dark_cyan = { "gui": "#00AF87", "cterm": "36" } +let s:ultramarine = { "gui": "#229EC0", "cterm": "38" } +let s:skyblue = { "gui": "#9BE7F8", "cterm": "195" } + +let s:white_pink = { "gui": "#FEF7FE", "cterm": "231" } +let s:white_pink_deep = { "gui": "#FEF0FE", "cterm": "255" } +let s:black_green = { "gui": "#053703", "cterm": "235" } +let s:black_green_bright = { "gui": "#074005", "cterm": "239" } +let s:middle_gray = { "gui": "#8a8a8a", "cterm": "245" } + +let s:light_gray = { "gui": "#E1DCDA", "cterm": "253" } +let s:light_green = { "gui": "#B7EFA5", "cterm": "157" } +let s:light_pink = { "gui": "#FEDCFE", "cterm": "225" } +let s:light_yellow = { "gui": "#EDE682", "cterm": "228" } +let s:light_red = { "gui": "#EB5A7C", "cterm": "204" } + +let s:dark_gray = { "gui": "#4D4A48", "cterm": "241" } +let s:dark_green = { "gui": "#09570A", "cterm": "22" } +let s:dark_yellow = { "gui": "#BC922B", "cterm": "3" } +let s:dark_pink = { "gui": "#B365A2", "cterm": "133" } +let s:dark_red = { "gui": "#D9372D", "cterm": "160" } +let s:NONE = { "gui": "NONE", "cterm": "NONE" } + +" Color Alias +if &background == "light" + let s:norm = s:black + let s:bg = s:NONE + let s:bg_subtle = s:white_pink_deep + let s:gray_fg = s:middle_gray + let s:green_fg = s:green + let s:yellow_fg = s:dark_yellow + let s:pink_fg = s:dark_pink + let s:cyan_fg = s:dark_cyan + let s:blue_fg = s:ultramarine + let s:red_fg = s:dark_red + let s:gray_bg = s:light_gray + let s:green_bg = s:light_green + let s:yellow_bg = s:light_yellow + let s:pink_bg = s:light_pink + let s:cyan_bg = s:light_cyan + let s:blue_bg = s:skyblue + let s:red_bg = s:light_red +endif + +if &background == "dark" + let s:norm = s:white + let s:bg = s:NONE + let s:bg_subtle = s:gray + let s:gray_fg = s:middle_gray + let s:green_fg = s:light_green + let s:yellow_fg = s:light_yellow + let s:pink_fg = s:light_pink + let s:cyan_fg = s:light_cyan + let s:blue_fg = s:skyblue + let s:red_fg = s:light_red + let s:gray_bg = s:dark_gray + let s:green_bg = s:green + let s:yellow_bg = s:dark_yellow + let s:pink_bg = s:pink + let s:cyan_bg = s:dark_cyan + let s:blue_bg = s:ultramarine + let s:red_bg = s:dark_red +endif +"}}} +"{{{ Highlight Function +" shamelessly stolen from pencil: https://github.com/reedes/vim-colors-pencil +function! s:hi(group, style) + if g:terminal_italics == 0 + if has_key(a:style, "cterm") && a:style["cterm"] == "italic" + unlet a:style.cterm + endif + if has_key(a:style, "term") && a:style["term"] == "italic" + unlet a:style.term + endif + endif + execute "highlight" a:group + \ "guifg=" (has_key(a:style, "fg") ? a:style.fg.gui : "NONE") + \ "guibg=" (has_key(a:style, "bg") ? a:style.bg.gui : "NONE") + \ "guisp=" (has_key(a:style, "sp") ? a:style.sp.gui : "NONE") + \ "gui=" (has_key(a:style, "gui") ? a:style.gui : "NONE") + \ "ctermfg=" (has_key(a:style, "fg") ? a:style.fg.cterm : "NONE") + \ "ctermbg=" (has_key(a:style, "bg") ? a:style.bg.cterm : "NONE") + \ "cterm=" (has_key(a:style, "cterm") ? a:style.cterm : "NONE") + \ "term=" (has_key(a:style, "term") ? a:style.term : "NONE") +endfunction + +if g:spell_undercurl == 1 + let s:attr_un = 'undercurl' +else + let s:attr_un = 'underline' +endif + +"}}} +"{{{ Common Highlighting +call s:hi("Normal", {"fg": s:norm, "bg": s:bg}) +call s:hi("Cursor", {}) +call s:hi("Comment", {"fg": s:gray_fg, "gui": "italic", "cterm": "italic", "term": "italic"}) + +call s:hi("Constant", {"fg": s:pink_fg}) +hi! link String Constant +hi! link Character Constant +hi! link Number Constant +hi! link Boolean Constant +hi! link Float Constant + +call s:hi("Identifier", {"fg": s:red_fg}) +hi! link Function Identifier + +call s:hi("Statement", {"fg": s:green_fg}) +hi! link Conditonal Statement +hi! link Repeat Statement +hi! link Label Statement +hi! link Operator Statement +hi! link Keyword Statement +hi! link Exception Statement + +call s:hi("PreProc", {"fg": s:blue_fg}) +hi! link Include PreProc +hi! link Define PreProc +hi! link Macro PreProc +hi! link PreCondit PreProc + +call s:hi("Type", {"fg": s:yellow_fg}) +hi! link StorageClass Type +hi! link Structure Type +hi! link Typedef Type + +call s:hi("Special", {"fg": s:orange}) +hi! link SpecialChar Special +hi! link Tag Special +hi! link Delimiter Special +hi! link SpecialComment Special +hi! link Debug Special + +call s:hi("Underlined", {"gui": "underline", "cterm": "underline"}) +call s:hi("Ignore", {"fg": s:bg_subtle}) +call s:hi("Error", {"fg": s:white, "bg": s:red_fg , "gui": "bold", "cterm": "bold"}) +call s:hi("Todo", {"bg": s:yellow_bg, "gui": "bold", "cterm": "bold"}) + +"}}} +"{{{ Semi-Common Highlighting +call s:hi("SpecialKey", {"fg": s:purple, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("NonText", {"fg": s:cyan_bg, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("Directory", {"fg": s:blue_fg, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("ErrorMsg", {"fg": s:red_fg, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("IncSearch", {"gui": "reverse", "cterm": "reverse", "term": "reverse"}) +call s:hi("Search", {"fg": s:norm, "bg": s:pink_bg}) +call s:hi("MoreMsg", {"fg": s:pink_fg, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("ModeMsg", {"fg": s:pink_fg, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("LineNr", {"fg": s:gray}) +call s:hi("CursorLineNr", {"fg": s:pink_fg, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("Question", {"fg": s:purple, "gui": "bold", "cterm": "bold", "term": "bold"}) +"call s:hi("StatusLine", {"fg": s:norm, "bg": s:green_bg, "gui": "bold", "cterm": "bold", "term": "bold"}) +"call s:hi("StatusLineNC", {"fg": s:norm, "bg": s:gray_bg}) +call s:hi("Conceal", {"fg": s:yellow_fg}) +call s:hi("VertSplit", {"gui": "reverse", "cterm": "reverse", "term": "reverse"}) +call s:hi("Title", {"fg": s:pink_fg, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("Visual", {"gui": "reverse", "cterm": "reverse", "term": "reverse"}) +call s:hi("VisualNOS", {"gui": "bold,underline", "cterm": "bold,underline", "term": "bold,underline"}) +call s:hi("WarningMsg", {"fg": s:orange, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("WildMenu", {"fg": s:norm, "bg": s:blue_bg}) +call s:hi("Folded", {"fg": s:green_fg, "bg": s:gray_bg}) +call s:hi("FoldColumn", {"fg": s:green_fg, "bg": s:gray_bg}) +call s:hi("DiffAdd", {"bg": s:green_bg}) +call s:hi("DiffChange", {"bg": s:yellow_bg}) +call s:hi("DiffDelete", {"bg": s:red_bg}) +call s:hi("DiffText", {"bg": s:blue_bg, "gui": "bold", "cterm": "bold", "term": "bold"}) +call s:hi("SignColumn", {"fg": s:green_fg, "bg": s:gray}) +if has("gui_running") + call s:hi("SpellBad", {"gui": s:attr_un, "sp": s:red_bg}) + call s:hi("SpellCap", {"gui": s:attr_un, "sp": s:yellow_bg}) + call s:hi("SpellRare", {"gui": s:attr_un, "sp": s:blue_bg}) + call s:hi("SpellLocal", {"gui": s:attr_un, "sp": s:green_bg}) +else + call s:hi("SpellBad", {"cterm": s:attr_un, "fg": s:red_fg}) + call s:hi("SpellCap", {"cterm": s:attr_un, "fg": s:yellow_fg}) + call s:hi("SpellRare", {"cterm": s:attr_un, "fg": s:blue_fg}) + call s:hi("SpellLocal", {"cterm": s:attr_un, "fg": s:green_fg}) +endif +call s:hi("Pmenu", {"bg": s:gray_bg}) +call s:hi("PmenuSel", {"bg": s:pink_bg}) +call s:hi("PmenuSbar", {"bg": s:gray_bg}) +call s:hi("PmenuThumb", {"bg": s:gray_bg}) +call s:hi("TabLine", {"bg": s:bg_subtle}) +call s:hi("TabLineSel", {"bg": s:pink_bg}) +call s:hi("TabLineFill", {"bg": s:bg_subtle}) +call s:hi("CursorColumn", {"bg": s:yellow_fg}) +call s:hi("CursorLine", {"bg": s:bg_subtle}) +call s:hi("ColorColumn", {"bg": s:bg_subtle}) +call s:hi("MatchParen", {"fg": s:pink_fg, "gui": "underline", "cterm": "underline"}) +call s:hi("qfLineNr", {"fg": s:gray}) + +"}}} +""{{{ Switching StatusLine bg +"function! s:changebg(group, color) +" execute "highlight" a:group "guibg=" a:color.gui "ctermbg=" a:color.cterm +"endfunction +" +"if g:switch_statusline_bg_in_insert == 1 +" "" Change Color when entering Insert Mode +" autocmd InsertEnter * call s:changebg("StatusLine", s:pink_bg) +" "" Revert Color to default when leaving Insert Mode +" autocmd InsertLeave * call s:changebg("StatusLine", s:green_bg) +"endif + +"}}} +" vim: set foldmethod=marker: + diff --git a/common/config/nvim/init.lua b/common/config/nvim/init.lua new file mode 100755 index 0000000..75ca825 --- /dev/null +++ b/common/config/nvim/init.lua @@ -0,0 +1,152 @@ +--[[ + ███╗ ██╗███████╗ ██████╗ ██╗ ██╗██╗███╗ ███╗ + ████╗ ██║██╔════╝██╔═══██╗██║ ██║██║████╗ ████║ + ██╔██╗ ██║█████╗ ██║ ██║██║ ██║██║██╔████╔██║ + ██║╚██╗██║██╔══╝ ██║ ██║╚██╗ ██╔╝██║██║╚██╔╝██║ + ██║ ╚████║███████╗╚██████╔╝ ╚████╔╝ ██║██║ ╚═╝ ██║ + ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═══╝ ╚═╝╚═╝ ╚═╝ + ------------------------------------------------------------------------------ + Author : srdusr + URL : https://github.com/srdusr/nvim.git + Description : System-agnostic, backwards-compatible config. + Bootstraps packer/lazy/builtin based on availability. + Use :PackerSync, :Lazy install, or built-in (v0.12+). + ------------------------------------------------------------------------------ +--]] + +-- Load impatient (Faster loading times) +local impatient_ok, impatient = pcall(require, "impatient") +if impatient_ok then + impatient.enable_profile() +end + +-- Schedule reading shadafile to improve the startup time +vim.opt.shadafile = "NONE" +vim.schedule(function() + vim.opt.shadafile = "" + vim.cmd("silent! rsh") +end) + +-- Improve speed by disabling some default plugins/modules +local builtins = { + "gzip", + "zip", + "zipPlugin", + "tar", + "tarPlugin", + "getscript", + "getscriptPlugin", + "vimball", + "vimballPlugin", + "2html_plugin", + --"matchit", + --"matchparen", + "logiPat", + "rrhelper", + "tutor_mode_plugin", + "spellfile_plugin", + "sleuth", + "fzf", +} + +local enable_netrw = true +local ok, _ = pcall(require, "nvim-tree") +if ok then + enable_netrw = false +end + +if not enable_netrw then + vim.g.loaded_netrw = 1 + vim.g.loaded_netrwPlugin = 1 + vim.g.loaded_netrwSettings = 1 + vim.g.loaded_netrwFileHandlers = 1 +end + +for _, plugin in ipairs(builtins) do + vim.g["loaded_" .. plugin] = 1 +end + + +-- Load/reload modules +local modules = { + -- SETUP/MANAGER -- + "setup.compat", -- Backwards compatibility/future proofing + "setup.manager", -- Package Manager (builtin/packer/lazy) + "setup.plugins", -- Plugins list + + -- USER/CORE -- + "user.keys", -- Keymaps + "user.mods", -- Modules/functions + "user.opts", -- Options + "user.view", -- Colorscheme/UI + + -- PLUGINS -- + "plugins.auto-session", + "plugins.treesitter", + "plugins.web-devicons", + "plugins.telescope", + "plugins.fzf", + "plugins.nvim-tree", + "plugins.neodev", + "plugins.lsp", + "plugins.cmp", + "plugins.quickfix", + "plugins.colorizer", + "plugins.prettier", + "plugins.git", + "plugins.fugitive", + "plugins.snippets", + "plugins.gitsigns", + "plugins.sniprun", + "plugins.surround", + "plugins.neoscroll", + "plugins.statuscol", + "plugins.trouble", + "plugins.goto-preview", + "plugins.autopairs", + "plugins.navic", + "plugins.toggleterm", + "plugins.zen-mode", + --"plugins.fidget", + "plugins.dap", + "plugins.neotest", + "plugins.heirline", + "plugins.indent-blankline", + "plugins.dashboard", + "plugins.which-key", + "plugins.harpoon", + "plugins.leetcode", + --"plugins.hardtime", + "plugins.notify", + "plugins.overseer", + "plugins.vimtex", + "plugins.interestingwords", + + --"plugins.nvim-tree", + --"plugins.telescope", + --"plugins.heirline", + --"plugins.fzf", + --"", + +} + +-- Refresh module cache +--for _, mod in ipairs(modules) do +-- package.loaded[mod] = nil +-- pcall(require, mod) +--end + +for _, mod in ipairs(modules) do + local ready, loaded = pcall(require, mod) + if ready and type(loaded) == "table" and loaded.setup then + local success, err = pcall(loaded.setup) + if not success then + vim.notify(string.format("Error setting up %s: %s", mod, err), vim.log.levels.ERROR) + end + elseif not ready then + vim.notify(string.format("Failed to load %s: %s", mod, loaded), vim.log.levels.WARN) + end +end + +--require("setup.manager").setup() -- Setup all managers +--require("user.view").setup() -- Colors/UI diff --git a/common/config/nvim/lsp/bashls.lua b/common/config/nvim/lsp/bashls.lua new file mode 100644 index 0000000..fc7d709 --- /dev/null +++ b/common/config/nvim/lsp/bashls.lua @@ -0,0 +1,4 @@ +return { + cmd = { "bash-language-server", "start" }, + filetypes = { "sh", "bash" } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/clangd.lua b/common/config/nvim/lsp/clangd.lua new file mode 100644 index 0000000..4a19600 --- /dev/null +++ b/common/config/nvim/lsp/clangd.lua @@ -0,0 +1,5 @@ +return { + cmd = { "clangd", "--background-index", "--clang-tidy", "--header-insertion=iwyu" }, + filetypes = { "c", "cpp", "objc", "objcpp", "cuda", "proto" }, + root_markers = { ".clangd", ".clang-tidy", ".clang-format", "compile_commands.json", "compile_flags.txt", "configure.ac" } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/cssls.lua b/common/config/nvim/lsp/cssls.lua new file mode 100644 index 0000000..e734c19 --- /dev/null +++ b/common/config/nvim/lsp/cssls.lua @@ -0,0 +1,4 @@ +return { + cmd = { "vscode-css-language-server", "--stdio" }, + filetypes = { "css", "scss", "less" } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/gopls.lua b/common/config/nvim/lsp/gopls.lua new file mode 100644 index 0000000..cf959c4 --- /dev/null +++ b/common/config/nvim/lsp/gopls.lua @@ -0,0 +1,41 @@ +return { + cmd = { "gopls" }, + filetypes = { "go", "gomod", "gowork", "gotmpl" }, + root_markers = { "go.work", "go.mod" }, + settings = { + gopls = { + analyses = { + fieldalignment = true, + nilness = true, + unusedparams = true, + unusedwrite = true, + useany = true + }, + codelenses = { + gc_details = false, + generate = true, + regenerate_cgo = true, + run_govulncheck = true, + test = true, + tidy = true, + upgrade_dependency = true, + vendor = true + }, + completeUnimported = true, + directoryFilters = { "-.git", "-.vscode", "-.idea", "-.vscode-test", "-node_modules" }, + gofumpt = true, + hints = { + assignVariableTypes = true, + compositeLiteralFields = true, + compositeLiteralTypes = true, + constantValues = true, + functionTypeParameters = true, + parameterNames = true, + rangeVariableTypes = true + }, + semanticTokens = true, + staticcheck = true, + usePlaceholders = true + } + } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/html.lua b/common/config/nvim/lsp/html.lua new file mode 100644 index 0000000..5b322b1 --- /dev/null +++ b/common/config/nvim/lsp/html.lua @@ -0,0 +1,4 @@ +return { + cmd = { "vscode-html-language-server", "--stdio" }, + filetypes = { "html" } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/jsonls.lua b/common/config/nvim/lsp/jsonls.lua new file mode 100644 index 0000000..6474e0a --- /dev/null +++ b/common/config/nvim/lsp/jsonls.lua @@ -0,0 +1,4 @@ +return { + cmd = { "vscode-json-language-server", "--stdio" }, + filetypes = { "json", "jsonc" } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/lua_ls.lua b/common/config/nvim/lsp/lua_ls.lua new file mode 100644 index 0000000..d248e2e --- /dev/null +++ b/common/config/nvim/lsp/lua_ls.lua @@ -0,0 +1,20 @@ +return { + cmd = { "lua-language-server" }, + filetypes = { "lua" }, + root_markers = { ".luarc.json", ".luarc.jsonc", ".luacheckrc", ".stylua.toml", "stylua.toml", "selene.toml", "selene.yml" }, + settings = { + Lua = { + diagnostics = { + disable = { "undefined-global", "lowercase-global", "unused-local", "unused-vararg", "trailing-space" }, + globals = { "vim", "use", "_G", "packer_plugins", "P" } + }, + telemetry = { + enable = false + }, + workspace = { + checkThirdParty = false, + library = { "/tmp/.mount_nvimOIpamk/usr/share/nvim/runtime", "${3rd}/luv/library", "${3rd}/busted/library" } + } + } + } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/pyright.lua b/common/config/nvim/lsp/pyright.lua new file mode 100644 index 0000000..f89d41f --- /dev/null +++ b/common/config/nvim/lsp/pyright.lua @@ -0,0 +1,12 @@ +return { + cmd = { "pyright-langserver", "--stdio" }, + filetypes = { "python" }, + root_markers = { "pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json" }, + settings = { + python = { + formatting = { + provider = "none" + } + } + } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/rust_analyzer.lua b/common/config/nvim/lsp/rust_analyzer.lua new file mode 100644 index 0000000..b4522f5 --- /dev/null +++ b/common/config/nvim/lsp/rust_analyzer.lua @@ -0,0 +1,5 @@ +return { + cmd = { "rust-analyzer" }, + filetypes = { "rust" }, + root_markers = { "Cargo.toml", "rust-project.json" } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/ts_ls.lua b/common/config/nvim/lsp/ts_ls.lua new file mode 100644 index 0000000..940fd3d --- /dev/null +++ b/common/config/nvim/lsp/ts_ls.lua @@ -0,0 +1,8 @@ +return { + cmd = { "typescript-language-server", "--stdio" }, + filetypes = { "javascript", "javascriptreact", "javascript.jsx", "typescript", "typescriptreact", "typescript.tsx" }, + init_options = { + disableAutomaticTypeAcquisition = true + }, + root_markers = { "tsconfig.json", "jsconfig.json", "package.json" } +}
\ No newline at end of file diff --git a/common/config/nvim/lsp/yamlls.lua b/common/config/nvim/lsp/yamlls.lua new file mode 100644 index 0000000..e52d322 --- /dev/null +++ b/common/config/nvim/lsp/yamlls.lua @@ -0,0 +1,4 @@ +return { + cmd = { "yaml-language-server", "--stdio" }, + filetypes = { "yaml", "yml" } +}
\ No newline at end of file diff --git a/common/config/nvim/lua/plugins/auto-session.lua b/common/config/nvim/lua/plugins/auto-session.lua new file mode 100755 index 0000000..d982e08 --- /dev/null +++ b/common/config/nvim/lua/plugins/auto-session.lua @@ -0,0 +1,39 @@ +local M = {} + +function M.setup() + local auto = pcall(require, 'auto-session') and require('auto-session') + if not auto then + return false + end + + local nvim_version = vim.version() + if nvim_version.major == 0 and nvim_version.minor < 5 then + return false + end + + -- Configure session options + vim.opt.sessionoptions:append("localoptions") -- Add localoptions to sessionoptions + + -- Set up auto-session + auto.setup({ + log_level = 'info', + auto_session_suppress_dirs = { '~/', '~/Projects', '~/projects', '~/Downloads', '~/downloads' }, + auto_session_use_git_branch = true, + bypass_save_filetypes = { "dashboard" }, + + -- Additional configuration to handle session options + pre_save_cmds = { + -- Ensure local options are saved with the session + function() vim.opt.sessionoptions:append("localoptions") end, + }, + + -- Post restore hook to ensure local options are properly set + post_restore = function() + vim.opt.sessionoptions:append("localoptions") + end, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/autopairs.lua b/common/config/nvim/lua/plugins/autopairs.lua new file mode 100755 index 0000000..22dcf27 --- /dev/null +++ b/common/config/nvim/lua/plugins/autopairs.lua @@ -0,0 +1,99 @@ +local M = {} + +--- Setup and configure nvim-autopairs +-- This function initializes and configures the autopairs plugin +-- @return boolean True if setup was successful, false otherwise +function M.setup() + local ok, autopairs = pcall(require, "nvim-autopairs") + if not ok then + return false + end + + -- Configure autopairs + autopairs.setup({ + check_ts = true, + ts_config = { + lua = { "string", "source" }, + javascript = { "string", "template_string" }, + java = false, + }, + map = "<M-e>", + pairs_map = { + ["<"] = ">", + }, + disable_filetype = { "TelescopePrompt", "spectre_panel" }, + disable_in_macro = true, + disable_in_visualblock = true, + enable_moveright = true, + enable_afterquote = true, -- add bracket pairs after quote + enable_check_bracket_line = false, --- check bracket in same line + enable_bracket_in_quote = true, -- + break_undo = true, -- switch for basic rule break undo sequence + --fast_wrap = { + -- chars = { "{", "[", "(", '"', "'" }, + -- pattern = string.gsub([[ [%'%"%)%>%]%)%}%,] ]], "%s+", ""), + -- offset = 0, -- Offset from pattern match + -- end_key = "$", + -- keys = "qwertyuiopzxcvbnmasdfghjkl", + -- check_comma = true, + -- highlight = "PmenuSel", + -- highlight_grey = "LineNr", + --}, +}) +local Rule = require("nvim-autopairs.rule") + +local cond = require("nvim-autopairs.conds") + +autopairs.add_rules({ + Rule("`", "'", "tex"), + Rule("$", "$", "tex"), + Rule(" ", " ") + :with_pair(function(opts) + local pair = opts.line:sub(opts.col, opts.col + 1) + return vim.tbl_contains({ "$$", "()", "{}", "[]", "<>" }, pair) + end) + :with_move(cond.none()) + :with_cr(cond.none()) + :with_del(function(opts) + local col = vim.api.nvim_win_get_cursor(0)[2] + local context = opts.line:sub(col - 1, col + 2) + return vim.tbl_contains({ "$ $", "( )", "{ }", "[ ]", "< >" }, context) + end), + Rule("$ ", " ", "tex"):with_pair(cond.not_after_regex(" ")):with_del(cond.none()), + Rule("[ ", " ", "tex"):with_pair(cond.not_after_regex(" ")):with_del(cond.none()), + Rule("{ ", " ", "tex"):with_pair(cond.not_after_regex(" ")):with_del(cond.none()), + Rule("( ", " ", "tex"):with_pair(cond.not_after_regex(" ")):with_del(cond.none()), + Rule("< ", " ", "tex"):with_pair(cond.not_after_regex(" ")):with_del(cond.none()), +}) + +autopairs.get_rule("$"):with_move(function(opts) + return opts.char == opts.next_char:sub(1, 1) +end) + +-- import nvim-cmp plugin (completions plugin) +local cmp = require("cmp") + +-- import nvim-autopairs completion functionality +local cmp_autopairs = require("nvim-autopairs.completion.cmp") + +-- make autopairs and completion work together +cmp.event:on( + "confirm_done", + cmp_autopairs.on_confirm_done({ + filetypes = { + tex = false, -- Disable for tex + }, + }) +) + +--local cmp_autopairs = require "nvim-autopairs.completion.cmp" +--local cmp_status_ok, cmp = pcall(require, "cmp") +--if not cmp_status_ok then +-- return +--end +--cmp.event:on("confirm_done", cmp_autopairs.on_confirm_done { map_char = { tex = "" } }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/cmp-gh-source.lua b/common/config/nvim/lua/plugins/cmp-gh-source.lua new file mode 100755 index 0000000..4990c35 --- /dev/null +++ b/common/config/nvim/lua/plugins/cmp-gh-source.lua @@ -0,0 +1,70 @@ +local ok, Job = pcall(require, 'plenary.job') +if not ok then + return +end + +local source = {} + +source.new = function() + local self = setmetatable({ cache = {} }, { __index = source }) + + return self +end + +source.complete = function(self, _, callback) + local bufnr = vim.api.nvim_get_current_buf() + + -- This just makes sure that we only hit the GH API once per session. + -- + -- You could remove this if you wanted, but this just makes it so we're + -- good programming citizens. + if not self.cache[bufnr] then + Job:new({ + -- Uses `gh` executable to request the issues from the remote repository. + 'gh', + 'issue', + 'list', + '--limit', + '1000', + '--json', + 'title,number,body', + + on_exit = function(job) + local result = job:result() + local ok, parsed = pcall(vim.json.decode, table.concat(result, '')) + if not ok then + vim.notify('Failed to parse gh result') + return + end + + local items = {} + for _, gh_item in ipairs(parsed) do + gh_item.body = string.gsub(gh_item.body or '', '\r', '') + + table.insert(items, { + label = string.format('#%s', gh_item.number), + documentation = { + kind = 'markdown', + value = string.format('# %s\n\n%s', gh_item.title, gh_item.body), + }, + }) + end + + callback({ items = items, isIncomplete = false }) + self.cache[bufnr] = items + end, + }):start() + else + callback({ items = self.cache[bufnr], isIncomplete = false }) + end +end + +source.get_trigger_characters = function() + return { '#' } +end + +source.is_available = function() + return vim.bo.filetype == 'gitcommit' +end + +require('cmp').register_source('gh_issues', source.new()) diff --git a/common/config/nvim/lua/plugins/cmp.lua b/common/config/nvim/lua/plugins/cmp.lua new file mode 100755 index 0000000..7de04ad --- /dev/null +++ b/common/config/nvim/lua/plugins/cmp.lua @@ -0,0 +1,67 @@ +local M = {} + +--- Setup and configure nvim-cmp +-- This function initializes and configures the completion plugin +-- @return boolean True if setup was successful, false otherwise +function M.setup() + -- Check Neovim version + local nvim_version = vim.version() + if nvim_version.major == 0 and nvim_version.minor < 5 then + return false + end + + -- Try to load required modules + local cmp = pcall(require, 'cmp') and require('cmp') + if not cmp then + return false + end + + local luasnip_ok, luasnip = pcall(require, 'luasnip') + if not luasnip_ok then + vim.notify("luasnip not found, some features may be limited", vim.log.levels.WARN) + end + + -- Setup nvim-cmp + cmp.setup({ + snippet = { + expand = function(args) + if luasnip_ok then luasnip.lsp_expand(args.body) end + end, + }, + mapping = cmp.mapping.preset.insert({ + ['<C-Space>'] = cmp.mapping.complete(), + ['<CR>'] = cmp.mapping.confirm({ select = true }), + ['<Tab>'] = cmp.mapping.select_next_item(), + ['<S-Tab>'] = cmp.mapping.select_prev_item(), + }), + sources = cmp.config.sources({ + { name = 'nvim_lsp' }, + { name = 'luasnip' }, + { name = 'buffer' }, + }), +}) + +vim.cmd([[ + highlight! link CmpItemMenu Comment + " gray + highlight! CmpItemAbbrDeprecated guibg=NONE gui=strikethrough guifg=#808080 + " blue + highlight! CmpItemAbbrMatch guibg=NONE guifg=#569CD6 + highlight! CmpItemAbbrMatchFuzzy guibg=NONE guifg=#569CD6 + " light blue + highlight! CmpItemKindVariable guibg=NONE guifg=#9CDCFE + highlight! CmpItemKindInterface guibg=NONE guifg=#9CDCFE + highlight! CmpItemKindText guibg=NONE guifg=#9CDCFE + " pink + highlight! CmpItemKindFunction guibg=NONE guifg=#C586C0 + highlight! CmpItemKindMethod guibg=NONE guifg=#C586C0 + " front + highlight! CmpItemKindKeyword guibg=NONE guifg=#D4D4D4 + highlight! CmpItemKindProperty guibg=NONE guifg=#D4D4D4 + highlight! CmpItemKindUnit guibg=NONE guifg=#D4D4D4 + ]]) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/colorizer.lua b/common/config/nvim/lua/plugins/colorizer.lua new file mode 100755 index 0000000..6019bc5 --- /dev/null +++ b/common/config/nvim/lua/plugins/colorizer.lua @@ -0,0 +1,8 @@ +local M = {} + +function M.setup() + -- No-op if colorizer is not installed + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/colorscheme.lua b/common/config/nvim/lua/plugins/colorscheme.lua new file mode 100755 index 0000000..7fbabc1 --- /dev/null +++ b/common/config/nvim/lua/plugins/colorscheme.lua @@ -0,0 +1,24 @@ +local M = {} + +-- List of preferred colorschemes in order of preference +local preferred_colorschemes = { + 'tokyonight', + 'desert', + 'default' +} + +function M.setup() + -- Try each colorscheme in order of preference + for _, scheme in ipairs(preferred_colorschemes) do + local ok = pcall(vim.cmd, 'colorscheme ' .. scheme) + if ok then + return true + end + end + + -- If all else fails, use the built-in default + vim.cmd('colorscheme default') + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/comment.lua b/common/config/nvim/lua/plugins/comment.lua new file mode 100755 index 0000000..392b279 --- /dev/null +++ b/common/config/nvim/lua/plugins/comment.lua @@ -0,0 +1,125 @@ +local M = {} + +--- Setup and configure comment.nvim +-- This function initializes and configures the comment plugin +-- @return boolean True if setup was successful, false otherwise +function M.setup() + local ok, comment = pcall(require, 'Comment') + if not ok then + vim.notify("Comment.nvim not found", vim.log.levels.WARN) + return false + end + + -- Configure comment.nvim + comment.setup({ + -- Add a space b/w comment and the line + padding = true, + + -- Whether the cursor should stay at its position + sticky = true, + + -- Lines to be ignored while (un)commenting + ignore = '^$', + + -- LHS of toggle mappings in NORMAL mode + toggler = { + -- Line-comment toggle keymap + line = 'gcc', + -- Block-comment toggle keymap + block = 'gbc', + }, + + -- LHS of operator-pending mappings in NORMAL and VISUAL mode + opleader = { + -- Line-comment keymap + line = 'gc', + -- Block-comment keymap + block = 'gb', + }, + + -- LHS of extra mappings + extra = { + -- Add comment on the line above + above = 'gcO', + -- Add comment on the line below + below = 'gco', + -- Add comment at the end of line + eol = 'gcA', + }, + + -- Enable keybindings + -- NOTE: If given `false` then the plugin won't create any mappings + mappings = { + -- Operator-pending mapping; `gcc` `gbc` `gc[count]{motion}` `gb[count]{motion}` + basic = true, + -- Extra mapping; `gco`, `gcO`, `gcA` + extra = true, + -- Extended mapping; `g>` `g<` `g>[count]{motion}` `g<[count]{motion}` + extended = false, + }, + + -- Function to call before (un)comment + pre_hook = nil, + + -- Function to call after (un)comment + post_hook = nil, + }) + + -- Additional keymaps for better UX + local keymap = vim.keymap.set + local opts = { noremap = true, silent = true } + + -- Toggle comment for current line or visual selection + keymap('n', '<leader>cc', '<Plug>(comment_toggle_linewise_current)', opts) + keymap('n', '<leader>bc', '<Plug>(comment_toggle_blockwise_current)', opts) + + -- Toggle comment for current line or visual selection and add new line + keymap('n', '<leader>cO', '<Plug>(comment_toggle_linewise_above)', opts) + keymap('n', '<leader>co', '<Plug>(comment_toggle_linewise_below)', opts) + + -- Toggle comment for visual selection + keymap('v', '<leader>cc', '<Plug>(comment_toggle_linewise_visual)', { noremap = false }) + keymap('v', '<leader>bc', '<Plug>(comment_toggle_blockwise_visual)', { noremap = false }) + + -- Filetype specific settings + local ft = require('Comment.ft') + + -- Set comment string for specific filetypes + ft.set('lua', { '--%s', '--[[%s]]' }) + ft.set('vim', { '" %s' }) + ft.set('python', { '# %s', '"""%s"""' }) + ft.set('javascript', { '// %s', '/*%s*/' }) + ft.set('typescript', { '// %s', '/*%s*/' }) + ft.set('css', { '/* %s */' }) + ft.set('html', { '<!-- %s -->' }) + + -- Set up autocommands for specific filetypes + local group = vim.api.nvim_create_augroup('CommentCustom', { clear = true }) + + -- Disable comment plugin for certain filetypes + vim.api.nvim_create_autocmd('FileType', { + group = group, + pattern = { + 'qf', 'help', 'man', 'notify', 'lspinfo', 'packer', + 'checkhealth', 'startuptime', 'Trouble', 'alpha', 'dashboard' + }, + callback = function() + vim.b.comment_disable = true + end, + }) + + -- Re-enable comment plugin for normal files + vim.api.nvim_create_autocmd('FileType', { + group = group, + pattern = { '*' }, + callback = function() + if vim.bo.buftype == '' then + vim.b.comment_disable = nil + end + end, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/dap.lua b/common/config/nvim/lua/plugins/dap.lua new file mode 100755 index 0000000..7de032c --- /dev/null +++ b/common/config/nvim/lua/plugins/dap.lua @@ -0,0 +1,265 @@ +local M = {} + +function M.setup() + local ok, dap = pcall(require, "dap") + if not ok or not dap then + return false + end + +-- options +dap.defaults.fallback.switchbuf = "uselast" +dap.defaults.fallback.focus_terminal = true +dap.defaults.fallback.external_terminal = { + command = "/usr/bin/wezterm", + args = { "-e" }, +} + +-- Autocmds +vim.api.nvim_create_autocmd("FileType", { + pattern = { "dap-float" }, + callback = function(event) + vim.keymap.set("n", "<Tab>", "", { buffer = event.buf, silent = true }) + vim.keymap.set("n", "<S-Tab>", "", { buffer = event.buf, silent = true }) + end, +}) + +dap.adapters.cppdbg = { + id = "cppdbg", + type = "executable", + --command = vim.fn.stdpath('data') .. '/mason/bin/OpenDebugAD7', + command = os.getenv("HOME") .. "/apps/cpptools/extension/debugAdapters/bin/OpenDebugAD7", + --command = cpptools:get_install_path() .. '/extension/debugAdapters/bin/OpenDebugAD7' +} + +dap.adapters.codelldb = { + type = "server", + port = "${port}", + --host = "localhost", + --host = '127.0.0.1', + --port = 13000, -- 💀 Use the port printed out or specified with `--port` + executable = { + --command = os.getenv("HOME") .. '/apps/codelldb/extension/adapter/codelldb', + command = os.getenv("HOME") .. "/.vscode-oss/extensions/vadimcn.vscode-lldb-1.9.0-universal/adapter/codelldb", + args = { "--port", "${port}" }, + }, + --detached = true, +} + +dap.adapters.lldb = { + type = "executable", + command = "/usr/bin/lldb-vscode", + name = "lldb", +} +dap.configurations.cpp = { + { + name = "Debugger", + --type = "lldb", + --type = "cppdbg", + type = "codelldb", + request = "launch", + cwd = "${workspaceFolder}", + program = function() + return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/", "file") + end, + --program = '${file}', + --program = function() + -- -- First, check if exists CMakeLists.txt + -- local cwd = vim.fn.getcwd() + -- if (file.exists(cwd, "CMakeLists.txt")) then + -- -- Todo. Then invoke cmake commands + -- -- Then ask user to provide execute file + -- return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/", "file") + -- else + -- local fileName = vim.fn.expand("%:t:r") + -- if (not file.exists(cwd, "bin")) then + -- -- create this directory + -- os.execute("mkdir " .. "bin") + -- end + -- local cmd = "!gcc -g % -o bin/" .. fileName + -- -- First, compile it + -- vim.cmd(cmd) + -- -- Then, return it + -- return "${fileDirname}/bin/" .. fileName + -- end + --end, + stopAtEntry = true, + args = {}, + --runInTerminal = true, + --runInTerminal = false, + --console = 'integratedTerminal', + + --MIMode = 'gdb', + --miDebuggerServerAddress = 'localhost:1234', + --miDebuggerPath = 'gdb-oneapi', + --miDebuggerPath = '/usr/bin/gdb', + externalConsole = true, + --setupCommands = { + -- { + -- text = '-enable-pretty-printing', + -- description = 'enable pretty printing', + -- ignoreFailures = false + -- } + --}, + }, +} + +-- If you want to use this for Rust and C, add something like this: +dap.configurations.c = dap.configurations.cpp +dap.configurations.rust = dap.configurations.cpp + +-- javascript +--dap.adapters.node2 = { +-- type = 'executable', +-- command = 'node-debug2-adapter', +-- args = {}, +--} + +--dap.configurations.javascript = { +-- { +-- name = 'Launch', +-- type = 'node2', +-- request = 'attach', +-- program = '${file}', +-- cwd = vim.fn.getcwd(), +-- sourceMaps = true, +-- protocol = 'inspector', +-- console = 'integratedTerminal', +-- }, +--} + +dap.adapters.python = { + type = "executable", + command = vim.trim(vim.fn.system("which python")), + args = { "-m", "debugpy.adapter" }, +} + +dap.configurations.python = { + { + -- The first three options are required by nvim-dap + type = "python", -- the type here established the link to the adapter definition: `dap.adapters.python` + request = "launch", + name = "Launch file", + -- Options below are for debugpy, see https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings for supported options + program = "${file}", -- This configuration will launch the current file if used. + stopOnEntry = true, + }, +} + +local dapui = require("dapui") +--local dap_ui_status_ok, dapui = pcall(require, "dapui") +--if not dap_ui_status_ok then +-- return +--end + +-- setup repl +--dap.repl.commands = vim.tbl_extend('force', dap.repl.commands, { +-- exit = { 'q', 'exit' }, +-- custom_commands = { +-- ['.run_to_cursor'] = dap.run_to_cursor, +-- ['.restart'] = dap.run_last +-- } +--}) + +-- Load dapui configuration only if it hasn't been loaded before + local dapui_ok, dapui = pcall(require, "dapui") + if dapui_ok and dapui then + dapui.setup({ + mappings = { + expand = "<CR>", + open = "o", + remove = "D", + edit = "e", + repl = "r", + toggle = "t", + }, + controls = { + enabled = true, + }, + layouts = { + { + elements = { + -- Elements can be strings or table with id and size keys. + { id = "watches", size = 0.25 }, + { id = "scopes", size = 0.25 }, + { id = "breakpoints", size = 0.25 }, + { id = "stacks", size = 0.25 }, + }, + size = 50, -- 40 columns + position = "left", + }, + { + elements = { + { id = "console", size = 0.6 }, + { id = "repl", size = 0.4 }, + }, + size = 0.3, + position = "bottom", + }, + }, + render = { + max_value_lines = 3, + }, + floating = { + max_height = nil, -- These can be integers or a float between 0 and 1. + max_width = nil, -- Floats will be treated as percentage of your screen. + border = "single", -- Border style. Can be "single", "double" or "rounded" + mappings = { + close = { "q", "<Esc>" }, + }, + }, + --icons = { expanded = "-", collapsed = "$" }, + icons = { + expanded = "", + collapsed = "", + current_frame = "", + }, + }) + vim.g.loaded_dapui = true +end + +-- Signs +local sign = vim.fn.sign_define +sign("DapBreakpoint", { text = "●", texthl = "DapBreakpoint", linehl = "", numhl = "" }) +sign("DapBreakpointCondition", { text = "◆", texthl = "DapBreakpointCondition", linehl = "", numhl = "" }) -- +sign("DapBreakpointRejected", { text = "R", texthl = "DiagnosticError", numhl = "DiagnosticError" }) +sign("DapLogPoint", { text = "L", texthl = "DapLogPoint", linehl = "", numhl = "" }) +sign("DapStopped", { text = "", texthl = "DiagnosticSignHint", numbhl = "", linehl = "" }) + +--sign('DapBreakpoint', { text = '', texthl = 'DiagnosticSignError', numbhl = '', linehl = '' }) +--sign("DapLogPoint", { text = '.>', texthl = 'DiagnosticInfo', numhl = 'DiagnosticInfo' }) +--vim.fn.sign_define("DapBreakpointCondition", { text = '?>', texthl = 'DiagnosticInfo', numhl = 'DiagnosticInfo' }) +--vim.fn.sign_define("DapStopped", { text = '=>', texthl = 'DiagnosticWarn', numhl = 'DiagnosticWarn' }) +--vim.fn.sign_define("DapBreakpoint", { text = '<>', texthl = 'DiagnosticInfo', numhl = 'DiagnosticInfo' }) + +dap.listeners.after.event_initialized["dapui_config"] = function() + dapui.open() +end +dap.listeners.before.event_terminated["dapui_config"] = function() + dapui.close() +end +dap.listeners.before.event_exited["dapui_config"] = function() + dapui.close() +end +dap.listeners.before.disconnect["dapui_config"] = function() + dapui.close() +end + +require("nvim-dap-virtual-text").setup({ + enabled = true, + enabled_commands = true, + highlight_changed_variables = true, + highlight_new_as_changed = false, + show_stop_reason = true, + commented = true, + only_first_definition = true, + all_references = false, + filter_references_pattern = "<module", + virt_text_pos = "eol", + all_frames = false, + virt_text_win_col = nil, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/dashboard.lua b/common/config/nvim/lua/plugins/dashboard.lua new file mode 100755 index 0000000..43a3461 --- /dev/null +++ b/common/config/nvim/lua/plugins/dashboard.lua @@ -0,0 +1,126 @@ +local M = {} + +--- Setup and configure dashboard.nvim +-- This function initializes and configures the dashboard plugin +-- @return boolean True if setup was successful, false otherwise +function M.setup() + local ok, db = pcall(require, 'dashboard') + if not ok then + return false + end + + local messages = { + "The only way to do great work is to love what you do. - Steve Jobs", + "Code is like humor. When you have to explain it, it's bad. - Cory House", + "First, solve the problem. Then, write the code. - John Johnson", + "Any fool can write code that a computer can understand. Good programmers write code that humans can understand. - Martin Fowler", + "The most disastrous thing that you can ever learn is your first programming language. - Alan Kay", + "The most important property of a program is whether it accomplishes the intention of its user. - C.A.R. Hoare", + "The best error message is the one that never shows up. - Thomas Fuchs", + "The most important skill for a programmer is the ability to effectively communicate ideas. - Gastón Jorquera", + "The only way to learn a new programming language is by writing programs in it. - Dennis Ritchie", + "The most damaging phrase in the language is 'We've always done it this way!' - Grace Hopper" + } + + local function get_random_message() + local random_index = math.random(1, #messages) + return messages[random_index] + end + +--vim.api.nvim_create_autocmd("VimEnter", { +-- callback = function() +-- -- disable line numbers +-- vim.opt_local.number = false +-- vim.opt_local.relativenumber = false +-- -- always start in insert mode +-- end, +--}) + + -- Configure dashboard + db.setup({ + theme = "hyper", + config = { + mru = { limit = 20, label = "" }, + project = { limit = 10 }, + header = { + [[ ███╗ ██╗ ███████╗ ██████╗ ██╗ ██╗ ██╗ ███╗ ███╗]], + [[ ████╗ ██║ ██╔════╝██╔═══██╗ ██║ ██║ ██║ ████╗ ████║]], + [[ ██╔██╗ ██║ █████╗ ██║ ██║ ██║ ██║ ██║ ██╔████╔██║]], + [[ ██║╚██╗██║ ██╔══╝ ██║ ██║ ╚██╗ ██╔╝ ██║ ██║╚██╔╝██║]], + [[ ██║ ╚████║ ███████╗╚██████╔╝ ╚████╔╝ ██║ ██║ ╚═╝ ██║]], + [[ ╚═╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝]], + }, + disable_move = false, + shortcut = { + { desc = " Plugins", group = "Number", action = "PackerStatus", key = "p" }, + { + desc = " Files", + group = "Number", + action = "Telescope find_files", + key = "f", + }, + { + desc = " TODO", + group = "Number", + action = ":edit ~/documents/main/inbox/tasks/TODO.md", + key = "t", + }, + { + desc = " New", + group = "Number", + action = "enew", + key = "e", + }, + { + desc = " Grep", + group = "Number", + action = "Telescope live_grep", + key = "g", + }, + { + desc = " Scheme", + group = "Number", + action = "Telescope colorscheme", + key = "s", + }, + { + desc = " Config", + group = "Number", + action = ":edit ~/.config/nvim/init.lua", + key = "c", + }, + }, + footer = function() + return { "", "" } + --return { "", GetRandomMessage() } + end, + }, + hide = { + statusline = false, + tabline = false, + winbar = false, + }, +}) + +-- Set keymaps only when dashboard is active +vim.api.nvim_create_autocmd("FileType", { + group = vim.api.nvim_create_augroup("DashboardMappings", { clear = true }), + pattern = "dashboard", + callback = function() + vim.keymap.set("n", "e", "<Cmd>DashboardNewFile<CR>", { buffer = true }) + vim.keymap.set("n", "q", "<Cmd>q!<CR>", { buffer = true }) + vim.keymap.set("n", "<C-o>", "<C-o><C-o>", { buffer = true }) -- Allow Ctrl + o to act normally + end, +}) +---- General +--DashboardHeader DashboardFooter +---- Hyper theme +--DashboardProjectTitle DashboardProjectTitleIcon DashboardProjectIcon +--DashboardMruTitle DashboardMruIcon DashboardFiles DashboardShotCutIcon +---- Doome theme +--DashboardDesc DashboardKey DashboardIcon DashboardShotCut + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/fidget.lua b/common/config/nvim/lua/plugins/fidget.lua new file mode 100755 index 0000000..d401c5f --- /dev/null +++ b/common/config/nvim/lua/plugins/fidget.lua @@ -0,0 +1,34 @@ +require("fidget").setup({ + --event = "LspAttach", + text = { + --spinner = "pipe", -- (Default) animation shown when tasks are ongoing + --spinner = "hamburger", -- animation shown when tasks are ongoing + --spinner = "dots_pulse", -- animation shown when tasks are ongoing + spinner = "dots", -- animation shown when tasks are ongoing + done = "✔", -- character shown when all tasks are complete + commenced = "Started", -- message shown when task starts + completed = "Completed", -- message shown when task completes + }, + fmt = { + task = function(task_name, message, percentage) + if task_name == "diagnostics" then + return false + end + return string.format( + "%s%s [%s]", + message, + percentage and string.format(" (%s%%)", percentage) or "", + task_name + ) + end, + }, + --sources = { -- Sources to configure + --["null-ls"] = { -- Name of source + --ignore = true, -- Ignore notifications from this source + --}, + --}, + debug = { + logging = false, -- whether to enable logging, for debugging + strict = false, -- whether to interpret LSP strictly + }, +}) diff --git a/common/config/nvim/lua/plugins/friendly-snippets.lua b/common/config/nvim/lua/plugins/friendly-snippets.lua new file mode 100755 index 0000000..2a7695e --- /dev/null +++ b/common/config/nvim/lua/plugins/friendly-snippets.lua @@ -0,0 +1,3 @@ +-- friendly-snippets plugin config (modular, robust) +local ok, _ = pcall(require, 'friendly-snippets') +-- No config needed, loaded by LuaSnip
\ No newline at end of file diff --git a/common/config/nvim/lua/plugins/fugitive.lua b/common/config/nvim/lua/plugins/fugitive.lua new file mode 100755 index 0000000..22620e3 --- /dev/null +++ b/common/config/nvim/lua/plugins/fugitive.lua @@ -0,0 +1,8 @@ +local M = {} + +function M.setup() + -- No-op if fugitive is not installed + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/fzf.lua b/common/config/nvim/lua/plugins/fzf.lua new file mode 100755 index 0000000..9e62c48 --- /dev/null +++ b/common/config/nvim/lua/plugins/fzf.lua @@ -0,0 +1,43 @@ +local M = {} + +if not fzfLua then + return M +end + +local ok_fzfLua, actions = pcall(require, "fzf-lua") +if not ok_fzfLua then + return +end + +local ok_fzfLua, actions = pcall(require, "fzf-lua.actions") +if not ok_fzfLua then + return +end + + +local ok, fzfLua = pcall(require, "fzf-lua") +if not ok then + vim.notify("fzf-lua not found", vim.log.levels.WARN) + return M +end + +fzfLua.setup({ + defaults = { + file_icons = "mini", + }, + winopts = { + row = 0.5, + height = 0.7, + }, + files = { + previewer = false, + }, +}) + +vim.keymap.set("n", "<leader>fz", "<cmd>FzfLua files<cr>", { desc = "Fuzzy find files" }) +vim.keymap.set("n", "<leader>fzg", "<cmd>FzfLua live_grep<cr>", { desc = "Fuzzy grep files" }) +vim.keymap.set("n", "<leader>fzh", "<cmd>FzfLua helptags<cr>", { desc = "Fuzzy grep tags in help files" }) +vim.keymap.set("n", "<leader>fzt", "<cmd>FzfLua btags<cr>", { desc = "Fuzzy search buffer tags" }) +vim.keymap.set("n", "<leader>fzb", "<cmd>FzfLua buffers<cr>", { desc = "Fuzzy search opened buffers" }) + +return M diff --git a/common/config/nvim/lua/plugins/git.lua b/common/config/nvim/lua/plugins/git.lua new file mode 100755 index 0000000..24a0871 --- /dev/null +++ b/common/config/nvim/lua/plugins/git.lua @@ -0,0 +1,8 @@ +local M = {} + +function M.setup() + -- No-op if git plugin is not installed + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/gitsigns.lua b/common/config/nvim/lua/plugins/gitsigns.lua new file mode 100755 index 0000000..7bbe637 --- /dev/null +++ b/common/config/nvim/lua/plugins/gitsigns.lua @@ -0,0 +1,85 @@ +local M = {} + +--- Setup and configure gitsigns +-- This function initializes and configures the git signs in the gutter +-- @return boolean True if setup was successful, false otherwise +function M.setup() + local ok, gitsigns = pcall(require, 'gitsigns') + if not ok then + return false + end + + gitsigns.setup({ + signs = { + --add = { + -- hl = "GitSignsAdd", + -- text = "▍", --│ + -- numhl = "GitSignsAddNr", + -- linehl = "GitSignsAddLn", + --}, + --change = { + -- hl = "GitSignsChange", + -- text = "▍", --│ + -- numhl = "GitSignsChangeNr", + -- linehl = "GitSignsChangeLn", + --}, + delete = { + hl = "GitSignsDelete", + text = "▁", --_━─ + numhl = "GitSignsDeleteNr", + linehl = "GitSignsDeleteLn", + }, + topdelete = { + hl = "GitSignsDelete", + text = "▔", --‾ + numhl = "GitSignsDeleteNr", + linehl = "GitSignsDeleteLn", + }, + changedelete = { + hl = "GitSignsDelete", + text = "~", + numhl = "GitSignsChangeNr", + linehl = "GitSignsChangeLn", + }, + }, + current_line_blame = true, + }) + +vim.api.nvim_command("highlight DiffAdd guibg=none guifg=#21c7a8") +vim.api.nvim_command("highlight DiffModified guibg=none guifg=#82aaff") +vim.api.nvim_command("highlight DiffDelete guibg=none guifg=#fc514e") +vim.api.nvim_command("highlight DiffText guibg=none guifg=#fc514e") +vim.cmd([[ +hi link GitSignsAdd DiffAdd +hi link GitSignsChange DiffModified +hi link GitSignsDelete DiffDelete +hi link GitSignsTopDelete DiffDelete +hi link GitSignsChangedDelete DiffDelete +]]) + -- Set up highlights + vim.cmd([[ + highlight DiffAdd guibg=none guifg=#21c7a8 + highlight DiffModified guibg=none guifg=#82aaff + highlight DiffDelete guibg=none guifg=#fc514e + highlight DiffText guibg=none guifg=#fc514e + + hi link GitSignsAdd DiffAdd + hi link GitSignsChange DiffModified + hi link GitSignsDelete DiffDelete + hi link GitSignsTopDelete DiffDelete + hi link GitSignsChangedelete DiffDelete + hi link GitSignsChangedeleteLn DiffDelete + hi link GitSignsChangedeleteNr DiffDeleteNr + ]]) + + return true +end + +return M +--'signs.delete.hl' is now deprecated, please define highlight 'GitSignsDelete' +--'signs.delete.linehl' is now deprecated, please define highlight 'GitSignsDeleteLn' +--'signs.delete.numhl' is now deprecated, please define highlight 'GitSignsDeleteNr' +--'signs.topdelete.hl' is now deprecated, please define highlight 'GitSignsTopdelete' +--'signs.topdelete.linehl' is now deprecated, please define highlight 'GitSignsTopdeleteLn' +--'signs.topdelete.numhl' is now deprecated, please define highlight 'GitSignsTopdeleteNr' + diff --git a/common/config/nvim/lua/plugins/goto-preview.lua b/common/config/nvim/lua/plugins/goto-preview.lua new file mode 100755 index 0000000..eb54a8c --- /dev/null +++ b/common/config/nvim/lua/plugins/goto-preview.lua @@ -0,0 +1,31 @@ +local M = {} + +function M.setup() + local ok, gp = pcall(require, 'goto-preview') + if not ok or not gp then + return false + end + + gp.setup { + width = 120; -- Width of the floating window + height = 15; -- Height of the floating window + border = {"↖", "─" ,"┐", "│", "┘", "─", "└", "│"}; -- Border characters of the floating window + default_mappings = false; -- Bind default mappings + debug = false; -- Print debug information + opacity = nil; -- 0-100 opacity level of the floating window where 100 is fully transparent. + resizing_mappings = false; -- Binds arrow keys to resizing the floating window. + post_open_hook = nil; -- A function taking two arguments, a buffer and a window to be ran as a hook. + references = { -- Configure the telescope UI for slowing the references cycling window. + telescope = require("telescope.themes").get_dropdown({ hide_preview = false }) + }; + -- These two configs can also be passed down to the goto-preview definition and implementation calls for one off "peak" functionality. + focus_on_open = true; -- Focus the floating window when opening it. + dismiss_on_move = false; -- Dismiss the floating window when moving the cursor. + force_close = true, -- passed into vim.api.nvim_win_close's second argument. See :h nvim_win_close + bufhidden = "wipe", -- the bufhidden option to set on the floating window. See :h bufhidden + } + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/hardtime.lua b/common/config/nvim/lua/plugins/hardtime.lua new file mode 100755 index 0000000..b440334 --- /dev/null +++ b/common/config/nvim/lua/plugins/hardtime.lua @@ -0,0 +1,29 @@ +-- Function to toggle the hardtime state and echo a message +local hardtime_enabled = true + +local hardtime = require("hardtime") + +hardtime.setup({ + -- hardtime config here + enabled = true, + restriction_mode = "hint", + disabled_filetypes = { "qf", "netrw", "NvimTree", "NvimTree_1", "lazy", "mason", "oil", "dashboard" }, + disable_mouse = false, + disabled_keys = { + ["<Up>"] = {}, + ["<Down>"] = {}, + ["<Left>"] = {}, + ["<Right>"] = {}, + }, +}) + +function ToggleHardtime() + hardtime.toggle() + hardtime_enabled = not hardtime_enabled + local message = hardtime_enabled and "hardtime on" or "hardtime off" + vim.cmd('echo "' .. message .. '"') +end + +return { + ToggleHardtime = ToggleHardtime, +} diff --git a/common/config/nvim/lua/plugins/harpoon.lua b/common/config/nvim/lua/plugins/harpoon.lua new file mode 100755 index 0000000..8e842b3 --- /dev/null +++ b/common/config/nvim/lua/plugins/harpoon.lua @@ -0,0 +1,50 @@ +local M = {} + +function M.setup() + local ok, harpoon = pcall(require, "harpoon") + if not ok or not harpoon then + return false + end + + harpoon.setup({ + menu = { + width = vim.api.nvim_win_get_width(0) - 4, + }, + --keys = { + -- { "mt", function() require("harpoon.mark").toggle_file() end, desc = "Toggle File" }, + -- { "mm", function() require("harpoon.ui").toggle_quick_menu() end, desc = "Harpoon Menu" }, + -- { "mc", function() require("harpoon.cmd-ui").toggle_quick_menu() end, desc = "Command Menu" }, + -- --{ "<leader>1", function() require("harpoon.ui").nav_file(1) end, desc = "File 1" }, + -- --{ "<leader>2", function() require("harpoon.ui").nav_file(2) end, desc = "File 2" }, + -- --{ "<leader>3", function() require("harpoon.term").gotoTerminal(1) end, desc = "Terminal 1" }, + -- --{ "<leader>4", function() require("harpoon.term").gotoTerminal(2) end, desc = "Terminal 2" }, + -- --{ "<leader>5", function() require("harpoon.term").sendCommand(1,1) end, desc = "Command 1" }, + -- --{ "<leader>6", function() require("harpoon.term").sendCommand(1,2) end, desc = "Command 2" }, + }) + + -- Set up keymaps safely + local function safe_keymap(mode, lhs, rhs, opts) + local opts_with_noremap = vim.tbl_extend('force', {noremap = true, silent = true}, opts or {}) + vim.keymap.set(mode, lhs, rhs, opts_with_noremap) + end + + safe_keymap("n", "<leader>ma", function() require('harpoon.mark').add_file() end, { desc = "Harpoon: Add file" }) + safe_keymap("n", "<leader>mt", function() require('harpoon.mark').toggle_file() end, { desc = "Harpoon: Toggle file" }) + safe_keymap("n", "<leader>mq", function() require('harpoon.ui').toggle_quick_menu() end, { desc = "Harpoon: Toggle quick menu" }) + safe_keymap("n", "<leader>mh", function() require('harpoon.ui').nav_file(1) end, { desc = "Harpoon: Navigate to file 1" }) + safe_keymap("n", "<leader>mj", function() require('harpoon.ui').nav_file(2) end, { desc = "Harpoon: Navigate to file 2" }) + safe_keymap("n", "<leader>mk", function() require('harpoon.ui').nav_file(3) end, { desc = "Harpoon: Navigate to file 3" }) + safe_keymap("n", "<leader>ml", function() require('harpoon.ui').nav_file(4) end, { desc = "Harpoon: Navigate to file 4" }) + + return true +end + +return M +-- +--vim.keymap.set("n", "<leader>a", mark.add_file) +--vim.keymap.set("n", "<C-e>", ui.toggle_quick_menu) +-- +--vim.keymap.set("n", "<C-h>", function() ui.nav_file(1) end) +--vim.keymap.set("n", "<C-t>", function() ui.nav_file(2) end) +--vim.keymap.set("n", "<C-n>", function() ui.nav_file(3) end) +--vim.keymap.set("n", "<C-s>", function() ui.nav_file(4) end) diff --git a/common/config/nvim/lua/plugins/heirline.lua b/common/config/nvim/lua/plugins/heirline.lua new file mode 100755 index 0000000..a4c2fc3 --- /dev/null +++ b/common/config/nvim/lua/plugins/heirline.lua @@ -0,0 +1,1497 @@ +local M = {} + +-- Safe require function to handle missing dependencies +local function safe_require(module) + local ok, result = pcall(require, module) + return ok and result or nil +end + +-- These will be initialized in M.setup() +local heirline = nil +local conditions = {} +local utils = {} +local colors = {} + +function M.setup() + heirline = safe_require("heirline") + if not heirline then + return + end + + -- Initialize conditions and utils after heirline is loaded + conditions = require("heirline.conditions") or {} + utils = require("heirline.utils") or {} + + + -- Initialize colors after safe_fg is defined + colors = { + bg = "NONE", + nobg = "NONE", + white = "#f8f8f2", + black = "#000000", + darkgray = "#23232e", + gray = "#2d2b3a", + lightgray = "#d6d3ea", + pink = "#f92672", + green = "#50fa7b", + blue = "#39BAE6", + yellow = "#f1fa8c", + orange = "#ffb86c", + purple = "#BF40BF", + violet = "#7F00FF", + red = "#ff5555", + cyan = "#66d9eC", + --diag = { + -- warn = safe_fg("DiagnosticSignWarn", "#ffb86c"), + -- error = safe_fg("DiagnosticSignError", "#ff5555"), + -- hint = safe_fg("DiagnosticSignHint", "#50fa7b"), + -- info = safe_fg("DiagnosticSignInfo", "#66d9eC"), + --}, + diag = { + warn = utils.get_highlight("DiagnosticSignWarn").fg, + error = utils.get_highlight("DiagnosticSignError").fg, + hint = utils.get_highlight("DiagnosticSignHint").fg, + info = utils.get_highlight("DiagnosticSignInfo").fg, + }, + git = { + active = "#f34f29", + del = "#ff5555", + add = "#50fa7b", + change = "#ae81ff", + }, + } + + -- Only load colors if heirline is available + if heirline.load_colors then + local ok, err = pcall(heirline.load_colors, colors) + if not ok then + vim.notify("Failed to load Heirline colors: " .. tostring(err), vim.log.levels.ERROR) + end + end + + local function get_icon(icon, fallback) + -- Check if we have Nerd Fonts available + local has_nerd_fonts = vim.g.statusline_has_nerd_fonts + if has_nerd_fonts == nil then + -- Cache the result to avoid repeated checks + if vim.fn.has('unix') == 1 and vim.fn.executable('fc-list') == 1 then + local handle = io.popen('fc-list | grep -i nerd') + local result = handle:read('*a') + handle:close() + has_nerd_fonts = result ~= "" + else + -- On non-Unix systems or if fc-list isn't available, assume no Nerd Fonts + has_nerd_fonts = false + end + vim.g.statusline_has_nerd_fonts = has_nerd_fonts + end + + -- Return the appropriate string based on font availability + local result = has_nerd_fonts and icon or (fallback or '') + -- Trim any whitespace to prevent layout issues + return vim.trim(result) + end + + -- Define all components after colors and utils are initialized + + --local Signs = { + -- Error = "✘", + -- Warn = "", + -- Hint = "◉", + -- Info = "", + --} + local Icons = { + Signs = { + Error = "✘", + Warn = "", + Hint = "◉", + Info = "", + LSP = get_icon("⚙️", "LSP"), + }, + ---- LSP/Debug + Error = get_icon("✘", "E"), + Warn = get_icon("", "W"), + Hint = get_icon("◉", "H"),-- + Info = get_icon("ℹ", "I"), + --LSP = get_icon("⚙️", "LSP"), + + -- Diagnostic + Diagnostic = { + error = get_icon("✘", "E"), + warn = get_icon("", "W"), + hint = get_icon("", "H"), + info = get_icon("ℹ", "I"), + }, + + +--local GitIcons = { +-- added = "✚", -- plus in diff style +-- modified = "", -- nf-oct-diff_modified +-- removed = "", -- nf-oct-diff_removed +--} +--added = "", -- nf-oct-diff_added +--modified = "", -- nf-oct-diff_modified +--removed = "", -- nf-oct-diff_removed +--local GitIcons = { +-- added = "", -- nf-fa-plus_square +-- modified = "", -- nf-fa-file_text_o +-- removed = "", -- nf-fa-minus_square +--} + -- Git + Git = { + branch = get_icon(" ", "⎇ "), + added = get_icon("+ ", "+"), + removed = get_icon("- ", "-"), + modified = get_icon("~ ", "~"), + renamed = get_icon("", "r"), + untracked = get_icon("", "?"), + ignored = get_icon("", "."), + }, + + -- UI Elements + UI = { + left_separator = get_icon("", ""), + right_separator = get_icon("", ""), + thin_separator = get_icon("▏", "|"), + ellipsis = get_icon("…", "..."), + arrow_left = get_icon("◀", "<"), + arrow_right = get_icon("▶", ">"), + close = get_icon("✕", "x"), + big_close = get_icon(" ", "x "), + modified = get_icon(" + ", "*"), + readonly = get_icon("", "RO"), + lock = get_icon("", "[L]"), + clock = get_icon("🕒", "[TIME]"), + buffer = get_icon("", "[BUF]"), + tab = get_icon("", "[TAB]"), + search = get_icon("🔍", "[SEARCH]"), + spell = get_icon("暈", "[SPELL]"), + whitespace = get_icon("␣", "[WS]"), + newline = get_icon("↵", "[NL]"), + indent = get_icon("▏", "|"), + fold = get_icon("", ">"), + fold_open = get_icon("", "v"), + fold_closed = get_icon("", ">"), + }, + + -- File types + File = { + default = get_icon("", "[F]"), + directory = get_icon("", "[D]"), + symlink = get_icon("", "[L]"), + executable = get_icon("", "[X]"), + image = get_icon("", "[IMG]"), + archive = get_icon("", "[ARC]"), + audio = get_icon("", "[AUD]"), + video = get_icon("", "[VID]"), + document = get_icon("", "[DOC]"), + config = get_icon("", "[CFG]"), + code = get_icon("", "[CODE]"), + terminal = get_icon("", "[TERM]"), + }, + + -- File format indicators + Format = { + unix = get_icon("", "[UNIX]"), + dos = get_icon("", "[DOS]"), + mac = get_icon("", "[MAC]"), + }, + + -- Version control + VCS = { + branch = get_icon("", "[BR]"), + git = get_icon("", "[GIT]"), + github = get_icon("", "[GH]"), + gitlab = get_icon("", "[GL]"), + bitbucket = get_icon("", "[BB]"), + }, + + -- Programming languages + Lang = { + lua = get_icon("", "[LUA]"), + python = get_icon("", "[PY]"), + javascript = get_icon("", "[JS]"), + typescript = get_icon("", "[TS]"), + html = get_icon("", "[HTML]"), + css = get_icon("", "[CSS]"), + json = get_icon("", "[JSON]"), + markdown = get_icon("", "[MD]"), + docker = get_icon("", "[DKR]"), + rust = get_icon("", "[RS]"), + go = get_icon("", "[GO]"), + java = get_icon("", "[JAVA]"), + c = get_icon("", "[C]"), + cpp = get_icon("", "[C++]"), + ruby = get_icon("", "[RB]"), + php = get_icon("", "[PHP]"), + haskell = get_icon("", "[HS]"), + scala = get_icon("", "[SCALA]"), + elixir = get_icon("", "[EXS]"), + clojure = get_icon("", "[CLJ]"), + }, + + -- UI Indicators + Indicator = { + error = get_icon("✘", "[E]"), + warning = get_icon("⚠", "[W]"), + info = get_icon("ℹ", "[I]"), + hint = get_icon("", "[H]"), + success = get_icon("✓", "[OK]"), + question = get_icon("?", "[?]"), + star = get_icon("★", "[*]"), + heart = get_icon("❤", "<3"), + lightning = get_icon("⚡", "[!]"), + check = get_icon("✓", "[√]"), + cross = get_icon("✗", "[x]"), + plus = get_icon("+", "[+]"), + recording = get_icon(" ", "q") + }, + + -- File operations + FileOp = { + new = get_icon("", "[NEW]"), + save = get_icon("💾", "[SAVE]"), + open = get_icon("📂", "[OPEN]"), + close = get_icon("✕", "[X]"), + undo = get_icon("↩", "[UNDO]"), + redo = get_icon("↪", "[REDO]"), + cut = get_icon("✂", "[CUT]"), + copy = get_icon("⎘", "[COPY]"), + paste = get_icon("📋", "[PASTE]"), + search = get_icon("🔍", "[FIND]"), + replace = get_icon("🔄", "[REPLACE]"), + }, + + -- Navigation + Nav = { + left = get_icon("←", "[<]"), + right = get_icon("→", "[>]"), + up = get_icon("↑", "[^]"), + down = get_icon("↓", "[v]"), + first = get_icon("⏮", "[<<]"), + last = get_icon("⏭", "[>>]"), + prev = get_icon("◀", "[<]"), + next = get_icon("▶", "[>]"), + back = get_icon("↩", "[B]"), + forward = get_icon("↪", "[F]"), + }, + + -- Editor states + State = { + insert = get_icon("", "[INS]"), + normal = get_icon("🚀", "[NOR]"), + visual = get_icon("👁", "[VIS]"), + replace = get_icon("🔄", "[REP]"), + command = get_icon("", "[CMD]"), + terminal = get_icon("", "[TERM]"), + select = get_icon("🔍", "[SEL]"), + }, + + -- Common symbols + Symbol = { + dot = get_icon("•", "•"), + bullet = get_icon("•", "•"), + middle_dot = get_icon("·", "·"), + ellipsis = get_icon("…", "..."), + check = get_icon("✓", "[OK]"), + cross = get_icon("✗", "[X]"), + arrow_right = get_icon(" ", "->"), + arrow_left = get_icon(" ", "<-"), + double_arrow_right = get_icon("»", ">>"), + double_arrow_left = get_icon("«", "<<"), + chevron_right = get_icon("›", ">"), + chevron_left = get_icon("‹", "<"), + }, + + -- Document symbols + DocSymbol = { + class = get_icon("", "[C]"), + function_icon = get_icon("", "[F]"), + method = get_icon("", "[M]"), + property = get_icon("", "[P]"), + field = get_icon("ﰠ", "[F]"), + constructor = get_icon("", "[C]"), + enum = get_icon("", "[E]"), + interface = get_icon("", "[I]"), + variable = get_icon("", "[V]"), + constant = get_icon("", "[C]"), + string = get_icon("", "[S]"), + number = get_icon("", "[N]"), + boolean = get_icon("◩", "[B]"), + array = get_icon("", "[A]"), + object = get_icon("⦿", "[O]"), + key = get_icon("🔑", "[K]"), + null = get_icon("NULL", "Ø"), + enum_member = get_icon("", "[E]"), + struct = get_icon("פּ", "[S]"), + event = get_icon("", "[E]"), + operator = get_icon("", "[O]"), + type_parameter = get_icon("", "[T]"), + }, + } + local Align = { provider = "%=", hl = { bg = colors.bg } } + local Space = { provider = " ", hl = { bg = colors.bg } } + local Tab = { provider = " " } + local LeftSpace = { provider = "" } + local RightSpace = { provider = "" } + + local ViMode = { + init = function(self) + self.mode = vim.fn.mode(1) + -- Store the initial mode + self.prev_mode = self.mode + + -- Set up autocommand to force redraw on mode change + vim.api.nvim_create_autocmd("ModeChanged", { + pattern = "*:*", + callback = function() + -- Only redraw if the mode actually changed + local current_mode = vim.fn.mode(1) + if current_mode ~= self.prev_mode then + self.prev_mode = current_mode + vim.schedule(function() + vim.cmd("redrawstatus") + end) + end + end, + }) + end, + static = { + mode_names = { + n = " NORMAL ", + no = "PENDING ", + nov = " N? ", + noV = " N? ", + ["no\22"] = " N? ", + niI = " Ni ", + niR = " Nr ", + niV = " Nv ", + nt = "TERMINAL", + v = " VISUAL ", + vs = " Vs ", + V = " V·LINE ", + ["\22"] = "V·BLOCK ", + ["\22s"] = "V·BLOCK ", + s = " SELECT ", + S = " S·LINE ", + ["\19"] = "S·BLOCK ", + i = " INSERT ", + ix = "insert x", + ic = "insert c", + R = "REPLACE ", + Rc = " Rc ", + Rx = " Rx ", + Rv = "V·REPLACE ", + Rvc = " Rv ", + Rvx = " Rv ", + c = "COMMAND ", + cv = " VIM EX ", + ce = " EX ", + r = " PROMPT ", + rm = " MORE ", + ["r?"] = "CONFIRM ", + ["!"] = " SHELL ", + t = "TERMINAL", + }, + }, + provider = function(self) + return " %2(" .. self.mode_names[self.mode] .. "%) " + end, + hl = function(self) + return { fg = "colors.black", bg = self.mode_color, bold = true } + end, + update = { + "ModeChanged", + "VimEnter", + "BufEnter", + "WinEnter", + "TabEnter", + pattern = "*:*", + callback = vim.schedule_wrap(function() + vim.cmd("redrawstatus") + end), + }, + } + + -- LSP + local LSPActive = { + condition = function() + local ok, _ = pcall(function() + local buf = vim.api.nvim_get_current_buf() + return #vim.lsp.get_clients({ bufnr = buf }) > 0 + end) + return ok or false + end, + update = { "LspAttach", "LspDetach", "BufEnter" }, + provider = function() + local ok, result = pcall(function() + local buf = vim.api.nvim_get_current_buf() + if not vim.api.nvim_buf_is_valid(buf) then return "" end + + local clients = vim.lsp.get_clients({ bufnr = buf }) + if not clients or #clients == 0 then return "" end + + local client_names = {} + for _, client in ipairs(clients) do + if client and client.name and client.name ~= "null-ls" then + table.insert(client_names, client.name) + end + end + + if #client_names > 0 then + return Icons.Signs.LSP .. " " .. table.concat(client_names, "/") .. " " + end + + return "" + end) + + if not ok then + vim.schedule(function() + vim.notify_once("Error in LSPActive provider: " .. tostring(result), vim.log.levels.DEBUG) + end) + return "" + end + + return result or "" + end, + hl = { fg = "lightgray", bold = false }, + } + + local Navic = { + condition = function() + local ok, navic = pcall(require, "nvim-navic") + return ok and navic.is_available() + end, + static = { + type_hl = { + File = "Directory", + Module = "@include", + Namespace = "@namespace", + Package = "@include", + Class = "@structure", + Method = "@method", + Property = "@property", + Field = "@field", + Constructor = "@constructor", + Enum = "@field", + Interface = "@type", + Function = "@function", + Variable = "@variable", + Constant = "@constant", + String = "@string", + Number = "@number", + Boolean = "@boolean", + Array = "@field", + Object = "@type", + Key = "@keyword", + Null = "@comment", + EnumMember = "@field", + Struct = "@structure", + Event = "@keyword", + Operator = "@operator", + TypeParameter = "@type", + }, + enc = function(line, col, winnr) + return bit.bor(bit.lshift(line, 16), bit.lshift(col, 6), winnr) + end, + dec = function(c) + local line = bit.rshift(c, 16) + local col = bit.band(bit.rshift(c, 6), 1023) + local winnr = bit.band(c, 63) + return line, col, winnr + end, + }, + init = function(self) + local data = require("nvim-navic").get_data() or {} + local children = {} + for i, d in ipairs(data) do + local pos = self.enc(d.scope.start.line, d.scope.start.character, self.winnr) + local child = { + { + provider = d.icon, + hl = self.type_hl[d.type], + }, + { + provider = d.name:gsub("%%", "%%%%"):gsub("%s*->%s*", ""), + on_click = { + minwid = pos, + callback = function(_, minwid) + local line, col, winnr = self.dec(minwid) + vim.api.nvim_win_set_cursor(vim.fn.win_getid(winnr), { line, col }) + end, + name = "heirline_navic", + }, + }, + } + if #data > 1 and i < #data then + table.insert(child, { + provider = " > ", + hl = { fg = "bright_fg" }, + }) + end + table.insert(children, child) + end + self.child = self:new(children, 1) + end, + provider = function(self) + return self.child:eval() + end, + hl = { fg = "gray" }, + update = "CursorMoved", + } + + -- Diagnostics + local Diagnostics = { + condition = conditions.has_diagnostics, + static = { + error_icon = Icons.Error, + warn_icon = Icons.Warn, + info_icon = Icons.Info, + hint_icon = Icons.Hint, + }, + init = function(self) + self.errors = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.ERROR }) + self.warnings = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.WARN }) + self.hints = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.HINT }) + self.info = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.INFO }) + end, + update = { "DiagnosticChanged", "BufEnter" }, + { + provider = function(self) + return self.errors > 0 and (self.error_icon .. " " .. self.errors .. " ") + end, + hl = { fg = colors.diag.error, bg = colors.bg }, + }, + { + provider = function(self) + return self.warnings > 0 and (self.warn_icon .. " " .. self.warnings .. " ") + end, + hl = { fg = colors.diag.warn, bg = colors.bg }, + }, + { + provider = function(self) + return self.info > 0 and (self.info_icon .. " " .. self.info .. " ") + end, + hl = { fg = colors.diag.info, bg = colors.bg }, + }, + { + provider = function(self) + return self.hints > 0 and (self.hint_icon .. " " .. self.hints .. " ") + end, + hl = { fg = colors.diag.hint, bg = colors.bg }, + }, + on_click = { + callback = function() + local ok, _ = pcall(require, "trouble") + if ok then + require("trouble").toggle({ mode = "document_diagnostics" }) + else + vim.diagnostic.setqflist() + end + end, + name = "heirline_diagnostics", + }, + } + + -- Git + local Git = { + condition = conditions.is_git_repo, + init = function(self) + self.status_dict = vim.b.gitsigns_status_dict or {} + self.has_changes = (self.status_dict.added or 0) ~= 0 or + (self.status_dict.removed or 0) ~= 0 or + (self.status_dict.changed or 0) ~= 0 + end, + { + provider = function() + return " " .. Icons.Git.branch .. " " + end, + hl = { fg = colors.git.active, bg = colors.bg }, + }, + { + provider = function(self) + return self.status_dict.head or "" + end, + hl = { fg = colors.white, bg = colors.bg }, + }, + { + condition = function(self) + return self.has_changes + end, + provider = "", + }, + { + provider = function(self) + local count = self.status_dict.added or 0 + return count > 0 and (" " .. Icons.Git.added .. count) or "" + end, + hl = { fg = colors.git.add, bg = colors.bg }, + }, + { + provider = function(self) + local count = self.status_dict.removed or 0 + return count > 0 and (" " .. Icons.Git.removed .. count) or "" + end, + hl = { fg = colors.git.del, bg = colors.bg }, + }, + { + provider = function(self) + local count = self.status_dict.changed or 0 + return count > 0 and (" " .. Icons.Git.modified .. count) or "" + end, + hl = { fg = colors.git.change, bg = colors.bg }, + }, + on_click = { + callback = function() + vim.defer_fn(function() + vim.cmd("Lazygit") + end, 100) + end, + name = "heirline_git", + }, + } + + -- FileNameBlock: FileIcon, FileName and friends + local FileNameBlock = { + init = function(self) + self.filename = vim.api.nvim_buf_get_name(0) + end, + hl = { bg = colors.bg }, + } + + local FileIcon = { + init = function(self) + local filename = self.filename or vim.api.nvim_buf_get_name(0) + local extension = vim.fn.fnamemodify(filename, ":e") + + local has_nerd_fonts = vim.g.statusline_has_nerd_fonts + if has_nerd_fonts == nil then + if vim.fn.has('unix') == 1 and vim.fn.executable('fc-list') == 1 then + local handle = io.popen('fc-list | grep -i nerd') + local result = handle:read('*a') + handle:close() + has_nerd_fonts = result ~= "" + else + has_nerd_fonts = false + end + vim.g.statusline_has_nerd_fonts = has_nerd_fonts + end + + local icon, icon_color + if has_nerd_fonts then + icon, icon_color = require("nvim-web-devicons").get_icon_color(filename, extension, { default = true }) + end + + if vim.fn.isdirectory(filename) == 1 then + self.icon = has_nerd_fonts and Icons.File.directory or "[DIR]" + self.icon_color = colors.blue + else + if has_nerd_fonts and icon then + self.icon = icon .. " " + self.icon_color = icon_color or colors.blue + else + if extension == "" then + self.icon = Icons.File.default + else + local file_icon = Icons.File[extension:lower()] or Icons.File.default + if type(file_icon) == "table" then + self.icon = file_icon[1] or Icons.File.default + else + self.icon = file_icon + end + end + self.icon_color = colors.blue + end + end + end, + provider = function(self) + return self.icon + end, + hl = function(self) + return { fg = self.icon_color, bold = true } + end, + } + + local FileName = { + provider = function(self) + local filename = vim.fn.fnamemodify(self.filename, ":.") + if filename == "" then + return "No Name" + end + if not conditions.width_percent_below(#filename, 0.25) then + filename = vim.fn.pathshorten(filename) + end + return filename + end, + hl = { fg = colors.white, bold = false, bg = colors.bg }, + } + + local FileFlags = { + { + provider = function() + if vim.bo.modified then + return " +" + end + end, + hl = { fg = colors.green, bg = colors.bg }, + }, + { + provider = function() + if not vim.bo.modifiable or vim.bo.readonly then + return " " .. Icons.UI.lock + end + end, + hl = { fg = colors.orange, bold = true, bg = colors.bg }, + }, + } + + local FileNameModifier = { + hl = function() + if vim.bo.modified then + return { fg = colors.green, bold = false, force = true } + end + end, + } + + -- FileType, FileEncoding and FileFormat + local FileType = { + provider = function() + return vim.bo.filetype + end, + hl = { fg = colors.white, bold = false, bg = colors.bg }, + } + + local FileEncoding = { + Space, + provider = function() + local enc = (vim.bo.fenc ~= "" and vim.bo.fenc) or vim.o.enc + return enc:lower() + end, + hl = { bg = colors.bg, bold = false }, + } + + local FileFormat = { + provider = function() + local fmt = vim.bo.fileformat + return fmt ~= "unix" and fmt:lower() or "" + end, + hl = { fg = utils.get_highlight("Statusline").fg, bold = true, bg = colors.bg }, + } + + local FileSize = { + provider = function() + local suffix = { "b", "k", "M", "G", "T", "P", "E" } + local filename = vim.api.nvim_buf_get_name(0) + local fsize = vim.fn.getfsize(filename) + fsize = (fsize < 0 and 0) or fsize + if fsize < 1024 then + return fsize .. suffix[1] + end + local i = math.floor((math.log(fsize) / math.log(1024))) + return string.format("%.2g%s", fsize / math.pow(1024, i), suffix[i + 1]) + end, + hl = { fg = utils.get_highlight("Statusline").fg, bold = true, bg = colors.bg }, + } + + local FileLastModified = { + provider = function() + local filename = vim.api.nvim_buf_get_name(0) + local ftime = vim.fn.getftime(filename) + return (ftime > 0) and os.date("%c", ftime) or "" + end, + hl = { fg = utils.get_highlight("Statusline").fg, bold = true, bg = colors.bg }, + } + + local Spell = { + condition = function() + return vim.wo.spell + end, + provider = function() + return " " .. Icons.Indicator.spell .. " " + end, + hl = { bold = true, fg = colors.yellow }, + } + + local HelpFileName = { + condition = function() + return vim.bo.filetype == "help" + end, + provider = function() + local filename = vim.api.nvim_buf_get_name(0) + return vim.fn.fnamemodify(filename, ":t") + end, + hl = { fg = colors.blue }, + } + + local SearchCount = { + condition = function() + return vim.v.hlsearch ~= 0 and vim.o.cmdheight == 0 + end, + init = function(self) + local ok, search = pcall(vim.fn.searchcount, { recompute = 1, maxcount = -1 }) + if ok and search.total then + self.search = search + end + end, + provider = function(self) + local search = self.search or { current = 0, total = 0, maxcount = 0 } + return string.format("[%d/%d]", search.current, math.min(search.total, search.maxcount)) + end, + update = { "CursorMoved", "CursorMovedI", "SearchWrapped" }, + } + + local MacroRec = { + condition = function() + return vim.fn.reg_recording() ~= "" and vim.o.cmdheight == 0 + end, + provider = function() + return Icons.Indicator.recording .. " " + end, + hl = { fg = "orange", bold = true }, + utils.surround({ "[", "]" }, nil, { + provider = function() + return vim.fn.reg_recording() + end, + hl = { fg = "green", bold = true }, + }), + update = { + "RecordingEnter", + "RecordingLeave", + callback = vim.schedule_wrap(function() + vim.cmd("redrawstatus") + end), + }, + } + + local ShowCmd = { + condition = function() + return vim.o.cmdheight == 0 + end, + provider = ":%3.5(%S%)", + update = { "CmdlineChanged" }, + } + + local cursor_location = { + { provider = "%1(%4l:%-3(%c%)%) %*", hl = { fg = colors.black, bold = true } }, + } + + local Ruler = { cursor_location } + + local WordCount = { + condition = function() + return conditions.buffer_matches({ + filetype = { + "markdown", + "txt", + "vimwiki", + }, + }) + end, + Space, + { + provider = function() + local ok, wordcount = pcall(vim.fn.wordcount) + return ok and wordcount.words and ("W:%d"):format(wordcount.words) or "" + end, + update = { "CursorMoved", "CursorMovedI", "InsertEnter", "TextChanged", "TextChangedI" }, + }, + } + + local WorkDir = { + init = function(self) + local is_local = vim.fn.haslocaldir(0) == 1 + self.icon = (is_local and "l" or "g") .. " " .. Icons.File.directory + local cwd = vim.fn.getcwd(0) + self.cwd = vim.fn.fnamemodify(cwd, ":~") + end, + hl = { fg = colors.blue, bold = true }, + on_click = { + callback = function() + vim.cmd("Telescope find_files cwd=" .. vim.fn.getcwd(0)) + end, + name = "heirline_workdir", + }, + flexible = 1, + { + provider = function(self) + local trail = self.cwd:sub(-1) == "/" and "" or "/" + return self.icon .. " " .. self.cwd .. trail .. " " + end, + }, + { + provider = function(self) + local cwd = vim.fn.pathshorten(self.cwd) + local trail = self.cwd:sub(-1) == "/" and "" or "/" + return self.icon .. " " .. cwd .. trail .. " " + end, + }, + { + provider = function(self) + return self.icon .. " " + end, + }, + } + + -- Build FileNameBlock + FileNameBlock = utils.insert( + FileNameBlock, + FileIcon, + utils.insert(FileNameModifier, FileName), + unpack(FileFlags), + { provider = "%<" } + ) + + local FileInfoBlock = { + init = function(self) + self.filename = vim.api.nvim_buf_get_name(0) + end, + } + + FileInfoBlock = utils.insert( + FileInfoBlock, + Space, + FileIcon, + FileType, + { provider = "%<" } + ) + + -- Create surrounded components with proper mode color functions + LeftSpace = utils.surround({ "", Icons.UI.right_separator }, function(self) + return self:mode_color() + end, { LeftSpace, hl = { fg = utils.get_highlight("statusline").bg, force = true } }) + + RightSpace = utils.surround({ Icons.UI.left_separator, "" }, function(self) + return self:mode_color() + end, { RightSpace, hl = { fg = utils.get_highlight("statusline").bg, force = true } }) + + LSPActive = utils.surround({ "", "" }, function(self) + return self:mode_color() + end, { Space, LSPActive, hl = { bg = colors.darkgray, force = true } }) + + FileInfoBlock = utils.surround({ "", "" }, function(self) + return self:mode_color() + end, { FileInfoBlock, Space, hl = { bg = colors.black, force = true } }) + + ViMode = utils.surround({ "", "" }, function(self) + return self:mode_color() + end, { ViMode, hl = { fg = colors.black, force = true } }) + + Ruler = utils.surround({ "", "" }, function(self) + return self:mode_color() + end, { Ruler, hl = { fg = colors.black, force = true } }) + + -- Statusline sections - FIXED: Removed duplicate LeftSpace from right section + local left = { + { RightSpace, hl = { bg = colors.nobg, force = true } }, + { ViMode, hl = { bg = utils.get_highlight("statusline").bg, bold = false } }, + { LeftSpace, hl = { bg = colors.nobg, force = true } }, + { Space, hl = { bg = colors.nobg, force = true } }, + { FileNameBlock, hl = { bg = colors.nobg, force = true } }, + { Space, hl = { bg = colors.nobg, force = true } }, + { Git, hl = { bg = colors.nobg, force = true } }, + } + + local middle = { + { Align, hl = { bg = colors.nobg, force = true } }, + { Align, hl = { bg = colors.nobg, force = true } }, + } + + -- FIXED: Right section now has proper sequence without duplicate LeftSpace + local right = { + { Diagnostics, hl = { bg = colors.nobg, force = true } }, + { Space, hl = { bg = colors.nobg, force = true } }, + { LSPActive, hl = { bg = colors.nobg, force = true } }, + { Space, hl = { bg = colors.nobg, force = true } }, + { FileInfoBlock, hl = { bg = colors.nobg, force = true } }, + { RightSpace, hl = { bg = colors.nobg, force = true } }, + { Ruler, hl = { fg = utils.get_highlight("statusline").bg, bold = false } }, + { LeftSpace, hl = { bg = colors.nobg, force = true } }, + } + + local sections = { left, middle, right } + local DefaultStatusline = { sections } + + -- Special statuslines for inactive/special buffers + local specialleft = { + { RightSpace, hl = { bg = colors.nobg, force = true } }, + { ViMode, hl = { bg = utils.get_highlight("statusline").bg, bold = false } }, + { LeftSpace, hl = { bg = colors.nobg, force = true } }, + } + + local specialmiddle = { + { Align, hl = { bg = colors.nobg, force = true } }, + { Align, hl = { bg = colors.nobg, force = true } }, + } + + local specialright = { + { RightSpace, hl = { bg = colors.nobg, force = true } }, + { Ruler, hl = { fg = utils.get_highlight("statusline").bg, bold = false } }, + { LeftSpace, hl = { bg = colors.nobg, force = true } }, + } + + local specialsections = { specialleft, specialmiddle, specialright } + + local InactiveStatusline = { + condition = conditions.is_not_active, + specialsections, + } + + local SpecialStatusline = { + condition = function() + return conditions.buffer_matches({ + buftype = { "nofile", "prompt", "help", "quickfix" }, + filetype = { "^git.*", "fugitive", "dashboard" }, + }) + end, + specialsections, + } + + local TerminalStatusline = { + condition = function() + return conditions.buffer_matches({ buftype = { "terminal" } }) + end, + specialsections, + } + + -- FIXED: Main StatusLine with better mode handling + local StatusLine = { + static = { + mode_colors = { + n = colors.blue, + no = colors.blue, + nov = colors.blue, + noV = colors.blue, + ["no\22"] = colors.blue, + niI = colors.blue, + niR = colors.blue, + niV = colors.blue, + nt = colors.blue, + v = colors.purple, + vs = colors.purple, + V = colors.purple, + ["\22"] = colors.purple, + ["\22s"] = colors.purple, + s = colors.purple, + S = colors.purple, + ["\19"] = colors.purple, + i = colors.green, + ix = colors.green, + ic = colors.green, + R = colors.red, + Rc = colors.red, + Rx = colors.red, + Rv = colors.red, + Rvc = colors.red, + Rvx = colors.red, + c = colors.orange, + cv = colors.orange, + ce = colors.orange, + r = colors.red, + rm = colors.red, + ["r?"] = colors.red, + ["!"] = colors.orange, + t = colors.orange, + }, + mode_color = function(self) + -- FIXED: Always get current mode to ensure updates + local mode = vim.fn.mode() + return self.mode_colors[mode] or colors.blue + end, + }, + -- FIXED: Add update triggers to ensure statusline refreshes properly + update = { + "ModeChanged", + "BufEnter", + "WinEnter", + "WinLeave", + "BufWinEnter", + "CmdlineLeave", + callback = vim.schedule_wrap(function() + vim.cmd("redrawstatus") + end), + }, + fallthrough = false, + SpecialStatusline, + TerminalStatusline, + InactiveStatusline, + DefaultStatusline, + } + + -- WinBar components + local WinbarFileNameBlock = { + init = function(self) + self.filename = vim.api.nvim_buf_get_name(0) + end, + hl = { bg = colors.bg }, + } + + local WinbarFileName = { + provider = function(self) + local filename = vim.fn.fnamemodify(self.filename, ":.") + if filename == "" then + return "No Name" + end + if not conditions.width_percent_below(#filename, 0.25) then + filename = vim.fn.pathshorten(filename) + end + return filename + end, + hl = { fg = colors.gray, bold = false, bg = colors.bg }, + } + + WinbarFileNameBlock = utils.insert( + WinbarFileNameBlock, + FileIcon, + utils.insert(WinbarFileName), + unpack(FileFlags), + { provider = "%<" } + ) + + vim.api.nvim_create_autocmd("User", { + pattern = "HeirlineInitWinbar", + callback = function(args) + local buf = args.buf + local buftype = vim.tbl_contains({ "prompt", "nofile", "help", "quickfix" }, vim.bo[buf].buftype) + local filetype = vim.tbl_contains({ "gitcommit", "fugitive" }, vim.bo[buf].filetype) + if buftype or filetype then + vim.opt_local.winbar = nil + end + end, + }) + + local On_click = { + minwid = function() + return vim.api.nvim_get_current_win() + end, + callback = function(_, minwid) + local winid = minwid + local buf = vim.api.nvim_win_get_buf(winid) + end, + } + + local CloseButton = { + condition = function(self) + return not vim.bo.modified + end, + update = { "WinNew", "WinClosed", "BufEnter" }, + { provider = " " }, + { + provider = Icons.UI.close, + hl = { fg = "gray" }, + On_click = { + minwid = function() + return vim.api.nvim_get_current_win() + end, + callback = function(_, minwid) + vim.api.nvim_win_close(minwid, true) + end, + name = "heirline_winbar_close_button", + }, + }, + } + + local Center = { + fallthrough = false, + { + condition = function() + return conditions.buffer_matches({ + buftype = { "terminal", "nofile", "prompt", "help", "quickfix" }, + filetype = { "dap-ui", "NvimTree", "^git.*", "fugitive", "dashboard" }, + }) + end, + init = function() + vim.opt_local.winbar = nil + end, + }, + { + condition = function() + return conditions.buffer_matches({ buftype = { "terminal" } }) + end, + FileType, + Space, + }, + { + condition = function() + return not conditions.is_active() + end, + utils.surround({ "", "" }, colors.nobg, { WinbarFileNameBlock }), + }, + utils.surround({ "", "" }, colors.nobg, { FileNameBlock }), + } + + local WinBar = { Space, Center } + + -- Tabline components + local TablineFileIcon = { + init = function(self) + local filename = self.filename + local extension = vim.fn.fnamemodify(filename, ":e") + + local has_nerd_fonts = vim.g.statusline_has_nerd_fonts + if has_nerd_fonts == nil then + if vim.fn.has('unix') == 1 and vim.fn.executable('fc-list') == 1 then + local handle = io.popen('fc-list | grep -i nerd') + local result = handle:read('*a') + handle:close() + has_nerd_fonts = result ~= "" + else + has_nerd_fonts = false + end + vim.g.statusline_has_nerd_fonts = has_nerd_fonts + end + + if has_nerd_fonts then + self.icon, self.icon_color = require("nvim-web-devicons").get_icon_color(filename, extension, { default = true }) + else + self.icon = "" + self.icon_color = colors.blue + + if vim.fn.isdirectory(filename) == 1 then + self.icon = "[DIR]" + else + local file_icon = Icons.File[extension:lower()] or Icons.File.default + if type(file_icon) == "table" then + self.icon = file_icon[1] or Icons.File.default + else + self.icon = file_icon + end + end + end + + if self.icon ~= "" then + self.icon = self.icon .. " " + end + end, + provider = function(self) + return self.icon or "" + end, + hl = function(self) + return { fg = self.icon_color or colors.blue } + end, + } + + local TablineFileName = { + provider = function(self) + local filename = vim.fn.fnamemodify(self.filename, ":t") + if filename == "" then + return "[No Name]" + end + return filename + end, + } + + local TablineFileFlags = { + { + condition = function(self) + return vim.api.nvim_buf_get_option(self.bufnr, "modified") + end, + provider = "%X " .. Icons.Indicator.plus .. " %X", + hl = { fg = "green" }, + }, + { + condition = function(self) + return not vim.api.nvim_buf_get_option(self.bufnr, "modifiable") or vim.api.nvim_buf_get_option(self.bufnr, "readonly") + end, + provider = function() + if vim.bo.readonly then + return " " .. Icons.UI.lock + end + return "" + end, + hl = { fg = "orange" }, + }, + } + + local TablineFileNameBlock = { + init = function(self) + self.filename = vim.api.nvim_buf_get_name(self.bufnr) + end, + hl = function(self) + if self.is_active then + return "TabLineSel" + else + return "TabLineFill" + end + end, + on_click = { + callback = function(_, minwid, _, button) + if button == "m" then + vim.api.nvim_buf_delete(minwid, { force = false }) + else + vim.api.nvim_win_set_buf(0, minwid) + end + end, + minwid = function(self) + return self.bufnr + end, + name = "heirline_tabline_buffer_callback", + }, + TablineFileIcon, + TablineFileName, + TablineFileFlags, + } + + local TablineCloseButton = { + condition = function(self) + return not vim.api.nvim_buf_get_option(self.bufnr, "modified") + end, + { provider = " " }, + { + provider = "" .. Icons.UI.close .. " %X", + hl = { fg = colors.red }, + on_click = { + callback = function(_, minwid) + vim.api.nvim_buf_delete(minwid, { force = false }) + end, + minwid = function(self) + return self.bufnr + end, + name = "heirline_tabline_close_buffer_callback", + }, + }, + } + + local TablineBufferBlock = utils.surround({ "", "" }, function(self) + if self.is_active then + return utils.get_highlight("TabLineSel").bg + else + return utils.get_highlight("TabLineFill").bg + end + end, { Tab, TablineFileNameBlock, TablineCloseButton }) + + local BufferLine = utils.make_buflist( + TablineBufferBlock, + { provider = Icons.Symbol.arrow_left, hl = { fg = colors.gray } }, + { provider = Icons.Symbol.arrow_right, hl = { fg = colors.gray } } + ) + + local Tabpage = { + provider = function(self) + return "%" .. self.tabnr .. "T " .. self.tabnr .. " %T" + end, + hl = function(self) + return self.is_active and "TabLineSel" or "TabLineFill" + end, + } + + local TabpageClose = { + provider = "%999X " .. Icons.UI.close .. " %X", + hl = { fg = colors.red, bg = colors.bg }, + } + + local TabPages = { + condition = function() + return #vim.api.nvim_list_tabpages() >= 2 + end, + { provider = "%=" }, + utils.make_tablist(Tabpage), + TabpageClose, + } + + local TabLineOffset = { + condition = function(self) + local win = vim.api.nvim_tabpage_list_wins(0)[1] + local bufnr = vim.api.nvim_win_get_buf(win) + self.winid = win + + if vim.api.nvim_buf_get_option(bufnr, "filetype") == "NvimTree" then + self.title = "NvimTree" + return true + end + end, + provider = function(self) + local title = self.title + local width = vim.api.nvim_win_get_width(self.winid) + local pad = math.ceil((width - #title) / 2) + return string.rep(" ", pad) .. title .. string.rep(" ", pad) + end, + hl = { fg = colors.white, bg = "#333842", bold = true }, + } + + local TabLine = { + TabLineOffset, + BufferLine, + TabPages, + } + + -- Buffer navigation functions + local function get_bufs() + return vim.tbl_filter(function(bufnr) + return vim.api.nvim_buf_is_loaded(bufnr) and vim.bo[bufnr].buflisted + end, vim.api.nvim_list_bufs()) + end + + local function goto_buf(index) + local bufs = get_bufs() + if index > #bufs then + index = #bufs + end + vim.api.nvim_win_set_buf(0, bufs[index]) + end + + local function add_key(key, index) + vim.keymap.set("n", "<A-" .. key .. ">", function() + goto_buf(index) + end, { noremap = true, silent = true }) + end + + for i = 1, 9 do + add_key(i, i) + end + add_key("0", 10) + + vim.o.showtabline = 2 + vim.cmd([[au FileType * if index(['wipe', 'delete', 'unload'], &bufhidden) >= 0 | set nobuflisted | endif]]) + + -- FIXED: Add proper autocmds for better statusline updates + local augroup = vim.api.nvim_create_augroup("HeirlineStatusline", { clear = true }) + + -- Force statusline refresh on mode changes and buffer events + vim.api.nvim_create_autocmd({ + "ModeChanged", + "BufEnter", + "BufWinEnter", + "WinEnter", + "WinLeave", + "CmdlineLeave", + "TermEnter", + "TermLeave" + }, { + group = augroup, + callback = function() + vim.schedule(function() + if vim.o.laststatus > 0 then + vim.cmd("redrawstatus!") + end + end) + end, + }) + + -- Final heirline setup + heirline.setup({ + statusline = StatusLine, + winbar = WinBar, + tabline = TabLine, + opts = { + disable_winbar_cb = function(args) + local buf = args.buf + if not vim.api.nvim_buf_is_valid(buf) then + return true + end + + local buftype = vim.tbl_contains( + { "prompt", "nofile", "help", "quickfix" }, + vim.bo[buf].buftype + ) + local filetype = vim.tbl_contains( + { "gitcommit", "fugitive" }, + vim.bo[buf].filetype + ) + return buftype or filetype + end, + } + }) + +end + +return M diff --git a/common/config/nvim/lua/plugins/indent-blankline.lua b/common/config/nvim/lua/plugins/indent-blankline.lua new file mode 100755 index 0000000..cbbcf27 --- /dev/null +++ b/common/config/nvim/lua/plugins/indent-blankline.lua @@ -0,0 +1,73 @@ +local M = {} + +--- Setup and configure indent-blankline.nvim +-- This function initializes and configures the indent guides +-- @return boolean True if setup was successful, false otherwise +function M.setup() + local ok, ibl = pcall(require, 'ibl') + if not ok then + return false + end + + local highlight = { + "RainbowRed", + "RainbowYellow", + "RainbowBlue", + "RainbowOrange", + "RainbowGreen", + "RainbowViolet", + "RainbowCyan", + } + + local hooks = require("ibl.hooks") + -- create the highlight groups in the highlight setup hook, so they are reset + -- every time the colorscheme changes + hooks.register(hooks.type.HIGHLIGHT_SETUP, function() + vim.api.nvim_set_hl(0, "RainbowRed", { fg = "#E06C75" }) + vim.api.nvim_set_hl(0, "RainbowYellow", { fg = "#E5C07B" }) + vim.api.nvim_set_hl(0, "RainbowBlue", { fg = "#61AFEF" }) + vim.api.nvim_set_hl(0, "RainbowOrange", { fg = "#D19A66" }) + vim.api.nvim_set_hl(0, "RainbowGreen", { fg = "#98C379" }) + vim.api.nvim_set_hl(0, "RainbowViolet", { fg = "#C678DD" }) + vim.api.nvim_set_hl(0, "RainbowCyan", { fg = "#56B6C2" }) + end) + + ibl.setup({ + indent = { highlight = highlight }, + exclude = { + filetypes = { + "", -- for all buffers without a file type + "NvimTree", + "Trouble", + "TelescopePrompt", + "TelescopeResults", + "mason", + "help", + "dashboard", + "packer", + "neogitstatus", + "Trouble", + "text", + "terminal", + "lazy", + }, + buftypes = { + "terminal", + "nofile", + "quickfix", + "prompt", + }, + }, + }) + + -- Toggle indent blankline with <leader>ti + vim.keymap.set('n', '<leader>ti', '<cmd>IBLToggle<CR>', { + noremap = true, + silent = true, + desc = 'Toggle indent guides' + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/interestingwords.lua b/common/config/nvim/lua/plugins/interestingwords.lua new file mode 100755 index 0000000..655ed42 --- /dev/null +++ b/common/config/nvim/lua/plugins/interestingwords.lua @@ -0,0 +1,499 @@ +local interestingwords = (function() + local api = vim.api + local fn = vim.fn + local uv = vim.loop + + local m = {} + + m.words = {} + m.colors = {} + m.limits = {} + m.capcity = 0 + m.next = 1 + + local get_default_config = function() + return { + colors = { "#aeee00", "#ff0000", "#0000ff", "#b88823", "#ffa724", "#ff2c4b" }, + search_count = true, + navigation = true, + scroll_center = true, + search_key = "<leader>hl", + cancel_search_key = "<leader>lh", + color_key = "<leader>ih", + cancel_color_key = "<leader>hi", + select_mode = "random", -- random or loop + } + end + + local init_colors = function() + for i, v in pairs(m.config.colors) do + local color = "InterestingWord" .. i + + api.nvim_set_hl(0, color, { bg = v, fg = "Black" }) + m.colors[color] = 595129 + i + m.capcity = m.capcity + 1 + end + m.limits.min = 595129 + 1 + m.limits.max = 595129 + #m.config.colors + end + + local get_reg_ex = function(word) + if vim.o.ignorecase and (not vim.o.smartcase or fn.match(word, "\\u") == -1) then + return "\\c\\V" .. word + else + return "\\C\\V" .. word + end + end + + local get_visual_selection = function() + local lines + local start_row, start_col = fn.getpos("v")[2], fn.getpos("v")[3] + local end_row, end_col = fn.getpos(".")[2], fn.getpos(".")[3] + if end_row < start_row then + start_row, end_row = end_row, start_row + start_col, end_col = end_col, start_col + elseif end_row == start_row and end_col < start_col then + start_col, end_col = end_col, start_col + end + start_row = start_row - 1 + start_col = start_col - 1 + end_row = end_row - 1 + if api.nvim_get_mode().mode == "V" then + lines = api.nvim_buf_get_text(0, start_row, 0, end_row, -1, {}) + elseif api.nvim_get_mode().mode == "v" then + lines = api.nvim_buf_get_text(0, start_row, start_col, end_row, end_col, {}) + end + vim.cmd("normal! ") + if lines == nil then + return "" + end + + local line = "" + for i, v in ipairs(lines) do + if i == 1 then + line = line .. fn.escape(v, "\\") + else + line = line .. "\\n" .. fn.escape(v, "\\") + end + end + + return line + end + + local uncolor = function(word) + if m.words[word] then + local windows = api.nvim_list_wins() + for _, i in ipairs(windows) do + pcall(function() + fn.matchdelete(m.words[word].mid, i) + end) + end + m.colors[m.words[word].color] = m.words[word].mid + m.words[word] = nil + end + end + + local get_rest_color_random = function() + local res = {} + for k, v in pairs(m.colors) do + if v ~= 0 then + table.insert(res, { color = k, mid = v }) + end + end + if #res == 0 then + return nil + end + + return res[math.random(#res)] + end + + local find_who_use_this = function(target_color) + for word, color in pairs(m.words) do + if color.color == target_color then + return word + end + end + return nil + end + + local get_rest_color_loop = function() + if m.next > m.capcity then + m.next = 1 + end + local color = "InterestingWord" .. m.next + if m.colors[color] == 0 then + local word = find_who_use_this(color) + if word ~= nil then + uncolor(word) + else + return nil + end + end + m.next = m.next + 1 + return { color = color, mid = m.colors[color] } + end + + local get_rest_color = function() + local selector = { + ["random"] = get_rest_color_random, + ["loop"] = get_rest_color_loop, + } + return selector[m.config.select_mode]() + end + + local color = function(word) + local color = get_rest_color() + if not color then + vim.notify("InterestingWords: max number of highlight groups reached") + return + end + + m.words[word] = {} + m.words[word].color = color.color + m.words[word].mid = color.mid + m.colors[color.color] = 0 + + local windows = api.nvim_list_wins() + for _, i in ipairs(windows) do + pcall(function() + fn.matchadd(m.words[word].color, word, 1, m.words[word].mid, { window = i }) + end) + end + end + + local recolorAllWords = function() + for k, v in pairs(m.words) do + pcall(function() + fn.matchadd(v.color, k, 1, v.mid, { window = 0 }) + end) + end + end + + local nearest_word_at_cursor = function() + for _, match_item in pairs(fn.getmatches()) do + if match_item.id >= m.limits.min or match_item.id <= m.limits.max then + local buf_content = fn.join(api.nvim_buf_get_lines(0, 0, -1, {}), "\n") + local cur_pos = #fn.join(api.nvim_buf_get_lines(0, 0, fn.line(".") - 1, {}), "\n") + + ((fn.line(".") == 1) and 0 or 1) + fn.col(".") - 1 + local lst_pos = 0 + while true do + local mat_pos = fn.matchstrpos(buf_content, match_item.pattern, lst_pos, 1) + if mat_pos[1] == "" then + break + end + if cur_pos >= mat_pos[2] and cur_pos < mat_pos[3] then + return match_item.pattern + end + lst_pos = mat_pos[3] + end + end + end + end + + local filter = function(word) + if #word <= 4 or (string.sub(word, 1, 4) ~= "\\c\\V" and string.sub(word, 1, 4) ~= "\\C\\V") then + return word + else + return string.sub(word, 5, -1) + end + end + + local display_search_count = function(word, count) + local icon = "" + m.search_count_extmark_id = api.nvim_buf_set_extmark(0, m.search_count_namespace, fn.line(".") - 1, 0, { + virt_text_pos = "eol", + virt_text = { + { icon .. count, "NonText" }, + }, + hl_mode = "combine", + }) + m.search_count_cache = icon .. " " .. filter(word) .. count + m.search_count_timer:again() + end + + local hide_search_count = function(bufnr) + if m.search_count_namespace then + api.nvim_buf_del_extmark(bufnr, m.search_count_namespace, m.search_count_extmark_id) + end + end + + local scroll_timer = vim.loop.new_timer() + local function scroll_up(cnt) + return vim.cmd("normal! " .. cnt .. "") + end + + local function scroll_down(cnt) + return vim.cmd("normal! " .. cnt .. "") + end + + local function stop_scrolling() + scroll_timer:stop() + end + + local scroll_to_center = function() + local window_height = api.nvim_win_get_height(0) + local lines = fn.winline() - math.floor(window_height / 2) + if lines == 0 then + return + end + local up = lines > 0 + lines = math.abs(lines) + + local move_lines = function(n) + return math.floor(n / 5) + 1 + end + + local each_time = function() + local lines_bak = lines + local circles = 0 + while lines_bak ~= 0 do + lines_bak = lines_bak - move_lines(lines_bak) + circles = circles + 1 + end + local pseudo_total_time = 300 + 15 * math.min((lines - 11), 10) + lines + return math.floor(pseudo_total_time / circles) + end + local t = each_time() + local time_total = 0 + + local scroll_callback = function() + local cnt = move_lines(lines) + if lines == 0 then + stop_scrolling() + return + else + lines = lines - cnt + end + + if up then + scroll_up(cnt) + else + scroll_down(cnt) + end + time_total = time_total + t + end + + scroll_timer:start(t, t, vim.schedule_wrap(scroll_callback)) + end + + m.lualine_get = function() + return m.search_count_cache + end + + m.lualine_has = function() + return m.search_count_cache ~= "" + end + + m.init_search_count = function() + m.search_count_extmark_id = 0 + m.search_count_namespace = api.nvim_create_namespace("custom/search_count") + m.search_count_timer = vim.loop.new_timer() + m.search_count_timer:start(0, 5000, function() + m.search_count_cache = "" + vim.defer_fn(function() + hide_search_count(0) + end, 100) + m.search_count_timer:stop() + end) + + vim.api.nvim_create_autocmd({ "CmdlineLeave" }, { + pattern = { "*" }, + callback = function(event) + if vim.v.event.abort then + return + end + if event.match == "/" or event.match == "?" then + vim.defer_fn(function() + local searched = m.search_count(fn.getreg("/")) + if searched and m.config.scroll_center then + scroll_to_center() + end + end, 100) + end + end, + }) + end + + m.search_count = function(word) + hide_search_count(0) + if word == "" then + return false + end + + local cur_cnt = 0 + local total_cnt = 0 + local buf_content = fn.join(api.nvim_buf_get_lines(0, 0, -1, {}), "\n") + local cur_pos = #fn.join(api.nvim_buf_get_lines(0, 0, fn.line(".") - 1, {}), "\n") + ((fn.line(".") == 1) and 0 or 1) + + fn.col(".") - 1 + local lst_pos = 0 + while true do + local mat_pos = fn.matchstrpos(buf_content, word, lst_pos, 1) + if mat_pos[1] == "" then + break + end + total_cnt = total_cnt + 1 + if cur_pos >= mat_pos[2] and cur_pos < mat_pos[3] then + cur_cnt = total_cnt + end + lst_pos = mat_pos[3] + end + + if total_cnt == 0 or cur_cnt == 0 then + return false + end + + local count = " [" .. cur_cnt .. "/" .. total_cnt .. "]" + display_search_count(word, count) + + return true + end + + m.NavigateToWord = function(forward) + local word = nearest_word_at_cursor() + if not word then + word = fn.getreg("/") + end + if word == "" then + return + end + + local search_flag = "" + if not forward then + search_flag = "b" + end + local n = fn.search(word, search_flag) + if n ~= 0 then + if m.config.scroll_center then + scroll_to_center() + end + else + vim.notify("Pattern not found: " .. filter(word)) + return + end + + if m.config.search_count then + m.search_count(word) + end + end + + m.InterestingWord = function(mode, search) + local word = "" + if mode == "v" then + word = get_visual_selection() + else + word = "\\<" .. fn.expand("<cword>") .. "\\>" + end + if #word == 0 then + return + end + word = get_reg_ex(word) + + if search then + if word == fn.getreg("/") then + fn.setreg("/", "") + word = "" + else + fn.setreg("/", word) + vim.cmd("set hls") + end + else + if m.words[word] then + uncolor(word) + word = "" + else + color(word) + end + end + + if m.config.search_count then + m.search_count(word) + end + end + + m.UncolorAllWords = function(search) + m.search_count("") + if search then + fn.setreg("/", "") + else + local windows = api.nvim_list_wins() + for _, v in pairs(m.words) do + for _, i in ipairs(windows) do + pcall(function() + fn.matchdelete(v.mid, i) + end) + end + m.colors[v.color] = v.mid + end + + m.words = {} + end + end + + m.setup = function(opt) + opt = opt or {} + m.config = vim.tbl_deep_extend("force", get_default_config(), opt) + + init_colors() + math.randomseed(uv.now()) + + local group = api.nvim_create_augroup("InterestingWordsGroup", { clear = true }) + api.nvim_create_autocmd({ "WinEnter" }, { + callback = function() + recolorAllWords() + local windows = api.nvim_list_wins() + for _, i in ipairs(windows) do + hide_search_count(api.nvim_win_get_buf(fn.win_getid(i))) + end + end, + group = group, + }) + + if m.config.navigation then + vim.keymap.set("n", "n", function() + m.NavigateToWord(true) + end, { noremap = true, silent = true, desc = "InterestingWord Navigation Forward" }) + vim.keymap.set("n", "N", m.NavigateToWord, + { noremap = true, silent = true, desc = "InterestingWord Navigation Backword" }) + end + + if m.config.search_key then + vim.keymap.set("n", m.config.search_key, function() + m.InterestingWord("n", true) + end, { noremap = true, silent = true, desc = "InterestingWord Toggle Search" }) + vim.keymap.set("x", m.config.search_key, function() + m.InterestingWord("v", true) + end, { noremap = true, silent = true, desc = "InterestingWord Toggle Search" }) + vim.keymap.set("n", m.config.cancel_search_key, function() + m.UncolorAllWords(true) + end, { noremap = true, silent = true, desc = "InterestingWord Unsearch" }) + end + + if m.config.color_key then + vim.keymap.set("n", m.config.color_key, function() + m.InterestingWord("n", false) + end, { noremap = true, silent = true, desc = "InterestingWord Toggle Color" }) + vim.keymap.set("x", m.config.color_key, function() + m.InterestingWord("v", false) + end, { noremap = true, silent = true, desc = "InterestingWord Toggle Color" }) + vim.keymap.set("n", m.config.cancel_color_key, function() + m.UncolorAllWords() + end, { noremap = true, silent = true, desc = "InterestingWord Uncolor" }) + end + + if m.config.search_count then + m.init_search_count() + end + end + + return m +end)() + +interestingwords.setup({ + select_mode = "loop", -- or "random" + scroll_center = false, + search_key = "<leader>S", + cancel_search_key = "<leader>C", + color_key = "<leader>H", + cancel_color_key = "<leader>C", + colors = { "#ff5f5f", "#5fafff", "#afff5f", "#ffd75f" }, +}) diff --git a/common/config/nvim/lua/plugins/leetcode.lua b/common/config/nvim/lua/plugins/leetcode.lua new file mode 100755 index 0000000..50369e1 --- /dev/null +++ b/common/config/nvim/lua/plugins/leetcode.lua @@ -0,0 +1,68 @@ +---@alias lc.lang +---| "cpp" +---| "java" +---| "python" +---| "python3" +---| "c" +---| "csharp" +---| "javascript" +---| "typescript" +---| "php" +---| "swift" +---| "kotlin" +---| "dart" +---| "golang" +---| "ruby" +---| "scala" +---| "rust" +---| "racket" +---| "erlang" +---| "elixir" + +---@alias lc.sql_lang +---| "pythondata" +---| "mysql" +---| "mssql" +---| "oraclesql" + +---@alias lc.domain +---| "com" +---| "cn" + +---@class lc.UserConfig +local M = { + ---@type lc.domain + domain = 'com', -- For now "com" is the only one supported + + ---@type string + arg = 'leetcode.nvim', + + ---@type lc.lang + lang = 'cpp', + + ---@type lc.sql_lang + sql = 'mysql', + + ---@type string + directory = vim.fn.stdpath('data') .. '/leetcode/', + + ---@type boolean + logging = true, + + console = { + ---@type boolean + open_on_runcode = false, + + size = { + width = '75%', ---@type string | integer + height = '75%', ---@type string | integer + }, + dir = 'row', ---@type "col" | "row" + }, + + description = { + width = '40%', ---@type string | integer + }, +} + +return M diff --git a/common/config/nvim/lua/plugins/loclist.lua b/common/config/nvim/lua/plugins/loclist.lua new file mode 100755 index 0000000..9b72a94 --- /dev/null +++ b/common/config/nvim/lua/plugins/loclist.lua @@ -0,0 +1,18 @@ +local M = {} + +function M.loclist_toggle() + for _, info in ipairs(vim.fn.getwininfo()) do + if info.loclist == 1 then + vim.cmd('lclose') + return + end + end + + if next(vim.fn.getloclist(0)) == nil then + print('loc list empty') + return + end + vim.cmd('lopen') +end + +return M diff --git a/common/config/nvim/lua/plugins/lsp.lua b/common/config/nvim/lua/plugins/lsp.lua new file mode 100755 index 0000000..5ed1152 --- /dev/null +++ b/common/config/nvim/lua/plugins/lsp.lua @@ -0,0 +1,674 @@ +local M = {} + +-- Safe require helper +local function safe_require(name) + local ok, mod = pcall(require, name) + return ok and mod or nil +end + +-- Autocmd groups for managing event listeners +local augroup_format = vim.api.nvim_create_augroup("LspFormattingOnSave", { clear = true }) +local augroup_diag_float = vim.api.nvim_create_augroup("ShowLineDiagnostics", { clear = true }) +local augroup_diag_load = vim.api.nvim_create_augroup("OpenDiagnosticsOnLoad", { clear = true }) +local augroup_highlight = vim.api.nvim_create_augroup("LspDocumentHighlight", { clear = true }) + +-- Border for floating windows +local border = { + { "┌", "FloatBorder" }, { "─", "FloatBorder" }, { "┐", "FloatBorder" }, + { "│", "FloatBorder" }, { "┘", "FloatBorder" }, { "─", "FloatBorder" }, + { "└", "FloatBorder" }, { "│", "FloatBorder" } +} + +-- Initialize LSP modules +local function init_modules() + -- Silently try to load each module + M.lspconfig = safe_require("lspconfig") + M.mason = safe_require("mason") + M.mason_lspconfig = safe_require("mason-lspconfig") + M.mason_tool_installer = safe_require("mason-tool-installer") + M.null_ls = safe_require("null-ls") + + if M.null_ls then + M.builtins = M.null_ls.builtins + end + + return true +end + +-- Check Neovim version compatibility and feature availability +local function has_feature(feature) + if feature == "diagnostic_api" then + return vim.fn.has("nvim-0.6") == 1 + elseif feature == "native_lsp_config" then + -- Check for both vim.lsp.enable AND vim.lsp.config + return vim.fn.has("nvim-0.11") == 1 and vim.lsp.enable ~= nil + elseif feature == "lsp_get_client_by_id" then + return vim.fn.has("nvim-0.10") == 1 + elseif feature == "cmp_nvim_lsp" then + return pcall(require, "cmp_nvim_lsp") + elseif feature == "virtual_text_disabled_by_default" then + return vim.fn.has("nvim-0.11") == 1 + elseif feature == "deprecated_lsp_handlers" then + -- vim.lsp.handlers.hover and signature_help deprecated in 0.12, removed in 0.13 + return vim.fn.has("nvim-0.12") == 0 + elseif feature == "new_lsp_config_api" then + -- New LSP config API available from 0.12+ + return vim.fn.has("nvim-0.12") == 1 and vim.lsp.config ~= nil + end + return false +end + +-- Backwards compatible capabilities setup +local function setup_capabilities() + local capabilities + + if has_feature("cmp_nvim_lsp") then + capabilities = require('cmp_nvim_lsp').default_capabilities() + elseif vim.lsp.protocol and vim.lsp.protocol.make_client_capabilities then + capabilities = vim.lsp.protocol.make_client_capabilities() + else + capabilities = {} + end + + -- Add snippet support if available + if capabilities.textDocument then + capabilities.textDocument.completion = capabilities.textDocument.completion or {} + capabilities.textDocument.completion.completionItem = + capabilities.textDocument.completion.completionItem or {} + capabilities.textDocument.completion.completionItem.snippetSupport = true + end + + -- Set offset encoding for newer versions (0.11+ supports utf-8 and utf-32) + if vim.fn.has("nvim-0.11") == 1 then + capabilities.offsetEncoding = { "utf-8", "utf-32", "utf-16" } + elseif vim.fn.has("nvim-0.9") == 1 then + capabilities.offsetEncoding = { "utf-8", "utf-16" } + end + + return capabilities +end + +-- Default LSP keymaps (fallback if external not available) +local function setup_fallback_keymaps(bufnr) + -- Only set up minimal fallbacks, prefer external setup + local opts = { buffer = bufnr, silent = true, noremap = true } + vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts) + vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts) + vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts) + vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts) +end + +-- Create LSP directory and config files for native LSP +local function setup_native_lsp_configs() + local config_path = vim.fn.stdpath("config") + local lsp_dir = config_path .. "/lsp" + + -- Create lsp directory if it doesn't exist + vim.fn.mkdir(lsp_dir, "p") + + -- LSP server configurations for native config + local server_configs = { + lua_ls = { + cmd = { "lua-language-server" }, + filetypes = { "lua" }, + root_markers = { ".luarc.json", ".luarc.jsonc", ".luacheckrc", ".stylua.toml", "stylua.toml", "selene.toml", "selene.yml" }, + settings = { + Lua = { + diagnostics = { + globals = { "vim", "use", "_G", "packer_plugins", "P" }, + disable = { + "undefined-global", + "lowercase-global", + "unused-local", + "unused-vararg", + "trailing-space" + }, + }, + workspace = { + library = { + vim.env.VIMRUNTIME, + "${3rd}/luv/library", + "${3rd}/busted/library", + }, + checkThirdParty = false, + }, + telemetry = { + enable = false, + }, + }, + }, + }, + + pyright = { + cmd = { "pyright-langserver", "--stdio" }, + filetypes = { "python" }, + root_markers = { "pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json" }, + settings = { + python = { + formatting = { + provider = "none" + } + } + } + }, + + ts_ls = { + cmd = { "typescript-language-server", "--stdio" }, + filetypes = { "javascript", "javascriptreact", "javascript.jsx", "typescript", "typescriptreact", "typescript.tsx" }, + root_markers = { "tsconfig.json", "jsconfig.json", "package.json" }, + init_options = { + disableAutomaticTypeAcquisition = true + }, + }, + + rust_analyzer = { + cmd = { "rust-analyzer" }, + filetypes = { "rust" }, + root_markers = { "Cargo.toml", "rust-project.json" }, + }, + + clangd = { + cmd = { "clangd", "--background-index", "--clang-tidy", "--header-insertion=iwyu" }, + filetypes = { "c", "cpp", "objc", "objcpp", "cuda", "proto" }, + root_markers = { ".clangd", ".clang-tidy", ".clang-format", "compile_commands.json", "compile_flags.txt", "configure.ac" }, + }, + + gopls = { + cmd = { "gopls" }, + filetypes = { "go", "gomod", "gowork", "gotmpl" }, + root_markers = { "go.work", "go.mod" }, + settings = { + gopls = { + gofumpt = true, + codelenses = { + gc_details = false, + generate = true, + regenerate_cgo = true, + run_govulncheck = true, + test = true, + tidy = true, + upgrade_dependency = true, + vendor = true, + }, + hints = { + assignVariableTypes = true, + compositeLiteralFields = true, + compositeLiteralTypes = true, + constantValues = true, + functionTypeParameters = true, + parameterNames = true, + rangeVariableTypes = true, + }, + analyses = { + fieldalignment = true, + nilness = true, + unusedparams = true, + unusedwrite = true, + useany = true, + }, + usePlaceholders = true, + completeUnimported = true, + staticcheck = true, + directoryFilters = { "-.git", "-.vscode", "-.idea", "-.vscode-test", "-node_modules" }, + semanticTokens = true, + }, + }, + }, + + -- Add more basic configs + bashls = { + cmd = { "bash-language-server", "start" }, + filetypes = { "sh", "bash" }, + }, + + --html = { + -- cmd = { "vscode-html-language-server", "--stdio" }, + -- filetypes = { "html" }, + --}, + + --cssls = { + -- cmd = { "vscode-css-language-server", "--stdio" }, + -- filetypes = { "css", "scss", "less" }, + --}, + + --jsonls = { + -- cmd = { "vscode-json-language-server", "--stdio" }, + -- filetypes = { "json", "jsonc" }, + --}, + + yamlls = { + cmd = { "yaml-language-server", "--stdio" }, + filetypes = { "yaml", "yml" }, + }, + } + + -- Write config files to lsp directory + for server_name, config in pairs(server_configs) do + local file_path = lsp_dir .. "/" .. server_name .. ".lua" + local file_content = "return " .. vim.inspect(config) + + -- Only write if file doesn't exist to avoid overwriting user customizations + if vim.fn.filereadable(file_path) == 0 then + local file = io.open(file_path, "w") + if file then + file:write(file_content) + file:close() + vim.notify("Created LSP config: " .. file_path, vim.log.levels.DEBUG) + end + end + end + + return vim.tbl_keys(server_configs) +end + +-- Set up LSP on_attach function +local function create_on_attach() + return function(client, bufnr) + -- Your existing keymap setup function from keys.lua + if _G.setup_lsp_keymaps then + _G.setup_lsp_keymaps(bufnr) + else + setup_fallback_keymaps(bufnr) + end + + -- Disable LSP formatting in favor of null-ls (if null-ls is available) + if M.null_ls then + client.server_capabilities.documentFormattingProvider = false + client.server_capabilities.documentRangeFormattingProvider = false + end + + -- Disable specific LSP capabilities to avoid conflicts + if client.name == "ruff" then + -- Disable ruff hover in favor of Pyright + client.server_capabilities.hoverProvider = false + elseif client.name == "ts_ls" then + -- Disable ts_ls formatting in favor of prettier via null-ls + client.server_capabilities.documentFormattingProvider = false + client.server_capabilities.documentRangeFormattingProvider = false + elseif client.name == "pyright" and M.null_ls then + -- Disable pyright formatting in favor of black/isort via null-ls + client.server_capabilities.documentFormattingProvider = false + client.server_capabilities.documentRangeFormattingProvider = false + end + + -- Set log level (backwards compatible) + if vim.lsp.set_log_level then + vim.lsp.set_log_level("warn") + end + + -- Document highlight on cursor hold + if client.server_capabilities and client.server_capabilities.documentHighlightProvider then + vim.api.nvim_create_autocmd("CursorHold", { + group = augroup_highlight, + buffer = bufnr, + callback = function() + if vim.lsp.buf.document_highlight then + vim.lsp.buf.document_highlight() + end + end, + }) + vim.api.nvim_create_autocmd("CursorMoved", { + group = augroup_highlight, + buffer = bufnr, + callback = function() + if vim.lsp.buf.clear_references then + vim.lsp.buf.clear_references() + end + end, + }) + end + end +end + +-- Set up basic LSP configuration +function M.setup() + -- Initialize all required modules + init_modules() + + -- Enable virtual_text diagnostics by default for 0.11+ (since it's disabled by default) + if has_feature("virtual_text_disabled_by_default") then + vim.diagnostic.config({ virtual_text = true }) + end + + -- Set up Mason if available (useful for tool management) + if M.mason then + M.mason.setup({ + ui = { + border = 'rounded', + icons = { + package_installed = '✓', + package_pending = '➜', + package_uninstalled = '✗' + } + } + }) + end + + -- Set up mason-tool-installer if available + if M.mason_tool_installer then + M.mason_tool_installer.setup({ + ensure_installed = { + -- Language servers + "lua-language-server", "pyright", "typescript-language-server", "rust-analyzer", + "clangd", "bash-language-server", "yaml-language-server", + -- Formatters + "stylua", "clang-format", "prettier", "shfmt", "black", "isort", "goimports", + "sql-formatter", "shellharden", + -- Linters/Diagnostics + "eslint_d", "selene", "flake8", "dotenv-linter", "phpcs", + -- Utilities + "jq" + }, + auto_update = false, + run_on_start = true, + start_delay = 3000, + }) + end + + -- Set up null-ls if available + if M.null_ls and M.builtins then + local sources = { + M.builtins.diagnostics.selene.with({ + condition = function(utils) + return utils.root_has_file({"selene.toml"}) + end, + }), + M.builtins.diagnostics.dotenv_linter, + M.builtins.diagnostics.tidy, + M.builtins.diagnostics.phpcs.with({ + condition = function(utils) + return utils.root_has_file({"phpcs.xml", "phpcs.xml.dist", ".phpcs.xml", ".phpcs.xml.dist"}) + end, + }), + + -- Formatters (prioritized over LSP formatting) + M.builtins.formatting.stylua.with({ + extra_args = { "--quote-style", "AutoPreferSingle", "--indent-width", "2", "--column-width", "160" }, + condition = function(utils) + return utils.root_has_file({"stylua.toml", ".stylua.toml"}) + end, + }), + M.builtins.formatting.prettier.with({ + extra_args = { "--single-quote", "--tab-width", "4", "--print-width", "100" }, + filetypes = { "javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "css", "scss", "less", "html", "json", "jsonc", "yaml", "markdown", "graphql", "handlebars" }, + prefer_local = "node_modules/.bin", + }), + M.builtins.formatting.black.with({ + extra_args = { "--fast" }, + prefer_local = ".venv/bin", + }), + M.builtins.formatting.isort.with({ + extra_args = { "--profile", "black" }, + prefer_local = ".venv/bin", + }), + M.builtins.formatting.goimports, + M.builtins.formatting.clang_format.with({ + extra_args = { "--style", "{BasedOnStyle: Google, IndentWidth: 4}" } + }), + M.builtins.formatting.shfmt.with({ + extra_args = { "-i", "2", "-ci" } + }), + M.builtins.formatting.shellharden, + M.builtins.formatting.sql_formatter, + M.builtins.formatting.dart_format, + + -- Code actions + M.builtins.code_actions.gitsigns, + M.builtins.code_actions.gitrebase, + } + + M.null_ls.setup({ + sources = sources, + update_in_insert = false, + on_attach = function(client, bufnr) + -- Disable LSP formatting in favor of null-ls + client.server_capabilities.documentFormattingProvider = false + client.server_capabilities.documentRangeFormattingProvider = false + + local function lsp_supports_method(client, method) + if client.supports_method then + return client:supports_method(method) + elseif client.server_capabilities then + local capability_map = { + ["textDocument/formatting"] = "documentFormattingProvider", + ["textDocument/rangeFormatting"] = "documentRangeFormattingProvider", + ["textDocument/hover"] = "hoverProvider", + ["textDocument/signatureHelp"] = "signatureHelpProvider", + ["textDocument/documentHighlight"] = "documentHighlightProvider", + } + local cap = capability_map[method] + return cap and client.server_capabilities[cap] + end + return false + end + + if lsp_supports_method(client, "textDocument/formatting") then + vim.api.nvim_create_autocmd("BufWritePre", { + group = augroup_format, + buffer = bufnr, + callback = function() + if vim.fn.has("nvim-0.8") == 1 then + vim.lsp.buf.format({ + async = false, + bufnr = bufnr, + filter = function(formatting_client) + return formatting_client.name == "null-ls" + end, + }) + else + vim.lsp.buf.formatting_sync() + end + end, + }) + end + end, + }) + end + + -- Set up LSP capabilities + local capabilities = setup_capabilities() + local on_attach = create_on_attach() + + -- Set up LSP handlers with version compatibility (avoid deprecated APIs) + if has_feature("deprecated_lsp_handlers") then + -- Use old handler setup for versions before 0.12 + if vim.lsp.handlers then + vim.lsp.handlers['textDocument/hover'] = vim.lsp.with( + vim.lsp.handlers.hover, { border = 'rounded' } + ) + + vim.lsp.handlers['textDocument/signatureHelp'] = vim.lsp.with( + vim.lsp.handlers.signature_help, { border = 'rounded' } + ) + end + else + -- Use new handler setup for 0.12+ (when old handlers are deprecated/removed) + if vim.lsp.handlers then + vim.lsp.handlers['textDocument/hover'] = vim.lsp.with( + vim.lsp.handlers['textDocument/hover'], { border = 'rounded' } + ) + + vim.lsp.handlers['textDocument/signatureHelp'] = vim.lsp.with( + vim.lsp.handlers['textDocument/signatureHelp'], { border = 'rounded' } + ) + end + end + + -- Choose configuration method based on Neovim version and available features + if has_feature("native_lsp_config") then + -- Set up native LSP configuration + local servers = setup_native_lsp_configs() + + -- Set default on_attach and capabilities for all LSP servers + vim.lsp.config('*', { + on_attach = on_attach, + capabilities = capabilities, + }) + + -- Enable the LSP servers + vim.lsp.enable(servers) + + elseif M.mason_lspconfig and M.lspconfig then + -- Set up mason-lspconfig if available + if M.mason_lspconfig then + M.mason_lspconfig.setup({ + ensure_installed = { + "lua_ls", "pyright", "ts_ls", "rust_analyzer", "clangd", "gopls", + "bashls", "html", "cssls", "jsonls", "yamlls" + }, + automatic_installation = true, + }) + end + + -- Use traditional lspconfig with mason + local enabled_servers = {} + + local server_configs = { + lua_ls = { + settings = { + Lua = { + diagnostics = { + globals = { "vim", "use", "_G", "packer_plugins", "P" }, + }, + workspace = { + library = { + vim.env.VIMRUNTIME, + "${3rd}/luv/library", + "${3rd}/busted/library", + }, + checkThirdParty = false, + }, + telemetry = { enable = false }, + }, + }, + }, + pyright = { + settings = { + python = { + formatting = { provider = "none" } + } + } + }, + ts_ls = { + init_options = { + disableAutomaticTypeAcquisition = true + }, + }, + clangd = { + cmd = { "clangd", "--background-index", "--clang-tidy", "--header-insertion=iwyu" }, + }, + gopls = { + settings = { + gopls = { + gofumpt = true, + usePlaceholders = true, + completeUnimported = true, + staticcheck = true, + }, + }, + }, + } + + M.mason_lspconfig.setup_handlers({ + function(server_name) + if not enabled_servers[server_name] then + local config = server_configs[server_name] or {} + config.on_attach = on_attach + config.capabilities = capabilities + M.lspconfig[server_name].setup(config) + enabled_servers[server_name] = true + end + end, + }) + + elseif M.lspconfig then + -- Fallback: Set up servers manually if mason-lspconfig is not available + local servers = { 'lua_ls', 'pyright', 'ts_ls', 'rust_analyzer', 'clangd', 'gopls', 'bashls', 'html', 'cssls', 'jsonls', 'yamlls' } + local enabled_servers = {} + + for _, server in ipairs(servers) do + if not enabled_servers[server] and M.lspconfig[server] then + local config = { + on_attach = on_attach, + capabilities = capabilities, + } + M.lspconfig[server].setup(config) + enabled_servers[server] = true + end + end + end + + return true +end + +-- Global toggle for diagnostics (backwards compatible) +vim.g.diagnostics_visible = true +function _G.toggle_diagnostics() + if has_feature("diagnostic_api") then + if vim.g.diagnostics_visible then + vim.g.diagnostics_visible = false + vim.diagnostic.disable() + else + vim.g.diagnostics_visible = true + vim.diagnostic.enable() + end + else + -- Fallback for older versions + if vim.g.diagnostics_visible then + vim.g.diagnostics_visible = false + vim.lsp.handlers["textDocument/publishDiagnostics"] = function() end + else + vim.g.diagnostics_visible = true + vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( + vim.lsp.diagnostic.on_publish_diagnostics, {} + ) + end + end +end + +-- Create Mason command if Mason is available +if M.mason then + vim.api.nvim_create_user_command("Mason", function() + require("mason.ui").open() + end, {}) +end + +-- Automatically show diagnostics in a float window for the current line +if has_feature("diagnostic_api") then + vim.api.nvim_create_autocmd("CursorHold", { + group = augroup_diag_float, + pattern = "*", + callback = function() + local opts = { + focusable = false, + close_events = { "BufLeave", "CursorMoved", "InsertEnter", "FocusLost" }, + border = border, + source = "always", + prefix = " ", + scope = "cursor", + } + vim.diagnostic.open_float(nil, opts) + end, + }) + + -- Autocmd to open the diagnostic window when a file with errors is opened + vim.api.nvim_create_autocmd({ "LspAttach", "BufReadPost" }, { + group = augroup_diag_load, + callback = function() + local has_errors = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.ERROR }) > 0 + if has_errors then + vim.diagnostic.setqflist({ + open = true, + title = "Diagnostics", + }) + end + end, + }) +end + +-- Create Toggle Diagnostic command +vim.api.nvim_create_user_command("ToggleDiagnostics", _G.toggle_diagnostics, { + desc = "Toggle global diagnostics visibility" +}) + +return M diff --git a/common/config/nvim/lua/plugins/lualine.lua b/common/config/nvim/lua/plugins/lualine.lua new file mode 100755 index 0000000..9c1cc43 --- /dev/null +++ b/common/config/nvim/lua/plugins/lualine.lua @@ -0,0 +1,22 @@ +-- lualine.nvim plugin config (modular, robust) +local ok, lualine = pcall(require, 'lualine') +if not ok then return end +local nvim_version = vim.version() +if nvim_version.major == 0 and nvim_version.minor < 5 then return end +lualine.setup({ + options = { + theme = 'auto', + icons_enabled = true, + section_separators = '', + component_separators = '', + disabled_filetypes = {}, + }, + sections = { + lualine_a = {'mode'}, + lualine_b = {'branch', 'diff', 'diagnostics'}, + lualine_c = {'filename'}, + lualine_x = {'encoding', 'fileformat', 'filetype'}, + lualine_y = {'progress'}, + lualine_z = {'location'}, + }, +})
\ No newline at end of file diff --git a/common/config/nvim/lua/plugins/luasnip.lua b/common/config/nvim/lua/plugins/luasnip.lua new file mode 100755 index 0000000..75f4c28 --- /dev/null +++ b/common/config/nvim/lua/plugins/luasnip.lua @@ -0,0 +1,13 @@ +-- LuaSnip plugin config (modular, robust) +local ok, luasnip = pcall(require, 'luasnip') +if not ok then return end +local nvim_version = vim.version() +if nvim_version.major == 0 and nvim_version.minor < 5 then return end +-- Load friendly-snippets if available +pcall(function() + require('luasnip.loaders.from_vscode').lazy_load() +end) +luasnip.config.set_config({ + history = true, + updateevents = "TextChanged,TextChangedI", +})
\ No newline at end of file diff --git a/common/config/nvim/lua/plugins/messages.lua b/common/config/nvim/lua/plugins/messages.lua new file mode 100755 index 0000000..8e46c09 --- /dev/null +++ b/common/config/nvim/lua/plugins/messages.lua @@ -0,0 +1,85 @@ +local M = { + 'Why do programmers prefer dark mode? Because light attracts bugs!', + 'Why did the AI break up with its computer? It found someone with better algorithms!', + "Why do Python programmers prefer snakes? Because they can't stand Java!", + 'Why did the developer go to the beach? To catch some rays and debug JavaScript!', + "Why was the HTML document lonely? It didn't have any <body> to share its content with!", + "Why did the CSS file break up with the HTML file? It couldn't stand the layout!", + 'Why do programmers always mix up Christmas and Halloween? Because Oct 31 == Dec 25!', + 'Why did the computer take up gardening? It wanted to improve its root system!', + 'Why do programmers prefer dark chocolate? It has better byte-size!', + "Why did the developer get mad at their computer? It couldn't understand their emotional code!", + 'Why was the JavaScript developer so good at relationships? They knew how to handle callbacks!', + 'Why did the coder go broke? They lost all their cache!', + 'Why did the SQL query go to therapy? It had too many inner joins!', + 'Why did the programmer plant a light bulb? They wanted to grow a power plant!', + 'Why did the computer keep its drink on the windowsill? It wanted a byte!', + "Why don't programmers like nature? It has too many bugs!", + 'Why did the developer go broke? They spent all their money on keyboard shortcuts!', + 'Why did the computer cross the road? To get to the other website!', + 'Why was the code cold? It left its Windows open!', + 'Why did the coder go to therapy? They had too many issues!', + 'Why was the function sad? It returned null!', + "Why did the programmer quit their job? They didn't get arrays!", + 'Why was the loop so fast? It was in a hurry!', + 'Why was the computer cold? It left its Windows open!', + "Why did the developer stay calm during the crisis? Because they knew how to 'handle' exceptions!", + "Why did the JavaScript developer always smile? Because they had 'callbacks' for everything!", + "Why did the programmer break up with their keyboard? It had too many 'commitment' issues!", + "Why don't Neovim users ever get lost in their text files? Because they always 'find' their way!", + "Why don't Neovim users need a GPS? Because they're experts at 'mapping' their routes!", + 'Why did the Neovim user become a musician? Because they can play the keyboard like a pro!', + "Why don't Neovim users ever lose track of time? Because they have a 'status line' to keep them informed!", + "Why did the Neovim user open a detective agency? Because they have an 'eye' for spotting code errors!", + 'Why did the developer bring a ladder to the coding competition? To take their code to the next level!', + "When your code is running slowly: 'It's not a bug; it's a feature that takes its time.'", + "Why did the programmer go to therapy? Because their code had too many 'issues'!", + "Why was the JavaScript developer sad? Because they didn't 'console' their feelings!", + "Why did the developer get locked out of their own codebase? They forgot the 'key'!", + 'Welcome to Neovim, where plugins multiply faster than rabbits!', + "How many programmers does it take to change a lightbulb? None, that's a hardware problem!", + "When you're debugging and can't find the issue: 'I swear, it was working yesterday!'", + "Why don't programmers trust stairs? Because they're always up to 'something'!", + "When you fix a bug without even trying: 'I guess I'm just that good.'", + 'Why was the computer cold? It left its Windows open!', + "Why do Java developers wear glasses? Because they don't C#!", + "Why did the programmer quit their job? They didn't get arrays!", + "When you write a one-liner that solves a complex problem: 'I am a genius, yes, I am.'", + "When you refactor your code and it breaks everything: 'I've made a huge mistake.'", + "When you accidentally close your editor with unsaved changes: 'Goodbye, cruel world.'", + "When you discover a bug on a Friday afternoon: 'Looks like we're working late again.'", + "When you realize your code from last year: 'Who wrote this junk? Oh, wait...'", + "When you write a comment and six months later can't understand it: 'I speak my own language.'", + "When you join a new project with zero documentation: 'Here be dragons.'", + "When you add a 'TODO' comment and hope someone else will deal with it: 'Not my problem.'", + "Remember, coding is not just about writing code; it's about solving problems.", + 'Stay curious and never stop learning. Technology is always evolving.', + "When debugging, don't guess; use systematic troubleshooting techniques.", + "Keep your code DRY (Don't Repeat Yourself) to make it more maintainable.", + 'Use meaningful variable and function names. Your code should read like a story.', + 'Always test your code thoroughly before deploying it. Automated tests are your friends.', + 'Spend time designing your code before jumping into implementation. Good architecture pays off.', + 'Learn to break down complex problems into smaller, manageable tasks.', + "Code with the future in mind. Write code that's easy to understand and maintain.", + 'Version control is your safety net. Use Git or other VCS systems religiously.', + 'Document your code and processes. It will save you and your team countless hours.', + "Don't optimize prematurely. Measure first, then optimize where it matters.", + "Read other people's code. It's a great way to learn different coding styles and techniques.", + 'Stay organized with your project structure. Consistency makes collaboration smoother.', + 'Take regular breaks to prevent burnout. Your productivity will thank you.', + 'Use comments sparingly but effectively. Explain why, not just what.', + 'Consider pair programming or code reviews to catch issues early and learn from others.', + 'Know when to ask for help. Programming is a team effort.', + "Programming is not just about the code; it's about the problem-solving mindset.", + 'Keep your development environment clean and well-maintained for consistent productivity.', + 'Learn from your mistakes and failures; they are valuable lessons in programming.', + 'When faced with a bug, isolate and reproduce it before attempting to fix it.', + "Why did the developer stay calm during the crisis? Because they knew how to 'handle' exceptions.", + "Why was the JavaScript developer always smiling? Because they had 'callbacks' for everything!", + "Why did the programmer break up with their keyboard? It had too many 'commitment' issues!", + "Margaret Hamilton coined the term 'software engineer.'", + 'Why did the function go to therapy? It had too many issues!', + "Why don't programmers like nature? It has too many bugs!", +} + +return M diff --git a/common/config/nvim/lua/plugins/modify-blend.lua b/common/config/nvim/lua/plugins/modify-blend.lua new file mode 100755 index 0000000..1b2c6d5 --- /dev/null +++ b/common/config/nvim/lua/plugins/modify-blend.lua @@ -0,0 +1,43 @@ +local ui = vim.api.nvim_list_uis()[1] + +local bufnr = vim.api.nvim_create_buf(true, true) +local win = vim.api.nvim_open_win(bufnr, true, { + relative = "editor", + --relative = "cursor", + width = ui.width, + height = ui.height, + anchor = "NE", + row = 10, + col = 10, + style = "minimal", + zindex = 50, +}) + +vim.api.nvim_win_set_option(win, "winblend", 1) + +local blend_start = 15 +local offset = 1 + +CANCEL = false +local timer = vim.loop.new_timer() +timer:start( + 0, + 50, + vim.schedule_wrap(function() + blend_start = blend_start + offset + + if blend_start > 90 then + offset = -1 + elseif blend_start < 10 then + offset = 1 + end + + if CANCEL or not vim.api.nvim_win_is_valid(win) then + timer:close() + timer:stop() + return + end + + vim.cmd([[highlight NormalFloat blend=]] .. tostring(blend_start)) + end) +) diff --git a/common/config/nvim/lua/plugins/navic.lua b/common/config/nvim/lua/plugins/navic.lua new file mode 100755 index 0000000..a574d5c --- /dev/null +++ b/common/config/nvim/lua/plugins/navic.lua @@ -0,0 +1,51 @@ +local M = {} + +function M.setup() + local ok, navic = pcall(require, "nvim-navic") + if not ok or not navic then + return false + end + + navic.setup({ + icons = { + File = " ", + Module = " ", + Namespace = " ", + Package = " ", + Class = " ", + Method = " ", + Property = " ", + Field = " ", + Constructor = " ", + Enum = "練", + Interface = "練", + Function = " ", + Variable = " ", + Constant = " ", + String = " ", + Number = " ", + Boolean = "◩ ", + Array = " ", + Object = " ", + Key = " ", + Null = "ﳠ ", + EnumMember = " ", + Struct = " ", + Event = " ", + Operator = " ", + TypeParameter = " " + }, + highlight = false, + separator = " > ", + depth_limit = 0, + depth_limit_indicator = "..", + safe_output = true, + lsp = { + auto_attach = true + } + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/neodev.lua b/common/config/nvim/lua/plugins/neodev.lua new file mode 100755 index 0000000..07843e1 --- /dev/null +++ b/common/config/nvim/lua/plugins/neodev.lua @@ -0,0 +1,45 @@ +local M = {} + +--- Setup and configure neodev +-- This function initializes neodev with configurations for better Lua development experience +-- @return boolean True if setup was successful, false otherwise +function M.setup() + local ok, neodev = pcall(require, 'neodev') + if not ok then + return false + end + + neodev.setup({ + --library = { plugins = { "nvim-dap-ui" }, types = true }, + --library = { plugins = { "neotest" }, types = true }, + library = { + enabled = true, -- when not enabled, neodev will not change any settings to the LSP server + -- these settings will be used for your Neovim config directory + runtime = true, -- runtime path + types = true, -- full signature, docs and completion of vim.api, vim.treesitter, vim.lsp and others + --plugins = { "neotest" }, + --{ "nvim-dap-ui" }, + --plugins = true, -- installed opt or start plugins in packpath + -- you can also specify the list of plugins to make available as a workspace library + -- plugins = { "nvim-treesitter", "plenary.nvim", "telescope.nvim" }, + plugins = { "nvim-treesitter", "plenary.nvim", "telescope.nvim", "neotest", "nvim-dap-ui" }, + }, + setup_jsonls = true, -- configures jsonls to provide completion for project specific .luarc.json files + -- for your Neovim config directory, the config.library settings will be used as is + -- for plugin directories (root_dirs having a /lua directory), config.library.plugins will be disabled + -- for any other directory, config.library.enabled will be set to false + override = function(root_dir, options) + end, + -- With lspconfig, Neodev will automatically setup your lua-language-server + -- If you disable this, then you have to set {before_init=require("neodev.lsp").before_init} + -- in your lsp start options + lspconfig = true, + -- much faster, but needs a recent built of lua-language-server + -- needs lua-language-server >= 3.6.0 + pathStrict = true, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/neoscroll.lua b/common/config/nvim/lua/plugins/neoscroll.lua new file mode 100755 index 0000000..f2ecb04 --- /dev/null +++ b/common/config/nvim/lua/plugins/neoscroll.lua @@ -0,0 +1,22 @@ +local M = {} + +function M.setup() + local ok, neoscroll = pcall(require, 'neoscroll') + if not ok then + return false + end + + -- Basic configuration + neoscroll.setup({ + mappings = {'<C-u>', '<C-d>', '<C-b>', '<C-f>', '<C-y>', '<C-e>', 'zt', 'zz', 'zb'}, + hide_cursor = true, + stop_eof = true, + respect_scrolloff = false, + cursor_scrolls_alone = true, + easing_function = 'quadratic', + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/neotest.lua b/common/config/nvim/lua/plugins/neotest.lua new file mode 100755 index 0000000..1034d33 --- /dev/null +++ b/common/config/nvim/lua/plugins/neotest.lua @@ -0,0 +1,38 @@ +local M = {} + +function M.setup() + local ok, neotest = pcall(require, "neotest") + if not ok or not neotest then + return false + end + + -- Safely require adapters + local python_ok, python_adapter = pcall(require, "neotest-python") + local plenary_ok, plenary_adapter = pcall(require, "neotest-plenary") + local vim_test_ok, vim_test_adapter = pcall(require, "neotest-vim-test") + + local adapters = {} + if python_ok and python_adapter then + table.insert(adapters, python_adapter({ + dap = { justMyCode = false }, + })) + end + + if plenary_ok and plenary_adapter then + table.insert(adapters, plenary_adapter) + end + + if vim_test_ok and vim_test_adapter then + table.insert(adapters, vim_test_adapter({ + ignore_file_types = { "python", "vim", "lua" }, + })) + end + + neotest.setup({ + adapters = adapters, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/notify.lua b/common/config/nvim/lua/plugins/notify.lua new file mode 100755 index 0000000..62a8f47 --- /dev/null +++ b/common/config/nvim/lua/plugins/notify.lua @@ -0,0 +1,36 @@ +local M = {} + +function M.setup() + local ok, notify = pcall(require, 'notify') + if not ok or not notify then + return false + end + + notify.setup({ + background_colour = '#000000', + icons = { + ERROR = '', + WARN = '', + INFO = '', + DEBUG = '', + TRACE = '✎', + } + }) + + -- Set highlight groups safely + local function set_hl(group, link) + vim.cmd(('hi default link %s %s'):format(group, link)) + end + + set_hl('NotifyERRORBody', 'Normal') + set_hl('NotifyWARNBody', 'Normal') + set_hl('NotifyINFOBody', 'Normal') + set_hl('NotifyDEBUGBody', 'Normal') + set_hl('NotifyTRACEBody', 'Normal') + set_hl('NotifyLogTime', 'Comment') + set_hl('NotifyLogTitle', 'Special') + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/nvim-tree.lua b/common/config/nvim/lua/plugins/nvim-tree.lua new file mode 100755 index 0000000..a212eab --- /dev/null +++ b/common/config/nvim/lua/plugins/nvim-tree.lua @@ -0,0 +1,479 @@ +local M = {} + +-- Safe require helper +local function safe_require(name) + local ok, mod = pcall(require, name) + return ok and mod or nil +end + + +local ok, api = pcall(require, 'nvim-tree.api') +if not ok then return end +local function on_attach(bufnr) + local function opts(desc) + return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true } + end + + local mappings = { + ["<C-]>"] = { api.tree.change_root_to_node, "CD" }, + ["<C-e>"] = { api.node.open.replace_tree_buffer, "Open: In Place" }, + ["<C-k>"] = { api.node.show_info_popup, "Info" }, + ["<C-r>"] = { api.fs.rename_sub, "Rename: Omit Filename" }, + ["<C-t>"] = { api.node.open.tab, "Open: New Tab" }, + ["<C-v>"] = { api.node.open.vertical, "Open: Vertical Split" }, + ["<C-x>"] = { api.node.open.horizontal, "Open: Horizontal Split" }, + ["<BS>"] = { api.node.navigate.parent_close, "Close Directory" }, + -- ["<CR>"] = { api.node.open.edit, "Open" }, + ["<Tab>"] = { api.node.open.preview, "Open Preview" }, + [">"] = { api.node.navigate.sibling.next, "Next Sibling" }, + ["<"] = { api.node.navigate.sibling.prev, "Previous Sibling" }, + ["."] = { api.node.run.cmd, "Run Command" }, + ["-"] = { api.tree.change_root_to_parent, "Up" }, + ["a"] = { api.fs.create, "Create" }, + ["bmv"] = { api.marks.bulk.move, "Move Bookmarked" }, + ["B"] = { api.tree.toggle_no_buffer_filter, "Toggle No Buffer" }, + ["c"] = { api.fs.copy.node, "Copy" }, + -- ["C"] = { api.tree.toggle_git_clean_filter, "Toggle Git Clean" }, + ["[c"] = { api.node.navigate.git.prev, "Prev Git" }, + ["]c"] = { api.node.navigate.git.next, "Next Git" }, + ["d"] = { api.fs.remove, "Delete" }, + ["D"] = { api.fs.trash, "Trash" }, + ["E"] = { api.tree.expand_all, "Expand All" }, + ["e"] = { api.fs.rename_basename, "Rename: Basename" }, + ["]e"] = { api.node.navigate.diagnostics.next, "Next Diagnostic" }, + ["[e"] = { api.node.navigate.diagnostics.prev, "Prev Diagnostic" }, + ["F"] = { api.live_filter.clear, "Clean Filter" }, + ["f"] = { api.live_filter.start, "Filter" }, + ["g?"] = { api.tree.toggle_help, "Help" }, + ["gy"] = { api.fs.copy.absolute_path, "Copy Absolute Path" }, + ["H"] = { api.tree.toggle_hidden_filter, "Toggle Dotfiles" }, + ["I"] = { api.tree.toggle_gitignore_filter, "Toggle Git Ignore" }, + ["J"] = { api.node.navigate.sibling.last, "Last Sibling" }, + ["K"] = { api.node.navigate.sibling.first, "First Sibling" }, + ["m"] = { api.marks.toggle, "Toggle Bookmark" }, + -- ["o"] = { api.node.open.edit, "Open" }, + ["O"] = { api.node.open.no_window_picker, "Open: No Window Picker" }, + ["p"] = { api.fs.paste, "Paste" }, + ["P"] = { api.node.navigate.parent, "Parent Directory" }, + ["q"] = { api.tree.close, "Close" }, + ["r"] = { api.fs.rename, "Rename" }, + ["R"] = { api.tree.reload, "Refresh" }, + ["s"] = { api.node.run.system, "Run System" }, + ["S"] = { api.tree.search_node, "Search" }, + ["U"] = { api.tree.toggle_custom_filter, "Toggle Hidden" }, + ["W"] = { api.tree.collapse_all, "Collapse" }, + ["x"] = { api.fs.cut, "Cut" }, + ["y"] = { api.fs.copy.filename, "Copy Name" }, + ["Y"] = { api.fs.copy.relative_path, "Copy Relative Path" }, + ["<2-LeftMouse>"] = { api.node.open.edit, "Open" }, + ["<2-RightMouse>"] = { api.tree.change_root_to_node, "CD" }, + + -- Mappings migrated from view.mappings.list + ["l"] = { api.node.open.edit, "Open" }, + ["<CR>"] = { api.node.open.edit, "Open" }, + ["o"] = { api.node.open.edit, "Open" }, + ["h"] = { api.node.navigate.parent_close, "Close Directory" }, + ["v"] = { api.node.open.vertical, "Open: Vertical Split" }, + ["C"] = { api.tree.change_root_to_node, "CD" }, + } + for keys, mapping in pairs(mappings) do + vim.keymap.set("n", keys, mapping[1], opts(mapping[2])) + end +end + +---- Icons configuration for nvim-tree +--local icons = { +-- webdev_colors = true, +-- git_placement = "before", +-- modified_placement = "after", +-- padding = " ", +-- symlink_arrow = " ➛ ", +-- show = { +-- file = true, +-- folder = true, +-- folder_arrow = true, +-- git = true, +-- modified = true, +-- }, +-- glyphs = { +-- default = "", +-- symlink = "", +-- bookmark = "", +-- modified = "●", +-- folder = { +-- arrow_closed = "", +-- arrow_open = "", +-- default = "", +-- open = "", +-- empty = "", +-- empty_open = "", +-- symlink = "", +-- symlink_open = "", +-- }, +-- git = { +-- unstaged = "✗", +-- staged = "✓", +-- unmerged = "", +-- renamed = "➜", +-- untracked = "★", +-- deleted = "", +-- ignored = "◌", +-- }, +-- }, +--} + +local icons = { + webdev_colors = true, + git_placement = "signcolumn", + modified_placement = "after", + padding = " ", + show = { + file = true, + folder = true, + folder_arrow = true, + git = true, + modified = true, + }, + + glyphs = { + default = "", + symlink = "", + folder = { + arrow_open = "", + arrow_closed = "", + default = " ", + open = " ", + empty = " ", + empty_open = " ", + symlink = "", + symlink_open = "", + }, + + git = { + deleted = "", + unmerged = "", + untracked = "", + unstaged = "", + staged = "", + renamed = "➜", + ignored = "◌", + }, + }, + web_devicons = { + folder = { + enable = true, + color = true, + }, + }, +} + +local float = { + enable = false, + open_win_config = function() + local screen_w = vim.o.columns + local screen_h = vim.o.lines - vim.o.cmdheight + local window_w = screen_w * WIDTH_RATIO + local window_h = screen_h * HEIGHT_RATIO + local window_w_int = math.floor(window_w) + local window_h_int = math.floor(window_h) + local center_x = (screen_w - window_w) / 2 + local center_y = ((vim.o.lines - window_h) / 2) - vim.o.cmdheight + return { + border = "rounded", + relative = "editor", + row = center_y, + col = center_x, + width = window_w_int, + height = window_h_int, + } + end, +} + +local renderer = { + group_empty = true, -- default: true. Compact folders that only contain a single folder into one node in the file tree. + highlight_git = false, + full_name = false, + highlight_opened_files = "icon", -- "none" (default), "icon", "name" or "all" + highlight_modified = "icon", -- "none", "name" or "all". Nice and subtle, override the open icon + root_folder_label = ":~:s?$?/..?", + indent_width = 2, + indent_markers = { + enable = true, + inline_arrows = true, + icons = { + corner = "└", + edge = "│", + item = "│", + bottom = "─", + none = " ", + }, + }, + icons = icons, +} + +local system_open = { cmd = "zathura" } + +local HEIGHT_RATIO = 0.8 +local WIDTH_RATIO = 0.15 +local view = { + cursorline = true, + float = float, + --signcolumn = 'no', + --width = function() + -- return math.floor(vim.opt.columns:get() * WIDTH_RATIO) + --end, + width = { max = 38, min = 38 }, + side = "left", +} + +-- Open nvim-tree when opening a directory +local function open_nvim_tree(data) + -- buffer is a directory + local directory = vim.fn.isdirectory(data.file) == 1 + + if not directory then + return + end + + -- change to the directory + vim.cmd.cd(data.file) + + -- open the tree + require("nvim-tree.api").tree.open() +end + + +-- Setup function +function M.setup() + -- Check if nvim-tree is installed + --local nvim_tree = safe_require('nvim-tree') + --if not nvim_tree then + -- return false + --end + + local nvim_tree = safe_require('nvim-tree') + if type(nvim_tree) ~= "table" or not nvim_tree.setup then + --vim.notify("[nvim-tree] Plugin did not load correctly", vim.log.levels.ERROR) + return false + end + + -- Setup nvim-tree + nvim_tree.setup({ + sync_root_with_cwd = true, + respect_buf_cwd = true, + disable_netrw = true, + hijack_netrw = true, + open_on_tab = false, + hijack_cursor = false, + update_cwd = true, + hijack_directories = { + enable = true, + auto_open = true, + }, + diagnostics = { + enable = true, + icons = { + error = "✘", + warning = "", + hint = "◉", + info = "", + }, + }, + filesystem_watchers = { + enable = true, + debounce_delay = 50, + ignore_dirs = { "node_modules", ".config/nvm" }, + }, + update_focused_file = { + enable = true, + update_cwd = true, + --update_root = true, + ignore_list = {}, + }, + --root_dirs = {}, + --system_open = { + -- --cmd = nil, + -- --args = {}, + --}, + system_open = system_open, + filters = { + dotfiles = false, + custom = {}, + }, + --git = { + -- enable = true, + -- ignore = true, + -- timeout = 500, + --}, + git = { ignore = false }, + view = view, + renderer = renderer, + --renderer = { + -- indent_markers = { + -- enable = false, + -- icons = { + -- corner = "└ ", + -- edge = "│ ", + -- none = " ", + -- }, + -- }, + -- icons = icons, + --}, + on_attach = on_attach, + notify = { + threshold = vim.log.levels.ERROR, + }, + log = { + enable = true, + truncate = true, + types = { + diagnostics = true, + git = true, + profile = true, + watcher = true, + }, + }, + trash = { + cmd = "gio trash", + require_confirm = true, + }, + modified = { + enable = true, + show_on_dirs = true, + show_on_open_dirs = true, + }, + actions = { + use_system_clipboard = true, + change_dir = { + enable = true, + global = false, + restrict_above_cwd = false, + }, + remove_file = { + close_window = true, + }, + open_file = { + quit_on_open = false, + resize_window = true, + window_picker = { + enable = true, + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", + exclude = { + filetype = { "notify", "packer", "qf", "diff", "fugitive", "fugitiveblame" }, + buftype = { "nofile", "terminal", "help" }, + }, + }, + }, + }, + }) + + + local api = require("nvim-tree.api") + --local event = api.events.Event + --api.events.subscribe(event.TreeOpen, function(_) + -- vim.cmd([[setlocal statuscolumn=\ ]]) + -- vim.cmd([[setlocal cursorlineopt=number]]) + -- vim.cmd([[setlocal fillchars+=vert:🮇]]) + -- vim.cmd([[setlocal fillchars+=horizup:🮇]]) + -- vim.cmd([[setlocal fillchars+=vertright:🮇]]) + --end) + + local function open_nvim_tree(data) + vim.cmd.cd(data.file:match("(.+)/[^/]*$")) + local directory = vim.fn.isdirectory(data.file) == 1 + if not directory then + return + end + require("nvim-tree.api").tree.open() + end + + -- Auto open nvim-tree when opening a directory + vim.api.nvim_create_autocmd({ "VimEnter" }, { pattern = { "*" }, callback = open_nvim_tree }) + + -- Change Root To Global Current Working Directory + local function change_root_to_global_cwd() + local api = require("nvim-tree.api") + local global_cwd = vim.fn.getcwd(-1, -1) + api.tree.change_root(global_cwd) + end + + local function copy_file_to(node) + local file_src = node["absolute_path"] + -- The args of input are {prompt}, {default}, {completion} + -- Read in the new file path using the existing file's path as the baseline. + local file_out = vim.fn.input("COPY TO: ", file_src, "file") + -- Create any parent dirs as required + local dir = vim.fn.fnamemodify(file_out, ":h") + vim.fn.system({ "mkdir", "-p", dir }) + -- Copy the file + vim.fn.system({ "cp", "-R", file_src, file_out }) + end + + local function edit_and_close(node) + api.node.open.edit(node, {}) + api.tree.close() + end + + --vim.api.nvim_create_augroup('NvimTreeRefresh', {}) + --vim.api.nvim_create_autocmd('BufEnter', { + -- pattern = 'NvimTree_1', + -- command = 'NvimTreeRefresh', + -- group = 'NvimTreeRefresh', + --}) + + vim.api.nvim_create_autocmd({ "CursorHold" }, { + pattern = "NvimTree*", + callback = function() + local def = vim.api.nvim_get_hl_by_name("Cursor", true) + vim.api.nvim_set_hl( + 0, + "Cursor", + vim.tbl_extend("force", def, { + blend = 100, + }) + ) + vim.opt.guicursor = "n-v-c-sm:block,i-ci-ve:ver25,r-cr-o:hor20,a:Cursor/lCursor" + end, + }) + + vim.api.nvim_create_autocmd({ "BufLeave", "WinClosed", "WinLeave" }, { + pattern = "NvimTree*", + callback = function() + local def = vim.api.nvim_get_hl_by_name("Cursor", true) + vim.api.nvim_set_hl( + 0, + "Cursor", + vim.tbl_extend("force", def, { + blend = 0, + }) + ) + vim.opt.guicursor = "n-v-c-sm:block,i-ci-ve:ver25,r-cr-o:hor20" + end, + }) + + vim.api.nvim_command("highlight NvimTreeNormal guibg=NONE ctermbg=NONE") + vim.api.nvim_command("highlight NvimTreeNormalNC guibg=NONE ctermbg=NONE guifg=NONE") + vim.api.nvim_command("highlight NvimTreeNormalFloat guibg=NONE ctermbg=NONE") + vim.api.nvim_command("highlight NvimTreeEndOfBuffer guibg=NONE ctermbg=NONE") --(NonText) + vim.api.nvim_command("highlight NvimTreeCursorLine guibg=#50fa7b guifg=#000000") + vim.api.nvim_command("highlight NvimTreeSymlinkFolderName guifg=#f8f8f2 guibg=NONE ctermbg=NONE") + vim.api.nvim_command("highlight NvimTreeFolderName guifg=#f8f8f2 guibg=NONE ctermbg=NONE") + vim.api.nvim_command("highlight NvimTreeRootFolder guifg=#f8f8f2 guibg=NONE ctermbg=NONE") + vim.api.nvim_command("highlight NvimTreeEmptyFolderName guifg=#f8f8f2 guibg=NONE ctermbg=NONE") --(Directory) + vim.api.nvim_command("highlight NvimTreeOpenedFolderName guifg=#f8f8f2 guibg=NONE ctermbg=NONE") --(Directory) + vim.api.nvim_command("highlight NvimTreeOpenedFile guifg=#50fa7b guibg=NONE ctermbg=NONE") + vim.api.nvim_command("highlight NvimTreeExecFile guifg=#ff882a guibg=none gui=NONE") + + return true +end + +---- Set highlights +--vim.cmd([[highlight NvimTreeNormal guibg=NONE ctermbg=NONE]]) +--vim.cmd([[highlight NvimTreeNormalNC guibg=NONE ctermbg=NONE guifg=NONE]]) +--vim.cmd([[highlight NvimTreeNormalFloat guibg=NONE ctermbg=NONE]]) +--vim.cmd([[highlight NvimTreeEndOfBuffer guibg=NONE ctermbg=NONE]]) +--vim.cmd([[highlight NvimTreeCursorLine guibg=#50fa7b guifg=#000000]]) + +-- Highlight Groups + +return M diff --git a/common/config/nvim/lua/plugins/overseer.lua b/common/config/nvim/lua/plugins/overseer.lua new file mode 100755 index 0000000..593d094 --- /dev/null +++ b/common/config/nvim/lua/plugins/overseer.lua @@ -0,0 +1,14 @@ +local M = {} + +function M.setup() + local ok, overseer = pcall(require, 'overseer') + if not ok or not overseer then + return false + end + + overseer.setup({}) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/plenary.lua b/common/config/nvim/lua/plugins/plenary.lua new file mode 100755 index 0000000..f572244 --- /dev/null +++ b/common/config/nvim/lua/plugins/plenary.lua @@ -0,0 +1,3 @@ +-- plenary.nvim plugin config (modular, robust) +local ok, _ = pcall(require, 'plenary') +-- No config needed
\ No newline at end of file diff --git a/common/config/nvim/lua/plugins/prettier.lua b/common/config/nvim/lua/plugins/prettier.lua new file mode 100755 index 0000000..ca57ea9 --- /dev/null +++ b/common/config/nvim/lua/plugins/prettier.lua @@ -0,0 +1,8 @@ +local M = {} + +function M.setup() + -- No-op if prettier is not installed + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/quickfix.lua b/common/config/nvim/lua/plugins/quickfix.lua new file mode 100755 index 0000000..4a76da0 --- /dev/null +++ b/common/config/nvim/lua/plugins/quickfix.lua @@ -0,0 +1,15 @@ +local M = {} + +M.close = function() + vim.cmd.cclose() +end + +M.open = function() + if vim.tbl_count(vim.fn.getqflist()) == 0 then + vim.notify('Nothing in quickfix list; not opening.', vim.log.levels.WARN) + else + vim.cmd.copen() + end +end + +return M diff --git a/common/config/nvim/lua/plugins/snippets.lua b/common/config/nvim/lua/plugins/snippets.lua new file mode 100755 index 0000000..989ad8a --- /dev/null +++ b/common/config/nvim/lua/plugins/snippets.lua @@ -0,0 +1,33 @@ +local M = {} + +function M.setup() + local ok, ls = pcall(require, "luasnip") + if not ok or not ls then + return false + end + + -- Safely load snippets + pcall(function() require("luasnip.loaders.from_lua").load({ paths = "~/.config/nvim/snippets/" }) end) + pcall(function() require("luasnip.loaders.from_vscode").lazy_load() end) + pcall(function() require("luasnip.loaders.from_snipmate").lazy_load() end) + + ls.config.set_config { + history = true, + updateevents = "TextChanged,TextChangedI", + enable_autosnippets = true, + region_check_events = "InsertEnter", + delete_check_events = "TextChanged", + store_selection_keys = "<Tab>", + ext_opts = { + [require("luasnip.util.types").choiceNode] = { + active = { + virt_text = { { "«", "GruvboxOrange" } }, + }, + }, + }, + } + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/sniprun.lua b/common/config/nvim/lua/plugins/sniprun.lua new file mode 100755 index 0000000..418e8cc --- /dev/null +++ b/common/config/nvim/lua/plugins/sniprun.lua @@ -0,0 +1,57 @@ +local status_ok, sniprun = pcall(require, 'sniprun') +if not status_ok then + return +end + +sniprun.setup({ + -- selected_interpreters = {}, --# use those instead of the default for the current filetype + -- repl_enable = { "Python3_original" }, --# enable REPL-like behavior for the given interpreters + -- repl_disable = {}, --# disable REPL-like behavior for the given interpreters + + -- interpreter_options = { --# intepreter-specific options, see docs / :SnipInfo <name> + -- GFM_original = { + -- use_on_filetypes = { "markdown.pandoc" }, --# the 'use_on_filetypes' configuration key is + -- --# available for every interpreter + -- }, + -- }, + + --# you can combo different display modes as desired + display = { + -- "Classic", --# display results in the command-line area + --'VirtualTextOk', --# display ok results as virtual text (multiline is shortened) + -- "VirtualTextErr", --# display error results as virtual text + -- "TempFloatingWindow", --# display results in a floating window + -- "LongTempFloatingWindow", --# same as above, but only long results. To use with VirtualText__ + 'Terminal', --# display results in a vertical split + -- "TerminalWithCode", --# display results and code history in a vertical split + -- "NvimNotify", --# display with the nvim-notify plugin + -- "Api" --# return output to a programming interface + }, + + display_options = { + terminal_width = 45, --# change the terminal display option width + notification_timeout = 5, --# timeout for nvim_notify output + }, + + --# You can use the same keys to customize whether a sniprun producing + --# no output should display nothing or '(no output)' + show_no_output = { + 'Classic', + 'TempFloatingWindow', --# implies LongTempFloatingWindow, which has no effect on its own + }, + + --# customize highlight groups (setting this overrides colorscheme) + -- snipruncolors = { + -- SniprunVirtualTextOk = { bg = "NONE", fg = "#66eeff", ctermbg = "Black", cterfg = "Cyan" }, + -- SniprunFloatingWinOk = { fg = "NONE", ctermfg = "Cyan" }, + -- SniprunVirtualTextErr = { bg = "#881515", fg = "#000000", ctermbg = "DarkRed", cterfg = "Black" }, + -- SniprunFloatingWinErr = { fg = "#881515", ctermfg = "DarkRed" }, + -- }, + + --# miscellaneous compatibility/adjustement settings + inline_messages = 0, --# inline_message (0/1) is a one-line way to display messages + --# to workaround sniprun not being able to display anything + + borders = 'single', --# display borders around floating windows + --# possible values are 'none', 'single', 'double', or 'shadow' +}) diff --git a/common/config/nvim/lua/plugins/statuscol.lua b/common/config/nvim/lua/plugins/statuscol.lua new file mode 100755 index 0000000..c538790 --- /dev/null +++ b/common/config/nvim/lua/plugins/statuscol.lua @@ -0,0 +1,37 @@ +local M = {} + +function M.setup() + local ok, statuscol = pcall(require, "statuscol") + if not ok or not statuscol then + return false + end + + local builtin_ok, builtin = pcall(require, "statuscol.builtin") + if not builtin_ok or not builtin then + return false + end + + statuscol.setup({ + segments = { + { text = { builtin.lnumfunc }, click = "v:lua.ScLa" }, + { text = { "%s" }, click = "v:lua.ScSa" }, + { text = { builtin.foldfunc }, click = "v:lua.ScFa" }, + }, + ft_ignore = { + "NvimTree", + "packer", + "NeogitStatus", + "toggleterm", + "dapui_scopes", + "dapui_breakpoints", + "dapui_stacks", + "dapui_watches", + "dapui_console", + "dapui_repl", + }, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/surround.lua b/common/config/nvim/lua/plugins/surround.lua new file mode 100755 index 0000000..71023c7 --- /dev/null +++ b/common/config/nvim/lua/plugins/surround.lua @@ -0,0 +1,35 @@ +local M = {} + +function M.setup() + local ok, surround = pcall(require, 'nvim-surround') + if not ok or not surround then + return false + end + + surround.setup({ + keymaps = { + insert = false, + insert_line = false, + normal = false, + normal_cur = false, + normal_line = false, + normal_cur_line = false, + visual = "<S-s>", + visual_line = false, + delete = false, + change = false, + }, + aliases = { + ["a"] = false, + ["b"] = false, + ["B"] = false, + ["r"] = false, + ["q"] = false, + ["s"] = false, + }, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/telescope.lua b/common/config/nvim/lua/plugins/telescope.lua new file mode 100755 index 0000000..5aca8ac --- /dev/null +++ b/common/config/nvim/lua/plugins/telescope.lua @@ -0,0 +1,740 @@ +local M = {} + +-- Safely require a module +-- @param name string The module name to require +-- @return table|nil The loaded module or nil if failed +local function safe_require(name) + local ok, mod = pcall(require, name) + return ok and mod or nil +end + +--- Setup and configure Telescope +-- This function initializes Telescope with default configurations and extensions +-- @return boolean True if setup was successful, false otherwise +function M.setup() + -- Check if Telescope is installed + local telescope = safe_require("telescope") + if not telescope then + return false + end + -- Require Telescope and fail early if missing + local telescope = safe_require("telescope") + if not telescope then + return false + end + + local actions = safe_require("telescope.actions") + local actions_set = safe_require("telescope.actions.set") + local actions_state = safe_require("telescope.actions.state") + local finders = safe_require("telescope.finders") + local pickers = safe_require("telescope.pickers") + local config_mod = safe_require("telescope.config") + local utils = safe_require("telescope.utils") + local previewers = require("telescope.previewers") + + local config = config_mod and config_mod.values or {} + + -- 🛡 Safe previewer to avoid nil path error + local safe_previewer = function() + return require("telescope.previewers").new_buffer_previewer({ + define_preview = function(self, entry) + if not entry or type(entry) ~= "table" then return end + + local path = entry.path or entry.filename or entry.value + if type(path) ~= "string" or path == "" then return end + + -- Avoid expanding things like " Recent Books" which aren't valid files + if path:match("^%s") then return end + + -- Resolve tilde if present + path = path:gsub("^~", vim.env.HOME) + + if vim.fn.filereadable(path) ~= 1 and vim.fn.isdirectory(path) ~= 1 then + return + end + + -- Protect against nil path being passed further + if not self.state or not self.state.bufnr or not self.state.bufname then return end + + local preview_utils = require("telescope.previewers.utils") + preview_utils.buffer_previewer_maker(path, self.state.bufnr, { + bufname = self.state.bufname, + callback = function(bufnr, success) + if not success then + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { "Failed to preview file." }) + end + end, + }) + end, + }) + end + + local function get_extension_actions(ext) + local ok, telescope_ext = pcall(require, "telescope._extensions." .. ext) + if not ok then return {} end + return telescope_ext.actions or {} + end + + telescope.setup({ + defaults = { + vimgrep_arguments = { + "rg", + "--color=never", + "--no-heading", + "--with-filename", + "--line-number", + "--column", + "--smart-case", + "--hidden", + "--fixed-strings", + "--trim", + }, + previewer = safe_previewer(), + prompt_prefix = " ", + selection_caret = " ", + entry_prefix = " ", + path_display = { "tail" }, + file_ignore_patterns = { + "packer_compiled.lua", + "~/.config/zsh/plugins", + "zcompdump", + "%.DS_Store", + "%.git/", + "%.spl", + "%[No Name%]", + "/$", + "node_modules", + "%.png", + "%.zip", + "%.pxd", + "^.local/", + "^.cache/", + "^downloads/", + "^music/", + }, + mappings = { + i = { + ["<C-n>"] = actions.cycle_history_next, + ["<C-p>"] = actions.cycle_history_prev, + ["<C-j>"] = actions.move_selection_next, + ["<C-k>"] = actions.move_selection_previous, + ["<Esc>"] = actions.close, + ["<?>"] = actions.which_key, + ["<Down>"] = actions.move_selection_next, + ["<Up>"] = actions.move_selection_previous, + ["<CR>"] = actions.select_default, + ["<C-x>"] = actions.select_horizontal, + ["<C-y>"] = actions.select_vertical, + ["<C-t>"] = actions.select_tab, + ["<C-c>"] = actions.delete_buffer, + ["<C-u>"] = actions.preview_scrolling_up, + ["<C-d>"] = actions.preview_scrolling_down, + ["<PageUp>"] = actions.results_scrolling_up, + ["<PageDown>"] = actions.results_scrolling_down, + ["<Tab>"] = actions.toggle_selection + actions.move_selection_worse, + ["<S-Tab>"] = actions.toggle_selection + actions.move_selection_better, + ["<C-q>"] = actions.send_to_qflist + actions.open_qflist, + ["<M-q>"] = actions.send_selected_to_qflist + actions.open_qflist, + ["<C-l>"] = actions.complete_tag, + ["<C-_>"] = actions.which_key, + }, + n = { + ["<esc>"] = actions.close, + ["<q>"] = actions.close, + ["<CR>"] = actions.select_default, + ["<C-x>"] = actions.select_horizontal, + ["<C-y>"] = actions.select_vertical, + ["<C-t>"] = actions.select_tab, + ["<C-c>"] = actions.delete_buffer, + ["<Tab>"] = actions.toggle_selection + actions.move_selection_worse, + ["<S-Tab>"] = actions.toggle_selection + actions.move_selection_better, + ["<C-q>"] = actions.send_to_qflist + actions.open_qflist, + ["<M-q>"] = actions.send_selected_to_qflist + actions.open_qflist, + ["j"] = actions.move_selection_next, + ["k"] = actions.move_selection_previous, + ["H"] = actions.move_to_top, + ["M"] = actions.move_to_middle, + ["L"] = actions.move_to_bottom, + ["<Down>"] = actions.move_selection_next, + ["<Up>"] = actions.move_selection_previous, + ["gg"] = actions.move_to_top, + ["G"] = actions.move_to_bottom, + ["<C-u>"] = actions.preview_scrolling_up, + ["<C-d>"] = actions.preview_scrolling_down, + ["<PageUp>"] = actions.results_scrolling_up, + ["<PageDown>"] = actions.results_scrolling_down, + ["cd"] = function(prompt_bufnr) + local selection = actions_state.get_selected_entry() + local dir = vim.fn.fnamemodify(selection.path, ":p:h") + actions.close(prompt_bufnr) + vim.cmd("silent lcd " .. dir) + end, + ["?"] = actions.which_key, + }, + }, + }, + preview = { + filesize_limit = 3, + timeout = 250, + }, + selection_strategy = "reset", + sorting_strategy = "ascending", + scroll_strategy = "limit", + color_devicons = true, + layout_strategy = "horizontal", + layout_config = { + horizontal = { + height = 0.95, + preview_cutoff = 70, + width = 0.92, + preview_width = { 0.55, max = 50 }, + }, + bottom_pane = { + height = 12, + preview_cutoff = 70, + prompt_position = "bottom", + }, + }, + find_files = { + cwd = vim.fn.getcwd(), + prompt_prefix = " ", + follow = true, + }, + extensions = { + file_browser = { + theme = "dropdown", + hijack_netrw = false, + mappings = { + i = { + ["<C-w>"] = function() vim.cmd("normal vbd") end, + ["<C-h>"] = function() + local fb_actions = get_extension_actions("file_browser") + if fb_actions.goto_parent_dir then + fb_actions.goto_parent_dir() + end + end, + }, + n = { + ["N"] = function() + local fb_actions = get_extension_actions("file_browser") + if fb_actions.create then + fb_actions.create() + end + end, + ["<C-h>"] = function() + local fb_actions = get_extension_actions("file_browser") + if fb_actions.goto_parent_dir then + fb_actions.goto_parent_dir() + end + end, + }, + }, + }, + }, + }) + + -- Load extensions + for _, ext in ipairs({ + "fzf", "ui-select", "file_browser", "changed_files", + "media_files", "notify", "dap", "session-lens", "recent_files" + }) do + pcall(telescope.load_extension, ext) + end + + -- Define the custom command findhere/startup + vim.cmd('command! Findhere lua require("plugins.telescope").findhere()') + + return true +end + +-- Find config files +local function _sys_path(repo_path) + local home = os.getenv("HOME") or vim.fn.expand("~") + + -- Case 1: Files in the OS-specific home folder (e.g., linux/home/.bashrc) + if repo_path:find("/home/", 1, true) then + local file = repo_path:match(".*/home/(.*)") + return home .. "/" .. file + -- Case 2: Files in the common folder (e.g., common/README.md) + elseif repo_path:find("common/", 1, true) then + local file = repo_path:match("common/(.*)") + return home .. "/" .. file + -- Case 3: Root-level files (e.g., profile/profile_script or README.md) + elseif repo_path:find("profile/", 1, true) or repo_path:find("README.md", 1, true) then + return home .. "/" .. repo_path + -- Case 4: System-level files (e.g., linux/etc/issue) + elseif repo_path:find("/etc/", 1, true) then + local file = repo_path:match(".*/etc/(.*)") + return "/etc/" .. file + -- Return nil for paths that don't match any known pattern + else + return nil + end +end + +function M.find_configs() + local telescope_builtin = require("telescope.builtin") + local tracked_files = {} + local home = os.getenv("HOME") or "~" + local original_dir = vim.fn.getcwd() + vim.fn.chdir(home) + + vim.api.nvim_create_autocmd("VimLeave", { + callback = function() + vim.fn.chdir(original_dir) + end, + }) + + -- Check if the bare repository exists + if vim.fn.isdirectory(home .. "/.cfg") == 1 then + -- Repository exists, use git to find tracked files + local handle = io.popen("git --git-dir=" .. home .. "/.cfg --work-tree=" .. home .. " ls-tree --name-only -r HEAD") + local cfg_files = "" + if handle then + cfg_files = handle:read("*a") or "" + handle:close() + end + + -- Process the list of files + for file in string.gmatch(cfg_files, "[^\n]+") do + file = vim.trim(file) + if file ~= "" then + local fullpath = _sys_path(file) + if fullpath and (vim.fn.filereadable(fullpath) == 1 or vim.fn.isdirectory(fullpath) == 1) then + table.insert(tracked_files, fullpath) + end + end + end + end + + -- If no files were found (either no repo or no tracked files), use fallback paths + if #tracked_files == 0 then + local fallback_dirs = { + home .. "/.config/nvim", + home .. "/.config/zsh", + home .. "/.config/tmux", + home .. "/.bashrc", + home .. "/.zshrc", + home .. "/.tmux.conf", + } + for _, path in ipairs(fallback_dirs) do + if vim.fn.filereadable(path) == 1 or vim.fn.isdirectory(path) == 1 then + table.insert(tracked_files, path) + end + end + end + + if #tracked_files == 0 then + vim.notify("[find_configs] No configuration files found to search.", vim.log.levels.WARN) + return + end + + -- Launch Telescope + telescope_builtin.find_files({ + hidden = true, + no_ignore = false, + prompt_title = " Find Configs", + results_title = "Config Files", + path_display = { "smart" }, + search_dirs = tracked_files, + layout_strategy = "horizontal", + layout_config = { preview_width = 0.65, width = 0.75 }, + previewer = true, + }) +end + +function M.find_scripts() + require("telescope.builtin").find_files({ + hidden = true, + no_ignore = true, + prompt_title = " Find Scripts", + path_display = { "smart" }, + search_dirs = { + "~/.scripts", + }, + layout_strategy = "horizontal", + layout_config = { preview_width = 0.65, width = 0.75 }, + }) +end + +function M.find_projects() + local search_dir = "~/projects" + local actions = safe_require("telescope.actions") + local actions_set = safe_require("telescope.actions.set") + local actions_state = safe_require("telescope.actions.state") + local finders = safe_require("telescope.finders") + local pickers = safe_require("telescope.pickers") + local config_mod = safe_require("telescope.config") + local config = config_mod and config_mod.values or {} + + pickers + .new({}, { + prompt_title = "Find Projects", + finder = finders.new_oneshot_job({ + "find", + vim.fn.expand(search_dir), + "-type", + "d", + "-maxdepth", + "1", + }), + previewer = require("telescope.previewers").vim_buffer_cat.new({}), + sorter = config.generic_sorter({}), + attach_mappings = function(prompt_bufnr, map) + actions_set.select:replace(function() + local entry = actions_state.get_selected_entry() + if entry ~= nil then + local dir = entry.value + actions.close(prompt_bufnr, false) + vim.fn.chdir(dir) + vim.cmd("e .") + vim.cmd("echon ''") + print("cwd: " .. vim.fn.getcwd()) + end + end) + return true + end, + }) + :find() +end + +function M.grep_notes() + local opts = {} + opts.hidden = false + opts.search_dirs = { + "~/documents/main/", + } + opts.prompt_prefix = " " + opts.prompt_title = " Grep Notes" + opts.path_display = { "smart" } + require("telescope.builtin").live_grep(opts) +end + +function M.find_notes() + require("telescope.builtin").find_files({ + hidden = true, + no_ignore = false, + prompt_title = " Find Notes", + path_display = { "smart" }, + search_dirs = { + "~/documents/main", + }, + layout_strategy = "horizontal", + layout_config = { preview_width = 0.65, width = 0.75 }, + }) +end + +function M.find_private() + require("telescope.builtin").find_files({ + hidden = true, + no_ignore = false, + prompt_title = " Find Notes", + path_display = { "smart" }, + search_dirs = { + "~/notes/private", + "~/notes", + }, + layout_strategy = "horizontal", + layout_config = { preview_width = 0.65, width = 0.75 }, + }) +end + +function M.find_books() + local search_dir = "~/documents/books" + local actions = safe_require("telescope.actions") + local actions_set = safe_require("telescope.actions.set") + local actions_state = safe_require("telescope.actions.state") + local finders = safe_require("telescope.finders") + local pickers = safe_require("telescope.pickers") + local config_mod = safe_require("telescope.config") + local config = config_mod and config_mod.values or {} + + vim.fn.jobstart("$HOME/.scripts/track-books.sh") + local recent_books_directory = vim.fn.stdpath("config") .. "/tmp/" + local recent_books_file = recent_books_directory .. "recent_books.txt" + + -- Check if recent_books.txt exists, create it if not + if vim.fn.filereadable(recent_books_file) == 0 then + vim.fn.mkdir(recent_books_directory, "p") -- Ensure the directory exists + vim.fn.writefile({}, recent_books_file) -- Create an empty file + end + + local search_cmd = "find " .. vim.fn.expand(search_dir) .. " -type d -o -type f -maxdepth 1" + + local recent_books = vim.fn.readfile(recent_books_file) + local search_results = vim.fn.systemlist(search_cmd) + + local results = {} + + -- Section for Recent Books + table.insert(results, " Recent Books") + for _, recent_book_path in ipairs(recent_books) do + local formatted_path = vim.fn.fnameescape(recent_book_path) + table.insert(results, formatted_path) + end + + -- Section for All Books + table.insert(results, " All Books") + local directories = {} + local files = {} + + for _, search_result in ipairs(search_results) do + if vim.fn.isdirectory(search_result) == 1 then + table.insert(directories, search_result) + else + table.insert(files, search_result) + end + end + + table.sort(directories) + table.sort(files) + + for _, dir in ipairs(directories) do + table.insert(results, dir) + end + + for _, file in ipairs(files) do + table.insert(results, file) + end + + local picker = pickers.new({}, { + prompt_title = "Find Books", + finder = finders.new_table({ + results = results, + }), + file_ignore_patterns = { + "%.git", + }, + previewer = require("telescope.previewers").vim_buffer_cat.new({}), + sorter = config.generic_sorter({}), + attach_mappings = function(prompt_bufnr, map) + actions_set.select:replace(function() + local entry = actions_state.get_selected_entry() + if entry ~= nil then + local path = entry.value + + actions.close(prompt_bufnr, false) + + -- Check if it's under "Recent Books" + if path == " Recent Books" or path == " All Books" then + vim.notify("Cannot select 'All Books'/'Recent Books', please select a book or directory.", + vim.log.levels.WARN, { title = "Find Books" }) + else + -- Determine whether it's a directory or a file + local is_directory = vim.fn.isdirectory(path) + if is_directory then + -- It's a directory, navigate to it in the current buffer + vim.cmd("e " .. path) + else + -- It's a file, open it + vim.cmd("e " .. path) + end + end + end + end) + return true + end, + }) + + picker:find() +end + +function M.grep_current_dir() + local buffer_dir = require("telescope.utils").buffer_dir() + local opts = { + prompt_title = "Live Grep in " .. buffer_dir, + cwd = buffer_dir, + } + require("telescope.builtin").live_grep(opts) +end + +-- Helper functions that depend on telescope availability +local function get_dropdown_theme() + return require("telescope.themes").get_dropdown({ + hidden = true, + no_ignore = true, + previewer = false, + prompt_title = "", + preview_title = "", + results_title = "", + layout_config = { + prompt_position = "top", + }, + }) +end + +-- Set current folder as prompt title +local function with_title(opts, extra) + extra = extra or {} + local path = opts.cwd or opts.path or extra.cwd or extra.path or nil + local title = "" + local buf_path = vim.fn.expand("%:p:h") + local cwd = vim.fn.getcwd() + if path ~= nil and buf_path ~= cwd then + title = require("plenary.path"):new(buf_path):make_relative(cwd) + else + title = vim.fn.fnamemodify(cwd, ":t") + end + + return vim.tbl_extend("force", opts, { + prompt_title = title, + }, extra or {}) +end + +-- Find here +function M.findhere() + -- Open file browser if argument is a folder + local arg = vim.api.nvim_eval("argv(0)") + if arg and (vim.fn.isdirectory(arg) ~= 0 or arg == "") then + vim.defer_fn(function() + require("telescope.builtin").find_files(with_title(get_dropdown_theme())) + end, 10) + end +end + +-- Find dirs +function M.find_dirs() + local root_dir = vim.fn.input("Enter the root directory: ") + + -- Check if root_dir is empty + if root_dir == "" then + print("No directory entered. Aborting.") + return + end + + local entries = {} + + -- Use vim.fn.expand() to get an absolute path + local root_path = vim.fn.expand(root_dir) + + local subentries = vim.fn.readdir(root_path) + if subentries then + for _, subentry in ipairs(subentries) do + local absolute_path = root_path .. "/" .. subentry + table.insert(entries, subentry) + end + end + + local actions = safe_require("telescope.actions") + local actions_set = safe_require("telescope.actions.set") + local actions_state = safe_require("telescope.actions.state") + local finders = safe_require("telescope.finders") + local pickers = safe_require("telescope.pickers") + local config_mod = safe_require("telescope.config") + local config = config_mod and config_mod.values or {} + + pickers + .new({}, { + prompt_title = "Change Directory or Open File", + finder = finders.new_table({ + results = entries, + }), + previewer = config.file_previewer({}), + sorter = config.generic_sorter({}), + attach_mappings = function(prompt_bufnr, map) + actions_set.select:replace(function() + local entry = actions_state.get_selected_entry() + if entry ~= nil then + local selected_entry = entry.value + actions.close(prompt_bufnr, false) + local selected_path = root_path .. "/" .. selected_entry + if vim.fn.isdirectory(selected_path) == 1 then + vim.fn.chdir(selected_path) + vim.cmd("e .") + print("cwd: " .. vim.fn.getcwd()) + else + vim.cmd("e " .. selected_path) + end + end + end) + return true + end, + }) + :find() +end + +-- Safe telescope function wrapper for keymaps +local function safe_telescope_call(module_path, func_name, fallback_msg) + return function() + local ok, module = pcall(require, module_path) + if ok and module[func_name] then + module[func_name]() + else + vim.notify(fallback_msg or ("Telescope plugin not available for " .. func_name), vim.log.levels.WARN) + end + end +end + +local function safe_telescope_builtin(func_name, fallback_msg) + return function(opts) + local ok, telescope_builtin = pcall(require, "telescope.builtin") + if not ok then + vim.notify(fallback_msg or ("Telescope builtin module (telescope.builtin) not found!"), vim.log.levels.ERROR) + vim.notify("Error details: " .. tostring(telescope_builtin), vim.log.levels.DEBUG) -- telescope_builtin will contain the error message here + return + end + + if not telescope_builtin[func_name] then + vim.notify(fallback_msg or ("Telescope builtin function '" .. func_name .. "' not found!"), vim.log.levels.ERROR) + vim.notify("Available builtin functions: " .. vim.inspect(vim.tbl_keys(telescope_builtin)), vim.log.levels.DEBUG) + return + end + + -- If both are ok, proceed + telescope_builtin[func_name](opts or {}) + end +end + +-- Safe builtin telescope functions +local function safe_telescope_builtin(func_name, fallback_msg) + return function(opts) + local ok, telescope_builtin = pcall(require, "telescope.builtin") + if ok and telescope_builtin[func_name] then + telescope_builtin[func_name](opts or {}) + else + vim.notify(fallback_msg or ("Telescope builtin not available: " .. func_name), vim.log.levels.WARN) + end + end +end + +-- Safe extension calls with better checking +local function safe_telescope_extension(ext_name, func_name, fallback_msg) + return function(opts) + local telescope_mod = package.loaded.telescope or require("telescope") + if not telescope_mod then + return + end + + -- Check if extension is loaded + if not telescope_mod.extensions or not telescope_mod.extensions[ext_name] then + vim.notify(fallback_msg or ("Telescope extension '" .. ext_name .. "' not available (plugin may not be installed)"), vim.log.levels.WARN) + return + end + + local ext_func = telescope_mod.extensions[ext_name][func_name] + if not ext_func then + vim.notify(fallback_msg or ("Function '" .. func_name .. "' not found in extension '" .. ext_name .. "'"), vim.log.levels.WARN) + return + end + + ext_func(opts or {}) + end +end + +-- Fallback-safe `find_files` +M.safe_find_files = function() + local builtin = safe_require("telescope.builtin") + if builtin and builtin.find_files then + builtin.find_files() + else + local file = vim.fn.input("Open file: ", "", "file") + if file ~= "" then vim.cmd("edit " .. file) end + end +end + +-- Export safe wrapper functions for external use +M.safe_telescope_call = safe_telescope_call +M.safe_telescope_builtin = safe_telescope_builtin +M.safe_telescope_extension = safe_telescope_extension + +return M diff --git a/common/config/nvim/lua/plugins/toggleterm.lua b/common/config/nvim/lua/plugins/toggleterm.lua new file mode 100755 index 0000000..6b7aad5 --- /dev/null +++ b/common/config/nvim/lua/plugins/toggleterm.lua @@ -0,0 +1,294 @@ +local M = {} + +--- Setup and configure toggleterm.nvim +-- This function initializes and configures the toggleterm plugin for terminal management +-- @return boolean True if setup was successful, false otherwise +function M.setup() + local ok, toggleterm = pcall(require, 'toggleterm') + if not ok or not toggleterm then + return false + end + + toggleterm.setup({ + --open_mapping = [[<leader>tt]], + autochdir = true, + hide_numbers = true, + shade_filetypes = {}, + shade_terminals = false, + --shading_factor = 1, + start_in_insert = true, + insert_mappings = true, + terminal_mappings = true, + persist_size = true, + direction = 'float', + --direction = "vertical", + --direction = "horizontal", + close_on_exit = true, + shell = vim.o.shell, + highlights = { + -- highlights which map to a highlight group name and a table of it's values + -- NOTE: this is only a subset of values, any group placed here will be set for the terminal window split + --Normal = { + -- background = "#000000", + --}, + --Normal = { guibg = 'Black', guifg = 'White' }, + --FloatBorder = { guibg = 'Black', guifg = 'DarkGray' }, + --NormalFloat = { guibg = 'Black' }, + float_opts = { + --winblend = 3, + }, + }, + size = function(term) + if term.direction == 'horizontal' then + return 7 + elseif term.direction == 'vertical' then + return math.floor(vim.o.columns * 0.4) + end + end, + float_opts = { + width = 70, + height = 15, + border = 'curved', + highlights = { + border = 'Normal', + --background = 'Normal', + }, + --winblend = 0, + }, + }) + + -- Set up keymaps for toggleterm + local Terminal = require('toggleterm.terminal').Terminal + + -- Custom terminal commands + local lazygit + if not Terminal then return end + local term = Terminal:new({ + cmd = 'lazygit', + dir = 'git_dir', + direction = 'float', + float_opts = { + border = 'curved', + }, + on_open = function(term) + vim.cmd('startinsert!') + vim.api.nvim_buf_set_keymap(term.bufnr, 'n', 'q', '<cmd>close<CR>', {noremap = true, silent = true}) + end, + }) + if term then + lazygit = term + end + + -- Toggle functions + local function _lazygit_toggle() + if not Terminal then return end + if not lazygit then + init_lazygit() + end + if lazygit then + pcall(lazygit.toggle, lazygit) + end + end + + -- Set up keymaps + vim.keymap.set('n', '<leader>tt', '<cmd>ToggleTerm<CR>', {noremap = true, silent = true, desc = 'Toggle Terminal'}) + vim.keymap.set('n', '<leader>tf', '<cmd>ToggleTerm direction=float<CR>', {noremap = true, silent = true, desc = 'Toggle Float Terminal'}) + vim.keymap.set('n', '<leader>th', '<cmd>ToggleTerm size=10 direction=horizontal<CR>', {noremap = true, silent = true, desc = 'Toggle Horizontal Terminal'}) + vim.keymap.set('n', '<leader>tv', '<cmd>ToggleTerm size=80 direction=vertical<CR>', {noremap = true, silent = true, desc = 'Toggle Vertical Terminal'}) + vim.keymap.set('n', '<leader>tl', _lazygit_toggle, {noremap = true, silent = true, desc = 'Toggle Lazygit'}) + + -- Terminal mode mappings + vim.keymap.set('t', '<esc>', [[<C-\><C-n>]], {noremap = true, silent = true}) + vim.keymap.set('t', 'jk', [[<C-\><C-n>]], {noremap = true, silent = true}) + vim.keymap.set('t', '<C-h>', [[<Cmd>wincmd h<CR>]], {noremap = true, silent = true}) + vim.keymap.set('t', '<C-j>', [[<Cmd>wincmd j<CR>]], {noremap = true, silent = true}) + vim.keymap.set('t', '<C-k>', [[<Cmd>wincmd k<CR>]], {noremap = true, silent = true}) + vim.keymap.set('t', '<C-l>', [[<Cmd>wincmd l<CR>]], {noremap = true, silent = true}) + + return true +end + +-- Terminal utility functions +local mods = {} + +-- Simple empty check function if mods.empty is not available +function mods.empty(v) + return v == nil or v == '' +end +local float_handler = function(term) + if not mods.empty(vim.fn.mapcheck('jk', 't')) then + vim.keymap.del('t', 'jk', { buffer = term.bufnr }) + vim.keymap.del('t', '<esc>', { buffer = term.bufnr }) + end +end + +function _G.set_terminal_keymaps() + local opts = { noremap = true } + --local opts = {buffer = 0} + --vim.api.nvim_buf_set_keymap(0, "i", ";to", "[[<Esc>]]<cmd>Toggleterm", opts) + vim.api.nvim_buf_set_keymap(0, 't', '<C-c>', [[<Esc>]], opts) + vim.api.nvim_buf_set_keymap(0, 't', '<esc>', [[<C-\><C-n>]], opts) + vim.api.nvim_buf_set_keymap(0, 't', 'jk', [[<C-\><C-n>]], opts) + vim.api.nvim_buf_set_keymap(0, 't', '<C-h>', [[<C-\><C-n><C-W>h]], opts) + vim.api.nvim_buf_set_keymap(0, 't', '<C-j>', [[<C-\><C-n><C-W>j]], opts) + vim.api.nvim_buf_set_keymap(0, 't', '<C-k>', [[<C-\><C-n><C-W>k]], opts) + vim.api.nvim_buf_set_keymap(0, 't', '<C-l>', [[<C-\><C-n><C-W>l]], opts) +end + +-- if you only want these mappings for toggle term use term://*toggleterm#* instead +vim.cmd('autocmd! TermOpen term://* lua set_terminal_keymaps()') +local Terminal +local horizontal_term, vertical_term + +-- Safely require toggleterm.terminal +local toggleterm_ok, toggleterm = pcall(require, 'toggleterm.terminal') +if toggleterm_ok and toggleterm and toggleterm.Terminal then + Terminal = toggleterm.Terminal + -- Initialize terminals only if Terminal is available + if Terminal then + local ok1, hterm = pcall(Terminal.new, Terminal, { hidden = true, direction = 'horizontal' }) + local ok2, vterm = pcall(Terminal.new, Terminal, { hidden = true, direction = 'vertical' }) + if ok1 then horizontal_term = hterm end + if ok2 then vertical_term = vterm end + end +end + +function Horizontal_term_toggle() + if horizontal_term then + pcall(horizontal_term.toggle, horizontal_term, 8, 'horizontal') + end +end + +function Vertical_term_toggle() + if vertical_term then + pcall(vertical_term.toggle, vertical_term, math.floor(vim.o.columns * 0.5), 'vertical') + end +end + +-- Initialize lazygit terminal instance +local lazygit = nil +local Cur_cwd = vim.fn.getcwd() + +-- Function to initialize lazygit terminal +local function init_lazygit() + if not Terminal then return nil end + if not lazygit then + local ok, term = pcall(function() + return Terminal:new({ + cmd = 'lazygit', + count = 5, + id = 1000, + dir = 'git_dir', + direction = 'float', + on_open = float_handler, + hidden = true, + float_opts = { + border = { '╒', '═', '╕', '│', '╛', '═', '╘', '│' }, + width = 150, + height = 40, + }, + }) + end) + if ok and term then + lazygit = term + end + end + return lazygit +end + +-- Initialize lazygit on first use +function Lazygit_toggle() + -- Initialize lazygit if not already done + if not init_lazygit() then return end + + -- cwd is the root of project. if cwd is changed, change the git. + local cwd = vim.fn.getcwd() + if cwd ~= Cur_cwd then + Cur_cwd = cwd + if lazygit then + lazygit:close() + end + lazygit = Terminal:new({ + cmd = "zsh --login -c 'lazygit'", + dir = 'git_dir', + direction = 'float', + hidden = true, + on_open = float_handler, + float_opts = { + border = { '╒', '═', '╕', '│', '╛', '═', '╘', '│' }, + width = 150, + height = 40, + }, + }) + end + if lazygit then + lazygit:toggle() + else + vim.notify("Failed to initialize lazygit terminal", vim.log.levels.ERROR) + end +end + +local node = nil +local ncdu = nil + +-- Initialize node terminal if Terminal is available +if Terminal then + local ok1, nterm = pcall(function() return Terminal:new({ cmd = 'node', hidden = true }) end) + local ok2, ncduterm = pcall(function() return Terminal:new({ cmd = 'ncdu', hidden = true }) end) + if ok1 then node = nterm end + if ok2 then ncdu = ncduterm end +end + +function _NODE_TOGGLE() + if not node then return end + pcall(node.toggle, node) +end + +function _NCDU_TOGGLE() + if not ncdu then return end + pcall(ncdu.toggle, ncdu) +end + +local htop = nil + +function _HTOP_TOGGLE() + if not Terminal then return end + if not htop then + local ok, term = pcall(function() return Terminal:new({ cmd = 'htop', hidden = true }) end) + if ok then htop = term end + end + if htop then + pcall(htop.toggle, htop) + end +end + +local python = nil + +function _PYTHON_TOGGLE() + if not Terminal then return end + if not python then + local ok, term = pcall(function() return Terminal:new({ cmd = 'python', hidden = true }) end) + if ok then python = term end + end + if python then + pcall(python.toggle, python) + end +end + +function Gh_dash() + Terminal:new({ + cmd = 'gh dash', + hidden = true, + direction = 'float', + on_open = float_handler, + float_opts = { + height = function() + return math.floor(vim.o.lines * 0.8) + end, + width = function() + return math.floor(vim.o.columns * 0.95) + end, + }, + }) + Gh_dash:toggle() +end diff --git a/common/config/nvim/lua/plugins/treesitter.lua b/common/config/nvim/lua/plugins/treesitter.lua new file mode 100755 index 0000000..9df99b8 --- /dev/null +++ b/common/config/nvim/lua/plugins/treesitter.lua @@ -0,0 +1,54 @@ +local M = {} + +function M.setup() + local ok, treesitter = pcall(require, "nvim-treesitter.configs") + if not ok or not treesitter then + return false + end + + -- Add custom parser directory to runtime path + vim.opt.runtimepath:append("$HOME/.local/share/treesitter") + + -- Configure treesitter + treesitter.setup({ + -- Install parsers in custom directory + parser_install_dir = "$HOME/.local/share/treesitter", + + -- Enable syntax highlighting + highlight = { + enable = true, + -- Disable additional regex-based highlighting to improve performance + additional_vim_regex_highlighting = false, + }, + + -- Enable indentation + indent = { + enable = true, + }, + + -- Additional modules to enable + incremental_selection = { + enable = true, + keymaps = { + init_selection = "gnn", + node_incremental = "grn", + scope_incremental = "grc", + node_decremental = "grm", + }, + }, + + -- Ensure parsers are installed automatically + ensure_installed = { + "bash", "c", "cpp", "css", "dockerfile", "go", "html", + "javascript", "json", "lua", "markdown", "python", "rust", + "toml", "typescript", "vim", "yaml" + }, + + -- Auto-install parsers + auto_install = true, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/trouble.lua b/common/config/nvim/lua/plugins/trouble.lua new file mode 100755 index 0000000..4a07e3b --- /dev/null +++ b/common/config/nvim/lua/plugins/trouble.lua @@ -0,0 +1,73 @@ +local M = {} + +--- Setup and configure trouble.nvim +-- This function initializes and configures the trouble plugin for diagnostics and references +-- @return boolean True if setup was successful, false otherwise +function M.setup() + local ok, trouble = pcall(require, 'trouble') + if not ok then + return false + end + + trouble.setup({ + position = "bottom", -- bottom, top, left, right + height = 10, + width = 50, + icons = { + indent = { + fold = { + open = "", + closed = "", + }, + }, + kinds = { + -- you can use LSP kind symbols or devicons here + -- remove if you want default + }, + }, + modes = { + diagnostics = { + groups = { "filename", "kind" }, + }, + symbols = { + format = "{kind_icon} {symbol.name} {symbol.kind} [{symbol.scope}]", + }, + }, + action_keys = { + close = "q", + cancel = "<esc>", + refresh = "r", + jump = { "<cr>", "<tab>" }, + open_split = { "<c-x>" }, + open_vsplit = { "<c-v>" }, + open_tab = { "<c-t>" }, + jump_close = { "o" }, + toggle_preview = "P", + hover = "K", + preview = "p", + close_folds = { "zM", "zm" }, + open_folds = { "zR", "zr" }, + toggle_fold = { "zA", "za" }, + previous = "k", + next = "j", + }, + indent_lines = true, + auto_open = false, + auto_close = false, + auto_preview = true, + auto_fold = false, + auto_jump = { "lsp_definitions" }, + signs = { + error = "", + warning = "▲", + info = "", + hint = "⚑", + other = "•", + }, + use_diagnostic_signs = true, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/vimtex.lua b/common/config/nvim/lua/plugins/vimtex.lua new file mode 100755 index 0000000..732e6ed --- /dev/null +++ b/common/config/nvim/lua/plugins/vimtex.lua @@ -0,0 +1,45 @@ +--ft = { "latex", "tex" }, +--if vim.loop.os_uname().sysname == "Linux" then +-- vim.g.vimtex_view_method = "zathura" +--end +--vim.g["vimtex_view_method"] = "zathura" -- main variant with xdotool (requires X11; not compatible with wayland) +--vim.g.vimtex_compiler_method = "pdflatex" +-- compilation configuration +vim.g["vimtex_compiler_method"] = "latexmk" +--vim.g["vimtex_compiler_method"] = "xelatex" +--vim.g["vimtex_compiler_method"] = "lualatex" +vim.g["vimtex_compiler_latexmk"] = { + callback = 1, + continuous = 1, + executable = "latexmk", + options = { + "-shell-escape", + "-verbose", + "-file-line-error", + "-synctex=1", + "-interaction=nonstopmode", + }, +} +vim.g["vimtex_view_enabled"] = 1 +vim.g["vimtex_view_zathura_check_libsynctex"] = 0 +--vim.g["vimtex_view_method"] = "zathura" -- main variant with xdotool (requires X11; not compatible with wayland) +if vim.loop.os_uname().sysname == "Linux" then + vim.g.vimtex_view_method = "zathura" +end +--vim.g.vimtex_view_method = "sioyek" +--vim.g["vimtex_view_method"] = "zathura_simple" -- for variant without xdotool to avoid errors in wayland +vim.g["vimtex_quickfix_mode"] = 0 -- suppress error reporting on save and build +vim.g["vimtex_mappings_enabled"] = 0 -- Ignore mappings +vim.g["vimtex_indent_enabled"] = 0 -- Auto Indent +vim.g["tex_flavor"] = "latex" -- how to read tex files +vim.g["tex_indent_items"] = 0 -- turn off enumerate indent +vim.g["tex_indent_brace"] = 0 -- turn off brace indent +--vim.g.vimtex_view_forward_search_on_start = 0 +--vim.g["vimtex_context_pdf_viewer"] = "zathura" -- external PDF viewer run from vimtex menu command +--vim.g["latex_view_general_viewer"] = "zathura" +vim.g["vimtex_log_ignore"] = { -- Error suppression: + "Underfull", + "Overfull", + "specifier changed to", + "Token not allowed in a PDF string", +} diff --git a/common/config/nvim/lua/plugins/web-devicons.lua b/common/config/nvim/lua/plugins/web-devicons.lua new file mode 100755 index 0000000..a565a31 --- /dev/null +++ b/common/config/nvim/lua/plugins/web-devicons.lua @@ -0,0 +1,125 @@ +local M = {} + +-- Cache the nerd fonts check with better error handling +local function get_nerd_fonts_available() + if vim.g.nerd_fonts_available ~= nil then + return vim.g.nerd_fonts_available + end + + local has_nerd_fonts = false + local ok, result = pcall(function() + if vim.fn.has('unix') == 1 and vim.fn.executable('fc-list') == 1 then + local handle = io.popen('fc-list | grep -i nerd 2>/dev/null') + if handle then + local result = handle:read('*a') + handle:close() + return result ~= "" + end + end + return false + end) + + has_nerd_fonts = ok and result or false + vim.g.nerd_fonts_available = has_nerd_fonts + return has_nerd_fonts +end + +-- Helper function to get icon with fallback and validation +local function get_icon(nerd_icon, fallback, color, cterm_color, name) + local has_nerd = get_nerd_fonts_available() + + -- Validate colors + if not color or color == '' then + color = '#6d8086' -- Default gray color + end + if not cterm_color or cterm_color == '' then + cterm_color = '102' -- Default gray for terminal + end + + -- Pick icon + local icon = has_nerd and nerd_icon or fallback + if not icon or icon == '' then + icon = has_nerd and '' or '[F]' + end + + return { + icon = icon, + color = color, + cterm_color = cterm_color, + name = name or 'File', + } +end + +function M.setup() + local ok, devicons = pcall(require, 'nvim-web-devicons') + if not ok or not devicons then + return false + end + + devicons.setup({ + color_icons = true, + override = { + -- Languages + js = get_icon('', '[JS]', '#f5c06f', '179', 'Js'), + jsx = get_icon('', '[JSX]', '#689fb6', '67', 'Jsx'), + ts = get_icon('', '[TS]', '#4377c1', '67', 'Ts'), + tsx = get_icon('', '[TSX]', '#4377c1', '67', 'Tsx'), + lua = get_icon('', '[LUA]', '#51a0cf', '74', 'Lua'), + py = get_icon('', '[PY]', '#3572A5', '67', 'Python'), + rb = get_icon('', '[RB]', '#701516', '124', 'Ruby'), + go = get_icon('', '[GO]', '#519aba', '74', 'Go'), + rs = get_icon('', '[RS]', '#dea584', '173', 'Rust'), + + -- Images + png = get_icon('', '[PNG]', '#d4843e', '173', 'Png'), + jpg = get_icon('', '[JPG]', '#16a085', '36', 'Jpg'), + jpeg = get_icon('', '[JPG]', '#16a085', '36', 'Jpeg'), + webp = get_icon('', '[WEBP]', '#3498db', '32', 'Webp'), + svg = get_icon('', '[SVG]', '#3affdb', '80', 'Svg'), + + -- Archives + zip = get_icon('', '[ZIP]', '#e6b422', '178', 'Zip'), + rar = get_icon('', '[RAR]', '#e6b422', '178', 'Rar'), + ['7z'] = get_icon('', '[7Z]', '#e6b422', '178', '7z'), + tar = get_icon('', '[TAR]', '#e6b422', '178', 'Tar'), + gz = get_icon('', '[GZ]', '#e6b422', '178', 'GZip'), + bz2 = get_icon('', '[BZ2]', '#e6b422', '178', 'BZip2'), + + -- Docs + md = get_icon('', '[MD]', '#519aba', '67', 'Markdown'), + txt = get_icon('', '[TXT]', '#6d8086', '102', 'Text'), + pdf = get_icon('', '[PDF]', '#e74c3c', '160', 'PDF'), + doc = get_icon('', '[DOC]', '#2c6ecb', '27', 'Word'), + docx = get_icon('', '[DOC]', '#2c6ecb', '27', 'Word'), + xls = get_icon('', '[XLS]', '#1d6f42', '29', 'Excel'), + xlsx = get_icon('', '[XLS]', '#1d6f42', '29', 'Excel'), + + -- Config + json = get_icon('', '[JSON]', '#f5c06f', '179', 'Json'), + yaml = get_icon('', '[YAML]', '#6d8086', '102', 'Yaml'), + toml = get_icon('', '[TOML]', '#6d8086', '102', 'Toml'), + conf = get_icon('', '[CFG]', '#6d8086', '102', 'Config'), + ini = get_icon('', '[INI]', '#6d8086', '102', 'Ini'), + + -- Shell + sh = get_icon('', '[SH]', '#4d5a5e', '59', 'Shell'), + zsh = get_icon('', '[ZSH]', '#89e051', '113', 'Zsh'), + bash = get_icon('', '[BASH]', '#89e051', '113', 'Bash'), + + -- Git + ['.gitignore'] = get_icon('', '[GIT]', '#e24329', '166', 'GitIgnore'), + ['.gitattributes'] = get_icon('', '[GIT]', '#e24329', '166', 'GitAttributes'), + ['.gitconfig'] = get_icon('', '[GIT]', '#e24329', '166', 'GitConfig'), + }, + default = { + icon = get_nerd_fonts_available() and '' or '[F]', + name = 'File', + color = '#6d8086', + cterm_color = '102', + }, + }) + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/which-key.lua b/common/config/nvim/lua/plugins/which-key.lua new file mode 100755 index 0000000..10015aa --- /dev/null +++ b/common/config/nvim/lua/plugins/which-key.lua @@ -0,0 +1,53 @@ +local M = {} + +function M.setup() + local ok, wk = pcall(require, 'which-key') + if not ok then + return false + end + + -- Basic configuration + wk.setup({ + plugins = { + marks = true, + registers = true, + spelling = { enabled = true, suggestions = 20 }, + presets = { + operators = true, + motions = true, + text_objects = true, + windows = true, + nav = true, + z = true, + g = true, + }, + }, + --window = { + -- border = "none", + -- position = "bottom", + -- margin = { 1, 0, 1, 0 }, + -- padding = { 2, 2, 2, 2 }, + -- winblend = 0 + --}, + --layout = { + -- height = { min = 4, max = 25 }, + -- width = { min = 20, max = 50 }, + -- spacing = 3, + -- align = "left" + --}, + --ignore_missing = false, + --hidden = { "<silent>", "<cmd>", "<Cmd>", "<CR>", "call", "lua", "^:", "^ " }, + --show_help = true, + --triggers = "<leader>", + --triggers_blacklist = { + -- i = { "j", "k" }, + -- v = { "j", "k" }, + --} + }) + + + + return true +end + +return M diff --git a/common/config/nvim/lua/plugins/zen-mode.lua b/common/config/nvim/lua/plugins/zen-mode.lua new file mode 100755 index 0000000..7e52854 --- /dev/null +++ b/common/config/nvim/lua/plugins/zen-mode.lua @@ -0,0 +1,7 @@ +local status, zenMode = pcall(require, "zen-mode") +if (not status) then return end + +zenMode.setup { +} + +vim.keymap.set('n', '<C-w>o', '<cmd>ZenMode<cr>', { silent = true }) diff --git a/common/config/nvim/lua/setup/compat.lua b/common/config/nvim/lua/setup/compat.lua new file mode 100755 index 0000000..ef90444 --- /dev/null +++ b/common/config/nvim/lua/setup/compat.lua @@ -0,0 +1,104 @@ +-- setup/compat.lua +-- Automatically patches deprecated APIs based on Neovim version + +-- Version check helper +local function has_version(major, minor, patch) + local v = vim.version() + patch = patch or 0 + return v.major > major + or (v.major == major and v.minor > minor) + or (v.major == major and v.minor == minor and v.patch >= patch) +end + +-- === GLOBAL PATCHES === -- + +-- Neovim 0.10+: vim.islist replaces deprecated vim.tbl_islist +if has_version(0, 10) then + if vim.tbl_islist == nil then + vim.tbl_islist = vim.islist + end +end + +-- Neovim 0.12+: vim.tbl_flatten removed → shim using vim.iter +if has_version(0, 12) then + vim.tbl_flatten = function(t) + return vim.iter(t):flatten():totable() + end +end + +-- === DEPRECATION SHIMS (0.13 / 1.0) === -- + +-- client.is_stopped → client:is_stopped() +if has_version(0, 13) then + local mt = getmetatable(vim.lsp._client or {}) + if mt and mt.__index and mt.__index.is_stopped then + mt.__index.is_stopped = function(client, ...) + return client:is_stopped(...) + end + end +end + +-- client.request → client:request() +if has_version(0, 13) then + local mt = getmetatable(vim.lsp._client or {}) + if mt and mt.__index and mt.__index.request then + mt.__index.request = function(client, ...) + return client:request(...) + end + end +end + +-- vim.validate{tbl} → vim.validate(tbl) +if has_version(1, 0) then + if type(vim.validate) == "function" then + local old_validate = vim.validate + vim.validate = function(arg) + -- Handle both forms for backward compatibility + if type(arg) == "table" then + return old_validate(arg) + else + return old_validate{ arg } + end + end + end +end + +-- Deprecated: vim.lsp.get_active_clients (moved in 0.11+) +if has_version(0, 11) then + if vim.lsp.get_active_clients == nil then + vim.lsp.get_active_clients = function(...) + return vim.lsp.get_clients(...) + end + end +end + +-- Deprecated: vim.diagnostic.setqflist / setloclist (moved in 0.11+) +if has_version(0, 11) then + if vim.diagnostic.setqflist == nil then + vim.diagnostic.setqflist = function(diags, opts) + return vim.diagnostic.toqflist(diags, opts) + end + end + if vim.diagnostic.setloclist == nil then + vim.diagnostic.setloclist = function(diags, opts) + return vim.diagnostic.toloclist(diags, opts) + end + end +end + +-- Deprecated: vim.lsp.buf.formatting/formatting_sync (removed in 0.8+) +if has_version(0, 8) then + if vim.lsp.buf.formatting == nil then + vim.lsp.buf.formatting = function(opts) + return vim.lsp.buf.format(opts) + end + end + if vim.lsp.buf.formatting_sync == nil then + vim.lsp.buf.formatting_sync = function(opts, timeout_ms) + return vim.lsp.buf.format(vim.tbl_extend("force", opts or {}, { timeout_ms = timeout_ms })) + end + end +end + +-- Return something to satisfy require() +return true diff --git a/common/config/nvim/lua/setup/manager.lua b/common/config/nvim/lua/setup/manager.lua new file mode 100755 index 0000000..9cf1d14 --- /dev/null +++ b/common/config/nvim/lua/setup/manager.lua @@ -0,0 +1,811 @@ +-- manager.lua + +local M = {} + +-- State tracking +local state = { + manager_invoked = nil, + initialized = false, + bootstrap_completed = {}, +} + +-- Path constants +local PATHS = { + lazy = vim.fn.stdpath("data") .. "/lazy/lazy.nvim", + packer = vim.fn.stdpath("data") .. "/site/pack/packer/start/packer.nvim", + packer_dir = vim.fn.stdpath("data") .. "/site/pack/packer/start", + builtin_dir = vim.fn.stdpath("data") .. "/nvim/site/pack/core/opt", +} + +-- Utility functions +local function safe_require(module) + local ok, result = pcall(require, module) + return ok and result or nil +end + +local function notify(msg, level) + vim.notify("[Manager] " .. msg, level or vim.log.levels.INFO) +end + +local function execute_git_command(cmd, _) + -- Use vim.fn.system instead of os.execute for better cross-platform support and error handling + local result = vim.fn.system(cmd) + return vim.v.shell_error == 0 +end + +local function get_nvim_version() + local version = vim.version() + if version then + return version.major, version.minor, version.patch + end + + -- Fallback for older versions + local version_str = vim.fn.execute("version"):match("NVIM v(%d+%.%d+%.%d+)") + if version_str then + local major, minor, patch = version_str:match("(%d+)%.(%d+)%.(%d+)") + return tonumber(major), tonumber(minor), tonumber(patch) + end + return 0, 0, 0 +end + +local function has_builtin_manager() + local major, minor = get_nvim_version() + return major > 0 or (major == 0 and minor >= 12) +end + +-- CRITICAL FIX: This function is essential to prevent runtime conflicts. +-- It removes the specified manager's directory from the runtimepath. +local function cleanup_manager(manager_name) + if manager_name == "packer" then + -- Reset packer state and remove from rtp + local packer = safe_require("packer") + if packer then + pcall(packer.reset) + end + -- Remove the entire packer directory from rtp + local packer_rtp = vim.fn.glob(PATHS.packer_dir) + if packer_rtp then + local rtp_items = vim.split(vim.o.rtp, ",") + local new_rtp_items = {} + for _, item in ipairs(rtp_items) do + if item ~= packer_rtp then + table.insert(new_rtp_items, item) + end + end + vim.o.rtp = table.concat(new_rtp_items, ",") + end + elseif manager_name == "lazy" then + -- Lazy.nvim clears its state on each run, but we can remove it from rtp for good measure + local lazy_rtp = vim.fn.glob(PATHS.lazy) + if lazy_rtp then + local rtp_items = vim.split(vim.o.rtp, ",") + local new_rtp_items = {} + for _, item in ipairs(rtp_items) do + if item ~= lazy_rtp then + table.insert(new_rtp_items, item) + end + end + vim.o.rtp = table.concat(new_rtp_items, ",") + end + elseif manager_name == "builtin" then + -- Built-in manager is handled by vim.opt.packpath and doesn't need manual cleanup from rtp + -- unless we want to disable its packages, which isn't the goal here. + end +end + +-- IMPROVED: Use vim.g for persistence instead of file system +local function save_manager_choice(manager_name) + vim.g.nvim_manager_choice = manager_name + -- Also save to data directory as a simple text file for true persistence across sessions + local data_dir = vim.fn.stdpath("data") + local choice_file = data_dir .. "/.manager_choice" + local file = io.open(choice_file, "w") + if file then + file:write(manager_name) + file:close() + end +end + +local function load_manager_choice() + -- First check vim.g (current session) + if vim.g.nvim_manager_choice then + return vim.g.nvim_manager_choice + end + + -- Then check persistent file + local data_dir = vim.fn.stdpath("data") + local choice_file = data_dir .. "/.manager_choice" + local file = io.open(choice_file, "r") + if file then + local choice = file:read("*a"):gsub("%s+", "") -- trim whitespace + file:close() + if choice and choice ~= "" then + vim.g.nvim_manager_choice = choice -- cache in session + return choice + end + end + + return nil +end + +--- Packer Manager Implementation +-- +-- Handles cloning, setup, and configuration of Packer.nvim. +local Packer = {} + +function Packer.bootstrap() + if state.bootstrap_completed.packer then + return true + end + + local fn = vim.fn + if fn.isdirectory(PATHS.packer_dir) == 0 then + fn.mkdir(PATHS.packer_dir, "p") + end + + if fn.empty(fn.glob(PATHS.packer)) > 0 then + local is_windows = vim.loop.os_uname().version:match("Windows") + local git_cmd + + if is_windows then + git_cmd = string.format( + 'git clone --depth=1 https://github.com/wbthomason/packer.nvim "%s" >nul 2>&1', + PATHS.packer + ) + else + git_cmd = string.format( + 'env -i PATH="%s" HOME="%s" git clone --depth=1 --quiet https://github.com/wbthomason/packer.nvim %q >/dev/null 2>&1', + os.getenv("PATH") or "/usr/bin:/bin", + os.getenv("HOME") or "/tmp", + PATHS.packer + ) + end + + if not execute_git_command(git_cmd, "Failed to clone packer.nvim") then + return false + end + end + + state.bootstrap_completed.packer = true + return true +end + +function Packer.setup() + if not Packer.bootstrap() then + return false + end + + -- Ensure packer.nvim is in the runtime path + vim.cmd("packadd packer.nvim") + + local packer = safe_require("packer") + if not packer then + notify("Failed to load packer.nvim", vim.log.levels.ERROR) + return false + end + + -- Reset any existing configuration from a previous run + pcall(packer.reset) + + packer.init({ + auto_reload_compiled = true, + display = { + open_fn = function() + return require("packer.util").float({ border = "rounded" }) + end, + }, + luarocks = { + python_cmd = 'python3' + }, + }) + + local plugins = safe_require("setup.plugins") + if not plugins then + notify("Failed to load plugins configuration", vim.log.levels.ERROR) + return false + end + + packer.startup(function(use) + use "wbthomason/packer.nvim" + for _, plugin in ipairs(plugins) do + -- CHECK FOR EXCLUDE HERE - Packer support for exclude option + if plugin.exclude and vim.tbl_contains(plugin.exclude, "packer") then + --notify("Excluding plugin for packer: " .. (plugin.name or plugin.as or plugin[1] or "unknown"), vim.log.levels.INFO) + goto continue + end + + -- Packer doesn't have a lazy option, so we ensure all plugins are loaded eagerly + -- by clearing any lazy-loading keys from the plugins table. + local packer_plugin = vim.deepcopy(plugin) + packer_plugin.event = nil + packer_plugin.keys = nil + packer_plugin.cmd = nil + packer_plugin.ft = nil + packer_plugin.lazy = nil + packer_plugin.exclude = nil -- Remove exclude from the actual plugin spec + use(packer_plugin) + ::continue:: + end + end) + + return true +end + +function Packer.is_available() + return vim.fn.isdirectory(PATHS.packer) == 1 +end + +--- Lazy.nvim Manager Implementation +-- +local Lazy = {} + +function Lazy.bootstrap() + if state.bootstrap_completed.lazy then + return true + end + + -- Check if lazy.nvim is already cloned + if not vim.loop.fs_stat(PATHS.lazy) then + local is_windows = vim.loop.os_uname().version:match("Windows") + local git_cmd + + if is_windows then + git_cmd = string.format( + 'git clone --filter=blob:none --branch=stable https://github.com/folke/lazy.nvim.git "%s" >nul 2>&1', + PATHS.lazy + ) + else + git_cmd = string.format( + 'env -i PATH="%s" HOME="%s" git clone --filter=blob:none --branch=stable --quiet https://github.com/folke/lazy.nvim.git %q >/dev/null 2>&1', + os.getenv("PATH") or "/usr/bin:/bin", + os.getenv("HOME") or "/tmp", + PATHS.lazy + ) + end + + if not execute_git_command(git_cmd, "Failed to clone lazy.nvim") then + return false + end + end + + state.bootstrap_completed.lazy = true + return true +end + +function Lazy.setup() + if not Lazy.bootstrap() then + return false + end + + -- Ensure lazy.nvim is in the runtime path before requiring it + vim.opt.rtp:prepend(PATHS.lazy) + + local lazy = safe_require("lazy") + if not lazy then + notify("Failed to load lazy.nvim", vim.log.levels.ERROR) + return false + end + + -- FIX: Correctly require plugins and set up lazy.nvim + local plugins = safe_require("setup.plugins") + if not plugins then + notify("Failed to load plugins configuration", vim.log.levels.ERROR) + return false + end + + -- Filter out excluded plugins for Lazy + local filtered_plugins = {} + for _, plugin in ipairs(plugins) do + -- CHECK FOR EXCLUDE HERE - Lazy support for exclude option + if plugin.exclude and vim.tbl_contains(plugin.exclude, "lazy") then + --notify("Excluding plugin for lazy: " .. (plugin.name or plugin[1] or "unknown"), vim.log.levels.INFO) + else + local lazy_plugin = vim.deepcopy(plugin) + lazy_plugin.exclude = nil -- Remove exclude from the actual plugin spec + table.insert(filtered_plugins, lazy_plugin) + end + end + + -- Setup Lazy.nvim with the correct options + lazy.setup(filtered_plugins, { + { + import = "plugins", + }, + defaults = { lazy = false }, -- Set plugins to be lazy-loaded by default + install = { missing = true }, -- CRITICAL FIX: This ensures missing plugins are installed + ui = { + border = "rounded", + }, + performance = { + rtp = { + disabled_plugins = { + "gzip", "matchit", "matchparen", "netrwPlugin", + "tarPlugin", "tohtml", "tutor", "zipPlugin", + }, + }, + }, + }) + + return true +end + +function Lazy.is_available() + return vim.loop.fs_stat(PATHS.lazy) ~= nil +end + +--- Built-in manager implementation (Neovim 0.12+) +-- +local Builtin = {} + +function Builtin.bootstrap() + if not has_builtin_manager() then + --notify("Built-in package manager not available in this Neovim version", vim.log.levels.WARN) + return false + end + + state.bootstrap_completed.builtin = true + return true +end + +function Builtin.setup() + if not has_builtin_manager() then + --notify("Built-in package manager not available in this Neovim version", vim.log.levels.WARN) + return false + end + + local plugins = safe_require("setup.plugins") + if not plugins then + notify("Failed to load plugins configuration", vim.log.levels.ERROR) + return false + end + + -- Convert plugins to builtin manager format + local builtin_specs = {} + for _, plugin in ipairs(plugins) do + -- CHECK FOR EXCLUDE HERE + if plugin.exclude and vim.tbl_contains(plugin.exclude, "builtin") then + --notify("Excluding plugin for builtin: " .. (plugin.name or plugin[1] or "unknown"), vim.log.levels.INFO) + goto continue + end + local spec = {} + + if type(plugin) == "string" then + -- Handle string format like "user/repo" + if plugin:match("^[%w%-_%.]+/[%w%-_%.]+$") then + -- It's a GitHub shorthand + spec.src = "https://github.com/" .. plugin + spec.name = plugin:match("/([%w%-_%.]+)$") -- Extract repo name + else + -- It's already a full URL + spec.src = plugin + end + elseif type(plugin) == "table" then + -- Handle table format + if plugin[1] and type(plugin[1]) == "string" then + -- Format like {"user/repo", ...} + if plugin[1]:match("^[%w%-_%.]+/[%w%-_%.]+$") then + spec.src = "https://github.com/" .. plugin[1] + spec.name = plugin[1]:match("/([%w%-_%.]+)$") + else + spec.src = plugin[1] + end + + -- Copy other properties + for k, v in pairs(plugin) do + if type(k) == "string" then + spec[k] = v + end + end + elseif plugin.src then + spec.src = plugin.src + for k, v in pairs(plugin) do + if k ~= "src" then + spec[k] = v + end + end + elseif plugin.url then + spec.src = plugin.url + for k, v in pairs(plugin) do + if k ~= "url" then + spec[k] = v + end + end + else + notify("Invalid plugin specification for built-in manager: " .. vim.inspect(plugin), vim.log.levels.WARN) + goto continue + end + + -- Handle name override + if plugin.name then + spec.name = plugin.name + elseif plugin.as then + spec.name = plugin.as + elseif not spec.name and spec.src then + -- Extract name from URL if not specified + spec.name = spec.src:match("/([%w%-_%.]+)%.git$") or spec.src:match("/([%w%-_%.]+)$") or spec.src + end + + -- Handle version + if plugin.version then + spec.version = plugin.version + end + + -- Remove keys that builtin manager doesn't understand + spec.lazy = nil + spec.event = nil + spec.keys = nil + spec.cmd = nil + spec.ft = nil + spec.dependencies = nil + spec.config = nil + spec.build = nil + spec.run = nil + spec.priority = nil + spec.as = nil + spec.url = nil + spec.exclude = nil + spec[1] = nil -- Remove positional argument + end + + if spec.src then + table.insert(builtin_specs, spec) + end + ::continue:: + end + + -- Debug: Show what we're about to install + --notify(string.format("Installing %d plugins with built-in manager", #builtin_specs), vim.log.levels.INFO) + + -- CRITICAL FIX: Call vim.pack.add with the specs directly, not wrapped in array + if #builtin_specs > 0 then + local ok, err = pcall(vim.pack.add, builtin_specs) + if not ok then + notify("Failed to add plugins: " .. tostring(err), vim.log.levels.ERROR) + return false + end + + --notify("Plugins added successfully. Use :Pack to install/update them.", vim.log.levels.INFO) + else + notify("No valid plugins found for built-in manager", vim.log.levels.WARN) + end + + -- Create user commands for convenience - FIXED COMMAND NAMES + vim.api.nvim_create_user_command("Package", function(opts) + local subcommand = opts.fargs[1] or "update" + local names = vim.list_slice(opts.fargs, 2) + + if subcommand == "add" then + -- For add, we need to re-run setup to add new plugins + --notify("Re-running builtin manager setup to add new plugins...") + Builtin.setup() + elseif subcommand == "update" then + if #names == 0 then + names = nil -- Update all plugins + end + vim.pack.update(names) + elseif subcommand == "status" then + local plugins = vim.pack.get() + print(string.format("Built-in manager: %d plugins managed", #plugins)) + for _, plugin in ipairs(plugins) do + local status = plugin.active and "active" or "inactive" + print(string.format(" %s (%s): %s", plugin.spec.name, status, plugin.path)) + end + else + -- Default behavior - treat as update + if subcommand then + table.insert(names, 1, subcommand) + end + if #names == 0 then + names = nil + end + vim.pack.update(names) + end + end, { + nargs = "*", + complete = function(arglead, cmdline, cursorpos) + local args = vim.split(cmdline, "%s+") + if #args <= 2 then + -- Complete subcommands + local subcommands = { "add", "update", "status" } + local matches = {} + for _, cmd in ipairs(subcommands) do + if cmd:find("^" .. arglead) then + table.insert(matches, cmd) + end + end + return matches + else + -- Complete plugin names + local plugins = vim.pack.get() + local names = {} + for _, plugin in ipairs(plugins) do + if plugin.spec.name:find("^" .. arglead) then + table.insert(names, plugin.spec.name) + end + end + return names + end + end, + desc = "Manage plugins with built-in manager. Usage: :Pack [add|update|status] [plugin_names...]" + }) + + ---- Keep the old command for backwards compatibility + --vim.api.nvim_create_user_command("PackageStatus", function() + -- vim.cmd("Pack status") + --end, { + -- nargs = 0, + -- desc = "Show status of plugins managed by built-in manager (deprecated, use :Pack status)" + --}) + + return true +end + +function Builtin.is_available() + return has_builtin_manager() +end + +--- Manager registry +-- +local MANAGERS = { + packer = Packer, + lazy = Lazy, + builtin = Builtin, +} + +--- Core management functions +-- +local function activate_manager(manager_name) + local manager = MANAGERS[manager_name] + if not manager then + notify("Unknown manager: " .. manager_name, vim.log.levels.ERROR) + return false + end + + -- Cleanup the old manager before activating the new one to prevent runtime conflicts. + if state.manager_invoked and state.manager_invoked ~= manager_name then + cleanup_manager(state.manager_invoked) + end + + if not manager.bootstrap() then + return false + end + + local ok = manager.setup() + if ok then + state.manager_invoked = manager_name + -- CRITICAL FIX: Persist the manager choice after successful setup + save_manager_choice(manager_name) + end + return ok +end + +--- Auto-detection and command setup +-- +local function setup_auto_detection() + -- Autocmd to activate Packer when Packer commands are used + vim.api.nvim_create_autocmd("CmdUndefined", { + pattern = "Packer*", + callback = function(event) + if state.manager_invoked ~= "packer" then + local ok = activate_manager("packer") + if ok then + -- Re-execute the original command after setup + vim.cmd(event.match) + end + end + end, + desc = "Auto-activate Packer when Packer commands are used" + }) + + -- Autocmd to activate Lazy when Lazy commands are used + vim.api.nvim_create_autocmd("CmdUndefined", { + pattern = "Lazy*", + callback = function(event) + if state.manager_invoked ~= "lazy" then + local ok = activate_manager("lazy") + if ok then + -- CRITICAL FIX: Use vim.schedule to defer the command execution + -- This ensures Lazy's setup is complete before running the command. + vim.schedule(function() + pcall(vim.cmd, event.match) + end) + end + end + end, + desc = "Auto-activate Lazy and re-execute command" + }) + + vim.api.nvim_create_autocmd("CmdUndefined", { + pattern = "Package*", + callback = function(event) + if state.manager_invoked ~= "builtin" and has_builtin_manager() then + local ok = activate_manager("builtin") + if ok then + vim.cmd(event.match) + end + end + end, + desc = "Auto-activate built-in manager when Pack commands are used" + }) +end + +--- Public API +-- +function M.setup() + if state.initialized then + return + end + + -- Initial bootstrap attempt for all managers to see what's available + for name, manager in pairs(MANAGERS) do + -- CRITICAL FIX: Always bootstrap, but don't set up yet + pcall(manager.bootstrap) + end + + -- CRITICAL FIX: Check for a previously saved choice + local persistent_choice = load_manager_choice() + if persistent_choice and MANAGERS[persistent_choice] then + -- If a choice exists, immediately activate that manager for this session + activate_manager(persistent_choice) + else + -- If no choice exists, set up the autocmds to wait for a command + setup_auto_detection() + end + + state.initialized = true +end + +function M.use_manager(manager_name) + if not state.initialized then + M.setup() + end + + local available = M.available_managers() + if not vim.tbl_contains(available, manager_name) then + notify(string.format("Manager '%s' is not available. Available: %s", + manager_name, table.concat(available, ", ")), vim.log.levels.WARN) + return false + end + + return activate_manager(manager_name) +end + +function M.available_managers() + local managers = {} + for name, manager in pairs(MANAGERS) do + if manager.is_available() then + table.insert(managers, name) + end + end + return managers +end + +function M.current_manager() + return state.manager_invoked +end + +function M.status() + local info = { + initialized = state.initialized, + current_manager = state.manager_invoked, + available_managers = M.available_managers(), + bootstrap_completed = state.bootstrap_completed, + } + + print("=== Neovim Plugin Manager Status ===") + print(string.format("Initialized: %s", tostring(info.initialized))) + print(string.format("Current Manager: %s", info.current_manager or "None")) + print(string.format("Available Managers: %s", table.concat(info.available_managers, ", "))) + + -- FIX: Properly format the Neovim version + local major, minor, patch = get_nvim_version() + print(string.format("Neovim Version: %d.%d.%d", major, minor, patch)) + print(string.format("Built-in Support: %s", tostring(has_builtin_manager()))) + + return info +end + +-- FIX: Added M.get_nvim_version function to the public API +function M.get_nvim_version() + local major, minor, patch = get_nvim_version() + return { major = major, minor = minor, patch = patch } +end + +function M.reset_nvim() + vim.ui.input({ + prompt = "Are you sure you want to reset Neovim? This will delete all data, state, cache, and plugins. (y/N): " + }, function(input) + if input and input:lower() == "y" then + local fn = vim.fn + local is_windows = vim.loop.os_uname().version:match("Windows") + + local paths_to_remove = { + fn.stdpath("data"), + fn.stdpath("state"), + fn.stdpath("cache"), + fn.stdpath("config") .. "/plugin", + } + + local cmd = "" + if is_windows then + local paths_quoted = {} + for _, path in ipairs(paths_to_remove) do + table.insert(paths_quoted, string.format('"%s"', path)) + end + cmd = "powershell -Command \"Remove-Item " .. + table.concat(paths_quoted, ", ") .. " -Recurse -Force -ErrorAction SilentlyContinue\"" + else + local paths_quoted = {} + for _, path in ipairs(paths_to_remove) do + table.insert(paths_quoted, vim.fn.shellescape(path)) + end + cmd = "rm -rf " .. table.concat(paths_quoted, " ") + end + + notify("Resetting Neovim... Please restart after this operation.") + + vim.defer_fn(function() + local result = os.execute(cmd) + if result ~= 0 then + notify("Reset command may have failed. You might need to delete directories manually.", vim.log.levels.WARN) + else + notify("Reset completed successfully. Please restart Neovim.") + end + end, 100) + else + notify("Reset cancelled.") + end + end) +end + +-- Clear manager choice function +function M.clear_choice() + vim.g.nvim_manager_choice = nil + local data_dir = vim.fn.stdpath("data") + local choice_file = data_dir .. "/.manager_choice" + os.remove(choice_file) + notify("Manager choice cleared. Next command will determine the manager.") +end + +vim.api.nvim_create_user_command("Reset", function() + M.reset_nvim() +end, { + nargs = 0, + desc = "Reset Neovim's data, state, cache, and plugin directories" +}) + +local function manager_command(opts) + local subcommand = opts.fargs[1] + + if subcommand == "status" then + M.status() + elseif subcommand == "packer" or subcommand == "Packer" then + M.use_manager("packer") + elseif subcommand == "lazy" or subcommand == "Lazy" then + M.use_manager("lazy") + elseif subcommand == "builtin" or subcommand == "built-in" or subcommand == "Builtin" or subcommand == "Built-in" then + M.use_manager("builtin") + elseif subcommand == "clear" then + M.clear_choice() + else + print("Unknown subcommand. Try 'status', 'packer', 'lazy', 'builtin' or 'clear'.") + end +end + +vim.api.nvim_create_user_command("Manager", manager_command, { + nargs = "+", + complete = function(arglead) + local subcommands = { "status", "packer", "Packer", "lazy", "Lazy", "builtin", "built-in", "Builtin", "Built-in", + "clear" } + local result = {} + for _, subcommand in ipairs(subcommands) do + if subcommand:find("^" .. arglead, 1) then + table.insert(result, subcommand) + end + end + return result + end, + desc = "Manage plugins. Subcommands: status, packer, lazy, builtin, clear" +}) + +return M diff --git a/common/config/nvim/lua/setup/plugins.lua b/common/config/nvim/lua/setup/plugins.lua new file mode 100755 index 0000000..0fb0886 --- /dev/null +++ b/common/config/nvim/lua/setup/plugins.lua @@ -0,0 +1,609 @@ +-- plugins.lua + +-- Helper to compare current Neovim version +local function version_at_least(minor, major) + local v = vim.version() + major = major or 0 + return v.major > major or (v.major == major and v.minor >= minor) +end + +local function version_below(minor, major) + local v = vim.version() + major = major or 0 + return v.major < major or (v.major == major and v.minor < minor) +end + +-- Normalize version input: number -> {0, number}, table -> itself +local function parse_version(ver) + if type(ver) == "number" then return 0, ver end + if type(ver) == "table" then return ver[1] or 0, ver[2] or 0 end + return 0, 0 +end + +-- Determine if plugin should be loaded based on version +local function should_load_plugin(min_version, max_version) + local min_major, min_minor = parse_version(min_version) + local max_major, max_minor = parse_version(max_version) + + local ok_min = not min_version or version_at_least(min_minor, min_major) + local ok_max = not max_version or version_below(max_minor, max_major) + + return ok_min and ok_max +end + +-- Helper to check if a table contains a specific value +local function contains(table, val) + for _, v in ipairs(table) do + if v == val then + return true + end + end + return false +end + +-- The master list of plugins with all potential options. +-- Keys like 'lazy', 'event', 'keys', 'dependencies' are for Lazy.nvim. +-- Keys like 'config', 'run', 'build' are for all managers. +local universal_plugins = { + -- Core + { "nvim-lua/plenary.nvim", lazy = true }, + { "lewis6991/impatient.nvim" }, + + { + "nvim-treesitter/nvim-treesitter", + min_version = 9, + event = "BufReadPre", + }, + { "nvim-treesitter/nvim-treesitter-textobjects", dependencies = { "nvim-treesitter/nvim-treesitter" } }, + { "nvim-treesitter/playground", cmd = "TSPlaygroundToggle" }, + + -- LSP + { "nvimtools/none-ls.nvim", event = "BufReadPre" }, + { "neovim/nvim-lspconfig", min_version = { 0, 9 }, event = "BufReadPre" }, + { + "mason-org/mason.nvim", + min_version = 10, + cmd = "Mason", + event = "BufReadPre", + }, + { "mason-org/mason-lspconfig.nvim", dependencies = { "mason-org/mason.nvim" } }, + { + "whoissethdaniel/mason-tool-installer.nvim", + dependencies = { "mason-org/mason.nvim" }, + event = "BufReadPre", + }, + { + "https://git.sr.ht/~whynothugo/lsp_lines.nvim", + name = 'lsp_lines.nvim', + config = function() + require("lsp_lines").setup() + vim.diagnostic.config({ + virtual_text = false, + }) + end, + event = "LspAttach", + }, + { "rmagatti/goto-preview", event = "LspAttach" }, + + -- Linters/Formatters + { "mhartington/formatter.nvim", event = "BufReadPre" }, + { + "jay-babu/mason-null-ls.nvim", + event = "BufReadPre", + }, + + -- Completion + { "hrsh7th/nvim-cmp", event = "InsertEnter", exclude = { "builtin" } }, + { "hrsh7th/cmp-nvim-lsp", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "hrsh7th/cmp-buffer", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "hrsh7th/cmp-path", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "hrsh7th/cmp-cmdline", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "petertriho/cmp-git", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "tamago324/cmp-zsh", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "f3fora/cmp-spell", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "hrsh7th/cmp-calc", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "saadparwaiz1/cmp_luasnip", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "hrsh7th/cmp-nvim-lsp-signature-help", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "rcarriga/cmp-dap", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "micangl/cmp-vimtex", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + + -- Snippets + { "L3MON4D3/LuaSnip", event = "InsertEnter" }, + { "rafamadriz/friendly-snippets", dependencies = { "L3MON4D3/LuaSnip" } }, + + -- Git + { "tpope/vim-fugitive", cmd = { "G", "Git", "Gdiffsplit" }, event = "VeryLazy" }, + { "kdheepak/lazygit.nvim", cmd = "LazyGit", keys = "<leader>gg" }, + { "lewis6991/gitsigns.nvim", min_version = 11, dependencies = { "nvim-lua/plenary.nvim" }, event = "BufReadPre" }, + + -- UI/UX & Enhancements + { "rcarriga/nvim-notify", lazy = false }, + { + "nvim-tree/nvim-tree.lua", + --cmd = { "NvimTreeToggle", "NvimTreeFocus", "NvimTreeFindFile" }, + --keys = { "<C-n>", "<leader>e" }, + lazy = false, + dependencies = { "nvim-tree/nvim-web-devicons"}, + config = function() + require("plugins.nvim-tree").setup() + end, + }, + { "ThePrimeagen/harpoon", keys = { "<leader>h" } }, + { "airblade/vim-rooter", event = "BufEnter" }, + { "ibhagwan/fzf-lua", cmd = "FzfLua" }, + + --- **Telescope** --- + { + "nvim-telescope/telescope.nvim", + dependencies = { "nvim-lua/plenary.nvim" }, + config = function() + require("plugins.telescope").setup() + end, + }, + { + "nvim-telescope/telescope-fzf-native.nvim", + build = "make", + cond = function() + return vim.fn.executable("make") == 1 + end, + dependencies = { "nvim-telescope/telescope.nvim" }, + }, + { "nvim-telescope/telescope-live-grep-args.nvim", dependencies = { "nvim-telescope/telescope.nvim" } }, + { "nvim-telescope/telescope-ui-select.nvim", dependencies = { "nvim-telescope/telescope.nvim" } }, + { "nvim-telescope/telescope-project.nvim", dependencies = { "nvim-telescope/telescope.nvim" } }, + { "nvim-telescope/telescope-media-files.nvim", dependencies = { "nvim-telescope/telescope.nvim" } }, + { "nvim-telescope/telescope-file-browser.nvim", dependencies = { "nvim-telescope/telescope.nvim" } }, + { "nvim-telescope/telescope-symbols.nvim", dependencies = { "nvim-telescope/telescope.nvim" } }, + { "nvim-telescope/telescope-dap.nvim", dependencies = { "nvim-telescope/telescope.nvim" } }, + { "axkirillov/telescope-changed-files", dependencies = { "nvim-telescope/telescope.nvim" } }, + { "smartpde/telescope-recent-files", dependencies = { "nvim-telescope/telescope.nvim" } }, + --- End Telescope --- + + -- Neovim UX + { "folke/neodev.nvim", ft = "lua" }, + { + "numToStr/Navigator.nvim", + lazy = false, + config = function() + require("Navigator").setup() + end, + }, + { "tpope/vim-eunuch", cmd = { "Rename", "Delete", "Mkdir" } }, + { "tpope/vim-unimpaired", lazy = true, event = "VeryLazy" }, + { "kylechui/nvim-surround", event = "VeryLazy" }, + { + "mbbill/undotree", + cmd = "UndotreeToggle", + keys = "<leader>u", + event = "BufReadPre" + }, + { + "myusuf3/numbers.vim", + event = "BufReadPost", + config = function() + vim.cmd("let g:numbers_exclude = ['dashboard']") + end, + }, + { "windwp/nvim-autopairs", event = "InsertEnter" }, + { "numToStr/Comment.nvim", keys = { "gc", "gb" }, event = "VeryLazy" }, + { "akinsho/toggleterm.nvim", cmd = { "ToggleTerm", "TermExec" } }, + { "tweekmonster/startuptime.vim", cmd = "StartupTime" }, + { "qpkorr/vim-bufkill", cmd = { "BD", "BUN" } }, + { + "ggandor/leap.nvim", + keys = { "s", "S" }, + event = "VeryLazy", + config = function() + require("leap").add_default_mappings() + end, + }, + { + "ggandor/flit.nvim", + keys = { "f", "F", "t", "T" }, + event = "VeryLazy", + config = function() + require("flit").setup() + end, + }, + { + "folke/which-key.nvim", + min_version = { 0, 10 }, + event = "VeryLazy", + --keys = "<leader>", + config = function() + require("which-key").setup() + end, + }, + { "folke/zen-mode.nvim", cmd = "ZenMode" }, + { "romainl/vim-cool", event = "VeryLazy" }, + { "antoinemadec/FixCursorHold.nvim", lazy = true }, + { "folke/trouble.nvim", cmd = { "Trouble", "TroubleToggle" } }, + + -- Colorschemes & Visuals (load immediately) + { "nyoom-engineering/oxocarbon.nvim", lazy = false, priority = 1000 }, + { "bluz71/vim-nightfly-guicolors", lazy = false, priority = 1000 }, + { "ayu-theme/ayu-vim", lazy = false, priority = 1000 }, + { "joshdick/onedark.vim", lazy = false, priority = 1000 }, + { "NTBBloodbath/doom-one.nvim", lazy = false, priority = 1000 }, + { "nyngwang/nvimgelion", lazy = false, priority = 1000 }, + { "projekt0n/github-nvim-theme", lazy = false, priority = 1000 }, + { "folke/tokyonight.nvim", lazy = false, priority = 1000 }, + { "ribru17/bamboo.nvim", lazy = false, priority = 1000 }, + + -- UI Enhancements + --{ "kyazdani42/nvim-web-devicons", lazy = true }, + { "nvim-tree/nvim-web-devicons", lazy = true }, + { "onsails/lspkind-nvim", dependencies = { "hrsh7th/nvim-cmp" }, exclude = { "builtin" } }, + { "kevinhwang91/nvim-ufo", dependencies = { "kevinhwang91/promise-async" }, event = "BufReadPre" }, + { + "luukvbaal/statuscol.nvim", + event = "WinNew", + config = function() + local builtin = require("statuscol.builtin") + require("statuscol").setup({ + relculright = true, + segments = { + { text = { builtin.foldfunc }, click = "v:lua.ScFa" }, + { text = { "%s" }, click = "v:lua.ScSa" }, + { text = { builtin.lnumfunc, " " }, click = "v:lua.ScLa" }, + }, + }) + end, + }, + { "lukas-reineke/indent-blankline.nvim", event = "BufReadPre" }, + { + "glepnir/dashboard-nvim", + dependencies = { "nvim-tree/nvim-web-devicons" }, + cmd = "Dashboard", + }, + { "karb94/neoscroll.nvim", event = "BufReadPre" }, + { "MunifTanjim/prettier.nvim", event = "BufWritePre" }, + { + "norcalli/nvim-colorizer.lua", + cmd = { "ColorizerToggle", "ColorizerAttachToBuffer" }, + config = function() + require("colorizer").setup({ + user_default_options = { + RGB = true, + RRGGBB = true, + names = false, + RRGGBBAA = false, + css = false, + css_fn = true, + mode = "foreground", + }, + }) + end, + }, + { "MunifTanjim/nui.nvim" }, + { "metakirby5/codi.vim", cmd = "Codi" }, + { + "kosayoda/nvim-lightbulb", + dependencies = { "antoinemadec/FixCursorHold.nvim" }, + event = "LspAttach", + }, + { "SmiteshP/nvim-navic", event = "LspAttach" }, + { + "rebelot/heirline.nvim", + event = "VeryLazy", + dependencies = { + "nvim-tree/nvim-web-devicons", -- For file icons + "lewis6991/gitsigns.nvim", -- For git status + }, + config = function() + -- Ensure gitsigns is loaded before Heirline + if package.loaded["gitsigns"] == nil then + require("gitsigns").setup() + end + local ok, heirline = pcall(require, "plugins.heirline") + if ok and heirline then + heirline.setup() + else + vim.notify("Failed to load Heirline configuration", vim.log.levels.ERROR) + end + end, + init = function() + -- Set up the statusline to use Heirline once it's loaded + vim.opt.statusline = "%{%v:lua.require'heirline'.eval_statusline()%}" + vim.opt.winbar = "%{%v:lua.require'heirline'.eval_winbar()%}" + vim.opt.tabline = "%{%v:lua.require'heirline'.eval_tabline()%}" + end, + }, + { + "samodostal/image.nvim", + config = function() + require("image").setup({}) + end, + ft = { "markdown" }, + }, + + -- Language Specific + { "simrat39/rust-tools.nvim", ft = "rust" }, + { + "saecki/crates.nvim", + dependencies = { "nvim-lua/plenary.nvim" }, + config = function() + require("crates").setup() + end, + ft = "rust", + }, + { + "akinsho/flutter-tools.nvim", + dependencies = { + "nvim-lua/plenary.nvim", + "stevearc/dressing.nvim", + }, + config = function() + require("flutter-tools").setup({ + debugger = { + enabled = true, + run_via_dap = true, + }, + }) + end, + ft = "dart", + }, + { + "iamcco/markdown-preview.nvim", + build = "cd app && npm install", + ft = "markdown", + config = function() + vim.g.mkdp_filetypes = { "markdown" } + vim.cmd("let g:mkdp_auto_close = 0") + end, + cmd = "MarkdownPreview", + }, + { + "ellisonleao/glow.nvim", + config = function() + local glow_path = vim.fn.executable("~/.local/bin/glow") == 1 and "~/.local/bin/glow" or "/usr/bin/glow" + require("glow").setup({ + style = "dark", + glow_path = glow_path, + }) + end, + ft = "markdown", + }, + + -- Debugging + { "mfussenegger/nvim-dap", event = "VeryLazy" }, + { "rcarriga/nvim-dap-ui", dependencies = { "mfussenegger/nvim-dap" }, cmd = "DapUI" }, + { "theHamsta/nvim-dap-virtual-text", dependencies = { "mfussenegger/nvim-dap" } }, + { "gabrielpoca/replacer.nvim", cmd = "Replacer" }, + { "jayp0521/mason-nvim-dap.nvim", dependencies = { "mason-org/mason.nvim" } }, + + -- Misc + { "rmagatti/auto-session", event = "VimEnter" }, + { "tpope/vim-sleuth", lazy = true }, + { "michaelb/sniprun", build = "bash ./install.sh", cmd = "SnipRun" }, + { "stevearc/overseer.nvim", cmd = "Overseer" }, + { + "nvim-neotest/neotest", + dependencies = { + "nvim-neotest/neotest-python", + "nvim-neotest/neotest-plenary", + "nvim-neotest/neotest-vim-test", + "nvim-neotest/nvim-nio", + }, + cmd = "Neotest", + }, + { "kawre/leetcode.nvim", cmd = "Leetcode" }, + { "m4xshen/hardtime.nvim", lazy = true }, + + -- LaTeX + { "lervag/vimtex", ft = "tex" }, +} + +-- Helper function to detect current manager +local function detect_current_manager() + -- Check if we're currently using lazy (by checking if lazy module exists) + if package.loaded["lazy"] or package.loaded["lazy.core.util"] then + return "lazy" + end + + -- Check if we're currently using packer + if package.loaded["packer"] then + return "packer" + end + + -- Check for builtin manager + if vim.plugins and vim.plugins.spec then + return "builtin" + end + + return "unknown" +end + +local function format_for_lazy(plugin) + -- Lazy.nvim's format is the closest to our universal format, so we can + -- largely just copy the table, with some specific adjustments. + local new_plugin = vim.deepcopy(plugin) + + -- Lazy.nvim uses `dependencies` key for dependencies, not `requires` + if new_plugin.requires then + new_plugin.dependencies = new_plugin.requires + new_plugin.requires = nil + end + + -- Change 'as' to 'name' for lazy + if new_plugin.as then + new_plugin.name = new_plugin.as + new_plugin.as = nil + end + + -- Change 'run' to 'build' for lazy + if new_plugin.run then + new_plugin.build = new_plugin.run + new_plugin.run = nil + end + + return new_plugin +end + +local function format_for_packer(plugin) + -- For Packer, we need to remove lazy-loading keys to force eager loading + local new_plugin = vim.deepcopy(plugin) + + -- Convert dependencies back to requires for packer + if new_plugin.dependencies then + new_plugin.requires = new_plugin.dependencies + new_plugin.dependencies = nil + end + + -- Convert name back to as for packer + if new_plugin.name then + new_plugin.as = new_plugin.name + new_plugin.name = nil + end + + -- Convert build back to run for packer + if new_plugin.build then + new_plugin.run = new_plugin.build + new_plugin.build = nil + end + + -- Remove lazy-loading keys to force eager loading in packer + new_plugin.event = nil + new_plugin.keys = nil + new_plugin.cmd = nil + new_plugin.ft = nil + new_plugin.lazy = nil + + return new_plugin +end + +local function format_for_builtin(plugin) + -- This function is now simplified, as the main loop handles flattening + local new_plugin = vim.deepcopy(plugin) + + -- Convert GitHub shorthand to full URL if needed + if type(new_plugin) == "string" then + if new_plugin:match("^[%w%-_%.]+/[%w%-_%.]+$") then + return { + src = "https://github.com/" .. new_plugin, + name = new_plugin:match("/([%w%-_%.]+)$") + } + else + return { src = new_plugin } + end + end + + -- Handle table format + if new_plugin[1] and type(new_plugin[1]) == "string" then + local repo = new_plugin[1] + if repo:match("^[%w%-_%.]+/[%w%-_%.]+$") then + new_plugin.src = "https://github.com/" .. repo + new_plugin.name = new_plugin.name or new_plugin.as or repo:match("/([%w%-_%.]+)$") + else + new_plugin.src = repo + end + new_plugin[1] = nil -- Remove positional argument + end + + -- Convert url to src if present + if new_plugin.url then + new_plugin.src = new_plugin.url + new_plugin.url = nil + end + + -- Convert 'as' to 'name' + if new_plugin.as then + new_plugin.name = new_plugin.as + new_plugin.as = nil + end + + -- Only keep the keys that vim.pack uses: src, name, and version + new_plugin.dependencies = nil + new_plugin.config = nil + new_plugin.build = nil + new_plugin.run = nil + new_plugin.cond = nil + new_plugin.min_version = nil + new_plugin.max_version = nil + new_plugin.lazy = nil + new_plugin.priority = nil + new_plugin.event = nil + new_plugin.keys = nil + new_plugin.cmd = nil + new_plugin.ft = nil + new_plugin.requires = nil + + return new_plugin +end + +-- Detect which manager is currently active and format plugins accordingly +local current_manager = detect_current_manager() +local plugins_to_process = {} +local processed_plugins = {} -- Use a set to avoid duplicates + +-- Flatten the plugin list for the builtin manager +if current_manager == "builtin" then + local function get_plugin_name(plugin) + if type(plugin) == "string" then + return plugin:match("/([%w%-_%.]+)$") or plugin + elseif type(plugin) == "table" then + -- Get name from 'name', 'as', or from the src/url + return plugin.name or plugin.as or (type(plugin[1]) == "string" and plugin[1]:match("/([%w%-_%.]+)$")) or + plugin.url:match("/([%w%-_%.]+)$") + end + end + + local function add_to_process(plugin) + local name = get_plugin_name(plugin) + if name and not processed_plugins[name] then + table.insert(plugins_to_process, plugin) + processed_plugins[name] = true + end + end + + for _, plugin in ipairs(universal_plugins) do + add_to_process(plugin) + if plugin.dependencies then + for _, dep in ipairs(plugin.dependencies) do + add_to_process(dep) + end + end + if plugin.requires then + for _, req in ipairs(plugin.requires) do + add_to_process(req) + end + end + end +else + plugins_to_process = universal_plugins +end + +local finalized_plugins = {} + +for _, plugin in ipairs(plugins_to_process) do + local cond_ok = true + + -- Check for the new 'exclude' option first + if plugin.exclude and contains(plugin.exclude, current_manager) then + cond_ok = false + end + + if cond_ok and (plugin.min_version or plugin.max_version) then + cond_ok = should_load_plugin(plugin.min_version, plugin.max_version) + end + if cond_ok and plugin.cond then + cond_ok = plugin.cond() + end + + if cond_ok then + local new_plugin + if current_manager == "lazy" then + new_plugin = format_for_lazy(plugin) + elseif current_manager == "packer" then + new_plugin = format_for_packer(plugin) + elseif current_manager == "builtin" then + new_plugin = format_for_builtin(plugin) + else + -- Default to lazy format if manager is unknown + new_plugin = format_for_lazy(plugin) + end + table.insert(finalized_plugins, new_plugin) + end +end + +return finalized_plugins diff --git a/common/config/nvim/lua/user/keys.lua b/common/config/nvim/lua/user/keys.lua new file mode 100755 index 0000000..63b64fa --- /dev/null +++ b/common/config/nvim/lua/user/keys.lua @@ -0,0 +1,928 @@ +-- ============================================================================ +-- Key Mappings +-- ============================================================================ + +local map = function(mode, l, r, opts) + if r == nil then + vim.notify("Attempted to map key '" .. l .. "' but RHS is nil", vim.log.levels.WARN) + return + end + opts = vim.tbl_extend('force', { + silent = true, + noremap = true + }, opts or {}) + vim.keymap.set(mode, l, r, opts) +end + +-- Leader key +vim.g.mapleader = ";" +vim.g.maplocalleader = "\\" + +-- Tmux/Vim navigation +local function smart_move(direction, tmux_cmd) + local curwin = vim.api.nvim_get_current_win() + vim.cmd('wincmd ' .. direction) + if curwin == vim.api.nvim_get_current_win() then + vim.fn.system('tmux select-pane ' .. tmux_cmd) + end +end + +-- Window Navigation +map('n', '<C-h>', function() smart_move('h', '-L') end) +map('n', '<C-j>', function() smart_move('j', '-D') end) +map('n', '<C-k>', function() smart_move('k', '-U') end) +map('n', '<C-l>', function() smart_move('l', '-R') end) + +-- Buffer Navigation +map('n', '<leader>bn', '<cmd>bnext<CR>') +map('n', '<leader>bp', '<cmd>bprevious<CR>') +--map('n', '<leader>bd', '<cmd>bdelete<CR>') +map('n', '<leader>ba', '<cmd>%bdelete<CR>') + + + +-- Get list of loaded buffers in order +local function get_buffers() + local bufs = {} + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_loaded(buf) then + table.insert(bufs, buf) + end + end + return bufs +end + +-- Swap two buffers by index in the buffer list +local function swap_buffers(idx1, idx2) + local bufs = get_buffers() + local buf1 = bufs[idx1] + local buf2 = bufs[idx2] + if not buf1 or not buf2 then return end + local name1 = vim.api.nvim_buf_get_name(buf1) + local name2 = vim.api.nvim_buf_get_name(buf2) + vim.cmd("b " .. buf1) + vim.cmd("file " .. name2) + vim.cmd("b " .. buf2) + vim.cmd("file " .. name1) +end + +-- Move current buffer left +vim.keymap.set("n", "<leader>bh", function() + local bufs = get_buffers() + local curr = vim.api.nvim_get_current_buf() + local idx + for i, b in ipairs(bufs) do if b == curr then idx = i break end end + if idx and idx > 1 then + swap_buffers(idx, idx-1) + end +end, { noremap = true, silent = true }) + +-- Move current buffer right +vim.keymap.set("n", "<leader>bl", function() + local bufs = get_buffers() + local curr = vim.api.nvim_get_current_buf() + local idx + for i, b in ipairs(bufs) do if b == curr then idx = i break end end + if idx and idx < #bufs then + swap_buffers(idx, idx+1) + end +end, { noremap = true, silent = true }) +-- Save and Quit +map('n', '<leader>w', '<cmd>w<CR>') +map('n', '<leader>q', '<cmd>q<CR>') +map('n', '<leader>wq', '<cmd>wq<CR>') +map('n', '<leader>Q', '<cmd>qa!<CR>') + +-- Resize Windows +map('n', '<M-Up>', '<cmd>resize -2<CR>') +map('n', '<M-Down>', '<cmd>resize +2<CR>') +map('n', '<M-Left>', '<cmd>vertical resize -2<CR>') +map('n', '<M-Right>', '<cmd>vertical resize +2<CR>') + +-- Quickfix and Location List +map('n', ']q', '<cmd>cnext<CR>zz') +map('n', '[q', '<cmd>cprev<CR>zz') +map('n', ']l', '<cmd>lnext<CR>zz') +map('n', '[l', '<cmd>lprev<CR>zz') + +-- Terminal Mode +map('t', '<Esc>', '<C-\\><C-n>') +map('t', '<C-h>', '<C-\\><C-n><C-w>h') +map('t', '<C-j>', '<C-\\><C-n><C-w>j') +map('t', '<C-k>', '<C-\\><C-n><C-w>k') +map('t', '<C-l>', '<C-\\><C-n><C-w>l') + +-- Insert mode escape +map('i', 'jk', '<ESC>') + +-- Tmux/(n)vim navigation +local function smart_move(direction, tmux_cmd) + local curwin = vim.api.nvim_get_current_win() + vim.cmd('wincmd ' .. direction) + if curwin == vim.api.nvim_get_current_win() then + vim.fn.system('tmux select-pane ' .. tmux_cmd) + end +end + +map('n', '<C-h>', function() smart_move('h', '-L') end, {silent = true}) +map('n', '<C-j>', function() smart_move('j', '-D') end, {silent = true}) +map('n', '<C-k>', function() smart_move('k', '-U') end, {silent = true}) +map('n', '<C-l>', function() smart_move('l', '-R') end, {silent = true}) + + +-- Jump to next match on line using `.` instead of `;` NOTE: commented out in favour of "ggandor/flit.nvim" +--map("n", ".", ";") + +-- Repeat last command using `<Space>` instead of `.` NOTE: commented out in favour of "ggandor/flit.nvim" +--map("n", "<Space>", ".") + +-- Reload nvim config +map("n", "<leader><CR>", +"<cmd>luafile ~/.config/nvim/init.lua<CR> | :echom ('Nvim config loading...') | :sl! | echo ('')<CR>") + +vim.keymap.set("t", "<Esc>", "<C-\\><C-n>", { desc = "Exit terminal mode" }) +vim.keymap.set("t", "<C-b>", "<C-\\><C-n>", { desc = "Exit terminal mode" }) + +--------------- Extended Operations --------------- +-- Conditional 'q' to quit on floating/quickfix/help windows otherwise still use it for macros +-- TODO: Have a list of if available on system/packages, example "Zen Mode" to not work on it (quit Zen Mode) +map("n", "q", function() + local config = vim.api.nvim_win_get_config(0) + if config.relative ~= "" then -- is_floating_window? + return ":silent! close!<CR>" + elseif vim.o.buftype == "quickfix" then + return ":quit<CR>" + elseif vim.o.buftype == "help" then + return ":close<CR>" + else + return "q" + end +end, { expr = true, replace_keycodes = true }) + +-- Minimalist Tab Completion +map("i", "<Tab>", function() + local col = vim.fn.col('.') - 1 + local line = vim.fn.getline('.') + local prev_char = line:sub(col, col) + if vim.fn.pumvisible() == 1 or prev_char:match("%w") then + return vim.api.nvim_replace_termcodes("<C-n>", true, true, true) + else + return vim.api.nvim_replace_termcodes("<Tab>", true, true, true) + end +end, { expr = true }) + +-- Shift-Tab for reverse completion +map("i", "<S-Tab>", function() + if vim.fn.pumvisible() == 1 then + return vim.api.nvim_replace_termcodes("<C-p>", true, true, true) + else + return vim.api.nvim_replace_termcodes("<S-Tab>", true, true, true) + end +end, { expr = true }) + + +-- Toggle completion +map("n", "<Leader>tc", ':lua require("user.mods").toggle_completion()<CR>') + +-- Minimalist Auto Completion +map("i", "<CR>", function() + -- Exit this keymap if nvim-cmp is present + local cmp_is_present, _ = pcall(require, "cmp") + if cmp_is_present and require("cmp").visible() then + return vim.api.nvim_replace_termcodes("<C-y>", true, true, true) + elseif cmp_is_present then + return vim.api.nvim_replace_termcodes("<CR>", true, true, true) + end + + -- when cmp is NOT present + if vim.fn.pumvisible() == 1 then + return vim.api.nvim_replace_termcodes("<C-y>", true, true, true) + else + return vim.api.nvim_replace_termcodes("<CR>", true, true, true) + end +end, { expr = true }) + +-- Closing compaction in insert mode +map("i", "[", "[]<Left>") +map("i", "(", "()<Left>") +map("i", "{", "{}<Left>") +map("i", "/*", "/**/<Left><Left>") + +-- Edit new file +map("n", "<leader>e", [[:e <C-R>=expand("%:h")..'/'<CR>]], { noremap = true, silent = true, desc = "New file" }) + +-- Write as sudo +map("c", "W!", "exe 'w !sudo tee >/dev/null %:p:S' | setl nomod", { silent = true, desc = "Write as Sudo" }) + +-- Don't format on save +map("c", "F!", ":noautocmd w<CR>") + +-- Combine buffers list with buffer name +map("n", "<Leader>b", ":buffers<CR>:buffer<Space>") + +-- Buffer confirmation +map("n", "<leader>y", ":BufferPick<CR>") + +-- Map buffer next, prev and delete to <leader>+(n/p/d) respectively and tab/s-tab +map("n", "<leader>n", ":bn<cr>") +map("n", "<leader>p", ":bp<cr>") +map("n", "<leader>d", ":bd<cr>") +map("n", "<TAB>", ":bnext<CR>") +map("n", "<S-TAB>", ":bprevious<CR>") + +-- Close all buffers and reopen last one +map("n", "<leader>D", ":update | %bdelete | edit # | normal `<CR>") + +-- Delete file of current buffer +map("n", "<leader>rm", "<CMD>call delete(expand('%')) | bdelete!<CR>") + +-- List marks +map("n", "<Leader>M", ":marks<CR>") + +-- Messages +map("n", "<Leader>m", ":messages<CR>") + +--- Clear messages or just refresh/redraw the screen +map("n", "<leader>i", function() + local ok, notify = pcall(require, "notify") + if ok then + notify.dismiss() + end +end) + +-- Toggle set number +map("n", "<leader>$", ":NumbersToggle<CR>") +map("n", "<leader>%", ":NumbersOnOff<CR>") + +-- Easier split navigations, just ctrl-j instead of ctrl-w then j +map("t", "<C-[>", "<C-\\><C-N>") +map("t", "<C-h>", "<C-\\><C-N><C-h>") +map("t", "<C-j>", "<C-\\><C-N><C-j>") +map("t", "<C-k>", "<C-\\><C-N><C-k>") +map("t", "<C-l>", "<C-\\><C-N><C-l>") + +-- Split window +map("n", "<leader>-", ":split<CR>") +map("n", "<leader>\\", ":vsplit<CR>") + +-- Close window +--map("n", "<leader>c", "<C-w>c") +map({ "n", "t", "c" }, "<leader>c", function() + local winid = vim.api.nvim_get_current_win() + local config = vim.api.nvim_win_get_config(winid) + + if config.relative ~= "" then + -- This is a floating window + vim.cmd("CloseFloatingWindows") + else + -- Not a float/close window + vim.cmd("close") + end +end, { desc = "Close current float or all floating windows" }) + +-- Resize Panes +map("n", "<Leader><", ":vertical resize +5<CR>") +map("n", "<Leader>>", ":vertical resize -5<CR>") +map("n", "<Leader>=", "<C-w>=") + +-- Mapping for left and right arrow keys in command-line mode +vim.api.nvim_set_keymap("c", "<A-h>", "<Left>", { noremap = true, silent = false }) -- Left Arrow +vim.api.nvim_set_keymap("c", "<A-l>", "<Right>", { noremap = true, silent = false }) -- Right Arrow + +-- Map Alt+(h/j/k/l) in insert(include terminal/command) mode to move directional +map({ "i", "t" }, "<A-h>", "<Left>") +map({ "i", "t" }, "<A-j>", "<Down>") +map({ "i", "t" }, "<A-k>", "<Up>") +map({ "i", "t" }, "<A-l>", "<Right>") + +-- Create tab, edit and move between them +map("n", "<C-T>n", ":tabnew<CR>") +map("n", "<C-T>e", ":tabedit") +map("n", "<leader>[", ":tabprev<CR>") +map("n", "<leader>]", ":tabnext<CR>") + +-- Vim TABs +map("n", "<leader>1", "1gt<CR>") +map("n", "<leader>2", "2gt<CR>") +map("n", "<leader>3", "3gt<CR>") +map("n", "<leader>4", "4gt<CR>") +map("n", "<leader>5", "5gt<CR>") +map("n", "<leader>6", "6gt<CR>") +map("n", "<leader>7", "7gt<CR>") +map("n", "<leader>8", "8gt<CR>") +map("n", "<leader>9", "9gt<CR>") +map("n", "<leader>0", "10gt<CR>") + +-- Hitting ESC when inside a terminal to get into normal mode +--map("t", "<Esc>", [[<C-\><C-N>]]) + +-- Move block (indentation) easily +--map("n", "<", "<<", term_opts) +--map("n", ">", ">>", term_opts) +--map("x", "<", "<gv", term_opts) +--map("x", ">", ">gv", term_opts) +--map("v", "<", "<gv") +--map("v", ">", ">gv") +--map("n", "<", "<S-v><<esc>change mode to normal") +--map("n", ">", "<S-v>><esc>change mode to normal") + +-- Visual mode: Indent and reselect the visual area, like default behavior but explicit +map("v", "<", "<gv", { desc = "Indent left and reselect" }) +map("v", ">", ">gv", { desc = "Indent right and reselect" }) + +-- Normal mode: Indent current line and enter Visual Line mode to repeat easily +map("n", "<", "v<<", { desc = "Indent left and select" }) +map("n", ">", "v>>", { desc = "Indent right and select" }) + +---- Visual mode: Indent and reselect the visual area, like default behavior but explicit +--map("v", "<", "<", { desc = "Indent left" }) +--map("v", ">", ">", { desc = "Indent right" }) +-- +---- Normal mode: Indent current line and enter Visual Line mode to repeat easily +--map("n", "<", "v<<", { desc = "Indent left and select" }) +--map("n", ">", "v>>", { desc = "Indent right and select" }) + +-- Set alt+(j/k) to switch lines of texts or simply move them +map("n", "<A-k>", ':let save_a=@a<Cr><Up>"add"ap<Up>:let @a=save_a<Cr>') +map("n", "<A-j>", ':let save_a=@a<Cr>"add"ap:let @a=save_a<Cr>') + +-- Toggle Diff +map("n", "<leader>df", "<Cmd>call utils#ToggleDiff()<CR>") + +-- Toggle Verbose +map("n", "<leader>uvt", "<Cmd>call utils#VerboseToggle()<CR>") + +-- Jump List +map("n", "<leader>j", "<Cmd>call utils#GotoJump()<CR>") + +-- Rename file +map("n", "<leader>rf", "<Cmd>call utils#RenameFile()<CR>") + +-- Map delete to Ctrl+l +map("i", "<C-l>", "<Del>") + +-- Clear screen +map("n", "<leader><C-l>", "<Cmd>!clear<CR>") + +-- Change file to an executable +map("n", "<Leader>x", +":lua require('user.mods').Toggle_executable()<CR> | :echom ('Toggle executable')<CR> | :sl! | echo ('')<CR>") +-- map("n", "<leader>x", ":!chmod +x %<CR>") + +vim.keymap.set("n", "<leader>cm", function() + vim.cmd("redir @+") + vim.cmd("silent messages") + vim.cmd("redir END") + vim.notify("Copied :messages to clipboard") +end, { desc = "Copy :messages to clipboard" }) + +-- Paste without replace clipboard +map("v", "p", '"_dP') + +map("n", "]p", 'm`o<Esc>"+p``', opts) + +map("n", "[p", 'm`O<Esc>"+p``', opts) + +-- Bind Ctrl-V to paste in insert/normal/command mode +map("i", "<C-v>", "<C-G>u<C-R><C-P>+", opts) +map("n", "<C-v>", '"+p', { noremap = true, silent = true }) +vim.api.nvim_set_keymap("c", "<C-v>", "<C-R>=getreg('+')<CR><BS>", { noremap = true, silent = false }) + +-- Change Working Directory to current project +map("n", "<leader>cd", ":cd %:p:h<CR>:pwd<CR>") + +-- Search and replace +map("v", "<leader>sr", 'y:%s/<C-r><C-r>"//g<Left><Left>c') + +-- Substitute globally and locally in the selected region. +map("n", "<leader>s", ":%s//g<Left><Left>") +map("v", "<leader>s", ":s//g<Left><Left>") + +-- Set line wrap +map("n", "<M-z>", function() + local wrap_status = vim.api.nvim_exec("set wrap ?", true) + + if wrap_status == "nowrap" then + vim.api.nvim_command("set wrap linebreak") + print("Wrap enabled") + else + vim.api.nvim_command("set wrap nowrap") + print("Wrap disabled") + end +end, { silent = true }) + +-- Toggle between folds +map("n", "<Space>", "&foldlevel ? 'zM' : 'zR'", { expr = true }) + +-- Use space to toggle fold +--map("n", "<Space>", "za") + +map("n", "<leader>.b", ":!cp % %.backup<CR>") + +-- Go to next window +map("n", "<leader>wn", "<C-w>w", { desc = "Next window" }) + +-- Go to previous window +map("n", "<leader>wp", "<C-w>W", { desc = "Previous window" }) + +-- Toggle transparency +map("n", "<leader>tb", ":call utils#Toggle_transparent_background()<CR>") + +-- Toggle zoom +map("n", "<leader>z", ":call utils#ZoomToggle()<CR>") +map("n", "<C-w>z", "<C-w>|<C-w>_") + +-- Toggle statusline +map("n", "<leader>sl", ":call utils#ToggleHiddenAll()<CR>") + +-- Open last closed buffer +map("n", "<C-t>", ":call utils#OpenLastClosed()<CR>") + + +-- Automatically set LSP keymaps when LSP attaches to a buffer +--vim.api.nvim_create_autocmd("LspAttach", { +-- callback = function(args) +-- local bufnr = args.buf +-- local opts = { buffer = bufnr } +-- map("n", "K", vim.lsp.buf.hover) +-- map("n", "gd", "<cmd>lua require('goto-preview').goto_preview_definition()<CR>") +-- map("n", "gi", "<cmd>lua require('goto-preview').goto_preview_implementation()<CR>") +-- map("n", "gr", "<cmd>lua require('goto-preview').goto_preview_references()<CR>") +-- map("n", "gD", vim.lsp.buf.declaration) +-- map("n", "<leader>k", vim.lsp.buf.signature_help) +-- map("n", "gt", "<cmd>lua require('goto-preview').goto_preview_type_definition()<CR>") +-- map("n", "gn", vim.lsp.buf.rename) +-- map("n", "ga", vim.lsp.buf.code_action) +-- map("n", "gf", function() vim.lsp.buf.format({ async = true }) end) +-- map("n", "go", vim.diagnostic.open_float) +-- map("n", "<leader>go", ":call utils#ToggleDiagnosticsOpenFloat()<CR> | :echom ('Toggle Diagnostics Float open/close...')<CR> | :sl! | echo ('')<CR>") +-- map("n", "gq", vim.diagnostic.setloclist) +-- map("n", "[d", vim.diagnostic.goto_prev) +-- map("n", "]d", vim.diagnostic.goto_next) +-- map("n", "gs", vim.lsp.buf.document_symbol) +-- map("n", "gw", vim.lsp.buf.workspace_symbol) +-- map("n", "<leader>wa", vim.lsp.buf.add_workspace_folder) +-- map("n", "<leader>wr", vim.lsp.buf.remove_workspace_folder) +-- map("n", "<leader>wl", function() +-- print(vim.inspect(vim.lsp.buf.list_workspace_folders())) +-- end) +-- end, +--}) + +---- LSP Global Keymaps (available in all buffers) +--map("n", "[d", vim.diagnostic.goto_prev, { desc = "LSP: Previous Diagnostic" }) +--map("n", "]d", vim.diagnostic.goto_next, { desc = "LSP: Next Diagnostic" }) +--map("n", "go", vim.diagnostic.open_float, { desc = "LSP: Open Diagnostic Float" }) +-- +---- LSP Buffer-local keymaps function (to be called from LSP on_attach) +--_G.setup_lsp_keymaps = function(bufnr) +-- local bmap = function(mode, l, r, opts) +-- opts = opts or {} +-- opts.silent = true +-- opts.noremap = true +-- opts.buffer = bufnr +-- vim.keymap.set(mode, l, r, opts) +-- end +-- +-- bmap("n", "K", vim.lsp.buf.hover, { desc = "LSP: Hover Documentation" }) +-- bmap("n", "gd", vim.lsp.buf.definition, { desc = "LSP: Go to Definition" }) +-- bmap("n", "gD", vim.lsp.buf.declaration, { desc = "LSP: Go to Declaration" }) +-- bmap("n", "gi", vim.lsp.buf.implementation, { desc = "LSP: Go to Implementation" }) +-- bmap("n", "gt", vim.lsp.buf.type_definition, { desc = "LSP: Go to Type Definition" }) +-- bmap("n", "gr", vim.lsp.buf.references, { desc = "LSP: Go to References" }) +-- bmap("n", "gn", vim.lsp.buf.rename, { desc = "LSP: Rename" }) +-- bmap("n", "ga", vim.lsp.buf.code_action, { desc = "LSP: Code Action" }) +-- bmap("n", "<leader>k", vim.lsp.buf.signature_help, { desc = "LSP: Signature Help" }) +-- bmap("n", "gs", vim.lsp.buf.document_symbol, { desc = "LSP: Document Symbols" }) +--end + +-- LSP Global Keymaps (available in all buffers) +map("n", "[d", vim.diagnostic.goto_prev, { desc = "LSP: Previous Diagnostic" }) +map("n", "]d", vim.diagnostic.goto_next, { desc = "LSP: Next Diagnostic" }) +map("n", "go", vim.diagnostic.open_float, { desc = "LSP: Open Diagnostic Float" }) +map("n", "<leader>go", ":call utils#ToggleDiagnosticsOpenFloat()<CR> | :echom ('Toggle Diagnostics Float open/close...')<CR> | :sl! | echo ('')<CR>") + +-- LSP Buffer-local keymaps function (to be called from LSP on_attach) +_G.setup_lsp_keymaps = function(bufnr) + local bmap = function(mode, l, r, opts) + opts = opts or {} + opts.silent = true + opts.noremap = true + opts.buffer = bufnr + vim.keymap.set(mode, l, r, opts) + end + + -- Your preferred keybindings + bmap("n", "K", function() + vim.lsp.buf.hover { border = "single", max_height = 25, max_width = 120 } + end, { desc = "LSP: Hover Documentation" }) + + bmap("n", "gd", function() + vim.lsp.buf.definition { + on_list = function(options) + -- Custom logic to avoid showing multiple definitions for Lua patterns like: + -- `local M.my_fn_name = function() ... end` + local unique_defs = {} + local def_loc_hash = {} + + for _, def_location in pairs(options.items) do + local hash_key = def_location.filename .. def_location.lnum + if not def_loc_hash[hash_key] then + def_loc_hash[hash_key] = true + table.insert(unique_defs, def_location) + end + end + + options.items = unique_defs + vim.fn.setloclist(0, {}, " ", options) + + -- Open location list if multiple definitions, otherwise jump directly + if #options.items > 1 then + vim.cmd.lopen() + else + vim.cmd([[silent! lfirst]]) + end + end, + } + end, { desc = "LSP: Go to Definition" }) + + bmap("n", "<C-]>", vim.lsp.buf.definition, { desc = "LSP: Go to Definition (Alt)" }) + bmap("n", "gD", vim.lsp.buf.declaration, { desc = "LSP: Go to Declaration" }) + bmap("n", "gi", vim.lsp.buf.implementation, { desc = "LSP: Go to Implementation" }) + bmap("n", "gt", vim.lsp.buf.type_definition, { desc = "LSP: Go to Type Definition" }) + bmap("n", "gr", vim.lsp.buf.references, { desc = "LSP: Go to References" }) + bmap("n", "gn", vim.lsp.buf.rename, { desc = "LSP: Rename" }) + bmap("n", "<leader>rn", vim.lsp.buf.rename, { desc = "LSP: Rename (Alt)" }) + bmap("n", "ga", vim.lsp.buf.code_action, { desc = "LSP: Code Action" }) + bmap("n", "<leader>ca", vim.lsp.buf.code_action, { desc = "LSP: Code Action (Alt)" }) + bmap("n", "<leader>k", vim.lsp.buf.signature_help, { desc = "LSP: Signature Help" }) + bmap("n", "<C-k>", vim.lsp.buf.signature_help, { desc = "LSP: Signature Help (Alt)" }) + bmap("n", "gs", vim.lsp.buf.document_symbol, { desc = "LSP: Document Symbols" }) + + -- Workspace folder management + bmap("n", "<leader>wa", vim.lsp.buf.add_workspace_folder, { desc = "LSP: Add Workspace Folder" }) + bmap("n", "<leader>wr", vim.lsp.buf.remove_workspace_folder, { desc = "LSP: Remove Workspace Folder" }) + bmap("n", "<leader>wl", function() + vim.print(vim.lsp.buf.list_workspace_folders()) + end, { desc = "LSP: List Workspace Folders" }) +end + +---------------- Plugin Operations ---------------- +-- Packer +map("n", "<leader>Pc", "<cmd>PackerCompile<cr>") +map("n", "<leader>Pi", "<cmd>PackerInstall<cr>") +map("n", "<leader>Ps", "<cmd>PackerSync<cr>") +map("n", "<leader>PS", "<cmd>PackerStatus<cr>") +map("n", "<leader>Pu", "<cmd>PackerUpdate<cr>") + +-- ToggleTerm +map({ "n", "t" }, "<leader>tt", "<cmd>ToggleTerm<CR>") +map({ "n", "t" }, "<leader>th", "<cmd>lua Horizontal_term_toggle()<CR>") +map({ "n", "t" }, "<leader>tv", "<cmd>lua Vertical_term_toggle()<CR>") + +-- LazyGit +map({ "n", "t" }, "<leader>gg", "<cmd>lua Lazygit_toggle()<CR>") + +map("n", "<leader>tg", "<cmd>lua Gh_dash()<CR>") + +-- Fugitive git bindings +map("n", "<leader>gs", vim.cmd.Git) +map("n", "<leader>ga", ":Git add %:p<CR><CR>") +--map("n", "<leader>gs", ":Gstatus<CR>") +--map("n", "<leader>gc", ":Gcommit -v -q<CR>") +map("n", "<leader>gt", ":Gcommit -v -q %:p<CR>") +map("n", "<leader>gd", ":Gdiff<CR>") +map("n", "<leader>ge", ":Gedit<CR>") +--map("n", "<leader>gr", ":Gread<Cj>") +map("n", "<leader>gw", ":Gwrite<CR><CR>") +map("n", "<leader>gl", ":silent! Glog<CR>:bot copen<CR>") +--map("n", "<leader>gp", ":Ggrep<Space>") +--map("n", "<Leader>gp", ":Git push<CR>") +--map("n", "<Leader>gb", ":Gblame<CR>") +map("n", "<leader>gm", ":Gmove<Space>") +--map("n", "<leader>gb", ":Git branch<Space>") +--map("n", "<leader>go", ":Git checkout<Space>") +--map("n", "<leader>gps", ":Dispatch! git push<CR>") +--map("n", "<leader>gpl", ":Dispatch! git pull<CR>") + +-- Telescope +-- Safe load of your custom Telescope module +-- This initial pcall for "plugins.telescope" is fine because it just checks if YOUR module is there. +-- The actual checks for Telescope's core modules happen *inside* your wrapper functions when called. +local telescope_ok, telescope_module = pcall(require, "plugins.telescope") + +if telescope_ok and telescope_module then + + -- Direct function calls from your plugins.telescope module + -- M.safe_find_files handles its own internal `builtin` check + map("n", "<leader>ff", telescope_module.safe_find_files, { desc = "Find files" }) + + -- For `find all files`, use your `safe_telescope_builtin` wrapper + -- You need to wrap it in a function to pass the options correctly. + map("n", "<leader>f.", function() + telescope_module.safe_telescope_builtin("find_files")({ hidden = true, no_ignore = true }) + end, { desc = "Find all files" }) + + + --- + --- Built-in Telescope functions + --- Note: safe_telescope_builtin returns a function, so you map directly to it. + --- + map("n", "<leader>fg", function() telescope_module.safe_telescope_builtin("live_grep")() end, { desc = "Live grep" }) + map("n", "<leader>fb", function() telescope_module.safe_telescope_builtin("buffers")() end, { desc = "Find buffers" }) + map("n", "<leader>fh", function() telescope_module.safe_telescope_builtin("help_tags")() end, { desc = "Help tags" }) + map("n", "<leader>fc", function() telescope_module.safe_telescope_builtin("commands")() end, { desc = "Commands" }) + map("n", "<leader>fd", function() telescope_module.safe_telescope_builtin("diagnostics")() end, { desc = "Diagnostics" }) + map("n", "<leader>fk", function() telescope_module.safe_telescope_builtin("keymaps")() end, { desc = "Keymaps" }) + map("n", "<leader>fr", function() telescope_module.safe_telescope_builtin("registers")() end, { desc = "Registers" }) + map("n", "<leader>ffc", function() telescope_module.safe_telescope_builtin("current_buffer_fuzzy_find")() end, { desc = "Current buffer fuzzy find" }) + -- Corrected the previous `fp` mapping that pointed to `pickers` + map("n", "<leader>fp", function() telescope_module.safe_telescope_builtin("oldfiles")() end, { desc = "Recently opened files" }) + + + --- + --- Telescope Extension functions + --- Note: safe_telescope_extension returns a function, so you map directly to it. + --- + map("n", "<leader>cf", function() telescope_module.safe_telescope_extension("changed_files", "changed_files")() end, { desc = "Changed files" }) + map("n", "<leader>fm", function() telescope_module.safe_telescope_extension("media_files", "media_files")() end, { desc = "Media files" }) + map("n", "<leader>fi", function() telescope_module.safe_telescope_extension("notify", "notify")() end, { desc = "Notifications" }) + map("n", "<Leader>fs", function() telescope_module.safe_telescope_extension("session-lens", "search_session")() end, { desc = "Search sessions" }) + map("n", "<Leader>frf", function() telescope_module.safe_telescope_extension("recent_files", "pick")() end, { desc = "Recent files" }) + map("n", "<Leader>f/", function() telescope_module.safe_telescope_extension("file_browser", "file_browser")() end, { desc = "File browser" }) + + + --- + --- Custom functions defined in plugins.telescope.lua + --- Note: safe_telescope_call returns a function, so you map directly to it. + --- (These were already correct as safe_telescope_call returns a callable function) + --- + map("n", "<leader>ffd", telescope_module.safe_telescope_call("plugins.telescope", "find_dirs"), { desc = "Find directories" }) + map("n", "<leader>ff.", telescope_module.safe_telescope_call("plugins.telescope", "find_configs"), { desc = "Find configs" }) + map("n", "<leader>ffs", telescope_module.safe_telescope_call("plugins.telescope", "find_scripts"), { desc = "Find scripts" }) + map("n", "<leader>ffw", telescope_module.safe_telescope_call("plugins.telescope", "find_projects"), { desc = "Find projects" }) + map("n", "<leader>ffB", telescope_module.safe_telescope_call("plugins.telescope", "find_books"), { desc = "Find books" }) + map("n", "<leader>ffn", telescope_module.safe_telescope_call("plugins.telescope", "find_notes"), { desc = "Find notes" }) + map("n", "<leader>fgn", telescope_module.safe_telescope_call("plugins.telescope", "grep_notes"), { desc = "Grep notes" }) + map("n", "<leader>fpp", telescope_module.safe_telescope_call("plugins.telescope", "find_private"), { desc = "Find private notes" }) + map("n", "<leader>fgc", telescope_module.safe_telescope_call("plugins.telescope", "grep_current_dir"), { desc = "Grep current directory" }) + +end +---- Fallback keymaps when telescope is not available +--map("n", "<leader>ff", function() +-- local file = vim.fn.input("Open file: ", "", "file") +-- if file ~= "" then +-- vim.cmd("edit " .. file) +-- end +--end, { desc = "Find files (fallback)" }) + +---- You can add other basic fallbacks here +--map("n", "<leader>fg", function() +-- vim.notify("Live grep requires telescope plugin", vim.log.levels.WARN) +--end, { desc = "Live grep (unavailable)" }) +----end + + +map("n", "<leader>fF", ":cd %:p:h<CR>:pwd<CR><cmd>lua require('user.mods').findFilesInCwd()<CR>", +{ noremap = true, silent = true, desc = "Find files in cwd" }) + +-- FZF +map("n", "<leader>fz", function() + local ok, fzf_lua = pcall(require, "fzf-lua") + if ok then + fzf_lua.files() -- no config, just open + else + local handle = io.popen("find . -type f | fzf") + if handle then + local result = handle:read("*a") + handle:close() + result = result:gsub("\n", "") + if result ~= "" then + vim.cmd("edit " .. vim.fn.fnameescape(result)) + end + else + vim.notify("fzf not found or failed to run", vim.log.levels.ERROR) + end + end +end, { desc = "FZF file picker (fzf-lua or fallback)" }) + +map("n", "gA", ":FzfLua lsp_code_actions<CR>") + +-- Nvim-tree +local function safe_nvim_tree_toggle() + local ok_tree, tree_api = pcall(require, "nvim-tree.api") + if ok_tree then + pcall(vim.cmd, "Rooter") -- silently run Rooter if available + tree_api.tree.toggle() + else + -- Fallback to netrw + local cur_buf = vim.api.nvim_get_current_buf() + local ft = vim.api.nvim_get_option_value("filetype", { buf = cur_buf }) + + if ft == "netrw" then + vim.cmd("close") + else + vim.cmd("Lexplore") + end + end +end + +map("n", "<leader>f", safe_nvim_tree_toggle, { desc = "Toggle file explorer" }) + +-- Undotree +map("n", "<leader>u", vim.cmd.UndotreeToggle) + +-- Markdown-preview +map("n", "<leader>md", "<Plug>MarkdownPreviewToggle") +map("n", "<leader>mg", "<CMD>Glow<CR>") + +-- Autopairs +map("n", "<leader>ww", "<cmd>lua require('user.mods').Toggle_autopairs()<CR>") + +-- Zen-mode toggle +map("n", "<leader>zm", "<CMD>ZenMode<CR> | :echom ('Zen Mode')<CR> | :sl! | echo ('')<CR>") + +-- Vim-rooter +local function safe_project_root() + if vim.fn.exists(":Rooter") == 2 then + vim.cmd("Rooter") + else + vim.cmd("cd %:p:h") + end +end +vim.keymap.set("n", "<leader>ro", safe_project_root, { desc = "Project root" }) + +-- Trouble (UI to show diagnostics) +local function safe_trouble_toggle(view, opts) + local ok, _ = pcall(require, "trouble") + if ok then + local cmd = "Trouble" + if view then + cmd = cmd .. " " .. view .. " toggle" + if opts then + cmd = cmd .. " " .. opts + end + else + cmd = cmd .. " diagnostics toggle" + end + vim.cmd(cmd) + else + vim.cmd("copen") + end +end + +-- Replace 'map' with 'vim.keymap.set' if not already a global alias +vim.keymap.set("n", "<leader>t", function() + safe_trouble_toggle() +end, { desc = "Diagnostics (Workspace)" }) + +vim.keymap.set("n", "<leader>tw", function() + vim.cmd("cd %:p:h | pwd") + safe_trouble_toggle("diagnostics") +end, { desc = "Diagnostics (Workspace)" }) + +vim.keymap.set("n", "<leader>td", function() + vim.cmd("cd %:p:h | pwd") + safe_trouble_toggle("diagnostics", "filter.buf=0") +end, { desc = "Diagnostics (Buffer)" }) + +vim.keymap.set("n", "<leader>tq", function() + vim.cmd("cd %:p:h | pwd") + safe_trouble_toggle("qflist") +end, { desc = "Quickfix List" }) + +vim.keymap.set("n", "<leader>tl", function() + vim.cmd("cd %:p:h | pwd") + safe_trouble_toggle("loclist") +end, { desc = "Location List" }) + +vim.keymap.set("n", "gR", function() + safe_trouble_toggle("lsp") +end, { desc = "LSP References/Definitions" }) + +-- Null-ls +map("n", "<leader>ls", ':lua require("null-ls").toggle({})<CR>') + + +-- Replacer +map("n", "<Leader>qr", ':lua require("replacer").run()<CR>') + +-- Quickfix +map("n", "<leader>q", function() + if vim.fn.getqflist({ winid = 0 }).winid ~= 0 then + require("plugins.quickfix").close() + else + require("plugins.quickfix").open() + end +end, { desc = "Toggle quickfix window" }) + +-- Move to the next and previous item in the quickfixlist +map("n", "]c", "<Cmd>cnext<CR>") +map("n", "[c", "<Cmd>cprevious<CR>") + +-- Location list +map("n", "<leader>l", '<cmd>lua require("plugins.loclist").loclist_toggle()<CR>') + +-- Dap (debugging) +local dap_ok, dap = pcall(require, "dap") +local dap_ui_ok, ui = pcall(require, "dapui") + +if not (dap_ok and dap_ui_ok) then + --require("notify")("nvim-dap or dap-ui not installed!", "warning") + return +end + +vim.fn.sign_define("DapBreakpoint", { text = "🐞" }) + +-- Start debugging session +map("n", "<leader>ds", function() + dap.continue() + ui.toggle({}) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<C-w>=", false, true, true), "n", false) -- Spaces buffers evenly +end) + +-- Set breakpoints, get variable values, step into/out of functions, etc. +map("n", "<leader>dC", dap.continue) +-- map("n", "<leader>dC", dap.close) +-- map("n", "<leader>dt", dap.terminate) +map("n", "<leader>dt", ui.toggle) +map("n", "<leader>dd", function() + dap.disconnect({ terminateDebuggee = true }) +end) +map("n", "<leader>dn", dap.step_over) +map("n", "<leader>di", dap.step_into) +map("n", "<leader>do", dap.step_out) +map("n", "<leader>db", dap.toggle_breakpoint) +map("n", "<leader>dB", function() + dap.clear_breakpoints() + require("notify")("Breakpoints cleared", "warn") +end) +map("n", "<leader>dl", function() + local ok, dap_widgets = pcall(require, "dap.ui.widgets") + if ok then dap_widgets.hover() end +end) +map("n", "<leader>de", function() + require("dapui").float_element() +end, { desc = "Open Element" }) +map("n", "<leader>dq", function() + require("dapui").close() + require("dap").repl.close() + local session = require("dap").session() + if session then + require("dap").terminate() + end + require("nvim-dap-virtual-text").refresh() +end, { desc = "Terminate Debug" }) +map("n", "<leader>dc", function() + require("telescope").extensions.dap.commands() +end, { desc = "DAP-Telescope: Commands" }) +--vim.keymap.set("n", "<leader>B", ":lua require'dap'.set_breakpoint(vim.fn.input('Breakpoint condition: '))<CR>") +--vim.keymap.set("v", "<leader>B", ":lua require'dap'.set_breakpoint(vim.fn.input('Breakpoint condition: '))<CR>") +--vim.keymap.set("n", "<leader>lp", ":lua require'dap'.set_breakpoint(nil, nil, vim.fn.input('Log point message: '))<CR>") +--vim.keymap.set("n", "<leader>dr", ":lua require'dap'.repl.open()<CR>") + +-- Close debugger and clear breakpoints +--map("n", "<leader>de", function() +-- dap.clear_breakpoints() +-- ui.toggle({}) +-- dap.terminate() +-- vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<C-w>=", false, true, true), "n", false) +-- require("notify")("Debugger session ended", "warn") +--end) + +-- Toggle Dashboard +map("n", "<leader><Space>", '<CMD>lua require("user.mods").toggle_dashboard()<CR>') + +-- Lsp Lines toggle +map("", "<Leader>ll", require("lsp_lines").toggle, { desc = "Toggle lsp_lines" }) + +-- SnipRun +map({ "n", "v" }, "<leader>r", "<Plug>SnipRun<CR>") + +-- Codi +map("n", "<leader>co", '<CMD>lua require("user.mods").toggleCodi()<CR>') + +-- Scratch buffer +map("n", "<leader>ss", '<CMD>lua require("user.mods").Scratch("float")<CR>') +map("n", "<leader>sh", '<CMD>lua require("user.mods").Scratch("horizontal")<CR>') +map("n", "<leader>sv", '<CMD>lua require("user.mods").Scratch("vertical")<CR>') + +-- Hardtime +map("n", "<leader>H", '<CMD>lua require("plugins.hardtime").ToggleHardtime()<CR>') + +-- Code Run +map("n", "<leader>rr", '<CMD>lua require("user.mods").toggleCodeRunner()<CR>') + +-- Run executable file +map("n", "<leader>rx", +":lua require('user.mods').RunCurrentFile()<CR>:echom 'Running executable file...'<CR>:sl!<CR>:echo ''<CR>") + +-- Set Files to current location as dir +map({ "n" }, "<leader>cf", "<CMD>e %:h<CR>") + +-- Vimtex +map("n", "<Leader>lc", ":VimtexCompile<cr>") +map("v", "<Leader>ls", ":VimtexCompileSelected<cr>") +map("n", "<Leader>li", ":VimtexInfo<cr>") +map("n", "<Leader>lt", ":VimtexTocToggle<cr>") +map("n", "<Leader>lv", ":VimtexView<cr>") diff --git a/common/config/nvim/lua/user/mods.lua b/common/config/nvim/lua/user/mods.lua new file mode 100755 index 0000000..b4e1579 --- /dev/null +++ b/common/config/nvim/lua/user/mods.lua @@ -0,0 +1,1427 @@ +-- ============================================================================ +-- Modules/Utility functions +-- ============================================================================ + +local M = {} + +-- Shorten Function Names +local fn = vim.fn +local api = vim.api + +--- Check if an executable exists +---@param name string The name of the executable to check +---@return boolean +function M.executable(name) + return fn.executable(name) > 0 +end + +--- Check if a feature is available in Neovim +---@param feat string The feature to check (e.g., 'nvim-0.7') +---@return boolean +function M.has(feat) + return fn.has(feat) == 1 +end + +--- Setup command aliases +---@param from string The alias +---@param to string The command to alias to +function M.setup_command_alias(from, to) + local cmd = string.format('cnoreabbrev <expr> %s (getcmdtype() == ":" && getcmdline() == "%s") ? "%s" : "%s"', + from, from, to, from) + api.nvim_command(cmd) +end + +--- Preserve cursor position while formatting +---@param cmd string The command to run +function M.preserve_cursor(cmd) + local cursor = api.nvim_win_get_cursor(0) + vim.cmd(cmd) + api.nvim_win_set_cursor(0, cursor) +end + +--- Toggle quickfix window +function M.toggle_quickfix() + local qf_exists = false + for _, win in pairs(fn.getwininfo()) do + if win.quickfix == 1 then + qf_exists = true + break + end + end + if qf_exists then + vim.cmd('cclose') + else + vim.cmd('copen') + end +end + +--- Toggle location list +function M.toggle_location() + local loc_exists = false + for _, win in pairs(fn.getwininfo()) do + if win.loclist == 1 then + loc_exists = true + break + end + end + if loc_exists then + vim.cmd('lclose') + else + vim.cmd('lopen') + end +end + +-- Setup command aliases +M.setup_command_alias('W', 'w') +M.setup_command_alias('Wq', 'wq') +M.setup_command_alias('WQ', 'wq') +M.setup_command_alias('Q', 'q') +M.setup_command_alias('Qa', 'qa') +M.setup_command_alias('QA', 'qa') + +-------------------------------------------------- + +--- Check whether a feature exists in Nvim +--- @feat: string +--- the feature name, like `nvim-0.7` or `unix`. +--- return: bool +M.has = function(feat) + if fn.has(feat) == 1 then + return true + end + + return false +end + +-------------------------------------------------- + +-- Format on save +local format_augroup = vim.api.nvim_create_augroup("LspFormatting", {}) + +local ok, null_ls = pcall(require, "null-ls") +if ok then + null_ls.setup({ + on_attach = function(client, bufnr) + if client.supports_method("textDocument/formatting") then + vim.api.nvim_clear_autocmds({ group = format_augroup, buffer = bufnr }) + vim.api.nvim_create_autocmd("BufWritePre", { + group = format_augroup, + buffer = bufnr, + callback = function() + if vim.lsp.buf.format then + vim.lsp.buf.format({ bufnr = bufnr }) + else + vim.lsp.buf.formatting_seq_sync() + end + end, + }) + end + end, + }) +end + +vim.cmd([[autocmd BufWritePre <buffer> lua vim.lsp.buf.format()]]) + + +-------------------------------------------------- + +---Determine if a value of any type is empty +---@param item any +---@return boolean? + +--- Checks if an item is considered "empty". +-- +-- An item is considered empty if: +-- - It is nil. +-- - It is an empty string. +-- - It is an empty table. +-- - It is a number equal to 0 (you might want to adjust this based on your definition of "empty" for numbers). +-- +-- @param item any The item to check. +-- @return boolean True if the item is empty, false otherwise. +function M.empty(item) + -- Case 1: item is nil + if item == nil then + return true + end + + local item_type = type(item) + + -- Case 2: empty string + if item_type == "string" then + return item == "" + end + + if item_type == "table" then + return vim.tbl_isempty(item) + end + if item_type == "number" then + return item == 0 -- Changed from item <= 0 for a stricter "empty" definition for numbers + end + + if item_type == "boolean" then + return not item -- Returns true if item is false, false if item is true + end + + return false +end + + +-------------------------------------------------- + +--- Create a dir if it does not exist +function M.may_create_dir(dir) + local res = fn.isdirectory(dir) + + if res == 0 then + fn.mkdir(dir, "p") + end +end + +-------------------------------------------------- + +--- Toggle cmp completion +vim.g.cmp_toggle_flag = false -- initialize +local normal_buftype = function() + return vim.api.nvim_buf_get_option(0, "buftype") ~= "prompt" +end +M.toggle_completion = function() + local ok, cmp = pcall(require, "cmp") + if ok then + local next_cmp_toggle_flag = not vim.g.cmp_toggle_flag + if next_cmp_toggle_flag then + print("completion on") + else + print("completion off") + end + cmp.setup({ + enabled = function() + vim.g.cmp_toggle_flag = next_cmp_toggle_flag + if next_cmp_toggle_flag then + return normal_buftype + else + return next_cmp_toggle_flag + end + end, + }) + else + print("completion not available") + end +end + +-------------------------------------------------- + +--- Make sure using latest neovim version +function M.get_nvim_version() + local actual_ver = vim.version() + + local nvim_ver_str = string.format("%d.%d.%d", actual_ver.major, actual_ver.minor, actual_ver.patch) + return nvim_ver_str +end + +function M.add_pack(name) + local status, error = pcall(vim.cmd, "packadd " .. name) + + return status +end + +-------------------------------------------------- + +-- Define a global function to retrieve LSP clients based on Neovim version +function M.get_lsp_clients(bufnr) + local mods = require("user.mods") + --local expected_ver = '0.10.0' + local nvim_ver = mods.get_nvim_version() + + local version_major, version_minor = string.match(nvim_ver, "(%d+)%.(%d+)") + version_major = tonumber(version_major) + version_minor = tonumber(version_minor) + + if version_major > 0 or (version_major == 0 and version_minor >= 10) then + return vim.lsp.get_clients({ buffer = bufnr }) + else + return vim.lsp.buf_get_clients() + end +end + +-------------------------------------------------- + +--- Toggle autopairs on/off (requires "windwp/nvim-autopairs") +function M.Toggle_autopairs() + local ok, autopairs = pcall(require, "nvim-autopairs") + if ok then + if autopairs.state.disabled then + autopairs.enable() + print("autopairs on") + else + autopairs.disable() + print("autopairs off") + end + else + print("autopairs not available") + end +end + +-------------------------------------------------- + +--- Make vim-rooter message disappear after making it's changes +--vim.cmd([[ +--let timer = timer_start(1000, 'LogTrigger', {}) +--func! LogTrigger(timer) +-- silent! +--endfunc +--]]) +-- +--vim.cmd([[ +--function! ConfigureChDir() +-- echo ('') +--endfunction +--" Call after vim-rooter changes the root dir +--autocmd User RooterChDir :sleep! | call LogTrigger(timer) | call ConfigureChDir() +--]]) + +function M.findFilesInCwd() + vim.cmd("let g:rooter_manual_only = 1") -- Toggle the rooter plugin + require("plugins.telescope").findhere() + vim.defer_fn(function() + vim.cmd("let g:rooter_manual_only = 0") -- Change back to automatic rooter + end, 100) +end + +--function M.findFilesInCwd() +-- vim.cmd("let g:rooter_manual_only = 1") -- Toggle the rooter plugin +-- require("plugins.telescope").findhere() +-- --vim.cmd("let g:rooter_manual_only = 0") -- Change back to automatic rooter +--end + +-------------------------------------------------- + +-- Toggle the executable permission +function M.Toggle_executable() + local current_file = vim.fn.expand("%:p") + local executable = vim.fn.executable(current_file) == 1 + + if executable then + -- File is executable, unset the executable permission + vim.fn.system("chmod -x " .. current_file) + --print(current_file .. ' is no longer executable.') + print("No longer executable") + else + -- File is not executable, set the executable permission + vim.fn.system("chmod +x " .. current_file) + --print(current_file .. ' is now executable.') + print("Now executable") + end +end + +-------------------------------------------------- + +-- Set bare dotfiles repository git environment variables dynamically + +-- Set git enviornment variables +--function M.Set_git_env_vars() +-- local git_dir_job = vim.fn.jobstart({ "git", "rev-parse", "--git-dir" }) +-- local command_status = vim.fn.jobwait({ git_dir_job })[1] +-- if command_status > 0 then +-- vim.env.GIT_DIR = vim.fn.expand("$HOME/.cfg") +-- vim.env.GIT_WORK_TREE = vim.fn.expand("~") +-- else +-- vim.env.GIT_DIR = nil +-- vim.env.GIT_WORK_TREE = nil +-- end +-- -- Launch terminal emulator with Git environment variables set +-- --require("toggleterm").exec(string.format([[%s %s]], os.getenv("SHELL"), "-i")) +--end + +------ + +local prev_cwd = "" + +function M.Set_git_env_vars() + local cwd = vim.fn.getcwd() + if prev_cwd == "" then + -- First buffer being opened, set prev_cwd to cwd + prev_cwd = cwd + elseif cwd ~= prev_cwd then + -- Working directory has changed since last buffer was opened + prev_cwd = cwd + local git_dir_job = vim.fn.jobstart({ "git", "rev-parse", "--git-dir" }) + local command_status = vim.fn.jobwait({ git_dir_job })[1] + if command_status > 0 then + vim.env.GIT_DIR = vim.fn.expand("$HOME/.cfg") + vim.env.GIT_WORK_TREE = vim.fn.expand("~") + else + vim.env.GIT_DIR = nil + vim.env.GIT_WORK_TREE = nil + end + end +end + +vim.cmd([[augroup my_git_env_vars]]) +vim.cmd([[ autocmd!]]) +vim.cmd([[ autocmd BufEnter * lua require('user.mods').Set_git_env_vars()]]) +vim.cmd([[ autocmd VimEnter * lua require('user.mods').Set_git_env_vars()]]) +vim.cmd([[augroup END]]) + +-------------------------------------------------- + +--- Update Tmux Status Vi-mode +function M.update_tmux_status() + -- Check if the current buffer has a man filetype + if vim.bo.filetype == "man" then + return + end + local mode = vim.api.nvim_eval("mode()") + -- Determine the mode name based on the mode value + local mode_name + if mode == "n" then + mode_name = "-- NORMAL --" + elseif mode == "i" or mode == "ic" then + mode_name = "-- INSERT --" + else + mode_name = "-- NORMAL --" --'-- COMMAND --' + end + + -- Write the mode name to the file + local file = io.open(os.getenv("HOME") .. "/.vi-mode", "w") + file:write(mode_name) + file:close() + if nvim_running then + -- Neovim is running, update the mode file and refresh tmux + VI_MODE = "" -- Clear VI_MODE to show Neovim mode + vim.cmd("silent !tmux refresh-client -S") + end + ---- Force tmux to update the status + vim.cmd("silent !tmux refresh-client -S") +end + +vim.cmd([[ + augroup TmuxStatus + autocmd! + autocmd InsertLeave,InsertEnter * lua require("user.mods").update_tmux_status() + autocmd VimEnter * lua require("user.mods").update_tmux_status() + autocmd BufEnter * lua require("user.mods").update_tmux_status() + autocmd ModeChanged * lua require("user.mods").update_tmux_status() + autocmd WinEnter,WinLeave * lua require("user.mods").update_tmux_status() + augroup END +]]) + +-- Add autocmd for <esc> +-- Add autocmd to check when tmux switches panes/windows +--autocmd InsertLeave,InsertEnter * lua require("user.mods").update_tmux_status() +--autocmd BufEnter * lua require("user.mods").update_tmux_status() +--autocmd WinEnter,WinLeave * lua require("user.mods").update_tmux_status() + +--autocmd WinEnter,WinLeave * lua require("user.mods").update_tmux_status() +--autocmd VimResized * lua require("user.mods").update_tmux_status() +--autocmd FocusGained * lua require("user.mods").update_tmux_status() +--autocmd FocusLost * lua require("user.mods").update_tmux_status() +--autocmd CmdwinEnter,CmdwinLeave * lua require("user.mods").update_tmux_status() + +-------------------------------------------------- + +-- function OpenEmulatorList() +-- local emulatorsBuffer = vim.api.nvim_create_buf(false, true) +-- vim.api.nvim_buf_set_lines(emulatorsBuffer, 0, 0, true, {"Some text"}) +-- vim.api.nvim_open_win( +-- emulatorsBuffer, +-- false, +-- { +-- relative='win', row=3, col=3, width=12, height=3 +-- } +-- ) +-- end +-- +-- vim.api.nvim_create_user_command('OpenEmulators', OpenEmulatorList, {}) + +--local api = vim.api +--local fn = vim.fn +--local cmd = vim.cmd +-- +--local function bufremove(opts) +-- local target_buf_id = api.nvim_get_current_buf() +-- +-- -- Do nothing if buffer is in modified state. +-- if not opts.force and api.nvim_buf_get_option(target_buf_id, 'modified') then +-- return false +-- end +-- +-- -- Hide target buffer from all windows. +-- vim.tbl_map(function(win_id) +-- win_id = win_id or 0 +-- +-- local current_buf_id = api.nvim_win_get_buf(win_id) +-- +-- api.nvim_win_call(win_id, function() +-- -- Try using alternate buffer +-- local alt_buf_id = fn.bufnr('#') +-- if alt_buf_id ~= current_buf_id and fn.buflisted(alt_buf_id) == 1 then +-- api.nvim_win_set_buf(win_id, alt_buf_id) +-- return +-- end +-- +-- -- Try using previous buffer +-- cmd('bprevious') +-- if current_buf_id ~= api.nvim_win_get_buf(win_id) then +-- return +-- end +-- +-- -- Create new listed scratch buffer +-- local new_buf = api.nvim_create_buf(true, true) +-- api.nvim_win_set_buf(win_id, new_buf) +-- end) +-- +-- return true +-- end, fn.win_findbuf(target_buf_id)) +-- +-- cmd(string.format('bdelete%s %d', opts.force and '!' or '', target_buf_id)) +--end +-- +---- Assign bufremove to a global variable +--_G.bufremove = bufremove + +--vim.cmd([[ +-- augroup NvimTreeDelete +-- autocmd! +-- autocmd FileType NvimTree lua require('user.mods').enew_on_delete() +-- augroup END +--]]) +-- +--function M.enew_on_delete() +-- if vim.bo.buftype == 'nofile' then +-- vim.cmd('enew') +-- end +--end + +-- Update Neovim +--function M.Update_neovim() +-- -- Run the commands to download and extract the latest version +-- os.execute("curl -L -o nvim-linux64.tar.gz https://github.com/neovim/neovim/releases/latest/download/nvim-linux64.tar.gz") +-- os.execute("tar xzvf nvim-linux64.tar.gz") +-- -- Replace the existing Neovim installation with the new version +-- os.execute("rm -rf $HOME/.local/bin/nvim") +-- os.execute("mv nvim-linux64 $HOME/.local/bin/nvim") +-- +-- -- Clean up the downloaded file +-- os.execute("rm nvim-linux64.tar.gz") +-- +-- -- Print a message to indicate the update is complete +-- print("Neovim has been updated to the latest version.") +--end +-- +---- Bind a keymap to the update_neovim function (optional) +--vim.api.nvim_set_keymap('n', '<leader>u', '<cmd> lua require("user.mods").Update_neovim()<CR>', { noremap = true, silent = true }) + +-- Define a function to create a floating window and run the update process inside it +function M.Update_neovim() + -- Create a new floating window + local bufnr, winid = vim.api.nvim_create_buf(false, true) + vim.api.nvim_open_win(bufnr, true, { + relative = "editor", + width = 80, + height = 20, + row = 2, + col = 2, + style = "minimal", + border = "single", + }) + + -- Function to append a line to the buffer in the floating window + local function append_line(line) + vim.api.nvim_buf_set_option(bufnr, "modifiable", true) + vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { line }) + vim.api.nvim_buf_set_option(bufnr, "modifiable", false) + end + + -- Download the latest version of Neovim + append_line("Downloading the latest version of Neovim...") + os.execute( + "curl -L -o nvim-linux64.tar.gz https://github.com/neovim/neovim/releases/latest/download/nvim-linux64.tar.gz") + append_line("Download complete.") + + -- Extract the downloaded archive + append_line("Extracting the downloaded archive...") + os.execute("tar xzvf nvim-linux64.tar.gz") + append_line("Extraction complete.") + + -- Replace the existing Neovim installation with the new version + append_line("Replacing the existing Neovim installation...") + os.execute("rm -rf $HOME/nvim") + os.execute("mv nvim-linux64 $HOME/nvim") + append_line("Update complete.") + + -- Clean up the downloaded file + append_line("Cleaning up the downloaded file...") + os.execute("rm nvim-linux64.tar.gz") + append_line("Cleanup complete.") + + -- Close the floating window after a delay + vim.defer_fn(function() + vim.api.nvim_win_close(winid, true) + end, 5000) -- Adjust the delay as needed +end + +-- Bind a keymap to the update_neovim function (optional) +vim.api.nvim_set_keymap("n", "<leader>U", '<cmd> lua require("user.mods").Update_neovim()<CR>', + { noremap = true, silent = true }) + +-------------------------------------------------- + +-- Fix or suppress closing nvim error message (/src/unix/core.c:147: uv_close: Assertion `!uv__is_closing(handle)' failed.) +vim.api.nvim_create_autocmd({ "VimLeave" }, { + callback = function() + vim.fn.jobstart("!notify-send 2>/dev/null &", { detach = true }) + end, +}) + +-------------------------------------------------- + +-- Rooter +--vim.cmd([[autocmd BufEnter * lua vim.cmd('Rooter')]]) + +-------------------------------------------------- + +-- Nvim-tree +local modifiedBufs = function(bufs) -- nvim-tree is also there in modified buffers so this function filter it out + local t = 0 + for k, v in pairs(bufs) do + if v.name:match("NvimTree_", "NvimTree1") == nil then + t = t + 1 + end + end + return t +end + +-- Deleting current file opened behaviour +function M.DeleteCurrentBuffer() + local cbn = vim.api.nvim_get_current_buf() + local buffers = vim.fn.getbufinfo({ buflisted = true }) + local size = #buffers + local idx = 0 + + for n, e in ipairs(buffers) do + if e.bufnr == cbn then + idx = n + break -- Exit loop as soon as we find the buffer + end + end + + if idx == 0 then + return + end + + if idx == size then + vim.cmd("bprevious") + else + vim.cmd("bnext") + end + + vim.cmd("silent! bdelete " .. cbn) + + -- Open a new blank window + vim.cmd("silent! enew") -- Opens a new vertical split + -- OR + -- vim.cmd("new") -- Opens a new horizontal split + -- Delay before opening a new split + --vim.defer_fn(function() + -- vim.cmd("enew") -- Opens a new vertical split + --end, 100) -- Adjust the delay as needed (in milliseconds) + -- Delay before closing the nvim-tree window +end + + +-- On :bd nvim-tree should behave as if it wasn't opened +-- Only run DeleteCurrentBuffer if NvimTree is loaded +vim.api.nvim_create_autocmd("FileType", { + pattern = "NvimTree", + callback = function() + local ok, mods = pcall(require, "user.mods") + if ok and type(mods.DeleteCurrentBuffer) == "function" then + mods.DeleteCurrentBuffer() + end + end, +}) + +-- Handle NvimTree window closure safely +vim.api.nvim_create_autocmd("BufEnter", { + nested = true, + callback = function() + local ok_utils, utils = pcall(require, "nvim-tree.utils") + if not ok_utils then return end + + if #vim.api.nvim_list_wins() == 1 and utils.is_nvim_tree_buf() then + local ok_api, api = pcall(require, "nvim-tree.api") + if not ok_api then return end + + vim.defer_fn(function() + -- Safely toggle tree off and on + pcall(api.tree.toggle, { find_file = true, focus = true }) + pcall(api.tree.toggle, { find_file = true, focus = true }) + vim.cmd("wincmd p") + end, 0) + end + end, +}) + +-- Dismiss notifications when opening nvim-tree window +local function isNvimTreeOpen() + local win = vim.fn.win_findbuf(vim.fn.bufnr("NvimTree")) + return vim.fn.empty(win) == 0 +end + +function M.DisableNotify() + if isNvimTreeOpen() then + require("notify").dismiss() + end +end + +vim.cmd([[ + autocmd! WinEnter,WinLeave * lua require('user.mods').DisableNotify() +]]) + +-------------------------------------------------- + +-- Toggle Dashboard +function M.toggle_dashboard() + if vim.bo.filetype == "dashboard" then + vim.cmd("bdelete") + else + vim.cmd("Dashboard") + end +end + +-------------------------------------------------- + +-- Helper function to suppress errors +local function silent_execute(cmd) + vim.fn["serverlist"]() -- Required to prevent 'Press ENTER' prompt + local result = vim.fn.system(cmd .. " 2>/dev/null") + vim.fn["serverlist"]() + return result +end + +-------------------------------------------------- + +-- Toggle Codi +-- Define a global variable to track Codi's state +local is_codi_open = false + +function M.toggleCodi() + if is_codi_open then + -- Close Codi + vim.cmd("Codi!") + is_codi_open = false + print("Codi off") + else + -- Open Codi + vim.cmd("Codi") + is_codi_open = true + print("Codi on") + end +end + +-------------------------------------------------- + +---- Function to create or toggle a scratch buffer +-- Define global variables to store the scratch buffer and window +local scratch_buf = nil +local scratch_win = nil + +-- Other global variables +local scratch_date = os.date("%Y-%m-%d") +local scratch_dir = vim.fn.expand("~/notes/private") +local scratch_file = "scratch-" .. scratch_date .. ".md" + +-- Function to close and delete a buffer +function CloseAndDeleteBuffer(bufnr) + if bufnr and vim.api.nvim_buf_is_valid(bufnr) then + vim.api.nvim_command("silent! bwipe " .. bufnr) + end +end + +function M.Scratch(Split_direction) + -- Check if the directory exists, and create it if it doesn't + if vim.fn.isdirectory(scratch_dir) == 0 then + vim.fn.mkdir(scratch_dir, "p") + end + + -- Determine the window type based on Split_direction + local current_window_type = "float" + if Split_direction == "float" then + current_window_type = "float" + elseif Split_direction == "vertical" then + current_window_type = "vertical" + elseif Split_direction == "horizontal" then + current_window_type = "horizontal" + end + + local file_path = scratch_dir .. "/" .. scratch_file + + if scratch_win and vim.api.nvim_win_is_valid(scratch_win) then + -- Window exists, save buffer to file and close it + WriteScratchBufferToFile(scratch_buf, file_path) + vim.cmd(":w!") + vim.api.nvim_win_close(scratch_win, true) + CloseAndDeleteBuffer(scratch_buf) + scratch_win = nil + scratch_buf = nil + else + if scratch_buf and vim.api.nvim_buf_is_valid(scratch_buf) then + -- Buffer exists, reuse it and open a new window + OpenScratchWindow(scratch_buf, current_window_type) + else + -- Buffer doesn't exist, create it and load the file if it exists + scratch_buf = OpenScratchBuffer(file_path) + OpenScratchWindow(scratch_buf, current_window_type) + end + end +end + +-- Function to write buffer contents to a file +function WriteScratchBufferToFile(buf, file_path) + if buf and vim.api.nvim_buf_is_valid(buf) then + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local content = table.concat(lines, "\n") + local escaped_file_path = vim.fn.fnameescape(file_path) + + -- Write the buffer content to the file + local file = io.open(escaped_file_path, "w") + if file then + file:write(content) + file:close() + end + end +end + +-- Function to create or open the scratch buffer +function OpenScratchBuffer(file_path) + local buf = vim.api.nvim_create_buf(true, false) + + -- Set the file name for the buffer + local escaped_file_path = vim.fn.fnameescape(file_path) + vim.api.nvim_buf_set_name(buf, escaped_file_path) + + -- Check if the file exists and load it if it does + if vim.fn.filereadable(file_path) == 1 then + local file_contents = vim.fn.readfile(file_path) + vim.api.nvim_buf_set_lines(buf, 0, -1, true, file_contents) + else + -- Insert initial content + vim.api.nvim_buf_set_lines(buf, 0, -1, true, { + "# Quick Notes - " .. scratch_date, + "--------------------------", + "", + }) + + -- Save the initial content to the file + vim.cmd(":w") + end + + return buf +end + +-- Function to open the scratch buffer in a window +function OpenScratchWindow(buf, current_window_type) + if buf and vim.api.nvim_buf_is_valid(buf) then + if current_window_type == "float" then + local opts = { + relative = "win", + width = 120, + height = 10, + border = "single", + row = 20, + col = 20, + } + scratch_win = vim.api.nvim_open_win(buf, true, opts) + -- Go to the last line of the buffer + vim.api.nvim_win_set_cursor(scratch_win, { vim.api.nvim_buf_line_count(buf), 1 }) + elseif current_window_type == "vertical" then + vim.cmd("vsplit") + vim.api.nvim_win_set_buf(0, buf) + scratch_win = 0 + elseif current_window_type == "horizontal" then + vim.cmd("split") + vim.api.nvim_win_set_buf(0, buf) + scratch_win = 0 + end + end +end + +-------------------------------------------------- + +---- Intercept file open +--local augroup = vim.api.nvim_create_augroup("user-autocmds", { clear = true }) +--local intercept_file_open = true +--vim.api.nvim_create_user_command("InterceptToggle", function() +-- intercept_file_open = not intercept_file_open +-- local intercept_state = "`Enabled`" +-- if not intercept_file_open then +-- intercept_state = "`Disabled`" +-- end +-- vim.notify("Intercept file open set to " .. intercept_state, vim.log.levels.INFO, { +-- title = "Intercept File Open", +-- ---@param win integer The window handle +-- on_open = function(win) +-- vim.api.nvim_buf_set_option(vim.api.nvim_win_get_buf(win), "filetype", "markdown") +-- end, +-- }) +--end, { desc = "Toggles intercepting BufNew to open files in custom programs" }) + +---- NOTE: Add "BufReadPre" to the autocmd events to also intercept files given on the command line, e.g. +---- `nvim myfile.txt` +--vim.api.nvim_create_autocmd({ "BufNew" }, { +-- group = augroup, +-- callback = function(args) +-- ---@type string +-- local path = args.match +-- ---@type integer +-- local bufnr = args.buf +-- +-- ---@type string? The file extension if detected +-- local extension = vim.fn.fnamemodify(path, ":e") +-- ---@type string? The filename if detected +-- local filename = vim.fn.fnamemodify(path, ":t") +-- +-- ---Open a given file path in a given program and remove the buffer for the file. +-- ---@param buf integer The buffer handle for the opening buffer +-- ---@param fpath string The file path given to the program +-- ---@param fname string The file name used in notifications +-- ---@param prog string The program to execute against the file path +-- local function open_in_prog(buf, fpath, fname, prog) +-- vim.notify(string.format("Opening `%s` in `%s`", fname, prog), vim.log.levels.INFO, { +-- title = "Open File in External Program", +-- ---@param win integer The window handle +-- on_open = function(win) +-- vim.api.nvim_buf_set_option(vim.api.nvim_win_get_buf(win), "filetype", "markdown") +-- end, +-- }) +-- local mods = require("user.mods") +-- local nvim_ver = mods.get_nvim_version() +-- +-- local version_major, version_minor = string.match(nvim_ver, "(%d+)%.(%d+)") +-- version_major = tonumber(version_major) +-- version_minor = tonumber(version_minor) +-- +-- if version_major > 0 or (version_major == 0 and version_minor >= 10) then +-- vim.system({ prog, fpath }, { detach = true }) +-- else +-- vim.fn.jobstart({ prog, fpath }, { detach = true }) +-- end +-- vim.api.nvim_buf_delete(buf, { force = true }) +-- end +-- +-- local extension_callbacks = { +-- ["pdf"] = function(buf, fpath, fname) +-- open_in_prog(buf, fpath, fname, "zathura") +-- end, +-- ["epub"] = function(buf, fpath, fname) +-- open_in_prog(buf, fpath, fname, "zathura") +-- end, +-- ["mobi"] = "pdf", +-- ["png"] = function(buf, fpath, fname) +-- open_in_prog(buf, fpath, fname, "vimiv") +-- end, +-- ["jpg"] = "png", +-- ["mp4"] = function(buf, fpath, fname) +-- open_in_prog(buf, fpath, fname, "vlc") +-- end, +-- ["gif"] = "mp4", +-- } +-- +-- ---Get the extension callback for a given extension. Will do a recursive lookup if an extension callback is actually +-- ---of type string to get the correct extension +-- ---@param ext string A file extension. Example: `png`. +-- ---@return fun(bufnr: integer, path: string, filename: string?) extension_callback The extension callback to invoke, expects a buffer handle, file path, and filename. +-- local function extension_lookup(ext) +-- local callback = extension_callbacks[ext] +-- if type(callback) == "string" then +-- callback = extension_lookup(callback) +-- end +-- return callback +-- end +-- +-- if extension ~= nil and not extension:match("^%s*$") and intercept_file_open then +-- local callback = extension_lookup(extension) +-- if type(callback) == "function" then +-- callback(bufnr, path, filename) +-- end +-- end +-- end, +--}) + +-------------------------------------------------- + +-- Delete [No Name] buffers +vim.api.nvim_create_autocmd("BufHidden", { + desc = "Delete [No Name] buffers", + callback = function(event) + if event.file == "" and vim.bo[event.buf].buftype == "" and not vim.bo[event.buf].modified then + vim.schedule(function() + pcall(vim.api.nvim_buf_delete, event.buf, {}) + end) + end + end, +}) + +-------------------------------------------------- + +local codeRunnerEnabled = false + +function M.toggleCodeRunner() + codeRunnerEnabled = not codeRunnerEnabled + if codeRunnerEnabled then + print("Code Runner enabled") + M.RunCode() -- Execute when enabled + else + print("Code Runner disabled") + -- Close the terminal window when disabled + local buffers = vim.fn.getbufinfo() + + for _, buf in ipairs(buffers) do + local type = vim.api.nvim_buf_get_option(buf.bufnr, "buftype") + if type == "terminal" then + vim.api.nvim_command("silent! bdelete " .. buf.bufnr) + end + end + end +end + +local function substitute(cmd) + cmd = cmd:gsub("%%", vim.fn.expand("%")) + cmd = cmd:gsub("$fileBase", vim.fn.expand("%:r")) + cmd = cmd:gsub("$filePath", vim.fn.expand("%:p")) + cmd = cmd:gsub("$file", vim.fn.expand("%")) + cmd = cmd:gsub("$dir", vim.fn.expand("%:p:h")) + cmd = cmd:gsub("#", vim.fn.expand("#")) + cmd = cmd:gsub("$altFile", vim.fn.expand("#")) + + return cmd +end + +function M.RunCode() + if not codeRunnerEnabled then + print("Code Runner is currently disabled. Toggle it on to execute code.") + return + end + local file_extension = vim.fn.expand("%:e") + local selected_cmd = "" + local supported_filetypes = { + html = { + default = "%", + }, + c = { + default = "gcc % -o $fileBase && ./$fileBase", + debug = "gcc -g % -o $fileBase && ./$fileBase", + }, + cs = { + default = "dotnet run", + }, + cpp = { + default = "g++ % -o $fileBase && ./$fileBase", + debug = "g++ -g % -o ./$fileBase", + competitive = "g++ -std=c++17 -Wall -DAL -O2 % -o $fileBase && $fileBase<input.txt", + }, + py = { + default = "python %", + }, + go = { + default = "go run %", + }, + java = { + default = "java %", + }, + js = { + default = "node %", + debug = "node --inspect %", + }, + lua = { + default = "lua %", + }, + ts = { + default = "tsc % && node $fileBase", + }, + rs = { + default = "rustc % && $fileBase", + }, + php = { + default = "php %", + }, + r = { + default = "Rscript %", + }, + jl = { + default = "julia %", + }, + rb = { + default = "ruby %", + }, + pl = { + default = "perl %", + }, + } + + local term_cmd = "bot 10 new | term " + local choices = {} + + -- Add 'default' as the first option if available + if supported_filetypes[file_extension]["default"] then + table.insert(choices, "default") + end + + -- Add 'debug' as the second option if available + if supported_filetypes[file_extension]["debug"] then + table.insert(choices, "debug") + end + + -- Add other available options + for key, _ in pairs(supported_filetypes[file_extension]) do + if key ~= "default" and key ~= "debug" then + table.insert(choices, key) + end + end + if #choices == 0 then + vim.notify("It doesn't contain any command", vim.log.levels.WARN, { title = "Code Runner" }) + elseif #choices == 1 then + selected_cmd = supported_filetypes[file_extension][choices[1]] + vim.cmd(term_cmd .. substitute(selected_cmd)) + else + vim.ui.select(choices, { + prompt = "Choose a command: ", + layout_config = { + height = 10, + width = 40, + prompt_position = "top", + -- other options as required + }, + }, function(choice) + selected_cmd = supported_filetypes[file_extension][choice] + if selected_cmd then + vim.cmd(term_cmd .. substitute(selected_cmd)) + end + end) + end + + if not supported_filetypes[file_extension] then + vim.notify("The filetype isn't included in the list", vim.log.levels.WARN, { title = "Code Runner" }) + end +end + +-------------------------------------------------- + +-- Run executable file +local interpreters = { + python = "python", + lua = "lua", + bash = "bash", + zsh = "zsh", + perl = "perl", + ruby = "ruby", + node = "node", + rust = "rust", + php = "php", +} + +function M.RunCurrentFile() + local file_path = vim.fn.expand("%:p") + local file = io.open(file_path, "r") + + if not file then + print("Error: Unable to open the file") + return + end + + local shebang = file:read() + file:close() + + local interpreter = shebang:match("#!%s*(.-)$") + if not interpreter then + print("Error: No shebang line found in the file") + return + end + + -- Remove leading spaces and any arguments, extracting the interpreter name + interpreter = interpreter:gsub("^%s*([^%s]+).*", "%1") + + local cmd = interpreters[interpreter] + + if not cmd then + cmd = interpreter -- Set the command to the interpreter directly + end + + -- Run the file using the determined interpreter + vim.fn.jobstart(cmd .. " " .. file_path, { + cwd = vim.fn.expand("%:p:h"), + }) +end + +-------------------------------------------------- + +-- Close all floating windows +vim.api.nvim_create_user_command("CloseFloatingWindows", function(opts) + for _, window_id in ipairs(vim.api.nvim_list_wins()) do + -- If window is floating + if vim.api.nvim_win_get_config(window_id).relative ~= "" then + -- Force close if called with ! + vim.api.nvim_win_close(window_id, opts.bang) + end + end +end, { bang = true, nargs = 0 }) + +-------------------------------------------------- + + +-- Platform detection +local uname = vim.loop.os_uname().sysname +local has = vim.fn.has + +local is_mac = has("mac") == 1 +local is_linux = uname == "Linux" +local is_windows = has("win32") == 1 or uname:find("Windows") +local is_wsl = has("wsl") == 1 or (uname:find("Linux") and (os.getenv("WSL_DISTRO_NAME") ~= nil)) +local is_termux = has("termux") == 1 or (os.getenv("PREFIX") and os.getenv("PREFIX"):find("com.termux")) +local os_name = (is_mac and "mac") or (is_linux and "linux") or (is_windows and "windows") or (is_wsl and "wsl") or (is_termux and "termux") or uname:lower() + +-- Check if a command exists +local function command_exists(cmd) + local handle = io.popen(cmd .. " --version 2>/dev/null") + if handle then + local result = handle:read("*a") + handle:close() + return result ~= "" + end + return false +end + +-- Detect clipboard tool on Linux +local function detect_clipboard_tool() + if command_exists("xclip") then return "xclip" end + if command_exists("xsel") then return "xsel" end + if command_exists("wl-copy") and command_exists("wl-paste") then return "wl-clipboard" end + return nil +end + +-- OSC52 clipboard copy fallback +local function osc52_copy(text) + local encoded = vim.fn.system("base64 | tr -d '\n'", text) + io.write(string.format("\027]52;c;%s\007", encoded)) +end + +---- Set clipboard +--function set_clipboard(text) +-- if not text or text == "" then return end +-- +-- if is_mac then +-- local handle = io.popen("pbcopy", "w") +-- if handle then +-- handle:write(text) +-- handle:close() +-- end +-- elseif is_linux then +-- local tool = detect_clipboard_tool() +-- if tool == "xclip" then +-- local handle = io.popen("xclip -selection clipboard", "w") +-- if handle then handle:write(text) handle:close() end +-- elseif tool == "xsel" then +-- local handle = io.popen("xsel --clipboard --input", "w") +-- if handle then handle:write(text) handle:close() end +-- elseif tool == "wl-clipboard" then +-- local handle = io.popen("wl-copy", "w") +-- if handle then handle:write(text) handle:close() end +-- else +-- osc52_copy(text) +-- vim.notify("Using OSC52 for clipboard (install xclip, xsel, or wl-clipboard for better support)", vim.log.levels.INFO) +-- end +-- elseif is_wsl or is_windows then +-- local handle = io.popen("clip", "w") +-- if handle then handle:write(text) handle:close() end +-- elseif is_termux then +-- local handle = io.popen("termux-clipboard-set", "w") +-- if handle then handle:write(text) handle:close() end +-- else +-- vim.notify("No clipboard support for OS: " .. os_name, vim.log.levels.WARN) +-- end +--end +-- +---- Clipboard sync autocmd setup +--local function setup_clipboard_sync() +-- local ok, Job = pcall(require, "plenary.job") +-- if not ok then +-- -- plenary not available, skip +-- return +-- end +-- +-- vim.api.nvim_create_augroup("clipboard_sync", { clear = true }) +-- vim.api.nvim_create_autocmd("TextYankPost", { +-- group = "clipboard_sync", +-- desc = "Sync yanked text to system clipboard", +-- pattern = "*", +-- callback = function() +-- local text = vim.fn.getreg("\"") +-- if text ~= nil and text ~= "" then +-- set_clipboard(text) +-- end +-- end, +-- }) +--end +--setup_clipboard_sync() +-- +---- Terminal clear function (optional) +--function clear_terminal() +-- vim.opt.scrollback = 1 +-- vim.api.nvim_feedkeys("i", "n", false) +-- vim.api.nvim_feedkeys("clear\r", "n", false) +-- vim.api.nvim_feedkeys("\x1b", "n", false) +-- vim.api.nvim_feedkeys("i", "n", false) +-- vim.defer_fn(function() +-- vim.opt.scrollback = 10000 +-- end, 100) +--end +-- +---- Get clipboard content (optional) +--function GetClipboard() +-- local handle +-- +-- if is_mac then +-- handle = io.popen("pbpaste", "r") +-- elseif is_linux then +-- local tool = detect_clipboard_tool() +-- if tool == "xclip" then +-- handle = io.popen("xclip -selection clipboard -o", "r") +-- elseif tool == "xsel" then +-- handle = io.popen("xsel --clipboard --output", "r") +-- elseif tool == "wl-clipboard" then +-- handle = io.popen("wl-paste", "r") +-- end +-- elseif is_wsl or is_windows then +-- handle = io.popen("powershell.exe Get-Clipboard", "r") +-- elseif is_termux then +-- handle = io.popen("termux-clipboard-get", "r") +-- end +-- +-- if handle then +-- local result = handle:read("*a") +-- handle:close() +-- return result or "" +-- end +-- +-- return "" +--end + +-------------------------------------------------- + +-- Cross-platform file/URL opener +function M.open_file_or_url(path) + local commands = { + mac = string.format('open "%s"', path), + linux = string.format('xdg-open "%s" &', path), + wsl = string.format('wslview "%s" &', path), + windows = string.format('start "" "%s"', path), + termux = string.format('am start -a android.intent.action.VIEW -d "%s"', path), + } + + local cmd = commands[M.os_name] + if cmd then + os.execute(cmd) + else + vim.notify("No supported file opener for this OS: " .. tostring(M.os_name), vim.log.levels.WARN) + end +end + +-------------------------------------------------- + +-- Automcmd to close netrw buffer when file is opened +vim.api.nvim_create_autocmd("FileType", { + pattern = "netrw", + callback = function() + vim.api.nvim_create_autocmd("BufEnter", { + once = true, + callback = function() + if vim.bo.filetype ~= "netrw" then + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.bo[buf].filetype == "netrw" then + vim.api.nvim_buf_delete(buf, { force = true }) + end + end + end + end, + }) + end, +}) + +-------------------------------------------------- + +-- Autocomplete +vim.api.nvim_create_autocmd("InsertCharPre", { + callback = function() + -- Exit the autocmd if nvim-cmp is present + local cmp_is_present, _ = pcall(require, "cmp") + if cmp_is_present then + return + end + + -- Skip unwanted buffer types (Telescope, NvimTree, etc.) + local ft = vim.bo.filetype + local bt = vim.bo.buftype + local ignore_ft = { + "TelescopePrompt", + "prompt", + "nofile", + "terminal", + "help", + "quickfix", + "lazy", + "neo-tree", + "NvimTree", + "starter", + "packer", + } + + if bt ~= "" or vim.tbl_contains(ignore_ft, ft) then + return + end + + local col = vim.fn.col(".") + local line = vim.fn.getline(".") + local function safe_sub(i) + return line:sub(i, i) + end + + local prev3 = safe_sub(col - 3) + local prev2 = safe_sub(col - 2) + local prev1 = safe_sub(col - 1) + local curr = vim.v.char + + if curr:match("%w") and prev3:match("%W") and prev2:match("%w") and prev1:match("%w") then + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes("<C-n>", true, true, true), + "n", + true + ) + end + end, +}) +-------------------------------------------------- + +M.has_treesitter = function ( bufnr ) + if not bufnr then + bufnr = vim.api.nvim_get_current_buf() + end + + local highlighter = require( "vim.treesitter.highlighter" ) + + if highlighter.active[ bufnr ] then + return true + else + return false + end +end + +M.parse_treesitter = function ( bufnr, range ) + local parser = vim.treesitter.get_parser( bufnr ) + + -- XXX https://neovim.io/doc/user/treesitter.html#LanguageTree%3Aparse() + parser:parse( range ) +end + +-- ... +return M diff --git a/common/config/nvim/lua/user/opts.lua b/common/config/nvim/lua/user/opts.lua new file mode 100755 index 0000000..bac80c3 --- /dev/null +++ b/common/config/nvim/lua/user/opts.lua @@ -0,0 +1,438 @@ +-- ============================================================================ +-- Options +-- ============================================================================ + +local uname = vim.loop.os_uname() +local system = uname.sysname +local shell = nil + +if system == "Windows_NT" then + -- Windows options + if vim.fn.executable("pwsh") == 1 then + shell = "pwsh" + elseif vim.fn.executable("powershell") == 1 then + shell = "powershell" + elseif vim.fn.executable("bash") == 1 then + shell = "bash" + end +else + -- Unix-like systems: use the user's default shell + local env_shell = os.getenv("SHELL") + if env_shell and vim.fn.executable(env_shell) == 1 then + shell = env_shell + else + -- fallback logic + if vim.fn.executable("zsh") == 1 then + shell = vim.fn.exepath("zsh") + elseif vim.fn.executable("bash") == 1 then + shell = vim.fn.exepath("bash") + end + end +end + +-- Finally set the shell if we found one +if shell then + vim.o.shell = shell +end + +-- Core Settings +vim.opt.encoding = 'utf-8' +vim.opt.fileencoding = 'utf-8' +vim.scriptencoding = 'utf-8' +vim.opt.termguicolors = true +vim.opt.mouse = 'a' +vim.opt.clipboard = 'unnamedplus' +vim.opt.hidden = true +vim.opt.updatetime = 300 +vim.opt.timeoutlen = 500 +vim.opt.ttimeoutlen = 10 + +-- Display +vim.opt.number = true +vim.opt.relativenumber = true +vim.opt.cursorline = true +vim.opt.signcolumn = 'yes' +vim.opt.showcmd = true +vim.opt.showmode = true +vim.opt.showmatch = true +vim.opt.laststatus = 2 +vim.opt.cmdheight = 1 +vim.opt.scrolloff = 5 +vim.opt.sidescrolloff = 5 +vim.opt.display = 'lastline' + +-- Indentation +vim.opt.autoindent = true +vim.opt.smartindent = true +vim.opt.expandtab = true +vim.opt.tabstop = 2 +vim.opt.shiftwidth = 2 +vim.opt.softtabstop = 2 +vim.opt.shiftround = true + +-- Search +vim.opt.hlsearch = true +vim.opt.incsearch = true +vim.opt.ignorecase = true +vim.opt.smartcase = true +vim.opt.inccommand = 'split' + +-- Window Management +vim.opt.splitright = true +vim.opt.splitbelow = true +vim.opt.winminwidth = 1 +vim.opt.winwidth = 5 + +-- File Handling +vim.opt.autoread = true +--vim.opt.autowrite = true +vim.opt.backup = true +vim.opt.backupdir = vim.fn.stdpath('cache') .. '/backup//' +vim.opt.directory = vim.fn.stdpath('cache') .. '/swap//' +vim.opt.undofile = true +vim.opt.undodir = vim.fn.stdpath('cache') .. '/undo//' +vim.opt.swapfile = false + +-- Wildmenu +vim.opt.wildmenu = true +vim.opt.wildmode = 'longest:full,full' +vim.opt.wildignorecase = true +vim.opt.wildignore = '*.o,*.obj,.git,*.rbc,*.pyc,__pycache__' + + +vim.scriptencoding = "utf-8" -- +vim.opt.encoding = "utf-8" -- +vim.opt.fileencoding = "utf-8" -- +vim.g.python3_host_prog = "/usr/bin/python3" -- +vim.g.loaded_python3_provider = 1 -- +vim.g.sh_noisk = 1 -- iskeyword word boundaries when editing a 'sh' file +vim.o.autochdir = true +--vim.o.writeany= true + +-- Clipboard +vim.opt.clipboard:append({ "unnamedplus" }) -- Install xclip or this will slowdown startup + +-- Behaviour +vim.opt.backspace = { "start", "eol", "indent" } -- Make backspace work as you would expect. +vim.opt.hidden = true -- Switch between buffers without having to save first. +vim.opt.conceallevel = 2 +vim.opt.splitbelow = true -- make split put the new buffer below the current buffer +vim.opt.splitright = true -- make vsplit put the new buffer on the right of the current buffer +vim.opt.scrolloff = 8 -- +vim.opt.sidescrolloff = 8 -- how many lines to scroll when using the scrollbar +vim.opt.autoread = true -- reload files if changed externally +vim.opt.display = "lastline" -- Show as much as possible of the last line. +vim.opt.inccommand = "split" -- +vim.opt.ttyfast = true -- Faster redrawing. +vim.opt.lazyredraw = false -- Only redraw when necessary +vim.opt.keywordprg = ":help" -- :help options +vim.opt.ruler = true -- +vim.opt.errorbells = false -- +vim.opt.list = true -- Show non-printable characters. +vim.opt.showmatch = true -- +vim.opt.matchtime = 3 -- +vim.opt.showbreak = "↪ " -- +vim.opt.linebreak = true -- +vim.opt.exrc = true -- +--vim.opt.autochdir = true -- or use this to use <:e> to create a file in current directory +vim.opt.autoread = true -- if a file is changed outside of vim, automatically reload it without asking +--vim.opt.notimeout = true -- Timeout on keycodes and not mappings +vim.opt.ttimeout = true -- Makes terminal vim work sanely +vim.opt.ttimeoutlen = 10 -- +--vim.opt.timeoutlen = 100 -- time to wait for a mapped sequence to complete (in milliseconds) +--vim.cmd([[set diffopt = vertical = true]]) -- diffs are shown side-by-side not above/below + +-- Indent/tab +vim.opt.breakindent = true -- +vim.opt.autoindent = true -- Indent according to previous line. +vim.opt.copyindent = true -- Copy indent from the previous line +vim.opt.smarttab = false -- +vim.opt.tabstop = 2 -- +vim.opt.expandtab = true -- Indent according to previous line. +--vim.opt.expandtab = true -- Use spaces instead of tabs. +vim.opt.softtabstop = 2 -- Tab key indents by 2 spaces. +vim.opt.shiftwidth = 2 -- >> indents by 2 spaces. +vim.opt.shiftround = true -- >> indents to next multiple of 'shiftwidth'. +vim.opt.smartindent = true -- smart indent + +-- Column/statusline/Cl + +-- Enable number and relativenumber by default +vim.opt.number = true +vim.opt.relativenumber = true + +-- Entering insert mode: disable relativenumber +vim.api.nvim_create_autocmd("InsertEnter", { + callback = function() + vim.opt.relativenumber = false + end, +}) + +-- Leaving insert mode: enable relativenumber +vim.api.nvim_create_autocmd("InsertLeave", { + callback = function() + vim.opt.relativenumber = true + end, +}) + +vim.opt.title = true -- +--vim.opt.colorcolumn = "+1" -- +vim.opt.signcolumn = "yes:1" -- always show the sign column +--vim.opt.signcolumn = "yes:" .. vim.o.numberwidth +--vim.opt.signcolumn = "number" +--vim.opt.signcolumn = "no" -- +vim.opt.laststatus = 3 -- " Always show statusline. +vim.opt.showmode = true -- Show current mode in command-line, example: -- INSERT -- mode +vim.opt.showcmd = true -- Show the command in the status bar +vim.opt.cmdheight = 1 -- +--vim.opt.cmdheight = 0 -- +vim.opt.report = 0 -- Always report changed lines. +--local autocmd = vim.api.nvim_create_autocmd +--autocmd("bufenter", { +-- pattern = "*", +-- callback = function() +-- if vim.bo.ft ~= "terminal" then +-- vim.opt.statusline = "%!v:lua.require'ui.statusline'.run()" +-- else +-- vim.opt.statusline = "%#normal# " +-- end +-- end, +--}) +---- With vertical splits, the statusline would still show up at the +---- bottom of the split. A quick fix is to just set the statusline +---- to empty whitespace (it can't be an empty string because then +---- it'll get replaced by the default stline). +--vim.opt.stl = " " + +-- Backup/undo/swap +local prefix = vim.env.XDG_CONFIG_HOME or vim.fn.expand("~/.config") +--vim.opt.undodir = os.getenv("HOME") .. "/.vim/undodir" +--vim.opt.undodir = { prefix .. "/nvim/tmp/.undo//" } +vim.opt.undodir = os.getenv("HOME") .. "/.vim/undodir" +vim.opt.directory = { prefix .. "/nvim/tmp/.swp//" } +vim.opt.backupdir = { prefix .. "/nvim/tmp/.backup//" } +vim.opt.undofile = true -- +vim.opt.swapfile = true -- +vim.opt.backup = true -- +--vim.opt.backupcopy = +-- Add timestamp as extension for backup files +vim.api.nvim_create_autocmd("BufWritePre", { + group = vim.api.nvim_create_augroup("timestamp_backupext", { clear = true }), + desc = "Add timestamp to backup extension", + pattern = "*", + callback = function() + vim.opt.backupext = "-" .. vim.fn.strftime("%Y%m%d%H%M") + end, +}) + +-- Format +--vim.opt.textwidth = 80 -- +vim.opt.isfname:append("@-@") +vim.cmd([[let &t_Cs = "\e[4:3m"]]) -- Undercurl +vim.cmd([[let &t_Ce = "\e[4:0m"]]) -- +vim.opt.path:append({ "**" }) -- Finding files - Search down into subfolder +vim.cmd("set whichwrap+=<,>,[,],h,l") -- +vim.cmd([[set iskeyword+=-]]) -- +--vim.cmd([[set formatoptions-=cro]]) -- TODO: this doesn't seem to work +vim.opt.formatoptions = vim.opt.formatoptions + - "t" -- wrap with text width + + "c" -- wrap comments + + "r" -- insert comment after enter + - "o" -- insert comment after o/O + - "q" -- allow formatting of comments with gq + - "a" -- format paragraphs + + "n" -- recognized numbered lists + - "2" -- use indent of second line for paragraph + + "l" -- long lines are not broken + + "j" -- remove comment when joining lines +vim.opt.wrapscan = true -- " Searches wrap around end-of-file. +--vim.wo.number = true -- +--vim.opt.wrap = false -- No Wrap lines +--vim.opt.foldmethod = 'manual' -- +--vim.opt.foldmethod = "expr" -- +vim.opt.foldmethod = "manual" +vim.opt.foldlevel = 3 +vim.opt.confirm = false +--vim.opt.shortmess:append("sI") +--vim.opt.shortmess = "a" +--vim.opt.shortmess = "sI" +--vim.o.shortmess = vim.o.shortmess:gsub('s', '') +vim.opt.shortmess = table.concat({ -- Use abbreviations and short messages in command menu line. + "f", -- Use "(3 of 5)" instead of "(file 3 of 5)". + "i", -- Use "[noeol]" instead of "[Incomplete last line]". + "l", -- Use "999L, 888C" instead of "999 lines, 888 characters". + "m", -- Use "[+]" instead of "[Modified]". + "n", -- Use "[New]" instead of "[New File]". + "r", -- Use "[RO]" instead of "[readonly]". + "w", -- Use "[w]", "[a]" instead of "written", "appended". + "x", -- Use "[dos]", "[unix]", "[mac]" instead of "[dos format]", "[unix format]", "[mac format]". + "o", -- Overwrite message for writing a file with subsequent message. + "O", -- Message for reading a file overwrites any previous message. + "s", -- Disable "search hit BOTTOM, continuing at TOP" such messages. + "t", -- Truncate file message at the start if it is too long. + "T", -- Truncate other messages in the middle if they are too long. + "I", -- Don't give the :intro message when starting. + "c", -- Don't give ins-completion-menu messages. + "F", -- Don't give the file info when editing a file. +}) +vim.opt.fillchars = { + horiz = "─", + horizup = "┴", + horizdown = "┬", + vert = "│", + vertleft = "┤", + vertright = "├", + verthoriz = "┼", + foldopen = "", + foldsep = "│", + foldclose = "", + fold = "─", + eob = " ", + --diff = "┃", + diff = "░", + msgsep = "━", + --msgsep = "‾", +} +vim.opt.listchars = { tab = "▸ ", trail = "·" } -- +--vim.opt.fillchars:append({ eob = " " }) -- remove the ~ from end of buffer +vim.opt.modeline = true -- +vim.opt.modelines = 3 -- modelines (comments that set vim options on a per-file basis) +--vim.opt.modelineexpr = true +--vim.opt.nofoldenable = true -- turn folding off +--vim.opt.foldenable = false -- turn folding off +vim.o.showtabline = 2 + +-- Highlights +vim.opt.incsearch = true -- Highlight while searching with / or ?. +vim.opt.hlsearch = true -- Keep matches highlighted. +vim.opt.ignorecase = true -- ignore case in search patterns UNLESS /C or capital in search +vim.opt.smartcase = true -- smart case +vim.opt.synmaxcol = 200 -- Only highlight the first 200 columns. +--vim.opt.winblend = 30 +--vim.opt.winblend = 5 +vim.opt.wildoptions = "pum" -- +--vim.opt.pumblend = 5 -- +vim.opt.pumblend = 12 -- +--vim.opt.pumblend=15 +vim.opt.pumheight = 10 -- pop up menu height + +-- Better Completion +vim.opt.complete = { ".", "w", "b", "u", "t" } -- +--vim.opt.completeopt = { "longest,menuone,preview" } -- +vim.opt.completeopt = { "menu", "menuone", "noselect" } +--vim.opt.completeopt = { "menuone", "noselect" } -- mostly just for cmp +--vim.opt.completeopt = { "menu", "menuone", "noselect" } -- + +-- Spellcheck +vim.opt.spelllang = { "en_gb", "en_us" } -- Set a list of preferred dictionaries +vim.opt.spell = true +--vim.opt.spellfile = "~/.config/nvim/spell/en.utf-8.add" -- Specify a personal dictionary file + +-- Wildmenu completion -- +vim.opt.wildmenu = true -- +vim.opt.wildmode = { "list:longest" } -- +vim.opt.wildignore:append({ ".hg", ".git", ".svn" }) -- Version control +vim.opt.wildignore:append({ "*.aux", "*.out", "*.toc" }) -- LaTeX intermediate files +vim.opt.wildignore:append({ "*.jpg", "*.bmp", "*.gif", "*.png", "*.jpeg" }) -- binary images +vim.opt.wildignore:append({ "*.o", "*.obj", "*.exe", "*.dll", "*.manifest" }) -- compiled object files +vim.opt.wildignore:append({ "*.spl" }) -- compiled spelling word lists +vim.opt.wildignore:append({ "*.sw?" }) -- Vim swap files +vim.opt.wildignore:append({ "*.DS_Store" }) -- OSX bullshit +vim.opt.wildignore:append({ "*.luac" }) -- Lua byte code +vim.opt.wildignore:append({ "migrations" }) -- Django migrations +vim.opt.wildignore:append({ "*.pyc" }) -- Python byte code +vim.opt.wildignore:append({ "*.orig" }) -- Merge resolution files +vim.opt.wildignore:append({ "*/node_modules/*" }) -- + +-- Shada +vim.opt.shada = "!,'1000,f1,<1000,s100,:1000,/1000,h" + +-- Sessions +vim.opt.sessionoptions = "blank,buffers,curdir,folds,help,tabpages,winsize,winpos,terminal" +--vim.opt.sessionoptions = "curdir,folds,help,options,tabpages,winsize,winpos,terminal,globals" -- +--vim.opt.sessionoptions = "buffers,curdir,folds,help,tabpages,winsize,winpos,terminal" +--vim.opt.sessionoptions:remove({ "blank", "buffers", "globals" }) + +-- Netrw file tree +vim.g.netrw_browse_split = 0 +vim.g.netrw_banner = 0 +vim.g.netrw_winsize = 25 + +-- " Load indent files, to automatically do language-dependent indenting. +--vim.cmd([[ +-- "filetype plugin indent on +--]]) +vim.cmd("filetype plugin on") +vim.cmd("filetype indent off") + +--vim.cmd([[ +-- "autocmd BufEnter * :syntax sync fromstart +-- "syntax enable +-- "set nocompatible +-- "autocmd FileType lua set comments=s1:---,m:--,ex:-- +--]]) + + +-- Fast macros without lazyredraw +vim.cmd([[ + set re=0 + nnoremap @ <cmd>execute "noautocmd norm! " . v:count1 . "@" . getcharstr()<cr> + xnoremap @ :<C-U>execute "noautocmd '<,'>norm! " . v:count1 . "@" . getcharstr()<cr> +]]) + +-- Stop annoying auto commenting on new lines +vim.cmd([[ + augroup annoying + au! + au BufEnter * set fo-=c fo-=r fo-=o + augroup end +]]) + +-- Cursorline +vim.cmd([[ " Only show cursorline in the current window and in normal mode + augroup cline + au! + au WinLeave,InsertEnter * set nocursorline + au WinEnter,InsertLeave * set cursorline + augroup END +]]) +vim.opt.cursorline = true -- +vim.opt.guicursor = "i:ver100,r:hor100" -- + +-- Trailing whitespace +vim.cmd([[ " Only show in insert mode + augroup trailing + au! + au InsertEnter * :set listchars-=trail:⌴ + au InsertLeave * :set listchars+=trail:⌴ + augroup END +]]) + +-- Line Return +vim.cmd([[ " Return to the same line when we reopen a file + augroup line_return + au! + au BufReadPost * + \ if line("'\"") > 0 && line("'\"") <= line("$") | + \ execute 'normal! g`"zvzz' | + \ endif + augroup END +]]) + +-- Enable mouse scrollback +vim.cmd([[ + set mouse=a + tnoremap <Esc> <C-\><C-n> + tnoremap <c-b> <c-\><c-n> + function! ClearTerminal() + set scrollback=1 + let &g:scrollback=1 + echo &scrollback + call feedkeys("\i") + call feedkeys("clear\<CR>") + call feedkeys("\<C-\>\<C-n>") + call feedkeys("\i") + sleep 100m + let &scrollback=s:scroll_value + endfunction +]]) diff --git a/common/config/nvim/lua/user/view.lua b/common/config/nvim/lua/user/view.lua new file mode 100755 index 0000000..f243194 --- /dev/null +++ b/common/config/nvim/lua/user/view.lua @@ -0,0 +1,180 @@ +-- ============================================================================ +-- View/UI +-- ============================================================================ + +local M = {} + +-- List of available themes (for reference or user selection UI) +M.available_themes = { + "nightfly", "ayu", "onedark", "doom-one", "nvimgelion", "github_dark", "tokyonight", "bamboo", "oxocarbon" +} + +-- Configuration +local default_colorscheme = "tokyonight" +local fallback_colorscheme = "default" + +-- Diagnostic icons +local Signs = { + Error = "✘", + Warn = "", + Hint = "◉", + Info = "", +} + +-- Setup Function +function M.setup() + -- Truecolor & syntax + vim.opt.termguicolors = true + vim.cmd("syntax on") + + -- Colorscheme setup with fallback + local ok = pcall(vim.cmd, "colorscheme " .. default_colorscheme) + if not ok then + vim.cmd("colorscheme " .. fallback_colorscheme) + end + + -- Optional: Tokyonight configuration + pcall(function() + require("tokyonight").setup({ + style = "night", + transparent = true, + transparent_sidebar = true, + dim_inactive = false, + styles = { + sidebars = "transparent", + floats = "transparent", + }, + }) + end) + + -- Highlight groups + local highlights = { + -- Core UI + { group = "Normal", options = { bg = "none" } }, + { group = "NormalNC", options = { bg = "none" } }, + { group = "NormalFloat", options = { bg = "none" } }, + { group = "Float", options = { bg = "none" } }, + { group = "FloatBorder", options = { bg = "none", fg = "#7f8493" } }, + { group = "StatusLine", options = { bg = "none" } }, + { group = "TabLine", options = { bg = "#333842", bold = true } }, + { group = "TabLineSel", options = { bg = "#333842", bold = true } }, + { group = "TabLineFill", options = { bg = "none", bold = true } }, + { group = "WinBar", options = { bg = "none", bold = true } }, + { group = "WinBarNC", options = { bg = "none" } }, + { group = "WinSeparator", options = { bg = "none", fg = "#444b62", bold = true } }, + { group = "EndOfBuffer", options = { bg = "none", fg = "#7f8493" } }, + { group = "NonText", options = { bg = "none", fg = "#555b71" } }, + { group = "LineNr", options = { bg = "none", fg = "#555b71" } }, + { group = "SignColumn", options = { bg = "none" } }, + { group = "FoldColumn", options = { bg = "none" } }, + { group = "CursorLine", options = { bg = "#3a3f52" } }, + { group = "CursorLineNr", options = { bg = "#3a3f52", fg = "#cdd6f4" } }, + { group = "CursorLineSign", options = { bg = "none" } }, + { group = "Title", options = { bg = "none", bold = true } }, + { group = "Comment", options = { bg = "none", fg = "#6b7089" } }, + { group = "MsgSeparator", options = { bg = "none" } }, + { group = "WarningMsg", options = { bg = "none", fg = "#e6c384" } }, + { group = "MoreMsg", options = { bg = "none", fg = "#7f8493" } }, + + -- Pop-up / menu + { group = "Pmenu", options = { bg = "none" } }, + { group = "PmenuSel", options = { fg = "black", bg = "white" } }, + { group = "PmenuThumb", options = { bg = "none" } }, + { group = "PmenuSbar", options = { bg = "none" } }, + { group = "PmenuExtra", options = { bg = "none" } }, + { group = "PmenuExtraSel", options = { bg = "none" } }, + { group = "WildMenu", options = { link = "PmenuSel" } }, + + -- Telescope + { group = "TelescopeNormal", options = { bg = "none" } }, + { group = "TelescopePromptNormal", options = { bg = "none" } }, + { group = "TelescopeResultsNormal", options = { bg = "none" } }, + { group = "TelescopePreviewNormal", options = { bg = "none" } }, + { group = "TelescopeBorder", options = { bg = "none", fg = "#7f8493" } }, + { group = "TelescopeMatching", options = { fg = "#cba6f7", bold = true } }, + + -- Blending + { group = "Winblend", options = { bg = "none" } }, + { group = "Pumblend", options = { bg = "none" } }, + + ---- NvimTree + --{ group = "NvimTreeNormal", options = { bg = "none", fg = "NONE" } }, + --{ group = "NvimTreeNormalNC", options = { bg = "none", fg = "NONE" } }, + --{ group = "NvimTreeNormalFloat", options = { bg = "none" } }, + --{ group = "NvimTreeEndOfBuffer", options = { bg = "none" } }, + --{ group = "NvimTreeCursorLine", options = { bg = "#50fa7b", fg = "#000000" } }, + --{ group = "NvimTreeSymlinkFolderName", options = { fg = "#f8f8f2", bg = "none" } }, + --{ group = "NvimTreeFolderName", options = { fg = "#f8f8f2", bg = "none" } }, + --{ group = "NvimTreeRootFolder", options = { fg = "#f8f8f2", bg = "none" } }, + --{ group = "NvimTreeEmptyFolderName", options = { fg = "#f8f8f2", bg = "none" } }, + --{ group = "NvimTreeOpenedFolderName", options = { fg = "#f8f8f2", bg = "none" } }, + --{ group = "NvimTreeOpenedFile", options = { fg = "#50fa7b", bg = "none" } }, + --{ group = "NvimTreeExecFile", options = { fg = "#ff882a", bg = "none" } }, + } + + for _, hl in ipairs(highlights) do + vim.api.nvim_set_hl(0, hl.group, hl.options) + end + + -- Reapply highlights on ColorScheme change + vim.api.nvim_create_autocmd("ColorScheme", { + group = vim.api.nvim_create_augroup("CustomHighlights", { clear = true }), + pattern = "*", + callback = function() + for _, hl in ipairs(highlights) do + vim.api.nvim_set_hl(0, hl.group, hl.options) + end + end, + }) + + -- Optional window separator styling + vim.cmd([[ + augroup CustomWinSeparator + autocmd! + autocmd WinEnter * setlocal winhl=WinSeparator:WinSeparatorA + autocmd WinLeave * setlocal winhl=WinSeparator:WinSeparator + augroup END + ]]) + + -- Diagnostics configuration + local border = "rounded" + vim.diagnostic.config({ + signs = { + text = { + [vim.diagnostic.severity.ERROR] = Signs.Error, + [vim.diagnostic.severity.WARN] = Signs.Warn, + [vim.diagnostic.severity.HINT] = Signs.Hint, + [vim.diagnostic.severity.INFO] = Signs.Info, + }, + }, + underline = true, + virtual_text = false, + virtual_lines = false, + float = { + show_header = true, + source = "always", + border = border, + focusable = true, + }, + update_in_insert = false, + severity_sort = true, + }) + + -- Fallback statusline if heirline is missing + local heirline_ok, _ = pcall(require, "heirline") + if not heirline_ok then + local statusline_path = vim.fn.stdpath("config") .. "/autoload/statusline.vim" + if vim.fn.filereadable(statusline_path) == 1 then + vim.cmd.source(statusline_path) + vim.api.nvim_create_autocmd("VimEnter", { + callback = function() + vim.cmd("call autoload#statusline#ActivateStatusline()") + end, + }) + else + vim.notify("Fallback statusline script not found:\n" .. statusline_path, vim.log.levels.ERROR) + end + end +end + +return M diff --git a/common/config/nvim/neovim.ps1 b/common/config/nvim/neovim.ps1 new file mode 100755 index 0000000..a63965e --- /dev/null +++ b/common/config/nvim/neovim.ps1 @@ -0,0 +1,917 @@ +# Neovim Installation Script for Windows (PowerShell) +# Created By: srdusr (PowerShell port) +# Project: Install/update/uninstall/change version Neovim script for Windows + +#Requires -Version 5.1 + +param( + [switch]$Force, + [switch]$NoPrompt +) + +# Color definitions +$Colors = @{ + Red = "Red" + Green = "Green" + Yellow = "Yellow" + Cyan = "Cyan" +} + +# Global variables +$Script:DownloadCommand = $null +$Script:IsAdmin = $false +$Script:ShowPrompt = $true +$Script:NeovimPath = "$env:LOCALAPPDATA\nvim" +$Script:NeovimBin = "$Script:NeovimPath\bin" +$Script:NeovimExe = "$Script:NeovimBin\nvim.exe" + +# Handle errors +function Write-ErrorMessage { + param([string]$Message) + Write-Host "Error: $Message" -ForegroundColor $Colors.Red +} + +# Handle success messages +function Write-SuccessMessage { + param([string]$Message) + Write-Host $Message -ForegroundColor $Colors.Green +} + +# Handle info messages +function Write-InfoMessage { + param([string]$Message) + Write-Host $Message -ForegroundColor $Colors.Cyan +} + +# Check if running as administrator +function Test-Administrator { + $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +# Check if necessary dependencies are installed +function Test-Dependencies { + Write-InfoMessage "Checking dependencies..." + + # Check for download tools + if (Get-Command curl -ErrorAction SilentlyContinue) { + $Script:DownloadCommand = "curl" + Write-InfoMessage "Found curl" + } elseif (Get-Command wget -ErrorAction SilentlyContinue) { + $Script:DownloadCommand = "wget" + Write-InfoMessage "Found wget" + } else { + Write-InfoMessage "Neither curl nor wget found. Will use PowerShell's Invoke-WebRequest" + $Script:DownloadCommand = "powershell" + } + + # Check for admin privileges + $Script:IsAdmin = Test-Administrator + if (-not $Script:IsAdmin) { + Write-Host "Warning: Not running as administrator. Some operations may fail." -ForegroundColor $Colors.Yellow + if (-not $NoPrompt) { + $continue = Read-Host "Continue anyway? (y/n)" + if ($continue -notin @('y', 'yes', 'Y', 'Yes')) { + exit 1 + } + } + } + + return $true +} + +# Find all Neovim installations +function Find-AllNeovimInstallations { + $installations = @() + + # Check common installation paths + $commonPaths = @( + "$env:LOCALAPPDATA\nvim", + "$env:ProgramFiles\Neovim", + "$env:ProgramFiles(x86)\Neovim", + "$env:APPDATA\nvim", + "$env:USERPROFILE\nvim", + "C:\tools\neovim", + "C:\neovim" + ) + + foreach ($path in $commonPaths) { + # Check for nvim.exe in bin subdirectory + if (Test-Path "$path\bin\nvim.exe") { + $installations += @{ + Path = $path + BinPath = "$path\bin" + Type = "Portable" + Version = Get-NeovimVersion -Path "$path\bin\nvim.exe" + } + } + # Check for nvim.exe directly in path + elseif (Test-Path "$path\nvim.exe") { + $installations += @{ + Path = $path + BinPath = $path + Type = "Portable" + Version = Get-NeovimVersion -Path "$path\nvim.exe" + } + } + # Check for nvim-win64 subdirectory (common structure) + elseif (Test-Path "$path\nvim-win64\bin\nvim.exe") { + $installations += @{ + Path = "$path\nvim-win64" + BinPath = "$path\nvim-win64\bin" + Type = "Portable" + Version = Get-NeovimVersion -Path "$path\nvim-win64\bin\nvim.exe" + } + } + } + + # Check for MSI installations in registry + try { + $uninstallKeys = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + foreach ($keyPath in $uninstallKeys) { + Get-ItemProperty $keyPath -ErrorAction SilentlyContinue | Where-Object { + $_.DisplayName -like "*Neovim*" -or $_.DisplayName -like "*nvim*" + } | ForEach-Object { + $installations += @{ + Path = $_.InstallLocation + BinPath = "$($_.InstallLocation)\bin" + Type = "MSI" + Version = $_.DisplayVersion + UninstallString = $_.UninstallString + ProductCode = $_.PSChildName + } + } + } + } + catch { + Write-InfoMessage "Could not check registry for MSI installations" + } + + # Check PATH for nvim.exe - this is crucial for detecting installations + $pathDirs = $env:PATH -split ';' | Where-Object { $_ -ne "" } + foreach ($dir in $pathDirs) { + $dir = $dir.Trim() + if (Test-Path "$dir\nvim.exe") { + # Determine the installation root + $installRoot = $dir + if ($dir -like "*\bin") { + $installRoot = Split-Path -Parent $dir + } + + # Check if we already found this installation + $alreadyFound = $false + foreach ($existing in $installations) { + if ($existing.Path -eq $installRoot -or $existing.BinPath -eq $dir) { + $alreadyFound = $true + break + } + } + + if (-not $alreadyFound) { + $installations += @{ + Path = $installRoot + BinPath = $dir + Type = "PATH" + Version = Get-NeovimVersion -Path "$dir\nvim.exe" + } + } + } + } + + return $installations +} + +# Get Neovim version from executable +function Get-NeovimVersion { + param([string]$Path) + + try { + $versionOutput = & $Path --version 2>$null | Select-String "NVIM v(\d+\.\d+\.\d+)" | ForEach-Object { $_.Matches[0].Groups[1].Value } + return $versionOutput + } + catch { + return "Unknown" + } +} + +# Complete uninstall of all Neovim instances +function Uninstall-AllNeovim { + param([switch]$Silent) + + Write-InfoMessage "Searching for all Neovim installations..." + $installations = Find-AllNeovimInstallations + + if ($installations.Count -eq 0) { + Write-InfoMessage "No Neovim installations found." + return $true + } + + if (-not $Silent) { + Write-Host "Found $($installations.Count) Neovim installation(s):" -ForegroundColor $Colors.Yellow + for ($i = 0; $i -lt $installations.Count; $i++) { + Write-Host " $($i + 1). $($installations[$i].Type) - $($installations[$i].Path) (v$($installations[$i].Version))" + } + + if (-not $NoPrompt) { + $confirm = Read-Host "Remove all installations? (y/n)" + if ($confirm -notin @('y', 'yes', 'Y', 'Yes')) { + Write-InfoMessage "Uninstall cancelled." + return $false + } + } + } + + $success = $true + foreach ($installation in $installations) { + Write-InfoMessage "Removing $($installation.Type) installation: $($installation.Path)" + + try { + if ($installation.Type -eq "MSI") { + # Uninstall MSI package + if ($installation.UninstallString) { + if ($installation.UninstallString -like "*msiexec*") { + # Extract the product code from the uninstall string + $productCode = $installation.ProductCode + if ($productCode -and $productCode -match "^\{.*\}$") { + $uninstallArgs = "/x `"$productCode`" /quiet /norestart" + Write-InfoMessage "Uninstalling MSI with product code: $productCode" + Start-Process -FilePath "msiexec.exe" -ArgumentList $uninstallArgs -Wait -NoNewWindow + } else { + # Fallback to parsing the uninstall string + $uninstallArgs = $installation.UninstallString -replace "MsiExec.exe", "" -replace "/I\{", "/x{" + $uninstallArgs += " /quiet /norestart" + Write-InfoMessage "Uninstalling MSI with args: $uninstallArgs" + Start-Process -FilePath "msiexec.exe" -ArgumentList $uninstallArgs -Wait -NoNewWindow + } + } else { + # Direct uninstall command + Write-InfoMessage "Running uninstall command: $($installation.UninstallString)" + Invoke-Expression $installation.UninstallString + } + + if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq $null) { + Write-SuccessMessage "Successfully uninstalled MSI package" + } else { + Write-ErrorMessage "MSI uninstall failed with exit code: $LASTEXITCODE" + $success = $false + } + } else { + Write-ErrorMessage "No uninstall string found for MSI package" + $success = $false + } + } else { + # Remove portable installation + if (Test-Path $installation.Path) { + Write-InfoMessage "Removing directory: $($installation.Path)" + # Force kill any processes that might be using files in the directory + Get-Process -Name "nvim*" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + + # Try to remove the directory + Remove-Item -Path $installation.Path -Recurse -Force -ErrorAction Stop + Write-SuccessMessage "Successfully removed directory: $($installation.Path)" + } else { + Write-InfoMessage "Directory already removed: $($installation.Path)" + } + } + } + catch { + Write-ErrorMessage "Failed to remove $($installation.Path): $_" + # Try alternative removal methods + try { + Write-InfoMessage "Attempting alternative removal method..." + if (Test-Path $installation.Path) { + # Use robocopy to move and delete (works around file locks) + $tempDir = "$env:TEMP\nvim_removal_$(Get-Random)" + robocopy "$($installation.Path)" "$tempDir" /E /MOVE /NFL /NDL /NJH /NJS /NC /NS /NP + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + Write-SuccessMessage "Successfully removed using alternative method: $($installation.Path)" + } + } + catch { + Write-ErrorMessage "Alternative removal also failed: $_" + $success = $false + } + } + } + + # Clean up PATH environment variable + Write-InfoMessage "Cleaning up PATH environment variable..." + + # Get both user and system PATH + $userPath = [Environment]::GetEnvironmentVariable("PATH", "User") + $systemPath = "" + + if ($Script:IsAdmin) { + try { + $systemPath = [Environment]::GetEnvironmentVariable("PATH", "Machine") + } + catch { + Write-InfoMessage "Could not access system PATH" + } + } + + # Create patterns for all found installations + $nvimPathPatterns = @() + foreach ($installation in $installations) { + $nvimPathPatterns += [regex]::Escape($installation.BinPath) + $nvimPathPatterns += [regex]::Escape($installation.Path) + } + + # Add common patterns + $nvimPathPatterns += @( + [regex]::Escape($Script:NeovimBin), + [regex]::Escape("$env:ProgramFiles\Neovim\bin"), + [regex]::Escape("$env:ProgramFiles(x86)\Neovim\bin"), + [regex]::Escape("C:\tools\neovim\nvim-win64\bin"), + [regex]::Escape("C:\tools\neovim\nvim-win64") + ) + + # Clean user PATH + $originalUserPath = $userPath + foreach ($pattern in $nvimPathPatterns) { + $userPath = $userPath -replace ";$pattern;", ";" + $userPath = $userPath -replace "^$pattern;", "" + $userPath = $userPath -replace ";$pattern$", "" + $userPath = $userPath -replace "^$pattern$", "" + } + + if ($userPath -ne $originalUserPath) { + try { + [Environment]::SetEnvironmentVariable("PATH", $userPath, "User") + Write-InfoMessage "Cleaned up user PATH" + } + catch { + Write-ErrorMessage "Failed to clean up user PATH: $_" + } + } + + # Clean system PATH if running as admin + if ($Script:IsAdmin -and $systemPath) { + $originalSystemPath = $systemPath + foreach ($pattern in $nvimPathPatterns) { + $systemPath = $systemPath -replace ";$pattern;", ";" + $systemPath = $systemPath -replace "^$pattern;", "" + $systemPath = $systemPath -replace ";$pattern$", "" + $systemPath = $systemPath -replace "^$pattern$", "" + } + + if ($systemPath -ne $originalSystemPath) { + try { + [Environment]::SetEnvironmentVariable("PATH", $systemPath, "Machine") + Write-InfoMessage "Cleaned up system PATH" + } + catch { + Write-ErrorMessage "Failed to clean up system PATH: $_" + } + } + } + + if ($success) { + Write-SuccessMessage "All Neovim installations have been removed successfully!" + } else { + Write-Host "Some installations could not be removed completely." -ForegroundColor $Colors.Yellow + } + + Write-InfoMessage "You may need to restart your shell for PATH changes to take effect." + return $success +} + +# Check if Neovim is already installed +function Test-NeovimInstalled { + if (Test-Path $Script:NeovimExe) { + return $true + } + + # Check if nvim is in PATH + if (Get-Command nvim -ErrorAction SilentlyContinue) { + return $true + } + + return $false +} + +# Download a file - FIXED VERSION +function Get-FileDownload { + param( + [string]$Url, + [string]$OutputPath + ) + + Write-InfoMessage "Downloading from: $Url" + Write-InfoMessage "Saving to: $OutputPath" + + try { + switch ($Script:DownloadCommand) { + "curl" { + # Use Start-Process instead of cmd /c for better compatibility + $curlArgs = @("-L", "--progress-bar", "-o", $OutputPath, $Url) + $process = Start-Process -FilePath "curl" -ArgumentList $curlArgs -Wait -NoNewWindow -PassThru + if ($process.ExitCode -ne 0) { + throw "Curl download failed with exit code: $($process.ExitCode)" + } + } + "wget" { + # Use Start-Process instead of cmd /c for better compatibility + $wgetArgs = @("--progress=bar", "--show-progress", "-O", $OutputPath, $Url) + $process = Start-Process -FilePath "wget" -ArgumentList $wgetArgs -Wait -NoNewWindow -PassThru + if ($process.ExitCode -ne 0) { + throw "Wget download failed with exit code: $($process.ExitCode)" + } + } + "powershell" { + # Enhanced PowerShell download with progress + Write-InfoMessage "Using PowerShell's Invoke-WebRequest..." + $ProgressPreference = 'Continue' + + # Create a WebClient for better progress reporting + $webClient = New-Object System.Net.WebClient + $webClient.Headers.Add("User-Agent", "PowerShell Neovim Installer") + + # Register progress event + Register-ObjectEvent -InputObject $webClient -EventName DownloadProgressChanged -Action { + $Global:DownloadProgress = $Event.SourceEventArgs.ProgressPercentage + Write-Progress -Activity "Downloading Neovim" -Status "Progress: $($Event.SourceEventArgs.ProgressPercentage)%" -PercentComplete $Event.SourceEventArgs.ProgressPercentage + } | Out-Null + + # Download the file + try { + $webClient.DownloadFile($Url, $OutputPath) + Write-Progress -Activity "Downloading Neovim" -Completed + } + finally { + $webClient.Dispose() + # Clean up event handlers + Get-EventSubscriber | Where-Object { $_.SourceObject -eq $webClient } | Unregister-Event + } + } + } + + # Verify the file was downloaded + if (-not (Test-Path $OutputPath)) { + throw "Downloaded file not found at: $OutputPath" + } + + $fileSize = (Get-Item $OutputPath).Length + if ($fileSize -eq 0) { + throw "Downloaded file is empty" + } + + Write-InfoMessage "Download completed successfully. File size: $($fileSize / 1MB) MB" + return $true + } + catch { + Write-ErrorMessage "Download failed: $_" + # Clean up partial download + if (Test-Path $OutputPath) { + Remove-Item -Path $OutputPath -Force -ErrorAction SilentlyContinue + } + return $false + } +} + +# Get available versions from GitHub API +function Get-AvailableVersions { + try { + $apiUrl = "https://api.github.com/repos/neovim/neovim/releases" + $response = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing + return $response | ForEach-Object { $_.tag_name } + } + catch { + Write-ErrorMessage "Failed to fetch available versions: $_" + return @() + } +} + +# Check if a specific version exists +function Test-VersionExists { + param([string]$Version) + + if ($Version -notmatch "^v") { + $Version = "v$Version" + } + + $versions = Get-AvailableVersions + return $versions -contains $Version +} + +# Get the latest stable version +function Get-LatestStableVersion { + try { + $apiUrl = "https://api.github.com/repos/neovim/neovim/releases/latest" + $response = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing + return $response.tag_name + } + catch { + Write-ErrorMessage "Failed to fetch latest version: $_" + return $null + } +} + +# Download specific version +function Get-SpecificVersion { + param([string]$Version) + + if ($Version -notmatch "^v") { + $Version = "v$Version" + } + + try { + $apiUrl = "https://api.github.com/repos/neovim/neovim/releases/tags/$Version" + $response = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing + + # Look for Windows assets + $asset = $response.assets | Where-Object { + $_.name -match "nvim-win64\.zip$" -or + $_.name -match "nvim-win64\.msi$" -or + $_.name -match "nvim-windows\.zip$" + } | Select-Object -First 1 + + if (-not $asset) { + Write-ErrorMessage "No Windows asset found for version ${Version}" + return $null + } + + $fileName = $asset.name + $downloadUrl = $asset.browser_download_url + + Write-InfoMessage "Found asset: $fileName" + + if (Get-FileDownload -Url $downloadUrl -OutputPath $fileName) { + return $fileName + } + } + catch { + Write-ErrorMessage "Failed to download version ${Version}: $_" + } + + return $null +} + +# Install Neovim from downloaded file +function Install-NeovimFromFile { + param( + [string]$FilePath, + [string]$Version = "Unknown", + [switch]$CleanInstall + ) + + try { + if ($CleanInstall) { + Write-InfoMessage "Performing clean installation - removing existing installations..." + Uninstall-AllNeovim -Silent + } + + Write-InfoMessage "Installing Neovim ${Version}..." + + $fileExtension = [System.IO.Path]::GetExtension($FilePath).ToLower() + + if ($fileExtension -eq ".msi") { + # Handle MSI installation + Write-InfoMessage "Installing from MSI package..." + $installArgs = @( + "/i", "`"$FilePath`"" + "/quiet" + "/norestart" + ) + + Start-Process -FilePath "msiexec.exe" -ArgumentList $installArgs -Wait -NoNewWindow + + if ($LASTEXITCODE -eq 0) { + Write-SuccessMessage "Neovim ${Version} installed successfully via MSI!" + + # MSI typically installs to Program Files, add to PATH if needed + $programFiles = "${env:ProgramFiles}\Neovim\bin" + $programFilesX86 = "${env:ProgramFiles(x86)}\Neovim\bin" + + $nvimPath = "" + if (Test-Path "$programFiles\nvim.exe") { + $nvimPath = $programFiles + } elseif (Test-Path "$programFilesX86\nvim.exe") { + $nvimPath = $programFilesX86 + } + + if ($nvimPath) { + $currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") + if ($currentPath -notlike "*$nvimPath*") { + [Environment]::SetEnvironmentVariable("PATH", "$currentPath;$nvimPath", "User") + Write-InfoMessage "Added Neovim to PATH: $nvimPath" + } + } + } else { + Write-ErrorMessage "MSI installation failed with exit code: $LASTEXITCODE" + return $false + } + } + elseif ($fileExtension -eq ".zip") { + # Handle ZIP installation + Write-InfoMessage "Installing from ZIP archive..." + + # Create installation directory + if (Test-Path $Script:NeovimPath) { + Remove-Item -Path $Script:NeovimPath -Recurse -Force + } + New-Item -Path $Script:NeovimPath -ItemType Directory -Force | Out-Null + + # Extract zip file + Write-InfoMessage "Extracting archive..." + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($FilePath, $Script:NeovimPath) + + # Find the nvim.exe in the extracted files + $nvimExe = Get-ChildItem -Path $Script:NeovimPath -Name "nvim.exe" -Recurse | Select-Object -First 1 + if (-not $nvimExe) { + Write-ErrorMessage "Could not find nvim.exe in extracted files" + return $false + } + + $nvimDir = Split-Path -Path (Get-ChildItem -Path $Script:NeovimPath -Name "nvim.exe" -Recurse | Select-Object -First 1).FullName + + # Move files to proper location if needed + if ($nvimDir -ne $Script:NeovimBin) { + if (Test-Path $Script:NeovimBin) { + Remove-Item -Path $Script:NeovimBin -Recurse -Force + } + Move-Item -Path $nvimDir -Destination $Script:NeovimBin -Force + } + + # Add to PATH if not already there + $currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") + if ($currentPath -notlike "*$Script:NeovimBin*") { + [Environment]::SetEnvironmentVariable("PATH", "$currentPath;$Script:NeovimBin", "User") + Write-InfoMessage "Added Neovim to PATH" + } + + Write-SuccessMessage "Neovim ${Version} installed successfully!" + Write-InfoMessage "Location: $Script:NeovimBin" + } + else { + Write-ErrorMessage "Unsupported file type: $fileExtension" + return $false + } + + # Clean up downloaded file + Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue + + Write-InfoMessage "You may need to restart your shell for PATH changes to take effect." + return $true + } + catch { + Write-ErrorMessage "Installation failed: $_" + return $false + } +} + +# Install nightly version +function Install-NightlyVersion { + param([switch]$CleanInstall) + + Write-InfoMessage "Installing Neovim Nightly..." + $url = "https://github.com/neovim/neovim/releases/download/nightly/nvim-win64.zip" + $fileName = "nvim-nightly.zip" + + if (Get-FileDownload -Url $url -OutputPath $fileName) { + return Install-NeovimFromFile -FilePath $fileName -Version "Nightly" -CleanInstall:$CleanInstall + } + return $false +} + +# Install stable version +function Install-StableVersion { + param([switch]$CleanInstall) + + Write-InfoMessage "Installing Neovim Stable..." + $latestVersion = Get-LatestStableVersion + if (-not $latestVersion) { + Write-ErrorMessage "Could not determine latest stable version" + return $false + } + + $fileName = Get-SpecificVersion -Version $latestVersion + if ($fileName) { + return Install-NeovimFromFile -FilePath $fileName -Version "Stable ($latestVersion)" -CleanInstall:$CleanInstall + } + return $false +} + +# Install specific version +function Install-SpecificVersionWrapper { + param( + [string]$Version, + [switch]$CleanInstall + ) + + Write-InfoMessage "Installing Neovim ${Version}..." + + if (-not (Test-VersionExists -Version $Version)) { + Write-ErrorMessage "Version ${Version} does not exist" + return $false + } + + $fileName = Get-SpecificVersion -Version $Version + if ($fileName) { + return Install-NeovimFromFile -FilePath $fileName -Version $Version -CleanInstall:$CleanInstall + } + return $false +} + +# Update/Install version menu +function Show-UpdateMenu { + # Check if there are existing installations + $installations = Find-AllNeovimInstallations + $hasExistingInstalls = $installations.Count -gt 0 + + if ($hasExistingInstalls) { + Write-Host "Existing Neovim installations found:" -ForegroundColor $Colors.Yellow + for ($i = 0; $i -lt $installations.Count; $i++) { + Write-Host " - $($installations[$i].Type) - $($installations[$i].Path) (v$($installations[$i].Version))" + } + Write-Host "" + + if (-not $NoPrompt) { + $cleanInstall = Read-Host "Perform clean installation (remove existing installations)? (y/n)" + $shouldClean = $cleanInstall -in @('y', 'yes', 'Y', 'Yes') + } else { + $shouldClean = $true + } + } else { + $shouldClean = $false + } + + $validChoice = $false + while (-not $validChoice) { + Write-Host "" + Write-Host "Select version to install/update to:" + Write-Host " 1. Nightly" + Write-Host " 2. Stable" + Write-Host " 3. Choose specific version by tag" + + $choice = Read-Host "Enter the number corresponding to your choice (1/2/3)" + + switch ($choice) { + "1" { + $result = Install-NightlyVersion -CleanInstall:$shouldClean + $validChoice = $true + $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User") + } + "2" { + $result = Install-StableVersion -CleanInstall:$shouldClean + $validChoice = $true + $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User") + } + "3" { + $version = Read-Host "Enter the specific version (e.g., v0.9.0)" + $result = Install-SpecificVersionWrapper -Version $version -CleanInstall:$shouldClean + $validChoice = $true + $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User") + } + default { + Write-ErrorMessage "Invalid choice. Please enter 1, 2, or 3." + } + } + } + + return $result +} + +# Uninstall Neovim (wrapper for backward compatibility) +function Uninstall-Neovim { + return Uninstall-AllNeovim +} + +# Check if Neovim is running +function Test-NeovimRunning { + $nvimProcesses = Get-Process -Name "nvim" -ErrorAction SilentlyContinue + if ($nvimProcesses) { + Write-Host "Error: Neovim is currently running. Please close Neovim before proceeding." -ForegroundColor $Colors.Red + + if (-not $NoPrompt) { + $choice = Read-Host "Do you want to forcefully terminate Neovim and continue? (y/n)" + if ($choice -in @('y', 'yes', 'Y', 'Yes')) { + $nvimProcesses | Stop-Process -Force + Write-InfoMessage "Neovim processes terminated" + } else { + Write-InfoMessage "Exiting..." + exit 1 + } + } else { + $nvimProcesses | Stop-Process -Force + Write-InfoMessage "Neovim processes terminated" + } + } +} + +# Check for updates +function Test-Updates { + Write-InfoMessage "Checking for updates..." + + try { + $latestVersion = Get-LatestStableVersion + if (-not $latestVersion) { + Write-ErrorMessage "Could not fetch latest version information" + return + } + + Write-InfoMessage "Latest stable version: $latestVersion" + + $installations = Find-AllNeovimInstallations + if ($hasExistingInstalls) { + Write-InfoMessage "Found $($installations.Count) installation(s):" + foreach ($installation in $installations) { + Write-InfoMessage " - $($installation.Type): v$($installation.Version) at $($installation.Path)" + if ($installation.Version -ne "Unknown" -and "v$($installation.Version)" -ne $latestVersion) { + Write-SuccessMessage " Update available: v$($installation.Version) → $latestVersion" + } elseif ("v$($installation.Version)" -eq $latestVersion) { + Write-InfoMessage " Up to date" + } + } + } else { + Write-InfoMessage "Neovim is not installed" + } + } + catch { + Write-ErrorMessage "Failed to check for updates: $_" + } +} + +# Main function +function Main { + Write-Host "Neovim Installation Script for Windows" -ForegroundColor $Colors.Cyan + Write-Host "=======================================" -ForegroundColor $Colors.Cyan + + # Check dependencies + if (-not (Test-Dependencies)) { + exit 1 + } + + # Check if Neovim is running + Test-NeovimRunning + + # Check if Neovim is installed + $installations = Find-AllNeovimInstallations + if ($installations.Count -gt 0) { + Write-SuccessMessage "Found $($installations.Count) Neovim installation(s):" + foreach ($installation in $installations) { + Write-InfoMessage " - $($installation.Type): v$($installation.Version) at $($installation.Path)" + } + } else { + Write-Host "Neovim is not installed." -ForegroundColor $Colors.Red + if (-not $NoPrompt) { + $choice = Read-Host "Install Neovim? (y/n)" + if ($choice -in @('y', 'yes', 'Y', 'Yes')) { + Show-UpdateMenu + return + } else { + Write-InfoMessage "Exiting..." + return + } + } + } + + # Main menu loop + while ($Script:ShowPrompt) { + Write-Host "" + Write-Host "Select an option:" + Write-Host " 1. Install/update Neovim" + Write-Host " 2. Check for updates" + Write-Host " 3. Uninstall all Neovim installations" + Write-Host " 4. Run Neovim" + Write-Host " 5. Quit" + + $choice = Read-Host "Enter a number or press 'q' to quit" + + switch ($choice) { + "1" { + Show-UpdateMenu + } + "2" { + Test-Updates + } + "3" { + Uninstall-AllNeovim + } + "4" { + if ($installations.Count -gt 0 -or (Get-Command nvim -ErrorAction SilentlyContinue)) { + Write-InfoMessage "Starting Neovim..." + & nvim + } else { + Write-ErrorMessage "Neovim is not installed" + } + } + { $_ -in @("5", "q", "Q", "quit", "exit") } { + Write-InfoMessage "Exiting..." + $Script:ShowPrompt = $false + } + default { + Write-ErrorMessage "Invalid choice. Please choose a valid option by entering the corresponding number or press 'q' to quit." + } + } + } +} + +# Run the main function +Main diff --git a/common/config/nvim/neovim.sh b/common/config/nvim/neovim.sh new file mode 100755 index 0000000..842abed --- /dev/null +++ b/common/config/nvim/neovim.sh @@ -0,0 +1,516 @@ +#!/bin/bash + +# Created By: srdusr +# Created On: Sat 12 Aug 2023 13:11:39 CAT +# Project: Install/update/uninstall/change version Neovim script, primarily for Linux but may work in other platforms + +# Dependencies: wget/curl, fuse, jq + +# Color definitions +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Handle errors +handle_error() { + local message="$1" + printf "${RED}Error: $message${NC}\n" +} + +# Check if necessary dependencies are installed +check_dependencies() { + if [ -x "$(command -v wget)" ]; then + DOWNLOAD_COMMAND="wget" + elif [ -x "$(command -v curl)" ]; then + DOWNLOAD_COMMAND="curl" + else + printf "${RED}Error: Neither wget nor curl found. Please install one of them to continue!${NC}\n" + exit 1 + fi + if ! command -v jq >/dev/null 2>&1; then + printf "${RED}Error: jq is required for specific version downloads. Please install jq!${NC}\n" + exit 1 + fi +} + +# Check for privilege escalation tools +check_privilege_tools() { + if [ -x "$(command -v sudo)" ]; then + PRIVILEGE_TOOL="sudo" + elif [ -x "$(command -v doas)" ]; then + PRIVILEGE_TOOL="doas" + elif [ -x "$(command -v pkexec)" ]; then + PRIVILEGE_TOOL="pkexec" + elif [ -x "$(command -v dzdo)" ]; then + PRIVILEGE_TOOL="dzdo" + elif [ "$(id -u)" -eq 0 ]; then + PRIVILEGE_TOOL="" # root + else + PRIVILEGE_TOOL="" # No privilege escalation mechanism found + printf "\n${RED}Error: No privilege escalation tool (sudo, doas, pkexec, dzdo, or root privileges) found. You may not have sufficient permissions to run this script.${NC}\n" + printf "\nAttempt to continue Installation (might fail without a privilege escalation tool)? [yes/no] " + read continue_choice + case $continue_choice in + [Yy] | [Yy][Ee][Ss]) ;; + [Nn] | [Nn][Oo]) exit ;; + *) handle_error "Invalid choice. Exiting..." && exit ;; + esac + fi +} + +# Check if Neovim is already installed +check_neovim_installed() { + if [ -x "$(command -v nvim)" ]; then + return 0 # Neovim is installed + else + return 1 # Neovim is not installed + fi +} + +# Nightly version +nightly_version() { + local url="https://github.com/neovim/neovim/releases/download/nightly/nvim-linux-x86_64.appimage" + install_neovim "$url" + local version_output=$(nvim --version) + version_id="Nightly $(echo "$version_output" | grep -oP 'NVIM \d+\.\d+\.\d+')" +} + +# Stable version +stable_version() { + #local url="https://github.com/neovim/neovim/releases/download/stable/nvim.appimage" + local url="https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.appimage" + install_neovim "$url" + local version_output=$(nvim --version) + version_id="Stable $(echo "$version_output" | grep -oP 'NVIM \d+\.\d+')" +} + +# Specific version +specific_version() { + local version="$1" + filename=$(download_specific_version "$version") + echo "Installing Neovim $version..." + if [ -x "$(command -v fusermount)" ]; then + chmod u+x "$filename" + $PRIVILEGE_TOOL cp "$filename" /usr/local/bin/nvim + $PRIVILEGE_TOOL chmod +x /usr/local/bin/nvim + echo "Installed Neovim to /usr/local/bin/nvim" + else + chmod u+x "$filename" + ./$filename --appimage-extract + $PRIVILEGE_TOOL cp squashfs-root/usr/bin/nvim /usr/local/bin + $PRIVILEGE_TOOL chmod +x /usr/local/bin/nvim + echo "Installed Neovim to /usr/local/bin/nvim" + fi +} + +# Download a file using wget or curl +download_file() { + local url="$1" + local output="$2" + + if [ "$DOWNLOAD_COMMAND" = "wget" ]; then + if ! "$DOWNLOAD_COMMAND" -q --show-progress -O "$output" "$url"; then + handle_error "Download failed. Exiting..." + exit 1 + fi + elif [ "$DOWNLOAD_COMMAND" = "curl" ]; then + if ! "$DOWNLOAD_COMMAND" --progress-bar -# -o "$output" "$url"; then + handle_error "Download failed. Exiting..." + exit 1 + fi + else + echo "Unsupported download command: $DOWNLOAD_COMMAND" + exit 1 + fi +} + +# Download the correct asset for a specific version using GitHub API and jq +download_specific_version() { + local version="$1" + if [[ $version != v* ]]; then + version="v$version" + fi + local api_url="https://api.github.com/repos/neovim/neovim/releases/tags/$version" + local json=$(curl -sSL "$api_url") + local os_name=$(uname -s) + local arch=$(uname -m) + local asset="" + local asset_name="" + local asset_candidates=() + + # Build candidate asset names based on platform/arch + if [[ "$os_name" == "Linux" ]]; then + if [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then + asset_candidates=("nvim-linux-arm64.tar.gz" "nvim-linux64.tar.gz" "nvim.appimage") + elif [[ "$arch" == "armv7l" ]]; then + asset_candidates=("nvim-linux-arm.tar.gz" "nvim.appimage") + else + asset_candidates=("nvim-linux-x86_64.appimage" "nvim-linux64.appimage" "nvim.appimage" "nvim-linux64.tar.gz") + fi + elif [[ "$os_name" == "Darwin" ]]; then + if [[ "$arch" == "arm64" ]]; then + asset_candidates=("nvim-macos-arm64.tar.gz" "nvim-macos.tar.gz") + else + asset_candidates=("nvim-macos.tar.gz" "nvim-macos-x86_64.tar.gz") + fi + elif [[ "$os_name" =~ MINGW|MSYS|CYGWIN ]]; then + asset_candidates=("nvim-win64.zip" "nvim-win64.msi") + fi + + # Find the first matching asset + for name in "${asset_candidates[@]}"; do + asset=$(echo "$json" | jq -r ".assets[] | select(.name==\"$name\") | .browser_download_url") + [[ -n "$asset" && "$asset" != "null" ]] && { asset_name="$name"; break; } + done + + if [[ -z "$asset" || "$asset" == "null" ]]; then + echo "No suitable asset found for your platform/arch in this release." >&2 + echo "Available assets:" >&2 + echo "$json" | jq -r '.assets[].name' >&2 + exit 1 + fi + + echo "DEBUG: Downloading asset: $asset_name from $asset" >&2 + download_file "$asset" "$asset_name" + + # Download checksum if available + checksum_url=$(echo "$json" | jq -r ".assets[] | select(.name==\"$asset_name.sha256sum\" or .name==\"$asset_name.sha256\") | .browser_download_url") + if [[ -n "$checksum_url" && "$checksum_url" != "null" ]]; then + checksum_file="${asset_name}.sha256sum" + echo "DEBUG: Downloading checksum: $checksum_file from $checksum_url" >&2 + download_file "$checksum_url" "$checksum_file" + echo "DEBUG: Contents of $checksum_file:" >&2 + cat "$checksum_file" >&2 + echo "Verifying checksum..." >&2 + # Try to handle both formats: with and without filename + if grep -q "$asset_name" "$checksum_file"; then + if command -v sha256sum >/dev/null 2>&1; then + sha256sum -c "$checksum_file" --ignore-missing >&2 + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 -c "$checksum_file" >&2 + else + echo "Warning: No sha256sum or shasum found, cannot verify checksum." >&2 + fi + else + # If the checksum file contains only the hash, not the filename + hash=$(head -n1 "$checksum_file" | awk '{print $1}') + if command -v sha256sum >/dev/null 2>&1; then + echo "$hash $asset_name" | sha256sum -c - >&2 + elif command -v shasum >/dev/null 2>&1; then + echo "$hash $asset_name" | shasum -a 256 -c - >&2 + else + echo "Warning: No sha256sum or shasum found, cannot verify checksum." >&2 + fi + fi + else + echo "Warning: No checksum file found for $asset_name." >&2 + fi + + echo "$asset_name" +} + +# Check if a specific version of Neovim exists +version_exists() { + local version="$1" + + # Add 'v' prefix if not present + if [[ $version != v* ]]; then + version="v$version" + fi + + # Fetch all the release tags from GitHub + ALL_TAGS=$(curl -s "https://api.github.com/repos/neovim/neovim/tags" | grep '"name":' | cut -d '"' -f 4) + + # Check if the desired version is in the list of release tags + if echo "$ALL_TAGS" | grep -q "$version"; then + return 0 # Version exists + else + return 1 # Version does not exist + fi +} + +# Update Neovim to the latest version (nightly/stable) +update_version() { + valid_choice=false + while [ "$valid_choice" = false ]; do + # Determine which version to update to (nightly/stable) + printf "Select version to install/update to:\n" + printf " 1. Nightly\n" + printf " 2. Stable\n" + printf " 3. Choose specific version by tag\n" + printf "Enter the number corresponding to your choice (1/2/3): " + read update_choice + + case $update_choice in + 1) + version="Nightly" + nightly_version + valid_choice=true + ;; + 2) + version="Stable" + stable_version + valid_choice=true + ;; + 3) + # Ask user for specific version + read -p "Enter the specific version (e.g., v0.1.0): " version + filename=$(download_specific_version "$version") + echo "Installing Neovim $version..." + if [ -x "$(command -v fusermount)" ]; then + chmod u+x "$filename" + $PRIVILEGE_TOOL cp "$filename" /usr/local/bin/nvim + $PRIVILEGE_TOOL chmod +x /usr/local/bin/nvim + echo "Installed Neovim to /usr/local/bin/nvim" + else + chmod u+x "$filename" + ./$filename --appimage-extract + $PRIVILEGE_TOOL cp squashfs-root/usr/bin/nvim /usr/local/bin + $PRIVILEGE_TOOL chmod +x /usr/local/bin/nvim + echo "Installed Neovim to /usr/local/bin/nvim" + fi + valid_choice=true + ;; + *) + handle_error "Invalid choice. Please enter a valid option (1, 2 or 3)." + ;; + esac + done +} + +# Install Neovim +install_neovim() { + local url="$1" + local install_action="$3" + + if [ "$install_action" = "installed" ]; then + printf "Downloading and installing Neovim $version...\n" + else + printf "${GREEN}Updating Neovim to the latest version ($version)...${NC}\n" + fi + + # Determine the platform-specific installation steps + case "$(uname -s)" in + Linux) + printf "Detected Linux OS.\n" + if [ -x "$(command -v fusermount)" ]; then + printf "FUSE is available. Downloading and running the AppImage...\n" + download_file "$url" "nvim.appimage" + chmod u+x nvim.appimage + "$PRIVILEGE_TOOL" cp nvim.appimage /usr/local/bin/nvim + "$PRIVILEGE_TOOL" mv nvim.appimage /usr/bin/nvim + else + printf "FUSE is not available. Downloading and extracting the AppImage...\n" + download_file "$url" "nvim.appimage" + chmod u+x nvim.appimage + ./nvim.appimage --appimage-extract + "$PRIVILEGE_TOOL" cp squashfs-root/usr/bin/nvim /usr/local/bin + "$PRIVILEGE_TOOL" mv squashfs-root/usr/bin/nvim /usr/bin + fi + ;; + + Darwin) + printf "Detected macOS.\n" + download_file "$url" "nvim-macos.tar.gz" + xattr -c ./nvim-macos.tar.gz + tar xzvf nvim-macos.tar.gz + "$PRIVILEGE_TOOL" cp nvim-macos/bin/nvim /usr/local/bin + "$PRIVILEGE_TOOL" mv nvim-macos/bin/nvim /usr/bin/nvim + ;; + + MINGW*) + printf "Detected Windows.\n" + download_file "$url" "nvim.appimage" + chmod +x nvim.appimage + if [ "$PRIVILEGE_TOOL" = "sudo" ]; then + "$PRIVILEGE_TOOL" cp nvim.appimage /usr/local/bin/nvim + "$PRIVILEGE_TOOL" mv /usr/local/bin/nvim /usr/bin + elif [ "$PRIVILEGE_TOOL" = "" ]; then + cp nvim.appimage /usr/local/bin/nvim + mv /usr/local/bin/nvim /usr/bin + else + printf "No privilege escalation tool found. Cannot install Neovim on Windows.\n" + fi + ;; + + *) + printf "Unsupported operating system.\n" + exit 1 + ;; + esac + # Check if the installation was successful + if [ $? -eq 0 ]; then + if [ "$install_action" = "installed" ]; then + printf "${GREEN}Neovim $version has been installed successfully!${NC}\n" + else + printf "${GREEN}Neovim has been updated successfully to $version!${NC}\n" + fi + else + printf "${RED}Error: Neovim installation/update failed.${NC}\n" + exit 1 + fi +} + +# Uninstall Neovim +uninstall_neovim() { + printf "${RED}Uninstalling Neovim...${NC}\n" + + # Detect the operating system to determine the appropriate uninstallation method + case "$(uname -s)" in + Linux) + printf "Detected Linux OS.\n" + "$PRIVILEGE_TOOL" rm /usr/local/bin/nvim + "$PRIVILEGE_TOOL" rm /usr/bin/nvim + ;; + + Darwin) + printf "Detected macOS.\n" + "$PRIVILEGE_TOOL" rm /usr/local/bin/nvim + "$PRIVILEGE_TOOL" rm /usr/bin/nvim + ;; + + MINGW*) + printf "Detected Windows.\n" + if [ "$PRIVILEGE_TOOL" = "sudo" ]; then + "$PRIVILEGE_TOOL" rm /usr/local/bin/nvim + "$PRIVILEGE_TOOL" rm /usr/bin/nvim + else + [ "$PRIVILEGE_TOOL" = "" ] + rm /usr/local/bin/nvim + rm /usr/bin/nvim + fi + ;; + *) + printf "Unsupported operating system.\n" + ;; + esac + + printf "${GREEN}Neovim has been uninstalled successfully!${NC}\n" +} + +# Check if Neovim is running +check_neovim_running() { + if pgrep nvim >/dev/null; then + printf "${RED}Error: Neovim is currently running. Please close Neovim before proceeding.${NC}\n" + read -p "Do you want to forcefully terminate Neovim and continue? [yes/no] " terminate_choice + + case $terminate_choice in + [Yy] | [Yy][Ee][Ss]) + pkill nvim # Forcefully terminate Neovim + ;; + [Nn] | [Nn][Oo]) + echo "Exiting..." + exit 1 + ;; + *) + handle_error "Invalid choice." + ;; + esac + fi +} + +check_neovim_running + +# Define the variable to control the prompt +SHOW_PROMPT=1 + +# Check if necessary dependencies are installed +check_dependencies + +# Check for privilege escalation tools +check_privilege_tools + +# Check if Neovim is already installed and ask the user if want to install it +if check_neovim_installed; then + printf "${GREEN}Neovim is already installed!${NC}\n" +else + printf "${RED}Neovim is not installed.${NC}\n" + read -p "Install Neovim? (y/n): " install_choice + + case $install_choice in + [Yy]) + update_version + ;; + [Nn]) + echo "Exiting..." + exit + ;; + *) + handle_error "Invalid choice. Please enter 'y' for yes or 'n' for no." + ;; + esac +fi + +# Check for updates and display breaking changes +check_version_updates() { + local latest_version_url="https://api.github.com/repos/neovim/neovim/releases/latest" + local latest_version="" + + if [ -x "$(command -v curl)" ]; then + latest_version=$(curl -sSL "$latest_version_url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + elif [ -x "$(command -v wget)" ]; then + latest_version=$(wget -qO - "$latest_version_url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + else + printf "${RED}Error: Neither curl nor wget found. Please install one of them to continue!${NC}\n" + exit 1 + fi + + if version_exists "$latest_version"; then + printf "${GREEN}An update is available!${NC}\n" + display_breaking_changes "$latest_version" + else + printf "You have the latest version of Neovim.\n" + fi +} + +# To display breaking changes for a specific version +display_breaking_changes() { + local version="$1" + local changelog_url="https://github.com/neovim/neovim/releases/tag/$version" + local changelog="" + + if [ -x "$(command -v curl)" ]; then + changelog=$(curl -sSL "$changelog_url" | grep -oE '<h1>Breaking Changes.*?</ul>' | sed 's/<[^>]*>//g') + elif [ -x "$(command -v wget)" ]; then + changelog=$(wget -qO - "$changelog_url" | grep -oE '<h1>Breaking Changes.*?</ul>' | sed 's/<[^>]*>//g') + else + printf "${RED}Error: Neither curl nor wget found. Please install one of them to continue!${NC}\n" + exit 1 + fi + + printf "\nBreaking Changes in Neovim $version:\n" + printf "$changelog\n" +} + +# Main loop +while [ "$SHOW_PROMPT" -gt 0 ]; do + printf "Select an option:\n" + printf " 1. Install/update Neovim\n" + printf " 2. Check for updates\n" + printf " 3. Uninstall Neovim\n" + printf " 4. Run Neovim\n" + printf " 5. Quit\n" + read -p "Enter a number or press 'q' to quit: " choice + + case $choice in + 1) + update_version + ;; + 2) + check_version_updates + ;; + 3) + uninstall_neovim + ;; + 4) + nvim + ;; + 5 | [Qq]) + echo "Exiting..." + exit + ;; + *) + handle_error "Invalid choice. Please choose a valid option by entering the corresponding number or press 'q' to 'quit'." + ;; + esac +done diff --git a/common/config/nvim/snippets/boilerplate.lua b/common/config/nvim/snippets/boilerplate.lua new file mode 100644 index 0000000..04e973a --- /dev/null +++ b/common/config/nvim/snippets/boilerplate.lua @@ -0,0 +1,75 @@ +local ls = require("luasnip") --{{{ +local s = ls.s +local i = ls.i +local t = ls.t + +local d = ls.dynamic_node +local c = ls.choice_node +local f = ls.function_node +local sn = ls.snippet_node + +local fmt = require("luasnip.extras.fmt").fmt +local rep = require("luasnip.extras").rep + +local snippets, autosnippets = {}, {} --}}} + +local group = vim.api.nvim_create_augroup("Lua Snippets", { clear = true }) +local file_pattern = "*.lua" + +local function cs(trigger, nodes, opts) --{{{ + local snippet = s(trigger, nodes) + local target_table = snippets + + local pattern = file_pattern + local keymaps = {} + + if opts ~= nil then + -- check for custom pattern + if opts.pattern then + pattern = opts.pattern + end + + -- if opts is a string + if type(opts) == "string" then + if opts == "auto" then + target_table = autosnippets + else + table.insert(keymaps, { "i", opts }) + end + end + + -- if opts is a table + if opts ~= nil and type(opts) == "table" then + for _, keymap in ipairs(opts) do + if type(keymap) == "string" then + table.insert(keymaps, { "i", keymap }) + else + table.insert(keymaps, keymap) + end + end + end + + -- set autocmd for each keymap + if opts ~= "auto" then + for _, keymap in ipairs(keymaps) do + vim.api.nvim_create_autocmd("BufEnter", { + pattern = pattern, + group = group, + callback = function() + vim.keymap.set(keymap[1], keymap[2], function() + ls.snip_expand(snippet) + end, { noremap = true, silent = true, buffer = true }) + end, + }) + end + end + end + + table.insert(target_table, snippet) -- insert snippet into appropriate table +end --}}} + +-- Start Refactoring -- + +-- End Refactoring -- + +return snippets, autosnippets diff --git a/common/config/nvim/snippets/lua.lua b/common/config/nvim/snippets/lua.lua new file mode 100644 index 0000000..eb46b67 --- /dev/null +++ b/common/config/nvim/snippets/lua.lua @@ -0,0 +1,264 @@ + +local ls = require("luasnip") --{{{ +local s = ls.s --> snippet +local i = ls.i --> insert node +local t = ls.t --> text node + +local d = ls.dynamic_node +local c = ls.choice_node --> takes in a pos as first arg and a table of nodes +local f = ls.function_node +local sn = ls.snippet_node + +local fmt = require("luasnip.extras.fmt").fmt +local rep = require("luasnip.extras").rep + +local snippets, autosnippets = {}, {} --}}} + +local group = vim.api.nvim_create_augroup("Lua Snippets", { clear = true }) +local file_pattern = "*.lua" + +local function cs(trigger, nodes, opts) --{{{ + local snippet = s(trigger, nodes) + local target_table = snippets + + local pattern = file_pattern + local keymaps = {} + + if opts ~= nil then + -- check for custom pattern + if opts.pattern then + pattern = opts.pattern + end + + -- if opts is a string + if type(opts) == "string" then + if opts == "auto" then + target_table = autosnippets + else + table.insert(keymaps, { "i", opts }) + end + end + + -- if opts is a table + if opts ~= nil and type(opts) == "table" then + for _, keymap in ipairs(opts) do + if type(keymap) == "string" then + table.insert(keymaps, { "i", keymap }) + else + table.insert(keymaps, keymap) + end + end + end + + -- set autocmd for each keymap + if opts ~= "auto" then + for _, keymap in ipairs(keymaps) do + vim.api.nvim_create_autocmd("BufEnter", { + pattern = pattern, + group = group, + callback = function() + vim.keymap.set(keymap[1], keymap[2], function() + ls.snip_expand(snippet) + end, { noremap = true, silent = true, buffer = true }) + end, + }) + end + end + end + + table.insert(target_table, snippet) -- insert snippet into appropriate table +end --}}} + +-- Start Refactoring -- + +cs("CMD", { -- [CMD] multiline vim.cmd{{{ + t({ "vim.cmd[[", " " }), + i(1, ""), + t({ "", "]]" }), +}) --}}} +cs("cmd", fmt("vim.cmd[[{}]]", { i(1, "") })) -- single line vim.cmd +cs({ -- github import for packer{{{ + trig = "https://github%.com/([%w-%._]+)/([%w-%._]+)!", + regTrig = true, + hidden = true, +}, { + t([[use "]]), + f(function(_, snip) + return snip.captures[1] + end), + t("/"), + f(function(_, snip) + return snip.captures[2] + end), + t({ [["]], "" }), + i(1, ""), +}, "auto") --}}} + +cs( -- {regexSnippet} LuaSnippet{{{ + "regexSnippet", + fmt( + [=[ +cs( -- {} +{{ trig = "{}", regTrig = true, hidden = true }}, fmt([[ +{} +]], {{ + {} +}})) + ]=], + { + i(1, "Description"), + i(2, ""), + i(3, ""), + i(4, ""), + } + ), + { pattern = "*/snippets/*.lua", "<C-d>" } +) --}}} +cs( -- [luaSnippet] LuaSnippet{{{ + "luaSnippet", + fmt( + [=[ +cs("{}", fmt( -- {} +[[ +{} +]], {{ + {} + }}){}) + ]=], + { + i(1, ""), + i(2, "Description"), + i(3, ""), + i(4, ""), + c(5, { + t(""), + fmt([[, "{}"]], { i(1, "keymap") }), + fmt([[, {{ pattern = "{}", {} }}]], { i(1, "*/snippets/*.lua"), i(2, "keymap") }), + }), + } + ), + { pattern = "*/snippets/*.lua", "jcs" } +) --}}} + +cs( -- choice_node_snippet luaSnip choice node{{{ + "choice_node_snippet", + fmt( + [[ +c({}, {{ {} }}), +]], + { + i(1, ""), + i(2, ""), + } + ), + { pattern = "*/snippets/*.lua", "jcn" } +) --}}} + +cs( -- [function] Lua function snippet{{{ + "function", + fmt( + [[ +function {}({}) + {} +end +]], + { + i(1, ""), + i(2, ""), + i(3, ""), + } + ), + "jff" +) --}}} +cs( -- [local_function] Lua function snippet{{{ + "local_function", + fmt( + [[ +local function {}({}) + {} +end +]], + { + i(1, ""), + i(2, ""), + i(3, ""), + } + ), + "jlf" +) --}}} +cs( -- [local] Lua local variable snippet{{{ + "local", + fmt( + [[ +local {} = {} + ]], + { i(1, ""), i(2, "") } + ), + "jj" +) --}}} +-- Tutorial Snippets go here -- +local myFirstSnippet = s("myFirstSnippet", { + t("Hi! This is my first snippet in Luasnip "), + i(1, "placeholder"), + t({"", "this is another text node", ""}), + i(2, "put here"), +}) +table.insert(snippets, myFirstSnippet) + + + +local mySecondSnippet = s( + "mySecondSnippet", + fmt( + [[ + local {} = function({}) + {} {{ im in a curly braces }} + end {} + ]], + { + i(1, "myVar"), + c(2, { t(""), i(1, "myArg") }), + i(3, "-- TODO: something"), + i(4, "-- nice") + } + ) +) +table.insert(snippets, mySecondSnippet) + +local myFirstAutoSnippet = s("automatic", { t("This was auto triggered") }) +table.insert(autosnippets, myFirstAutoSnippet) + +local mySecondAutoSnippet = s({ trig = "digit(%d)(%d)", regTrig = true }, { + f(function(_, snip) + return snip.captures[1] .. " + " + end), + f(function(_, snip) + return snip.captures[2] + end), +}) +table.insert(autosnippets, mySecondAutoSnippet) + + + + + + + + +-- End Refactoring -- + +return snippets, autosnippets + + + + + + + + + + + + + + diff --git a/common/config/nvim/snippets/markdown.lua b/common/config/nvim/snippets/markdown.lua new file mode 100644 index 0000000..d0d1487 --- /dev/null +++ b/common/config/nvim/snippets/markdown.lua @@ -0,0 +1,58 @@ +local ls = require("luasnip") +local s = ls.s +local i = ls.i +local t = ls.t + +local d = ls.dynamic_node +local c = ls.choice_node +local f = ls.function_node +local sn = ls.snippet_node + +local fmt = require("luasnip.extras.fmt").fmt +local rep = require("luasnip.extras").rep + +-- -- + +local snippets = {} +local autosnippets = {} + +local autocmd = vim.api.nvim_create_autocmd +local augroup = vim.api.nvim_create_augroup +local map = vim.keymap.set +local opts = { noremap = true, silent = true, buffer = true } +local group = augroup("Markdown Snippets", { clear = true }) + +local function cs(trigger, nodes, keymap) --> cs stands for create snippet + local snippet = s(trigger, nodes) + table.insert(snippets, snippet) + + if keymap ~= nil then + local pattern = "*.md" + if type(keymap) == "table" then + pattern = keymap[1] + keymap = keymap[2] + end + autocmd("BufEnter", { + pattern = pattern, + group = group, + callback = function() + map({ "i" }, keymap, function() + ls.snip_expand(snippet) + end, opts) + end, + }) + end +end + +local function lp(package_name) -- Load Package Function + package.loaded[package_name] = nil + return require(package_name) +end + +-- Utility Functions -- + +-- Start Refactoring -- + +-- Start Refactoring -- + +return snippets, autosnippets diff --git a/common/config/wezterm/wezterm.lua b/common/config/wezterm/wezterm.lua new file mode 100644 index 0000000..4f4bd07 --- /dev/null +++ b/common/config/wezterm/wezterm.lua @@ -0,0 +1,206 @@ +local wezterm = require("wezterm") + +---- Function to unset Ctrl+C keybinding +--local function unsetCtrlCKeybinding(window) +-- local keys = window:get_config().keys +-- for i, key in ipairs(keys) do +-- if key.key == 'c' and key.mods == 'CTRL' then +-- table.remove(keys, i) +-- break +-- end +-- end +-- window:set_config({ keys = keys }) +--end +-- +---- Event handler to unset Ctrl+C keybinding when using nvim +--wezterm.on("spawn_command", function(window, pane) +-- local cmd = pane:get_command() +-- if cmd and cmd[1] == "nvim" then +-- unsetCtrlCKeybinding(window) +-- end +--end) + +--local function isNvimRunning(window) +-- local pane = window.active_pane +-- local cmd = pane:get_command() +-- return cmd and cmd[1] == "nvim" +--end +-- +---- Function to modify keybindings based on the current program +--local function updateKeybindings(window) +-- local isNvim = isNvimRunning(window) +-- +-- local keys = {} +-- if not isNvim then +-- -- Add the default Ctrl+C keybinding +-- keys = { +-- { +-- key = "c", +-- mods = "CTRL", +-- action = wezterm.action{CopyTo = "ClipboardAndPrimarySelection"} +-- } +-- } +-- end +-- +-- window:set_config({ +-- keys = keys +-- }) +--end +-- +---- Event handler to update keybindings when the active program changes +--wezterm.on("update-right-status", function(window) +-- updateKeybindings(window) +--end) + +wezterm.on("toggle-opacity", function(window) + local overrides = window:get_config_overrides() or {} + if not overrides.window_background_opacity then + overrides.window_background_opacity = 1.0 + elseif overrides.window_background_opacity == 1.0 then + overrides.window_background_opacity = 0.6 + else + overrides.window_background_opacity = nil + end + window:set_config_overrides(overrides) +end) + +return { + front_end = "OpenGL", + --font = wezterm.font 'JetBrains Mono', + font = wezterm.font_with_fallback({ + { + family = "JetBrains Mono", + --intensity = 'Normal', + weight = "Medium", + italic = false, + harfbuzz_features = { "calt=0", "clig=0", "liga=0" }, + }, + { family = "Hack Nerd Font", weight = "Medium" }, + { + family = "Fira Code", + harfbuzz_features = { "zero" }, + }, + { family = "Terminus", weight = "Bold" }, + "Noto Color Emoji", + }), + font_size = 9, + warn_about_missing_glyphs = false, + adjust_window_size_when_changing_font_size = false, + line_height = 1.0, + --dpi = 96.0, + -- Keybinds + disable_default_key_bindings = true, + use_dead_keys = false, + mouse_bindings = { + -- Ctrl-click will open the link under the mouse cursor + { + event = { Up = { streak = 1, button = "Left" } }, + mods = "CTRL", + action = wezterm.action.OpenLinkAtMouseCursor, + }, + }, + keys = { + --leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 1000 }, + { + key = "O", + mods = "CTRL|SHIFT", + action = wezterm.action({ EmitEvent = "toggle-opacity" }), + }, + { key = "R", mods = "CTRL", action = "ReloadConfiguration" }, + { key = "Y", mods = "CTRL", action = "ShowDebugOverlay" }, + { + key = "-", + mods = "CTRL", + action = wezterm.action.DecreaseFontSize, + }, + { + key = "=", + mods = "CTRL", + action = wezterm.action.IncreaseFontSize, + }, + { + key = "0", + mods = "CTRL", + action = wezterm.action.ResetFontSize, + }, + { + key = "v", + mods = "CTRL", + action = wezterm.action({ PasteFrom = "Clipboard" }), + }, + --{ + -- key = "c", + -- mods = "CTRL", + -- action = wezterm.action({ CopyTo = "ClipboardAndPrimarySelection" }), + --}, + { + key = "c", + mods = "CTRL", + action = wezterm.action_callback(function(window, pane) + local has_selection = window:get_selection_text_for_pane(pane) ~= "" + if has_selection then + window:perform_action(wezterm.action({ CopyTo = "ClipboardAndPrimarySelection" }), pane) + window:perform_action("ClearSelection", pane) + else + window:perform_action(wezterm.action({ SendKey = { key = "c", mods = "CTRL" } }), pane) + end + end), + }, + }, + -- Aesthetic Night Colorscheme + bold_brightens_ansi_colors = true, + -- Padding + window_padding = { + left = 5, + right = 5, + top = 6, + bottom = 4, + }, + -- Cursor style + --default_cursor_style = "BlinkingUnderline", + default_cursor_style = "BlinkingBar", + cursor_blink_rate = 700, + -- needed to prevent 'easing' from using 40%+ cpu util ... + --animation_fps = 1, + force_reverse_video_cursor = true, + colors = { + cursor_bg = "white", + compose_cursor = "orange", + --cursor_border = 'white', + }, + + -- Tab Bar + enable_tab_bar = false, + --hide_tab_bar_if_only_one_tab = true, + --show_tab_index_in_tab_bar = false, + tab_bar_at_bottom = false, + + -- General + -- X11 + enable_wayland = false, + audible_bell = "Disabled", + + visual_bell = { + fade_in_duration_ms = 5, + fade_out_duration_ms = 5, + target = "CursorColor", + }, + automatically_reload_config = true, + scrollback_lines = 3500, + --inactive_pane_hsb = { saturation = 1.0, brightness = 1.0 }, + --text_background_opacity = 0.3, + window_background_opacity = 0.8, + --window_background_image = '/path/to/wallpaper.jpg', + --window_background_image_hsb = { + -- -- Darken the background image by reducing it to 1/3rd + -- brightness = 0.3, + -- -- You can adjust the hue by scaling its value. + -- -- a multiplier of 1.0 leaves the value unchanged. + -- hue = 1.0, + -- -- You can adjust the saturation also. + -- saturation = 1.0, + --}, + window_close_confirmation = "NeverPrompt", + --color_scheme = 'transparent', + use_resize_increments = true, +} diff --git a/common/config/zsh/.zshenv b/common/config/zsh/.zshenv new file mode 100644 index 0000000..cdab5b7 --- /dev/null +++ b/common/config/zsh/.zshenv @@ -0,0 +1,346 @@ +# Load local/system wide binaries and scripts +export PATH=$HOME/.bin:$HOME/.local/bin:$HOME/.scripts:/usr/local/bin:/sbin:/usr/sbin:$PATH +export PATH="/data/data/com.termux/files/usr/local/bin:$PATH" + +# List of directories to ignore (relative to ~/.scripts) +EXCLUDE_DIRS=("assets" "test") + +# Add .scripts to path +if [ -d "$HOME/.scripts" ]; then + while IFS= read -r -d '' dir; do + # Extract relative path + rel_path="${dir#$HOME/.scripts/}" + + # Check if the directory is in the exclude list + skip=false + for exclude in "${EXCLUDE_DIRS[@]}"; do + if [[ "$rel_path" == "$exclude"* ]]; then + skip=true + break + fi + done + + # Add to PATH if not excluded + if [ "$skip" = false ]; then + PATH="$dir:$PATH" + fi + done < <(find "$HOME/.scripts" -type d -print0) +fi + +# Global TERM color +export TERM=xterm-256color + + +# Conditionally set default term +available_terms=("wezterm" "kitty" "alacritty" "xterm") +for term in "${available_terms[@]}"; do + if command -v "$term" &> /dev/null; then + export TERMINAL="$term" + break + fi +done + +# Default Programs: +export EDITOR=$(command -v nvim || echo "vim") +export TEXEDIT="$EDITOR" +export FCEDIT="$EDITOR" +export VISUAL="$EDITOR" +export GIT_EDITOR="$EDITOR" +export COLORTERM="truecolor" +export TERM="xterm-256color" +export READER="zathura" +export BROWSER="firefox" +export OPENER="xdg-open" +if command -v nvim &> /dev/null; then + export MANPAGER="sh -c 'col -b | nvim -c \"set ft=man ts=8 nomod nolist nonu noma\" -c \"autocmd VimEnter * call feedkeys(\\\"\\<CR>q\\\")\" -'" +else + export MANPAGER="bat" +fi +export MANROFFOPT="-c" +export PAGER="less" +export FAQ_STYLE='github' +export VIDEO="mpv" +export IMAGE="phototonic" + +# XDG Paths: +export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} +export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share} +export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache} +export INPUTRC="${XDG_CONFIG_HOME:-$HOME/.config}/inputrc" + +export ZDOTDIR="$XDG_CONFIG_HOME/zsh" +export HISTFILE="$ZDOTDIR/.zhistory" # History filepath +export HISTSIZE=1000000 # Maximum events for internal history +export SAVEHIST=1000000 # Maximum events in history file +export BANG_HIST # Treat the '!' character specially during expansion. +export EXTENDED_HISTORY # Write the history file in the ":start:elapsed;command" format. +export INC_APPEND_HISTORY # Write to the history file immediately, not when the shell exits. +export SHARE_HISTORY # Share history between all sessions. +export HIST_EXPIRE_DUPS_FIRST # Expire duplicate entries first when trimming history. +export HIST_IGNORE_DUPS # Don't record an entry that was just recorded again. +export HIST_IGNORE_ALL_DUPS # Delete old recorded entry if new entry is a duplicate. +export HIST_FIND_NO_DUPS # Do not display a line previously found. +export HIST_IGNORE_SPACE # Don't record an entry starting with a space. +export HIST_SAVE_NO_DUPS # Don't write duplicate entries in the history file. +export HIST_REDUCE_BLANKS # Remove superfluous blanks before recording entry. +export HIST_VERIFY # Don't execute immediately upon history expansion. +export HIST_BEEP # Beep when accessing nonexistent history. +export INC_APPEND_HISTORY + +# Customize `ls` colours +export LSCOLORS=ExGxBxDxCxEgEdxbxgxcxd + +# Other XDG paths: +export RIPGREP_CONFIG_PATH="$XDG_CONFIG_HOME/ripgrep/ripgreprc" +export DOCKER_CONFIG="$XDG_CONFIG_HOME/docker" +export VSCODE_PORTABLE="$XDG_DATA_HOME/vscode" +export GTK2_RC_FILES="$XDG_CONFIG_HOME/gtk-2.0/gtkrc" +export PATH="/usr/bin/cmake:$PATH" +export PATH=$PATH:/opt/google/chrome +export DISCORD_USER_DATA_DIR="$XDG_DATA_HOME" +export LYNX_CFG="$XDG_CONFIG_HOME/.lynxrc" + +# Manage Arch linux build sources +export ASPROOT="${XDG_CACHE_HOME:-$HOME/.cache}/asp" + +# Homebrew +#export PATH=/opt/homebrew/bin:$PATH +export PATH="/opt/homebrew/sbin:$PATH" + +# Nix-profile +export PATH=$HOME/.nix-profile/bin:$PATH + +# GnuPG +export GPG_TTY=$(tty) +#export GNUPGHOME="$XDG_CONFIG_HOME/gnupg" + +# Nvim +export NVIM_TUI_ENABLE_TRUE_COLOR=1 + +# Let FZF use ripgrep by default +if type rg &> /dev/null; then + export FZF_DEFAULT_COMMAND="rg --files --hidden --glob '!{node_modules/*,.git/*}'" + export FZF_DEFAULT_OPTS='-m --height 50% --border' + export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" +fi + +# Zoxide (cd alternative) +if command -v zoxide >/dev/null 2>&1; then + eval "$(zoxide init zsh)" +fi + +export XDG_MENU_PREFIX=gnome- + +# enable git scripts +export DEVELOPMENT_DIRECTORY="$HOME/code" + +# Android Home +export ANDROID_HOME=/opt/android-sdk +export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH +#export PATH=$ANDROID_HOME/cmdline-tools/bin:$PATH +export PATH=$ANDROID_HOME/tools:$PATH +export PATH=$ANDROID_HOME/tools/bin:$PATH +export PATH=$ANDROID_HOME/platform-tools:$PATH +# Android emulator PATH +export PATH=$ANDROID_HOME/emulator:$PATH +# Android SDK ROOT PATH +export ANDROID_SDK_ROOT=/opt/android-sdk +export PATH=$ANDROID_SDK_ROOT:$PATH +#export ANDROID_SDK_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/android" + +# Programming Environment Variables: + +# Rust +export RUSTUP_HOME=${XDG_DATA_HOME:-$HOME/.local/share}/rustup +export CARGO_HOME=${XDG_DATA_HOME:-$HOME/.local/share}/cargo +export PATH="${CARGO_HOME}/bin:${RUSTUP_HOME}/bin:$PATH" +#export PATH="$PATH:$CARGO_HOME/bin" +#[[ -d $CARGO_HOME/bin ]] && path=($CARGO_HOME/bin $path) +if which rustc > /dev/null; then export RUST_BACKTRACE=1; fi +#export PATH="$HOME/.cargo/bin:$PATH" +#export CARGO_HOME=${XDG_DATA_HOME}/cargo +#export RUSTUP_HOME=${XDG_DATA_HOME}/rustup + + +# Dotnet +# # Currently dotnet does not support XDG ( https://github.com/dotnet/sdk/issues/10390 ) +#export DOTNET_TOOLS_DIR="$HOME/.dotnet/tools" +export DOTNET_HOME=${XDG_DATA_HOME:-$HOME/.local/share}/dotnet +export DOTNET_CLI_HOME="$XDG_CONFIG_HOME/dotnet" +#mkdir -p "$DOTNET_CLI_HOME"; +export PATH="$PATH":"$DOTNET_HOME"/tools +export DOTNET_ROOT=/opt/dotnet +# Disable telemetry for dotnet apps +export DOTNET_CLI_TELEMETRY_OPTOUT=1 + + +# Java +#export JAVA_HOME=/usr/lib/jvm/default-java +#export JAVA_HOME='/usr/lib/jvm/java-8-openjdk' +#export JAVA_HOME='/usr/lib/jvm/java-10-openjdk' +#export JAVA_HOME='/usr/lib/jvm/java-11-openjdk' +#export JAVA_HOME='/usr/lib/jvm/java-17-openjdk' +export JAVA_HOME='/usr/lib/jvm/java-20-openjdk' +#export PATH=$JAVA_HOME/bin:$PATH +export _JAVA_OPTIONS=-Djava.util.prefs.userRoot="$XDG_CONFIG_HOME"/java +#export DEFAULT_JVM_OPTS='"-Dcom.android.sdklib.toolsdir=$APP_HOME" -XX:+IgnoreUnrecognizedVMOptions' +#export _JAVA_AWT_WM_NONREPARENTING=1 +#export JAVA_OPTS='-XX:+IgnoreUnrecognizedVMOptions --add-modules java.se.ee' +#export JAVA_OPTS='-XX:+IgnoreUnrecognizedVMOptions --add-modules java.xml.bind' +#Windows: +#set JAVA_OPTS=-XX:+IgnoreUnrecognizedVMOptions --add-modules java.se.ee + + +# Dart/Flutter +export PATH="/opt/flutter/bin:/usr/lib/dart/bin:$PATH" + + +# Go +export GO_PATH=${XDG_DATA_HOME}/go +export GOPATH="${XDG_DATA_HOME:-$HOME/.local/share}/go" + + +# Javascript +# NVM +export NVM_DIR="$HOME/.config/nvm" +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" +#[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm + +# global node installs (gross) +[[ -d "$XDG_DATA_HOME/node/bin" ]] && path=($XDG_DATA_HOME/node/bin $path) +export NODE_REPL_HISTORY="$XDG_DATA_HOME"/node_repl_history +export NPM_CONFIG_USERCONFIG=$XDG_CONFIG_HOME/npm/npmrc +#export NPM_CONFIG_INIT_AUTHOR_NAME='srdusr' +#export NPM_CONFIG_INIT_AUTHOR_EMAIL='trevorgray@srdusr.com' +#export NPM_CONFIG_INIT_AUTHOR_URL='https://srdusr.com' +#export NPM_CONFIG_INIT_LICENSE='GPL-3.0' +#export NPM_CONFIG_INIT_VERSION='0.0.0' +#export NPM_CONFIG_SIGN_GIT_TAG='true' + +export BUN_INSTALL="$HOME/.bun" +export PATH="$BUN_INSTALL/bin:$PATH" + +# Register Bun completion +#fpath=("$HOME/.bun" $fpath) + +# Yarn +#if command -v yarn >/dev/null 2>&1; then +# export PATH="$PATH:`yarn global bin`" +#fi +#export PATH="$(yarn global bin):$PATH" +#YARN_PATH="$HOME/.yarn/bin" +#YARN_BIN_EXPORT="$HOME/.config/yarn/global/node_modules/.bin" + +# Ruby +export GEM_PATH="$XDG_DATA_HOME/ruby/gems" +export GEM_SPEC_CACHE="$XDG_DATA_HOME/ruby/specs" +export GEM_HOME="$XDG_DATA_HOME/ruby/gems" +#if [[ -d ~/.gem/ruby ]]; then +# ver=$(find ~/.gem/ruby/* -maxdepth 0 | sort -rV | head -n 1) +# export PATH="$PATH:${ver}/bin" +#fi + + +# Python +# lazy load pyenv +#export PYENV_ROOT=${PYENV_ROOT:-$HOME/.pyenv} +#[[ -a $PYENV_ROOT/bin/pyenv ]] && path=($PYENV_ROOT/bin $path) +#if type pyenv &> /dev/null || [[ -a $PYENV_ROOT/bin/pyenv ]]; then +# function pyenv() { +# unset pyenv +# path=($PYENV_ROOT/shims $path) +# eval "$(command pyenv init -)" +# if which pyenv-virtualenv-init > /dev/null; then +# eval "$(pyenv virtualenv-init -)" +# export PYENV_VIRTUALENV_DISABLE_PROMPT=1 +# fi +# pyenv $@ +# } +#fi +#export WORKON_HOME="$XDG_DATA_HOME/virtualenvs" +#export WORKON_HOME=$HOME/.virtualenvs +#export VIRTUALENVWRAPPER_PYTHON=`which python3` +#export VIRTUALENVWRAPPER_PYTHON=$(which python3) +#export VIRTUALENVWRAPPER_VIRTUALENV=`which virtualenv` +#source /usr/local/bin/virtualenvwrapper.sh + +# Check if virtualenvwrapper.sh exists before sourcing +if command -v virtualenvwrapper.sh >/dev/null 2>&1; then + export WORKON_HOME="$HOME/.virtualenvs" + export VIRTUALENVWRAPPER_PYTHON="$(command -v python3)" + export VIRTUALENVWRAPPER_VIRTUALENV="$(command -v virtualenv)" + source "$(command -v virtualenvwrapper.sh)" +fi + +export VIRTUAL_ENV_DISABLE_PROMPT=false +export JUPYTER_CONFIG_DIR="$XDG_CONFIG_HOME/jupyter" +export IPYTHONDIR="$XDG_CONFIG_HOME/jupyter" + +# Python +[[ "$(uname)" == "Darwin" ]] && export PYTHON_CONFIGURE_OPTS="--enable-framework" +[[ "$(uname)" == "Linux" ]] && export PYTHON_CONFIGURE_OPTS="--enable-shared" + +export PYENV_ROOT="$HOME/.pyenv" +export PATH="$PYENV_ROOT/bin:$PATH" + +# PHP +PATH="$HOME/.config/composer/vendor/bin:$PATH" + + +# Lua +export PATH=$PATH:/usr/local/luarocks/bin +#export PATH="$XDG_DATA_HOME/luarocks/bin:$PATH" + +#ver=$(find lua* -maxdepth 0 | sort -rV | head -n 1) +#export LUA_PATH="$LUA_PATH:${ver}/share/lua/5.1/?.lua;${ver}/share/lua/5.1/?/init.lua;;" +#export LUA_CPATH="$LUA_CPATH:${ver}/lib/lua/5.1/?.so;;" + +#LUAROCKS_PREFIX=/usr/local +#export LUA_PATH="$LUAROCKS_PREFIX/share/lua/5.1/?.lua;$LUAROCKS_PREFIX/share/lua/5.1/?/init.lua;;" +#export LUA_CPATH="$LUAROCKS_PREFIX/lib/lua/5.1/?.so;;" + +#export LUA_PATH="<path-to-add>;;" +#export LUA_CPATH="./?.so;/usr/local/lib/lua/5.3/?.so; +# /usr/local/share/lua/5.3/?.so;<path-to-add>" + + +# Program settings +#export MOZ_USE_XINPUT2="1" # Mozilla smooth scrolling/touchpads. +# Pixel-perfect Firefox touchpad scrolling +export MOZ_USE_XINPUT2=1 + +# Cmake +export PKG_CONFIG_PATH="/usr/local/lib64/pkgconfig:$PKG_CONFIG_PATH" + + +# Scaling +#export QT_AUTO_SCREEN_SCALE_FACTOR=0 +#export QT_SCALE_FACTOR=1 +#export QT_SCREEN_SCALE_FACTORS="1;1;1" +#export GDK_SCALE=1 +#export GDK_DPI_SCALE=1 + +## Prevent duplicate paths +#typeset -U PATH path +# +## Default most programs to use fcitx global keyboard configurations +#export GTK_IM_MODULE='fcitx' +#export QT_IM_MODULE='fcitx' +#export SDL_IM_MODULE='fcitx' +#export XMODIFIERS='@im=fcitx' + + +# Start blinking +export LESS_TERMCAP_mb=$(tput bold; tput setaf 2) # green +# Start bold +export LESS_TERMCAP_md=$(tput bold; tput setaf 2) # green +# Start stand out +export LESS_TERMCAP_so=$(tput bold; tput setaf 3) # yellow +# End standout +export LESS_TERMCAP_se=$(tput rmso; tput sgr0) +# Start underline +export LESS_TERMCAP_us=$(tput smul; tput bold; tput setaf 1) # red +# End Underline +export LESS_TERMCAP_ue=$(tput sgr0) +# End bold, blinking, standout, underline +export LESS_TERMCAP_me=$(tput sgr0). diff --git a/common/config/zsh/.zshrc b/common/config/zsh/.zshrc new file mode 100644 index 0000000..0ada4f5 --- /dev/null +++ b/common/config/zsh/.zshrc @@ -0,0 +1,73 @@ +# ███████╗███████╗██╗ ██╗██████╗ ██████╗ +# ╚══███╔╝██╔════╝██║ ██║██╔══██╗██╔════╝ +# ███╔╝ ███████╗███████║██████╔╝██║ +# ███╔╝ ╚════██║██╔══██║██╔══██╗██║ +# ███████╗███████║██║ ██║██║ ██║╚██████╗ +# ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ + +# Profile zsh time +#zmodload zsh/zprof + +# If not running interactively, and not being sourced, don’t do anything +[[ $- != *i* ]] && [[ "${BASH_SOURCE[0]:-${(%):-%N}}" == "$0" ]] && return + +# Terminal key bindings +#stty intr '^q' # Free Ctrl+C for copy use Ctrl+Q instead for Interrupt +stty lnext '^-' # Free Ctrl+V for paste use Ctrl+- instead for Literal next +stty stop undef # Disable Ctrl+S to freeze terminal +stty start undef # Disable Ctrl+Q nfreeze terminal + +# Set the current prompt file (e.g., prompt, or prompt_minimal) +ZSH_PROMPT="${ZSH_PROMPT:-prompt}" +#ZSH_PROMPT="${ZSH_PROMPT:-prompt_minimal}" +#ZSH_PROMPT="${ZSH_PROMPT:-prompt_new}" +#ZSH_PROMPT="${ZSH_PROMPT:-prompt_simple}" + +# Source common Zsh files (excluding any that start with 'prompt') +ZSH_SOURCES=() + +for zsh_source in "$HOME"/.config/zsh/user/*.zsh; do + if [[ $(basename "$zsh_source") == prompt* && $(basename "$zsh_source" .zsh) != "$ZSH_PROMPT" ]]; then + continue + fi + ZSH_SOURCES+=("$zsh_source") +done + +# Source ZSH files +for zsh_source in "${ZSH_SOURCES[@]}"; do + source "$zsh_source" +done + +# Faster SSH +if [[ -n "$SSH_CLIENT" ]]; then + export KEYTIMEOUT=10 +else + export KEYTIMEOUT=15 +fi + +# Prevent non-login shell anomalies or toolchain misidentification in VS Code +if [[ "${TERM_PROGRAM:-}" == "vscode" ]]; then + unset ARGV0 +fi + +########## Source Plugins, should be last ########## + +# Load fzf keybindings and completion if fzf is installed +if command -v fzf >/dev/null 2>&1; then + FZF_BASE="/usr/local/bin/fzf/shell" + [[ -f "${FZF_BASE}/key-bindings.zsh" ]] && source "${FZF_BASE}/key-bindings.zsh" + [[ -f "${FZF_BASE}/completion.zsh" ]] && source "${FZF_BASE}/completion.zsh" +fi + +# Source plugins +for plugin in \ + "$HOME/.config/zsh/plugins/zsh-you-should-use/you-should-use.plugin.zsh" \ + "$HOME/.config/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" \ + "$HOME/.config/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh" \ + "$HOME/.config/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh" +do + [ -f "$plugin" ] && source "$plugin" +done + +# Profile zsh time +#zprof # At the end of .zshrc diff --git a/common/config/zsh/user/aliases.zsh b/common/config/zsh/user/aliases.zsh new file mode 100644 index 0000000..c205a9b --- /dev/null +++ b/common/config/zsh/user/aliases.zsh @@ -0,0 +1,208 @@ +########## Aliases ########## + +# Define alias for nvim/vim (fallback to vim) +if command -v nvim > /dev/null; then + alias vi='nvim' +else + alias vi='vim' +fi + +#alias vv='$(history -p !vim)' +alias vv="vim -c 'norm! ^O'" + +# Confirmation # +alias mv='mv -i' +alias cp='cp -i' +alias ln='ln -i' + +# Disable 'rm' +#alias rm='function _rm() { echo -e "\033[0;31mrm\033[0m is disabled, use \033[0;32mtrash\033[0m or \033[0;32mdel \033[0m\033[0;33m$1\033[0m"; }; _rm' +#alias del='/bin/rm' + +# Use lsd for ls if available +if command -v lsd >/dev/null 2>&1; then + alias ls='lsd --color=auto --group-directories-first' +fi + +# ls variants +alias l='ls -FAh --group-directories-first' +alias la='ls -lAFh --group-directories-first' +alias lt='ls -lFAht --group-directories-first' +alias lr='ls -RFAh --group-directories-first' + +# more ls variants +alias ldot='ls -ld .* --group-directories-first' +alias lS='ls -1FASsh --group-directories-first' +alias lart='ls -1Fcart --group-directories-first' +alias lrt='ls -1Fcrt --group-directories-first' + +# ls with different alphabethical sorting +#unalias ll +#ll() { LC_COLLATE=C ls "$@" } + +# suffix aliases +alias -g CP='| xclip -selection clipboard -rmlastnl' +alias -g LL="| less exit 2>1 /dev/null" +alias -g CA="| cat -A" +alias -g KE="2>&1" +alias -g NE="2>/dev/null" +alias -g NUL=">/dev/null 2>&1" + +alias grep='grep --color=auto --exclude-dir={.git,.svn,.hg}' +alias egrep='egrep --color=auto --exclude-dir={.git,.svn,.hg}' +alias egrep='fgrep --color=auto --exclude-dir={.git,.svn,.hg}' + +#alias hist="grep '$1' $HISTFILE" +alias hist="history | grep $1" + + +alias gdb='gdb -q' +alias rust-gdb='rust-gdb -q' + +alias cd="cd-clear-ls" +alias clear='newline_clear' + +# List upto last 10 visited directories using "d" and quickly cd into any specific one +alias d="dirs -v | head -10" + +# Using just a number from "0" to "9" +alias 0="cd +0" +alias 1="cd +1" +alias 2="cd +2" +alias 3="cd +3" +alias 4="cd +4" +alias 5="cd +5" +alias 6="cd +6" +alias 7="cd +7" +alias 8="cd +8" +alias 9="cd +9" + +alias sudo='sudo ' # zsh: elligible for alias expansion/fix syntax highlight +alias sedit='sudoedit' +#alias se='sudoedit' +alias se='sudo -e' +alias :q='exit 2>1 /dev/null' +alias disk-destroyer='$(command -v dd)' +alias dd='echo "Warning use command: disk-destroyer"' +alias sc="systemctl" +alias jc="journalctl" +alias jck="journalctl -k" # Kernel +alias jce='sudo journalctl -b --priority 0..3' # error +alias journalctl-error='sudo journalctl -b --priority 0..3' +alias jcssh="sudo journalctl -u sshd" +alias tunnel='ssh -fNTL' +# tty aliases +#if [[ "$TERM" == 'linux' ]]; then +# alias tmux='/usr/bin/tmux -L linux' +#fi +#alias logout="loginctl kill-user $(whoami)" + +logout() { + local wm + wm="$(windowManagerName)" + if [[ -n "$wm" ]]; then + echo "Logging out by killing window manager: $wm" + pkill "$wm" + else + echo "No window manager detected!" >&2 + fi +} +alias lg="logout" + +#alias suspend='systemctl suspend && betterlockscreen -l' # Suspend(sleep) and lock screen if using systemctl +#alias suspend='systemctl suspend' # Suspend(sleep) and lock screen if using systemctl +alias suspend='loginctl suspend' # Suspend(sleep) and lock screen if using systemctl +#alias shutdown='loginctl poweroff' # Suspend(sleep) and lock screen if using systemctl +#alias shutdown='sudo /sbin/shutdown -h' +#alias poweroff='loginctl poweroff' +#alias reboot='loginctl reboot' +alias reboot='sudo reboot' +#alias hibernate='systemctl hibernate' # Hibernate +alias lock='DISPLAY=:0 xautolock -locknow' # Lock my workstation screen from my phone +alias oports="sudo lsof -i -P -n | grep -i 'listen'" # List open ports +alias keyname="xev | sed -n 's/[ ]*state.* \([^ ]*\)).*/\1/p'" +alias wget=wget --hsts-file="$XDG_CACHE_HOME/wget-hsts" # wget does not support environment variables +alias open="xdg-open" +alias pp='getlast 2>&1 |&tee -a output.txt' +#alias lg='la | grep' +alias pg='ps aux | grep' +alias py='python' +alias py3='python3' +alias activate='source ~/.local/share/venv/bin/activate' +alias sha256='shasum -a 256' +alias rgf='rg -F' +alias weather='curl wttr.in/durban' +alias diary='nvim "$HOME/documents/main/inbox/diary/$(date +'%Y-%m-%d').md"' +alias wifi='nmcli dev wifi show-password' +alias ddg='w3m lite.duckduckgo.com' +alias rss='newsboat' +alias vpn='protonvpn' +alias yt-dl="yt-dlp -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4' --restrict-filename" +#alias com.obsproject.Studio="obs" +#alias obs="com.obsproject.Studio" +#alias obs-stuido="obs" + +# Time aliases +alias utc='TZ=Africa/Johannesburg date' +alias ber='TZ=Europe/Berlin date' +alias nyc='TZ=America/New_York date' +alias sfo='TZ=America/Los_Angeles date' +alias utc='TZ=Etc/UTC date' + +alias src='source $ZDOTDIR/.zshrc' +alias p=proxy + +alias cheat='~/.scripts/cheat.sh ~/documents/notes/cheatsheets' +alias crypto='curl -s rate.sx | head -n -2 | tail -n +10' +#alias todo='glow "$HOME"/documents/main/notes/TODO.md' + +alias todo='$EDITOR "$(find "$HOME"/documents/main -type f -iname "todo.md" | head -n 1)"' +alias android-studio='/opt/android-studio/bin/studio.sh' # android-studio +alias nomachine='/usr/NX/bin/nxplayer' # nomachine +alias firefox="firefox-bin" +alias discord="vesktop-bin" +alias fetch="fastfetch" +alias batt='upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|to full|percentage"' +alias emerge-fetch='sudo tail -f /var/log/emerge-fetch.log' +alias spotify="env LD_PRELOAD=/usr/local/lib/spotify-adblock.so spotify %U" + +alias proofread='firejail --private --private-tmp --net=none --seccomp --caps.drop=all zathura' + +# NVM +if [ -s "$NVM_DIR/nvm.sh" ]; then + nvm_cmds=(nvm node npm yarn) + for cmd in "${nvm_cmds[@]}"; do + alias "$cmd"="unalias ${nvm_cmds[*]} && unset nvm_cmds && . $NVM_DIR/nvm.sh && $cmd" + done +fi + +# Kubernetes +if command -v kubectl > /dev/null; then + replaceNS() { kubectl config view --minify --flatten --context=$(kubectl config current-context) | yq ".contexts[0].context.namespace=\"$1\"" ; } + alias kks='KUBECONFIG=<(replaceNS "kube-system") kubectl' + alias kam='KUBECONFIG=<(replaceNS "authzed-monitoring") kubectl' + alias kas='KUBECONFIG=<(replaceNS "authzed-system") kubectl' + alias kar='KUBECONFIG=<(replaceNS "authzed-region") kubectl' + alias kt='KUBECONFIG=<(replaceNS "tenant") kubectl' + + if command -v kubectl-krew > /dev/null; then + path=($XDG_CONFIG_HOME/krew/bin $path) + fi + + rmfinalizers() { + kubectl get deployment "$1" -o json | jq '.metadata.finalizers = null' | kubectl apply -f - + } +fi + +# Castero +castero() { + if [[ -f ~/.local/share/venv/bin/activate ]]; then + . ~/.local/share/venv/bin/activate + fi + command castero "$@" +} + +# Zoxide (cd alternative) +if command -v zoxide >/dev/null 2>&1; then + eval "$(zoxide init zsh)" +fi diff --git a/common/config/zsh/user/bindings.zsh b/common/config/zsh/user/bindings.zsh new file mode 100644 index 0000000..52cab06 --- /dev/null +++ b/common/config/zsh/user/bindings.zsh @@ -0,0 +1,175 @@ +########## Vi mode ########## +bindkey -v + +local WORDCHARS='*?_-.[]~=&;!#$%^(){}<>' +backward-kill-dir () { + local WORDCHARS=${WORDCHARS/\/} + zle backward-kill-word + zle -f kill +} + +zle -N backward-kill-dir +bindkey '^[^?' backward-kill-dir +bindkey "^W" backward-kill-dir + +bindkey -M viins '^[[3~' delete-char +bindkey -M vicmd '^[[3~' delete-char +bindkey -v '^?' backward-delete-char +bindkey -r '\e/' +bindkey -s jk '\e' +#bindkey "^W" backward-kill-word +bindkey "^H" backward-delete-char # Control-h also deletes the previous char +bindkey "^U" backward-kill-line +bindkey "^[j" history-search-forward # or you can bind it to the down key "^[[B" +bindkey "^[k" history-search-backward # or you can bind it to Up key "^[[A" + +bindkey '^[[D' backward-char # Left arrow +bindkey '^[[C' forward-char # Right arrow +bindkey '^[D' backward-char # Left arrow +bindkey '^[C' forward-char # Right arrow +bindkey '[C' forward-word +bindkey '[D' backward-word +bindkey -M viins '^[[D' backward-char # Left arrow +bindkey -M viins '^[[C' forward-char # Right arrow + +bindkey -M vicmd '^[[D' backward-char # Left arrow +bindkey -M vicmd '^[[C' forward-char # Right arrow + +# Define the 'autosuggest-execute' and 'autosuggest-accept' ZLE widgets +autoload -Uz autosuggest-execute autosuggest-accept +zle -N autosuggest-execute +zle -N autosuggest-accept +bindkey '^X' autosuggest-execute +bindkey '^Y' autosuggest-accept +bindkey '\M-l' accept-and-complete-next-history + +# Accept completion with <tab> or Ctrl+i and go to next/previous suggestions with Vi like keys: Ctrl+n/p +zmodload -i zsh/complist +accept-and-complete-next-history() { + zle expand-or-complete-prefix +} +zle -N accept-and-complete-next-history +#bindkey -M menuselect '^i' accept-and-complete-next-history +bindkey '^n' expand-or-complete +bindkey '^p' reverse-menu-complete +#bindkey '^I' expand-or-complete +#bindkey '^[[Z]]' reverse-menu-complete +bindkey -M menuselect '^[' undo + +# Edit line in vim with alt-e +autoload edit-command-line; zle -N edit-command-line +bindkey '^e' edit-command-line +bindkey '^[e' edit-command-line # alt + e + +# Allow CTRL+D to exit zsh with partial command line (non empty line) +exit_zsh() { exit } +zle -N exit_zsh +bindkey '^D' exit_zsh + +# Copy/Paste +# Safe clipboard copy +smart_copy() { + local text="${LBUFFER}${RBUFFER}" + + # Prefer Wayland, fallback to X11, then others + if command -v wl-copy >/dev/null 2>&1 && [[ "$WAYLAND_DISPLAY" || "$XDG_SESSION_TYPE" == "wayland" ]]; then + echo -n "$text" | wl-copy --foreground --type text/plain 2>/dev/null || true + elif command -v xclip >/dev/null 2>&1 && [[ "$DISPLAY" || "$XDG_SESSION_TYPE" == "x11" || "$XDG_SESSION_TYPE" == "x11-xwayland" ]]; then + echo -n "$text" | xclip -selection clipboard 2>/dev/null || true + elif [[ "$(uname -s)" == "Darwin" ]] && command -v pbcopy >/dev/null 2>&1; then + echo -n "$text" | pbcopy 2>/dev/null || true + elif [[ "$OSTYPE" == "cygwin" || "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + echo -n "$text" | clip.exe 2>/dev/null || true + else + echo "smart_copy: No supported clipboard utility found." >&2 + fi +} + +# Safe clipboard paste +smart_paste() { + local clip="" + if command -v wl-paste >/dev/null 2>&1 && [[ "$WAYLAND_DISPLAY" || "$XDG_SESSION_TYPE" == "wayland" ]]; then + clip=$(wl-paste --no-newline 2>/dev/null) + elif command -v xclip >/dev/null 2>&1 && [[ "$DISPLAY" || "$XDG_SESSION_TYPE" == "x11" || "$XDG_SESSION_TYPE" == "x11-xwayland" ]]; then + clip=$(xclip -selection clipboard -o 2>/dev/null) + elif [[ "$(uname -s)" == "Darwin" ]] && command -v pbpaste >/dev/null 2>&1; then + clip=$(pbpaste 2>/dev/null) + elif [[ "$OSTYPE" == "cygwin" || "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + clip=$(powershell.exe -Command 'Get-Clipboard -Raw' 2>/dev/null | tr -d '\r') + else + echo "smart_paste: No supported clipboard utility found." >&2 + fi + + LBUFFER+="$clip" + zle reset-prompt +} + +# Register widgets +zle -N smart_copy +zle -N smart_paste + +# Bind keys (optional: choose your preferred) +bindkey '^V' smart_paste +bindkey -M viins '^V' smart_paste +bindkey -M vicmd '^V' smart_paste +bindkey -M vicmd 'p' smart_paste + +bindkey '^Y' smart_copy +bindkey -M viins '^Y' smart_copy +bindkey -M vicmd '^Y' smart_copy +bindkey -M vicmd 'y' smart_copy + +# In vi mode, map Alt-H and Alt-L +#bindkey -M viins "^[u" go_up # Alt-H to go up +#bindkey -M viins "^[o" go_into # Alt-L to go into a directory + + +# Newline and clear +function newline_clear() { + printf "\n" + command clear +} + +zle -N newline_clear + +no_tmux_clear() { + zle clear-screen +} +zle -N no_tmux_clear + +# Newline before clear +if [[ -n "$TMUX" ]]; then + # Bind Ctrl-L to send newline and clear screen + bindkey '^L' newline_clear +else + bindkey '^L' no_tmux_clear +fi + +# use ctrl-z to toggle in and out of bg +function toggle_fg_bg() { + if [[ $#BUFFER -eq 0 ]]; then + BUFFER="fg" + zle accept-line + else + BUFFER="" + zle clear-screen + fi +} +zle -N toggle_fg_bg +bindkey '^Z' toggle_fg_bg + + + + +## Custom key bindings to control history behavior +#bindkey -M vicmd '^[[C' vi-forward-char # Right arrow in normal mode - just move cursor +#bindkey -M vicmd '^[[D' vi-backward-char # Left arrow in normal mode - just move cursor +#bindkey -M vicmd '^A' beginning-of-line # Ctrl-A - go to beginning of line +#bindkey -M vicmd '^E' end-of-line # Ctrl-E - go to end of line + +# Disable automatic suggestion accept on right arrow in normal mode + +## Additional vi-mode key bindings to prevent unwanted history completion +## Disable automatic history completion in normal mode +#bindkey -M vicmd '^[[C' vi-forward-char # Right arrow - just move right, don't complete +#bindkey -M vicmd '^[[D' vi-backward-char # Left arrow - just move left diff --git a/common/config/zsh/user/completion.zsh b/common/config/zsh/user/completion.zsh new file mode 100644 index 0000000..2445548 --- /dev/null +++ b/common/config/zsh/user/completion.zsh @@ -0,0 +1,172 @@ +#!/bin/zsh + +########## Completion(s) ########## + +autoload -Uz compinit +_comp_path="${XDG_CACHE_HOME:-$HOME/.cache}/zcompdump" + +# Expands globs in conditional expressions +if [[ $_comp_path(#qNmh-20) ]]; then + # -C (skip function check) implies -i (skip security check). + compinit -C -d "$_comp_path" +else + mkdir -p "$_comp_path:h" + compinit -i -d "$_comp_path" + # Keep $_comp_path younger than cache time even if it isn't regenerated. + touch "$_comp_path" +fi +unset _comp_path + +# Skip the not really helpful global compinit +skip_global_compinit=0 + +DISABLE_MAGIC_FUNCTIONS=true + + +#zstyle ':completion:*' menu select=1 +#zstyle ':completion:*:directory-stack' list-colors '=(#b) #([0-9]#)*( *)==95=38;5;12' + +# Options +#setopt COMPLETE_IN_WORD # Complete from both ends of a word. +##setopt ALWAYS_TO_END # Move cursor to the end of a completed word. +##setopt PATH_DIRS # Perform path search even on command names with slashes. +#setopt AUTO_MENU # Show completion menu on a successive tab press. +#setopt AUTO_LIST # Automatically list choices on ambiguous completion. +#setopt AUTO_PARAM_SLASH # If completed parameter is a directory, add a trailing slash. +#setopt EXTENDED_GLOB # Needed for file modification glob modifiers with compinit. +#unsetopt MENU_COMPLETE # Do not autoselect the first completion entry. + +## Disable all custom completions +unsetopt COMPLETE_IN_WORD +#unsetopt AUTO_MENU +#unsetopt AUTO_LIST +#unsetopt AUTO_PARAM_SLASH +#unsetopt EXTENDED_GLOB +#unsetopt MENU_COMPLETE # Do not autoselect the first completion entry. + +# Optional: Uncomment to disable waiting dots on completion +# COMPLETION_WAITING_DOTS="false" + +setopt ALWAYS_TO_END # Move cursor to the end of a completed word. +setopt PATH_DIRS # Perform path search even on command names with slashes. +setopt AUTO_MENU # Show completion menu on a successive tab press. +setopt AUTO_LIST # Automatically list choices on ambiguous completion. +setopt AUTO_PARAM_SLASH # If completed parameter is a directory, add a trailing slash. +setopt EXTENDED_GLOB # Needed for file modification glob modifiers with compinit. +unsetopt MENU_COMPLETE # Do not autoselect the first completion entry. + +# Test the behavior with just the basics +compinit +# Variables +LS_COLORS=${LS_COLORS:-'di=34:ln=35:so=32:pi=33:ex=31:bd=36;01:cd=33;01:su=31;40;07:sg=36;40;07:tw=32;40;07:ow=33;40;07:'} + +# Styles +# Defaults. +zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS} +zstyle ':completion:*:default' list-prompt '%S%M matches%s' + +# Use caching to make completion for commands such as dpkg and apt usable. +zstyle ':completion::complete:*' use-cache on +zstyle ':completion::complete:*' cache-path "${XDG_CACHE_HOME:-$HOME/.cache}/zcompcache" + + +# Group matches and describe. +zstyle ':completion:*:*:*:*:*' menu select +zstyle ':completion:*:matches' group 'yes' +zstyle ':completion:*:options' description 'yes' +zstyle ':completion:*:options' auto-description '%d' +zstyle ':completion:*:corrections' format ' %F{green}-- %d (errors: %e) --%f' +zstyle ':completion:*:descriptions' format ' %F{yellow}-- %d --%f' +zstyle ':completion:*:messages' format ' %F{purple} -- %d --%f' +zstyle ':completion:*:warnings' format ' %F{red}-- no matches found --%f' +zstyle ':completion:*' format ' %F{yellow}-- %d --%f' +zstyle ':completion:*' group-name '' +zstyle ':completion:*' verbose yes + +# Fuzzy match mistyped completions. +zstyle ':completion:*' completer _complete _match _approximate +zstyle ':completion:*:match:*' original only +zstyle ':completion:*:approximate:*' max-errors 1 numeric + +# Increase the number of errors based on the length of the typed word. But make +# sure to cap (at 7) the max-errors to avoid hanging. +zstyle -e ':completion:*:approximate:*' max-errors 'reply=($((($#PREFIX+$#SUFFIX)/3>7?7:($#PREFIX+$#SUFFIX)/3))numeric)' + +# Don't complete unavailable commands. +zstyle ':completion:*:functions' ignored-patterns '(_*|pre(cmd|exec))' + +# Array completion element sorting. +zstyle ':completion:*:*:-subscript-:*' tag-order indexes parameters + +# Directories +zstyle ':completion:*:*:cd:*' tag-order local-directories directory-stack path-directories +zstyle ':completion:*:*:cd:*:directory-stack' menu yes select +zstyle ':completion:*:-tilde-:*' group-order 'named-directories' 'path-directories' 'users' 'expand' +zstyle ':completion:*' squeeze-slashes true + +# History +zstyle ':completion:*:history-words' stop yes +zstyle ':completion:*:history-words' remove-all-dups yes +zstyle ':completion:*:history-words' list false +zstyle ':completion:*:history-words' menu yes + +# Environment Variables +zstyle ':completion::*:(-command-|export):*' fake-parameters ${${${_comps[(I)-value-*]#*,}%%,*}:#-*-} + +# Populate hostname completion. But allow ignoring custom entries from static +# */etc/hosts* which might be uninteresting. +zstyle -a ':completion:*:hosts' etc-host-ignores '_etc_host_ignores' + +zstyle -e ':completion:*:hosts' hosts 'reply=( + ${=${=${=${${(f)"$(cat {/etc/ssh/ssh_,~/.ssh/}known_hosts(|2)(N) 2> /dev/null)"}%%[#| ]*}//\]:[0-9]*/ }//,/ }//\[/ } + ${=${(f)"$(cat /etc/hosts(|)(N) <<(ypcat hosts 2> /dev/null))"}%%(\#${_etc_host_ignores:+|${(j:|:)~_etc_host_ignores}})*} + ${=${${${${(@M)${(f)"$(cat ~/.ssh/config 2> /dev/null)"}:#Host *}#Host }:#*\**}:#*\?*}} +)' + +# Don't complete uninteresting users... +zstyle ':completion:*:*:*:users' ignored-patterns \ + adm amanda apache avahi beaglidx bin cacti canna clamav daemon \ + dbus distcache dovecot fax ftp games gdm gkrellmd gopher \ + hacluster haldaemon halt hsqldb ident junkbust ldap lp mail \ + mailman mailnull mldonkey mysql nagios \ + named netdump news nfsnobody nobody nscd ntp nut nx openvpn \ + operator pcap postfix postgres privoxy pulse pvm quagga radvd \ + rpc rpcuser rpm shutdown squid sshd sync uucp vcsa xfs '_*' + +# ... unless we really want to. +zstyle '*' single-ignored show + +# Ignore multiple entries. +zstyle ':completion:*:(rm|kill|diff):*' ignore-line other +zstyle ':completion:*:rm:*' file-patterns '*:all-files' + +# Kill +zstyle ':completion:*:*:*:*:processes' command 'ps -u $LOGNAME -o pid,user,command -w' +zstyle ':completion:*:*:kill:*:processes' list-colors '=(#b) #([0-9]#) ([0-9a-z-]#)*=01;36=0=01' +zstyle ':completion:*:*:kill:*' menu yes select +zstyle ':completion:*:*:kill:*' force-list always +zstyle ':completion:*:*:kill:*' insert-ids single + +# Man +zstyle ':completion:*:manuals' separate-sections true +zstyle ':completion:*:manuals.(^1*)' insert-sections true + +# Media Players +zstyle ':completion:*:*:mpg123:*' file-patterns '*.(mp3|MP3):mp3\ files *(-/):directories' +zstyle ':completion:*:*:mpg321:*' file-patterns '*.(mp3|MP3):mp3\ files *(-/):directories' +zstyle ':completion:*:*:ogg123:*' file-patterns '*.(ogg|OGG|flac):ogg\ files *(-/):directories' +zstyle ':completion:*:*:mocp:*' file-patterns '*.(wav|WAV|mp3|MP3|ogg|OGG|flac):ogg\ files *(-/):directories' + +# Mutt +if [[ -s "$HOME/.mutt/aliases" ]]; then + zstyle ':completion:*:*:mutt:*' menu yes select + zstyle ':completion:*:mutt:*' users ${${${(f)"$(<"$HOME/.mutt/aliases")"}#alias[[:space:]]}%%[[:space:]]*} +fi + +# SSH/SCP/RSYNC +zstyle ':completion:*:(ssh|scp|rsync):*' tag-order 'hosts:-host:host hosts:-domain:domain hosts:-ipaddr:ip\ address *' +zstyle ':completion:*:(scp|rsync):*' group-order users files all-files hosts-domain hosts-host hosts-ipaddr +zstyle ':completion:*:ssh:*' group-order users hosts-domain hosts-host users hosts-ipaddr +zstyle ':completion:*:(ssh|scp|rsync):*:hosts-host' ignored-patterns '*(.|:)*' loopback ip6-loopback localhost ip6-localhost broadcasthost +zstyle ':completion:*:(ssh|scp|rsync):*:hosts-domain' ignored-patterns '<->.<->.<->.<->' '^[-[:alnum:]]##(.[-[:alnum:]]##)##' '*@*' +zstyle ':completion:*:(ssh|scp|rsync):*:hosts-ipaddr' ignored-patterns '^(<->.<->.<->.<->|(|::)([[:xdigit:].]##:(#c,2))##(|%*))' '127.0.0.<->' '255.255.255.255' '::1' 'fe80::*' diff --git a/common/config/zsh/user/functions.zsh b/common/config/zsh/user/functions.zsh new file mode 100644 index 0000000..56f0ca1 --- /dev/null +++ b/common/config/zsh/user/functions.zsh @@ -0,0 +1,1607 @@ +# Dotfiles Management System +if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then + # Core git wrapper with repository as work-tree + _config() { + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" "$@" + } + + # Detect OS + case "$(uname -s)" in + Linux) CFG_OS="linux" ;; + Darwin) CFG_OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) CFG_OS="windows" ;; + *) CFG_OS="other" ;; + esac + + # Map system path to repository path + _repo_path() { + local f="$1" + + # If it's an absolute path that's not in HOME, handle it specially + if [[ "$f" == /* && "$f" != "$HOME/"* ]]; then + echo "$CFG_OS/${f#/}" + return + fi + + # Check for paths that should go to the repository root + case "$f" in + common/*|linux/*|macos/*|windows/*|profile/*|README.md) + echo "$f" + return + ;; + "$HOME/"*) + f="${f#$HOME/}" + ;; + esac + + # Default: put under OS-specific home + echo "$CFG_OS/home/$f" + } + + _sys_path() { + local repo_path="$1" + local os_path_pattern="$CFG_OS/" + + # Handle OS-specific files that are not in the home subdirectory + if [[ "$repo_path" == "$os_path_pattern"* && "$repo_path" != */home/* ]]; then + echo "/${repo_path#$os_path_pattern}" + return + fi + + case "$repo_path" in + # Common configs → OS-specific config dirs + common/config/*) + case "$CFG_OS" in + linux) + local base="${XDG_CONFIG_HOME:-$HOME/.config}" + echo "$base/${repo_path#common/config/}" + ;; + macos) + echo "$HOME/Library/Application Support/${repo_path#common/config/}" + ;; + windows) + echo "$LOCALAPPDATA\\${repo_path#common/config/}" + ;; + *) + echo "$HOME/.config/${repo_path#common/config/}" + ;; + esac + ;; + + # Common assets → stay in repo + common/assets/*) + echo "$HOME/.cfg/$repo_path" + ;; + + # Other common files (dotfiles like .bashrc, .gitconfig, etc.) → $HOME + common/*) + echo "$HOME/${repo_path#common/}" + ;; + + # OS-specific home + */home/*) + echo "$HOME/${repo_path#*/home/}" + ;; + + # Profile configs and README → stay in repo + profile/*|README.md) + echo "$HOME/.cfg/$repo_path" + ;; + + # Default fallback + *) + echo "$HOME/.cfg/$repo_path" + ;; + + esac + } + + # Prompts for sudo if needed and runs the command + _sudo_prompt() { + if [[ $EUID -eq 0 ]]; then + "$@" + else + if command -v sudo >/dev/null; then + sudo "$@" + elif command -v doas >/dev/null; then + doas "$@" + elif command -v pkexec >/dev/null; then + pkexec "$@" + else + echo "Error: No privilege escalation tool found." + return 1 + fi + fi + } + + # Main config command + config() { + local cmd="$1"; shift + local target_dir="" + # Parse optional --target flag for add + if [[ "$cmd" == "add" ]]; then + while [[ "$1" == --* ]]; do + case "$1" in + --target|-t) + target_dir="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + return 1 + ;; + esac + done + fi + + case "$cmd" in + add) + local file_path + for file_path in "$@"; do + local repo_path + if [[ -n "$target_dir" ]]; then + local rel_path + if [[ "$file_path" == /* ]]; then + rel_path="$(basename "$file_path")" + else + rel_path="$file_path" + fi + repo_path="$target_dir/$rel_path" + else + repo_path="$(_repo_path "$file_path")" + fi + + local full_repo_path="$HOME/.cfg/$repo_path" + mkdir -p "$(dirname "$full_repo_path")" + cp -a "$file_path" "$full_repo_path" + + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" add "$repo_path" + + echo "Added: $file_path -> $repo_path" + done + ;; + rm) + local rm_opts="" + local file_path_list=() + + for arg in "$@"; do + if [[ "$arg" == "-"* ]]; then + rm_opts+=" $arg" + else + file_path_list+=("$arg") + fi + done + + for file_path in "${file_path_list[@]}"; do + local repo_path="$(_repo_path "$file_path")" + + if [[ "$rm_opts" == *"-r"* ]]; then + _config rm --cached -r "$repo_path" + else + _config rm --cached "$repo_path" + fi + + eval "rm $rm_opts \"$file_path\"" + echo "Removed: $file_path" + done + ;; + sync) + local direction="${1:-to-repo}"; shift + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ "$direction" == "to-repo" ]]; then + if [[ -e "$sys_file" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + cp -a "$sys_file" "$full_repo_path" + echo "Synced to repo: $sys_file" + fi + elif [[ "$direction" == "from-repo" ]]; then + if [[ -e "$full_repo_path" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + local dest_dir="$(dirname "$sys_file")" + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + echo "Synced from repo: $sys_file" + fi + fi + done + ;; + status) + local auto_synced=() + while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ -e "$sys_file" && -e "$full_repo_path" ]]; then + if ! diff -q "$full_repo_path" "$sys_file" >/dev/null 2>&1; then + cp -fa "$sys_file" "$full_repo_path" + auto_synced+=("$repo_file") + fi + fi + done < <(_config ls-files) + if [[ ${#auto_synced[@]} -gt 0 ]]; then + echo "=== Auto-synced Files ===" + for repo_file in "${auto_synced[@]}"; do + echo "synced: $(_sys_path "$repo_file") -> $repo_file" + done + echo + fi + _config status + echo + ;; + deploy) + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" # destination only + + # Only continue if the source exists + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if needed + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Deployed: $repo_file -> $sys_file" + fi + done + ;; + checkout) + echo "Checking out dotfiles from .cfg..." + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" + + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if it doesn't exist + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Checked out: $repo_file -> $sys_file" + fi + done + ;; + backup) + local timestamp=$(date +%Y%m%d%H%M%S) + local backup_dir="$HOME/.dotfiles_backup/$timestamp" + echo "Backing up existing dotfiles to $backup_dir..." + + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + if [[ -e "$sys_file" ]]; then + local dest_dir_full="$backup_dir/$(dirname "$repo_file")" + mkdir -p "$dest_dir_full" + cp -a "$sys_file" "$backup_dir/$repo_file" + fi + done + echo "Backup complete. To restore, copy files from $backup_dir to their original locations." + ;; + *) + _config "$cmd" "$@" + ;; + esac + } +fi + +# Make SUDO_ASKPASS agnostic: pick the first available askpass binary. +# You can predefine SUDO_ASKPASS env var to force a particular path. +: "${SUDO_ASKPASS:=""}" + +# list of common askpass binaries (order: preferred -> fallback) +_askpass_candidates=( + "$SUDO_ASKPASS" # user-specified (if absolute path) + "/usr/lib/ssh/x11-ssh-askpass" + "/usr/libexec/openssh/ssh-askpass" + "/usr/lib/ssh/ssh-askpass" + "/usr/bin/ssh-askpass" + "/usr/bin/ssh-askpass-gtk" + "/usr/bin/ssh-askpass-gnome" + "/usr/bin/ssh-askpass-qt" + "/usr/bin/ksshaskpass" + "/usr/bin/zenity" # use zenity --entry as wrapper (see below) + "/usr/bin/mate-ssh-askpass" + "/usr/bin/xdg-open" # last-resort GUI helper (not ideal) +) + +find_askpass() { + for p in "${_askpass_candidates[@]}"; do + [ -z "$p" ] && continue + # if user gave a path in SUDO_ASKPASS we accept it only if it's executable + if [ -n "$SUDO_ASKPASS" ] && [ "$p" = "$SUDO_ASKPASS" ]; then + [ -x "$p" ] && { printf '%s\n' "$p"; return 0; } + continue + fi + + # if candidate is an absolute path, test directly + if [ "${p#/}" != "$p" ]; then + [ -x "$p" ] && { printf '%s\n' "$p"; return 0; } + continue + fi + + # otherwise try to resolve via PATH + if command -v "$p" >/dev/null 2>&1; then + # For zenity, we will use a small wrapper (see below) + printf '%s\n' "$(command -v "$p")" + return 0 + fi + done + + return 1 +} + +# If zenity is chosen, use a thin wrapper script so sudo -A can call it like an askpass binary. +# This wrapper will be created in $XDG_RUNTIME_DIR or /tmp (non-persistent). +create_zenity_wrapper() { + local wrapper + wrapper="${XDG_RUNTIME_DIR:-/tmp}/.sudo_askpass_zenity.sh" + cat >"$wrapper" <<'EOF' +#!/bin/sh +# simple zenity askpass wrapper for sudo +# prints password to stdout so sudo -A works +zenity --entry --title="Authentication" --text="Elevated privileges are required" --hide-text 2>/dev/null || exit 1 +EOF + chmod 700 "$wrapper" + printf '%s\n' "$wrapper" +} + +# Set askpass +if [ -z "$SUDO_ASKPASS" ]; then + candidate="$(find_askpass || true)" + if [ -n "$candidate" ]; then + if command -v zenity >/dev/null 2>&1 && [ "$(command -v zenity)" = "$candidate" ]; then + # create the wrapper and export it + wrapper="$(create_zenity_wrapper)" + export SUDO_ASKPASS="$wrapper" + else + export SUDO_ASKPASS="$candidate" + fi + else + # optional: leave unset or set to empty to avoid mistakes + unset SUDO_ASKPASS + fi +fi +# debug: (uncomment to print what was chosen) +# printf 'SUDO_ASKPASS -> %s\n' "${SUDO_ASKPASS:-<none>}" + + +# Git +# No arguments: `git status` +# With arguments: acts like `git` +g() { + if [ $# -gt 0 ]; then + git "$@" # If arguments are provided, pass them to git + else + git status # Otherwise, show git status + fi +} + +# Complete g like git +compdef g=git + +# Git alias commands +ga() { g add "$@"; } # ga: Add files to the staging area +gaw() { g add -A && g diff --cached -w | g apply --cached -R; } # gaw: Add all changes to the staging area and unstage whitespace changes +grm() { g rm "$@"; } +gb() { g branch "$@"; } # gb: List branches +gbl() { g branch -l "$@"; } # gbl: List local branches +gbD() { g branch -D "$@"; } # gbD: Delete a branch +gbu() { g branch -u "$@"; } # gbu: Set upstream branch +ge() { g clone "$@"; } +gc() { g commit "$@"; } # gc: Commit changes +gcm() { g commit -m "$@"; } # gcm: Commit with a message +gca() { g commit -a "$@"; } # gca: Commit all changes +gcaa() { g commit -a --amend "$@"; } # gcaa: Amend the last commit +gcam() { g commit -a -m "$@"; } # gcam: Commit all changes with a message +gce() { g commit -e "$@"; } # gce: Commit with message and allow editing +gcfu() { g commit --fixup "$@"; } # gcfu: Commit fixes in the context of the previous commit +gco() { g checkout "$@"; } # gco: Checkout a branch or file +gcob() { g checkout -b "$@"; } # gcob: Checkout a new branch +gcoB() { g checkout -B "$@"; } # gcoB: Checkout a new branch, even if it exists +gcp() { g cherry-pick "$@"; } # gcp: Cherry-pick a commit +gcpc() { g cherry-pick --continue "$@"; } # gcpc: Continue cherry-picking after resolving conflicts +gd() { g diff "$@"; } # gd: Show changes +#gd^() { g diff HEAD^ HEAD "$@"; } # gd^: Show changes between HEAD^ and HEAD +gds() { g diff --staged "$@"; } # gds: Show staged changes +gl() { g lg "$@"; } # gl: Show a customized log +glg() { g log --graph --decorate --all "$@"; } # glg: Show a customized log with graph +gls() { # Query `glog` with regex query. + query="$1" + shift + glog --pickaxe-regex "-S$query" "$@" +} +gdc() { g diff --cached "$@"; } # gdc: Show changes between the working directory and the index +gu() { g pull "$@"} # gu: Pull +gp() { g push "$@"} # gp: Push +gpom() { g push origin main "$@"; } # gpom: Push changes to origin main +gr() { g remote "$@"; } # gr: Show remote +gra() { g rebase --abort "$@"; } # gra: Abort a rebase +grb() { g rebase --committer-date-is-author-date "$@"; } # grb: Rebase with the author date preserved +grbom() { grb --onto master "$@"; } # grbom: Rebase onto master +grbasi() { g rebase --autosquash --interactive "$@"; } # grbasi: Interactive rebase with autosquash +grc() { g rebase --continue "$@"; } # grc: Continue a rebase +grs() { g restore --staged "$@"; } # grs: Restore changes staged for the next commit +grv() { g remote -v "$@"; } # grv: Show remote URLs after each name +grh() { g reset --hard "$@"; } # grh: Reset the repository and the working directory +grH() { g reset HEAD "$@"; } # grH: Reset the index but not the working directory +#grH^() { g reset HEAD^ "$@"; } # grH^: Reset the index and working directory to the state of the HEAD's first parent +gs() { g status -sb "$@"; } # gs: Show the status of the working directory and the index +gsd() { g stash drop "$@"; } # gsd: Drop a stash +gsl() { g stash list --date=relative "$@"; } # gsl: List all stashes +gsp() { g stash pop "$@"; } # gsp: Apply and remove a single stash +gss() { g stash show "$@"; } # gss: Show changes recorded in the stash as a diff +gst() { g status "$@"; } # gst: Show the status of the working directory and the index +gsu() { g standup "$@"; } # gsu: Customized standup command +gforgotrecursive() { g submodule update --init --recursive --remote "$@"; } # gforgotrecursive: Update submodules recursively +gfp() { g commit --amend --no-edit && g push --force-with-lease "$@"; } # gfp: Amending the last commit and force-pushing + +# Temporarily unset GIT_WORK_TREE +function git_without_work_tree() { + # Only proceed if a git command is being run + if [ "$1" = "git" ]; then + shift + # Check if the current directory is inside a Git work tree + if git rev-parse --is-inside-work-tree &>/dev/null; then + # If inside a work tree, temporarily unset GIT_WORK_TREE + GIT_WORK_TREE_OLD="$GIT_WORK_TREE" + unset GIT_WORK_TREE + git "$@" + export GIT_WORK_TREE="$GIT_WORK_TREE_OLD" + else + # If not inside a work tree, call git command directly + git "$@" + fi + else + # If it's not a git command, just execute it normally + command "$@" + fi +} + +# Set alias conditionally +#alias git='git_without_work_tree git' + +# Set bare dotfiles repository git environment variables dynamically +function set_git_env_vars() { + # Do nothing unless ~/.cfg exists and is a bare git repo + [[ -d "$HOME/.cfg" ]] || return + git --git-dir="$HOME/.cfg" rev-parse --is-bare-repository &>/dev/null || return + + # Skip if last command was a package manager + if [[ "${(%)${(z)history[1]}}" =~ ^(pacman|yay|apt|dnf|brew|npm|pip|gem|go|cargo) ]]; then + return + fi + + # Only set env vars if not already inside another Git repo + if ! git rev-parse --is-inside-work-tree &>/dev/null; then + export GIT_DIR="$HOME/.cfg" + export GIT_WORK_TREE="$(realpath ~)" + else + unset GIT_DIR + unset GIT_WORK_TREE + fi +} + +# Hook and initial call +function chpwd() { set_git_env_vars } +set_git_env_vars + +# Git Subtrees +function gsp() { + # Config file for subtrees + # + # Format: + # <prefix>;<remote address>;<remote branch> + # # Lines starting with '#' will be ignored + GIT_SUBTREE_FILE="$PWD/.gitsubtrees" + + if [ ! -f "$GIT_SUBTREE_FILE" ]; then + echo "Nothing to do - file <$GIT_SUBTREE_FILE> does not exist." + return + fi + + if ! command -v config &> /dev/null; then + echo "Error: 'config' command not found. Make sure it's available in your PATH." + return + fi + + OLD_IFS=$IFS + IFS=$'\n' + for LINE in $(cat "$GIT_SUBTREE_FILE"); do + + # Skip lines starting with '#'. + if [[ $LINE = \#* ]]; then + continue + fi + + # Parse the current line. + PREFIX=$(echo "$LINE" | cut -d';' -f 1) + REMOTE=$(echo "$LINE" | cut -d';' -f 2) + BRANCH=$(echo "$LINE" | cut -d';' -f 3) + + # Pull from the remote. + echo "Executing: git subtree pull --prefix=$PREFIX $REMOTE $BRANCH" + if git subtree pull --prefix="$PREFIX" "$REMOTE" "$BRANCH"; then + echo "Subtree pull successful for $PREFIX." + else + echo "Error: Subtree pull failed for $PREFIX." + fi + done + + IFS=$OLD_IFS +} + +# Print previous command into a file +getlast () { + fc -nl $((HISTCMD - 1)) +} + +# Copy the current command to a file +copy_command_to_file() { + # Only write the last command if BUFFER is not empty + if [[ -n "$BUFFER" ]]; then + echo "$BUFFER" > ~/command_log.txt # Overwrite with the latest command + else + # If the buffer is empty, remove the previous log file + command rm -f ~/command_log.txt # Optionally remove the log if no command is present + fi +} + +# Display the latest command from the log in the user input +display_latest_command() { + if [[ -f ~/command_log.txt ]]; then + # Read the last command from the log + local last_command + last_command=$(< ~/command_log.txt) + + # Only display if the last command is not empty + if [[ -n "$last_command" ]]; then + BUFFER="$last_command" # Set the BUFFER to the last command + CURSOR=${#BUFFER} # Set the cursor to the end of the command + fi + fi + zle reset-prompt # Refresh the prompt +} + +# Go up a directory +go_up() { + copy_command_to_file # Copy the current command to a file + BUFFER="" # Clear the current command line + cd .. || return # Change directory and return if it fails + display_latest_command # Display the latest command in the user input +} + +# Initialize a variable to store the previous directory +previous_dir="" + +# Function to change directories +go_into() { + copy_command_to_file # Copy the current command to a file + + # Use fzf or another tool to choose the directory + local dir + dir=$( (ls -d */; echo "Go Last directory:") | fzf --height 40% --reverse --tac) # Include previous directory as an option + + if [[ -n "$dir" ]]; then + # Check if the user selected the previous directory + if [[ "$dir" == Previous:* ]]; then + cd - || return # Change to the previous directory + else + cd "${dir%/}" || return # Change directory if a selection is made (remove trailing slash) + fi + + # Save the current directory to previous_dir + previous_dir=$(pwd) # Update previous_dir to current directory after changing + BUFFER="" # Clear the current command line + display_latest_command # Display the last command if available + fi +} + +# Register functions as ZLE widgets +zle -N go_up +zle -N go_into + + +# XDG_GAMES_DIR: +# Path to user-dirs config +USER_DIRS_FILE="$HOME/.config/user-dirs.dirs" + +if [ -f "$USER_DIRS_FILE" ]; then + # Extract directory names from user-dirs config + _dirs=( + ${(f)"$(grep '^XDG_.*_DIR=' "$USER_DIRS_FILE" \ + | cut -d= -f2 \ + | tr -d '"' \ + | sed "s|^\$HOME/||")"} + ) + + _lowercase_count=0 + _total_count=0 + for d in "${_dirs[@]}"; do + [ -n "$d" ] || continue + _total_count=$(( _total_count + 1 )) + case "$d" in + [a-z0-9]*) + _lowercase_count=$(( _lowercase_count + 1 )) + ;; + esac + done + + # Require majority lowercase (≥70%) + if [ "$_total_count" -gt 0 ]; then + _percent=$(( 100 * _lowercase_count / _total_count )) + if [ "$_percent" -ge 70 ]; then + # Ensure the lowercase games directory exists + if [ ! -d "$HOME/games" ]; then + mkdir -p "$HOME/games" + fi + + # Create symbolic link if it doesn't already exist + if [ ! -L "$HOME/Games" ] && [ ! -d "$HOME/Games" ]; then + ln -s "$HOME/games" "$HOME/Games" + fi + + export XDG_GAMES_DIR="$HOME/games" + fi + fi + + unset _dirs _lowercase_count _total_count _percent +fi + + +# Enter directory and list contents +function cd-clear-ls() { + if [ -n "$1" ]; then + builtin cd "$@" 2>/dev/null || { echo "cd: no such file or directory: $1"; return 1; } + else + builtin cd ~ || return 1 + fi + + echo -e "\033[H\033[J" # Clear screen but keep scroll buffer + + if [ "$PWD" != "$HOME" ] && git rev-parse --is-inside-work-tree &>/dev/null; then + ls -a + else + ls + fi +} + +# cd using "up n" as a command up as many directories, example "up 3" +up() { + # default parameter to 1 if non provided + declare -i d=${@:-1} + # ensure given parameter is non-negative. Print error and return if it is + (( $d < 0 )) && (>&2 echo "up: Error: negative value provided") && return 1; + # remove last d directories from pwd, append "/" in case result is empty + cd "$(pwd | sed -E 's;(/[^/]*){0,'$d'}$;;')/"; +} + +# cd into $XDG_CONFIG_HOME/$1 directory +c() { + local root=${XDG_CONFIG_HOME:-~/.config} + local dname="$root/$1" + if [[ ! -d "$dname" ]]; then + return + fi + cd "$dname" +} + +# Make and cd into directory and any parent directories +mkcd () { + if [[ -z "$1" ]]; then + echo "Usage: mkcd <dir>" 1>&2 + return 1 + fi + mkdir -p "$1" + cd "$1" +} + +bak() { + if [[ -e "$1" ]]; then + echo "Found: $1" + mv "${1%.*}"{,.bak} + elif [[ -e "$1.bak" ]]; then + echo "Found: $1.bak" + mv "$1"{.bak,} + fi +} + +back() { + for file in "$@"; do + cp -r "$file" "$file".bak + done +} + +# tre is a shorthand for tree +tre() { + tree -aC -I \ + '.git|.hg|.svn|.tmux|.backup|.vim-backup|.swap|.vim-swap|.undo|.vim-undo|*.bak|tags' \ + --dirsfirst "$@" \ + | less +} + +# switch from/to project/package dir +pkg() { + if [ "$#" -eq 2 ]; then + ln -s "$(readlink -f $1)" "$(readlink -f $2)"/._pkg + ln -s "$(readlink -f $2)" "$(readlink -f $1)"/._pkg + else + cd "$(readlink -f ./._pkg)" + fi +} + +# Prepare C/C++ project for Language Server Protoco +lsp-prep() { + (cd build && cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) \ + && ln -sf build/compile_commands.json +} + +reposize() { + url=`echo $1 \ + | perl -pe 's#(?:https?://github.com/)([\w\d.-]+\/[\w\d.-]+).*#\1#g' \ + | perl -pe 's#git\@github.com:([\w\d.-]+\/[\w\d.-]+)\.git#\1#g' + ` + printf "https://github.com/$url => " + curl -s https://api.github.com/repos/$url \ + | jq '.size' \ + | numfmt --to=iec --from-unit=1024 +} + +# Launch a program in a terminal without getting any output, +# and detach the process from terminal +# (can then close the terminal without terminating process) +-echo() { + "$@" &> /dev/null & disown +} + +# Reload shell +function reload() { + local compdump_files="$ZDOTDIR/.zcompdump*" + + if ls $compdump_files &> /dev/null; then + rm -f $compdump_files + fi + + exec $SHELL -l +} + +#pom() { +# local -r HOURS=${1:?} +# local -r MINUTES=${2:-0} +# local -r POMODORO_DURATION=${3:-25} +# +# bc <<< "(($HOURS * 60) + $MINUTES) / $POMODORO_DURATION" +#} + +#mnt() { +# local FILE="/mnt/external" +# if [ ! -z $2 ]; then +# FILE=$2 +# fi +# +# if [ ! -z $1 ]; then +# sudo mount "$1" "$FILE" -o rw +# echo "Device in read/write mounted in $FILE" +# fi +# +# if [ $# = 0 ]; then +# echo "You need to provide the device (/dev/sd*) - use lsblk" +# fi +#} +# +#umnt() { +# local DIRECTORY="/mnt" +# if [ ! -z $1 ]; then +# DIRECTORY=$1 +# fi +# MOUNTED=$(grep $DIRECTORY /proc/mounts | cut -f2 -d" " | sort -r) +# cd "/mnt" +# sudo umount $MOUNTED +# echo "$MOUNTED unmounted" +#} + +mntmtp() { + local DIRECTORY="$HOME/mnt" + if [ ! -z $2 ]; then + local DIRECTORY=$2 + fi + if [ ! -d $DIRECTORY ]; then + mkdir $DIRECTORY + fi + + if [ ! -z $1 ]; then + simple-mtpfs --device "$1" "$DIRECTORY" + echo "MTPFS device in read/write mounted in $DIRECTORY" + fi + + if [ $# = 0 ]; then + echo "You need to provide the device number - use simple-mtpfs -l" + fi +} + +umntmtp() { + local DIRECTORY="$HOME/mnt" + if ; then + DIRECTORY=$1 + fi + cd $HOME + umount $DIRECTORY + echo "$DIRECTORY with mtp filesystem unmounted" +} +duckduckgo() { + lynx -vikeys -accept_all_cookies "https://lite.duckduckgo.com/lite/?q=$@" +} + +wikipedia() { + lynx -vikeys -accept_all_cookies "https://en.wikipedia.org/wiki?search=$@" +} + +#function filesize() { +# # Check if 'du' supports the -b option, which provides sizes in bytes. +# if du -b /dev/null > /dev/null 2>&1; then +# local arg=-sbh; # If supported, use -sbh options for 'du'. +# else +# local arg=-sh; # If not supported, use -sh options for 'du'. +# fi +# +# # Check if no arguments are provided. +# if [ "$#" -eq 0 ]; then +# # Calculate and display sizes for all files and directories in cwd. +# du $arg ./* +# else +# # Calculate and display sizes for the specified files and directories. +# du $arg -- "$@" +# fi +#} +# + +fgl() { + git log --graph --color=always \ + --format="%C(auto)%h%d %s %C(black)%C(bold)%cr" "$@" | + fzf --ansi --no-sort --reverse --tiebreak=index --bind=ctrl-s:toggle-sort \ + --bind "ctrl-m:execute: + (grep -o '[a-f0-9]\{7\}' | head -1 | + xargs -I % sh -c 'git show --color=always % | less -R') << 'FZF-EOF' + {} +FZF-EOF" +} + +fgb() { + local branches branch + branches=$(git --no-pager branch -vv) && + branch=$(echo "$branches" | fzf +m) && + git checkout $(echo "$branch" | awk '{print $1}' | sed "s/.* //") +} + +# +--------+ +# | Pacman | +# +--------+ + +# TODO can improve that with a bind to switch to what was installed +fpac() { + pacman -Slq | fzf --multi --reverse --preview 'pacman -Si {1}' | xargs -ro sudo pacman -S +} + +fyay() { + yay -Slq | fzf --multi --reverse --preview 'yay -Si {1}' | xargs -ro yay -S +} + +# +------+ +# | tmux | +# +------+ + +fmux() { + prj=$(find $XDG_CONFIG_HOME/tmuxp/ -execdir bash -c 'basename "${0%.*}"' {} ';' | sort | uniq | nl | fzf | cut -f 2) + echo $prj + [ -n "$prj" ] && tmuxp load $prj +} + +# ftmuxp - propose every possible tmuxp session +ftmuxp() { + if [[ -n $TMUX ]]; then + return + fi + + # get the IDs + ID="$(ls $XDG_CONFIG_HOME/tmuxp | sed -e 's/\.yml$//')" + if [[ -z "$ID" ]]; then + tmux new-session + fi + + create_new_session="Create New Session" + + ID="${create_new_session}\n$ID" + ID="$(echo $ID | fzf | cut -d: -f1)" + + if [[ "$ID" = "${create_new_session}" ]]; then + tmux new-session + elif [[ -n "$ID" ]]; then + # Change name of urxvt tab to session name + printf '\033]777;tabbedx;set_tab_name;%s\007' "$ID" + tmuxp load "$ID" + fi +} + +# ftmux - help you choose tmux sessions +ftmux() { + if [[ ! -n $TMUX ]]; then + # get the IDs + ID="`tmux list-sessions`" + if [[ -z "$ID" ]]; then + tmux new-session + fi + create_new_session="Create New Session" + ID="$ID\n${create_new_session}:" + ID="`echo $ID | fzf | cut -d: -f1`" + if [[ "$ID" = "${create_new_session}" ]]; then + tmux new-session + elif [[ -n "$ID" ]]; then + printf '\033]777;tabbedx;set_tab_name;%s\007' "$ID" + tmux attach-session -t "$ID" + else + : # Start terminal normally + fi + fi +} + +# +-------+ +# | Other | +# +-------+ + +# List install files for dotfiles +fdot() { + file=$(find "$DOTFILES/install" -exec basename {} ';' | sort | uniq | nl | fzf | cut -f 2) + [ -n "$file" ] && "$EDITOR" "$DOTFILES/install/$file" +} + +# List projects +fwork() { + result=$(find ~/workspace/* -type d -prune -exec basename {} ';' | sort | uniq | nl | fzf | cut -f 2) + [ -n "$result" ] && cd ~/workspace/$result +} + +# Open pdf with Zathura +fpdf() { + result=$(find -type f -name '*.pdf' | fzf --bind "ctrl-r:reload(find -type f -name '*.pdf')" --preview "pdftotext {} - | less") + [ -n "$result" ] && nohup zathura "$result" &> /dev/null & disown +} + +# Open epubs with Zathura +fepub() { + result=$(find -type f -name '*.epub' | fzf --bind "ctrl-r:reload(find -type f -name '*.epub')") + [ -n "$result" ] && nohup zathura "$result" &> /dev/null & disown +} + +# Search and find directories in the dir stack +fpop() { + # Only work with alias d defined as: + + # alias d='dirs -v' + # for index ({1..9}) alias "$index"="cd +${index}"; unset index + + d | fzf --height="20%" | cut -f 1 | source /dev/stdin +} + +#ip() { +# emulate -LR zsh +# +# if [[ $1 == 'get' ]]; then +# res=$(curl -s ipinfo.io/ip) +# echo -n $res | xsel --clipboard +# echo "copied $res to clipboard" +# # only run ip if it exists +# elif (( $+commands[ip] )); then +# command ip $* +# fi +#} + +ssh-create() { + if [ ! -z "$1" ]; then + ssh-keygen -f $HOME/.ssh/$1 -t rsa -N '' -C "$1" + chmod 700 $HOME/.ssh/$1* + fi +} + +guest() { + local guest="$1" + shift + + local port + if [[ "$#" -ge 2 && "${@: -1}" =~ ^[0-9]+$ ]]; then + port="${@: -1}" + set -- "${@:1:$(($#-1))}" + fi + + if [[ -z "$guest" || "$#" -lt 1 ]]; then + echo "Send file(s) or directories to remote machine" + echo "Usage: guest <guest-alias> <file-or-directory>... [port]" + return 1 + fi + + # Auto-detect port + if [[ -z "$port" ]]; then + if nc -z localhost 22220 2>/dev/null; then + port=22220 + elif nc -z localhost 22 2>/dev/null; then + port=22 + else + echo "No known SSH port (22220 or 22) is open. Specify a port manually." + return 1 + fi + fi + + for src in "$@"; do + src="${src/#\~/$HOME}" + if [[ ! -e "$src" ]]; then + echo "Error: '$src' does not exist." + continue + fi + + local abs_path dest_dir rel_dir rsync_src rsync_dest + + abs_path=$(realpath "$src") + rel_dir="${abs_path#$HOME/}" + dest_dir=$(dirname "$rel_dir") + + # Ensure target dir exists remotely + ssh -p "$port" "$guest" "mkdir -p ~/$dest_dir" + + if [[ -d "$src" ]]; then + # Add trailing slash to copy contents instead of nesting the dir + rsync_src="${src%/}/" + rsync_dest="~/$rel_dir/" + else + rsync_src="$src" + rsync_dest="~/$dest_dir/" + fi + + echo "Sending '$src' to '$guest:$rsync_dest'..." + rsync -avz -e "ssh -p $port" "$rsync_src" "$guest:$rsync_dest" + done +} +historystat() { + history 0 | awk '{print $2}' | sort | uniq -c | sort -n -r | head +} + +promptspeed() { + for i in $(seq 1 10); do /usr/bin/time zsh -i -c exit; done +} + + +matrix() { + local lines=$(tput lines) + cols=$(tput cols) + + # Check if tmux is available + if command -v tmux > /dev/null; then + # Save the current status setting + local status_setting=$(tmux show -g -w -v status) + + # Turn off tmux status + tmux set -g status off + else + echo "tmux is not available. Exiting." + return 1 + fi + + # Function to restore terminal state + restore_terminal() { + # Clear the screen + clear + + # Bring back tmux status to its original setting + if command -v tmux > /dev/null; then + tmux set -g status "$status_setting" + fi + } + + trap 'restore_terminal' INT + + awkscript=' + { + letters="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()" + lines=$1 + random_col=$3 + c=$4 + letter=substr(letters,c,1) + cols[random_col]=0; + for (col in cols) { + line=cols[col]; + cols[col]=cols[col]+1; + printf "\033[%s;%sH\033[2;32m%s", line, col, letter; + printf "\033[%s;%sH\033[1;37m%s\033[0;0H", cols[col], col, letter; + if (cols[col] >= lines) { + cols[col]=0; + } + } + } + ' + + echo -e "\e[1;40m" + clear + + while :; do + echo $lines $cols $(( $RANDOM % $cols)) $(( $RANDOM % 72 )) + sleep 0.05 + done | awk "$awkscript" + + # Restore terminal state + restore_terminal +} + +## Reload shell +function reload() { + local compdump_files="$ZDOTDIR/.zcompdump*" + + if ls $compdump_files &> /dev/null; then + rm -f $compdump_files + fi + + exec $SHELL -l +} +## Generate a secure password +function passgen() { + LC_ALL=C tr -dc ${1:-"[:graph:]"} < /dev/urandom | head -c ${2:-20} +} +## Encode/Decode string using base64 +function b64e() { + echo "$@" | base64 +} + +function b64d() { + echo "$@" | base64 -D +} +# Search through all man pages +function fman() { + man -k . | fzf -q "$1" --prompt='man> ' --preview $'echo {} | tr -d \'()\' | awk \'{printf "%s ", $2} {print $1}\' | xargs -r man' | tr -d '()' | awk '{printf "%s ", $2} {print $1}' | xargs -r man +} +# Back up a file. Usage "backupthis <filename>" +backupthis() { + cp -riv $1 ${1}-$(date +%Y%m%d%H%M).backup; +} + +# Spawn a clone of current terminal +putstate () { + declare +x >~/environment.tmp + declare -x >>~/environment.tmp + echo cd "$PWD" >>~/environment.tmp +} + +getstate () { + . ~/environment.tmp +} + + +# Tmux layout +openSession () { + tmux split-window -h -t + tmux split-window -v -t + tmux resize-pane -U 5 +} + +# archive compress +compress() { + if [[ -n "$1" ]]; then + local file=$1 + shift + case "$file" in + *.tar ) tar cf "$file" "$*" ;; + *.tar.bz2 ) tar cjf "$file" "$*" ;; + *.tar.gz ) tar czf "$file" "$*" ;; + *.tgz ) tar czf "$file" "$*" ;; + *.zip ) zip "$file" "$*" ;; + *.rar ) rar "$file" "$*" ;; + * ) tar zcvf "$file.tar.gz" "$*" ;; + esac + else + echo 'usage: compress <foo.tar.gz> ./foo ./bar' + fi +} + +extract() { + if [[ -f "$1" ]] ; then + local filename + filename=$(basename "$1") + local foldername="${filename%%.*}" + local fullpath + fullpath=$(perl -e 'use Cwd "abs_path";print abs_path(shift)' "$1") + local didfolderexist=false + + if [[ -d "$foldername" ]]; then + didfolderexist=true + read -p "$foldername already exists, do you want to overwrite it? (y/n) " -n 1 + echo + if [[ "$REPLY" =~ ^[Nn]$ ]]; then + return + fi + fi + + mkdir -p "$foldername" && cd "$foldername" || return + + case "$1" in + *.tar.bz2) tar xjf "$fullpath" ;; + *.tar.gz) tar xzf "$fullpath" ;; + *.tar.xz) tar Jxf "$fullpath" ;; + *.tar.Z) tar xzf "$fullpath" ;; + *.tar) tar xf "$fullpath" ;; + *.taz) tar xzf "$fullpath" ;; + *.tb2) tar xjf "$fullpath" ;; + *.tbz) tar xjf "$fullpath" ;; + *.tbz2) tar xjf "$fullpath" ;; + *.tgz) tar xzf "$fullpath" ;; + *.txz) tar Jxf "$fullpath" ;; + *.rar) unrar x -o+ "$fullpath" >/dev/null ;; + *.zip) unzip -o "$fullpath" ;; + *) + echo "'$1' cannot be extracted via extract()" \ + && cd .. \ + && ! "$didfolderexist" \ + && rm -r "$foldername" + ;; + esac + else + echo "'$1' is not a valid file" + fi +} + +ports() { + local result + result=$(sudo netstat -tulpn | grep LISTEN) + echo "$result" | fzf +} + +trash() { + case "$1" in + --list) + ls -A1 ~/.local/share/Trash/files/ + ;; + --empty) + ls -A1 ~/.local/share/Trash/files/ && \rm -rfv ~/.local/share/Trash/files/* + ;; + --restore) + gio trash --restore "$(gio trash --list | fzf | cut -f 1)" + ;; + --delete) + trash_files=$(ls -A ~/.local/share/Trash/files/ | fzf --multi); echo $trash_files | xargs -I {} rm -rf ~/.local/share/Trash/files/{} + ;; + *) + gio trash "$@" + ;; + esac +} + +what() { + type "$1" + echo "$PATH" +} + +shutdown() { + if [ "$#" -eq 0 ]; then + sudo /sbin/shutdown -h now + else + sudo /sbin/shutdown -h "$@" + fi +} + +windowManagerName () { + local window=$( + xprop -root -notype + ) + + local identifier=$( + echo "${window}" | + awk '$1=="_NET_SUPPORTING_WM_CHECK:"{print $5}' + ) + + local attributes=$( + xprop -id "${identifier}" -notype -f _NET_WM_NAME 8t + ) + + local name=$( + echo "${attributes}" | + grep "_NET_WM_NAME = " | + cut --delimiter=' ' --fields=3 | + cut --delimiter='"' --fields=2 + ) + + echo "${name}" +} + +logout() { + local wm + wm="$(windowManagerName)" + if [[ -n "$wm" ]]; then + echo "Logging out by killing window manager: $wm" + pkill "$wm" + else + echo "No window manager detected!" >&2 + fi +} + +# Gentoo +emg() { + if [[ -z "$1" ]]; then + echo "Usage: emg [USE_FLAGS] package [package...]" + return 1 + fi + + if [[ "$1" =~ ^[^-].* ]]; then + local use_flags="$1" + shift + sudo USE="$use_flags" emerge -av "$@" + else + sudo emerge -av "$@" + fi +} + +# Remove command from history +forget () { # Accepts one history line number as argument or search term + if [[ -z "$1" ]]; then + echo "Usage: hist <history_number> | hist -s <search_term>" + return 1 + fi + + if [[ "$1" == "-s" ]]; then + if [[ -z "$2" ]]; then + echo "Usage: hist -s <search_term>" + return 1 + fi + + local search_term="$2" + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + LC_ALL=C sed -i "/${search_term}/d" "$HISTFILE" # GNU sed + else + LC_ALL=C sed -i '' "/${search_term}/d" "$HISTFILE" # BSD/macOS sed + fi + + fc -R "$HISTFILE" + echo "Deleted all history entries matching '$search_term'." + else + local num=$1 + local cmd=$(fc -ln $num $num 2>/dev/null) + + if [[ -z "$cmd" ]]; then + echo "No history entry found for index $num" + return 1 + fi + + history -d $num + + local escaped_cmd=$(echo "$cmd" | sed 's/[\/&]/\\&/g') + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + LC_ALL=C sed -i "/${escaped_cmd}/d" "$HISTFILE" + else + LC_ALL=C sed -i '' "/${escaped_cmd}/d" "$HISTFILE" + fi + + fc -R "$HISTFILE" + echo "Deleted '$cmd' from history." + fi +} + +# Remove hist command itself +remove_hist_command() { + [[ $1 != 'hist '* ]] +} + +remove_hist_command + + +search() { + # Search for a pattern in the specified directory (non-recursive). + dir="${1:-.}" + ls -1 "$dir" | grep -i "$2" +} + +deepsearch() { + # Perform a recursive search for a pattern in the specified directory. + dir="${1:-.}" + find "$dir" -iname "$2" +} + +notes() { + local base_dir="$HOME/documents/main/" + + if [[ -z "$1" ]]; then + # No argument → cd to notes directory + cd "$base_dir" || return + return + fi + + local target="$1" # The argument itself + + # Use find to check if the file exists anywhere in the base directory + local found_files=($(find "$base_dir" -type f -name "$target")) + + if [[ ${#found_files[@]} -eq 1 ]]; then + # Only one match found, open it directly + $EDITOR "${found_files[0]}" + elif [[ ${#found_files[@]} -gt 1 ]]; then + # Multiple files found, prompt the user to select one + echo "Multiple files found for '$target'. Please choose one:" + PS3="Please enter a number to select a file (1-${#found_files[@]}): " + select selected_file in "${found_files[@]}"; do + if [[ -n "$selected_file" ]]; then + $EDITOR "$selected_file" + break + else + echo "Invalid selection, try again." + fi + done + else + # If no match found, search for a directory + local found_dir=$(find "$base_dir" -type d -name "$target" -print -quit) + + if [[ -n "$found_dir" ]]; then + # Directory found, cd into it + cd "$found_dir" || return + else + # If no match found, create the file and open it + local full_target="$base_dir/$target" + mkdir -p "$(dirname "$full_target")" + $EDITOR "$full_target" + fi + fi +} + +# Enable tab completion for files and directories +_notes_complete() { + local base_dir="$HOME/documents/main" + compadd -o nospace -- $(find "$base_dir" -type f -o -type d -printf '%P\n') +} + +compdef _notes_complete notes + + +ship() { + local binary_dir="$HOME/.local/share" + local bin_symlink_dir="$HOME/.local/bin" + local project_dirs=( + "$HOME/projects/" + "$HOME/src/" + "$HOME/src/site/" + ) + + mkdir -p "$binary_dir" "$bin_symlink_dir" + + local project_dir="" + + if [[ -n "$1" ]]; then + # Project name specified + for dir in "${project_dirs[@]}"; do + if [[ -d "$dir/$1" ]]; then + project_dir="$dir/$1" + break + fi + done + + if [[ -z "$project_dir" ]]; then + echo "Project '$1' not found." + return 1 + fi + else + # No argument: pick latest edited + local bin_file + bin_file=$(find "${project_dirs[@]}" -type f -name "Cargo.toml" -exec stat --format="%Y %n" {} \; 2>/dev/null | sort -nr | head -n1 | cut -d' ' -f2-) + + if [[ -z "$bin_file" ]]; then + echo "No Cargo.toml found." + return 1 + fi + + project_dir=$(dirname "$bin_file") + fi + + cd "$project_dir" || return + echo "Building project in $project_dir..." + + # Build it + cargo build --release || { echo "Build failed"; return 1; } + + # Assume binary has same name as project dir + local binary_name + binary_name=$(basename "$project_dir") + local built_binary="target/release/$binary_name" + + if [[ -x "$built_binary" ]]; then + echo "Copying $built_binary to $binary_dir/$binary_name" + cp "$built_binary" "$binary_dir/$binary_name" + + # Create/Update symlink + local symlink_path="$bin_symlink_dir/$binary_name" + ln -sf "$binary_dir/$binary_name" "$symlink_path" + + echo "Binary is now at: $binary_dir/$binary_name" + echo "Symlink created at: $symlink_path" + else + echo "Built binary not found: $built_binary" + echo "You may need to manually specify the output binary." + fi +} + + +forge() { + + local install=no + local usage="Usage: forge [--install]" + + # Handle --install flag + if [[ "$1" == "--install" ]]; then + install=yes + shift + elif [[ "$1" == "-h" || "$1" == "--help" ]]; then + echo "$usage" + return 0 + fi + + if [[ -f "CMakeLists.txt" ]]; then + echo "📦 CMake project detected" + [[ ! -d build ]] && mkdir build + cmake -B build -DCMAKE_BUILD_TYPE=Release || return 1 + cmake --build build || return 1 + [[ "$install" == "yes" ]] && sudo cmake --install build + + elif [[ -f "meson.build" ]]; then + echo "📦 Meson project detected" + if [[ ! -d build ]]; then + meson setup build || return 1 + fi + ninja -C build || return 1 + [[ "$install" == "yes" ]] && sudo ninja -C build install + + elif [[ -f "Makefile" ]]; then + echo "📦 Makefile project detected" + # Try `make all`, fallback to `make` if `all` fails + if make -q all 2>/dev/null; then + make all || return 1 + else + make || return 1 + fi + [[ "$install" == "yes" ]] && sudo make install + + else + echo "❌ No supported build system found." + return 1 + fi +} + +# Windows Path: +windows_home() { + for dir in /mnt/windows/Users/*(N); do + base=${dir:t} # `:t` is zsh's "tail" = basename + if [[ -d $dir && ! $base =~ ^(All Users|Default|Default User|Public|nx|desktop.ini)$ ]]; then + echo "$dir" + return 0 + fi + done + return 1 +} + +if winhome_path=$(windows_home); then + hash -d winhome="$winhome_path" +fi + +# Allow nnn filemanager to cd on quit +nnn() { + declare -x +g NNN_TMPFILE=$(mktemp --tmpdir $0.XXXX) + trap "command rm -f $NNN_TMPFILE" EXIT + =nnn $@ + [ -s $NNN_TMPFILE ] && source $NNN_TMPFILE +} diff --git a/common/config/zsh/user/options.zsh b/common/config/zsh/user/options.zsh new file mode 100644 index 0000000..99840d7 --- /dev/null +++ b/common/config/zsh/user/options.zsh @@ -0,0 +1,66 @@ +# Recursion limits +FUNCNEST=999 + +DISABLE_MAGIC_FUNCTIONS=true + +# Enable various options for Zsh behavior +setopt interactive_comments # Allow comments to appear in interactive mode +unsetopt BEEP # Disable the system beep (to prevent annoying beeps) +setopt extendedglob # Enable extended globbing for complex pattern matching +setopt nomatch # Prevent errors when a glob pattern doesn't match any files +setopt notify # Notify when background jobs complete +setopt completeinword # Allow tab completion within words +setopt prompt_subst # Allow prompt variables to be substituted + +# Enable automatic directory navigation +setopt autocd # Automatically change to a directory if the directory name is typed alone +setopt AUTO_PUSHD # Save more directory history, and use "cd -" with tab completion + +# Hide history of commands starting with a space +setopt histignorespace # Do not save commands that start with a space in the history + + +# --- Detect terminal control characters and behavior --- + +# Set these to true/false to run on every new tmux/terms +: ${CHECK_ON_TMUX_CHANGES:=false} +: ${CHECK_ON_NEW_INSTANCES:=false} + +# Fast terminal fingerprinting for optimizing prompt rendering +function initialize_terminal_fingerprint() { + # --- Fast terminal fingerprint --- + TERM_BASIC="$TERM-$COLORTERM" + TERM_TMUX="" + [[ "$CHECK_ON_TMUX_CHANGES" == "true" && -n "$TMUX" ]] && TERM_TMUX="-tmux$TMUX_PANE" + TERM_INSTANCE="" + [[ "$CHECK_ON_NEW_INSTANCES" == "true" ]] && TERM_INSTANCE="-$$" + # Combine fingerprint parts only if they're non-empty (faster than function call) + CURRENT_TERM_FINGERPRINT="${TERM_BASIC}${TERM_TMUX}${TERM_INSTANCE}" + # Only run detection if terminal has changed (single comparison) + if [[ "$CURRENT_TERM_FINGERPRINT" != "$LAST_TERM_FINGERPRINT" ]]; then + export LAST_TERM_FINGERPRINT="$CURRENT_TERM_FINGERPRINT" + # Fast reset + export CTRL_C_SIGINT=false + export CTRL_V_PASTE=false + # Fast SIGINT check (single command, no pipes) + INTR_CHAR=$(stty -a 2>/dev/null | sed -n 's/.*intr = \([^;]*\);.*/\1/p' | tr -d ' ') + [[ "$INTR_CHAR" == "^C" ]] && export CTRL_C_SIGINT=true + # Check if Ctrl+V is bound to lnext terminal function + LNEXT_CHAR=$(stty -a 2>/dev/null | sed -n 's/.*lnext = \([^;]*\);.*/\1/p' | tr -d ' ') + # If lnext is NOT ^V, then Ctrl+V might work as paste + if [[ "$LNEXT_CHAR" != "^V" ]]; then + # Check if clipboard tools exist + if [[ -n "$WAYLAND_DISPLAY" && -x "$(command -v wl-paste)" ]]; then + export CTRL_V_PASTE=true + elif [[ -n "$DISPLAY" && -x "$(command -v xclip)" ]]; then + export CTRL_V_PASTE=true + fi + fi + # Print status only if debug is enabled + [[ -n "$DEBUG_TERM_DETECT" ]] && echo "Terminal: CTRL_C_SIGINT=$CTRL_C_SIGINT CTRL_V_PASTE=$CTRL_V_PASTE" + fi +} + +# Initialize terminal fingerprint on startup +initialize_terminal_fingerprint + diff --git a/common/config/zsh/user/prompt.zsh b/common/config/zsh/user/prompt.zsh new file mode 100644 index 0000000..c55a835 --- /dev/null +++ b/common/config/zsh/user/prompt.zsh @@ -0,0 +1,679 @@ +#!/bin/zsh + +########## Prompt(s) ########## + +# Autoload necessary functions for vcs_info and coloring +autoload -Uz vcs_info +autoload -Uz add-zsh-hook +autoload -U colors && colors + +# Enable prompt substitution +setopt prompt_subst + +# Display git branch status and color +precmd_vcs_info() { vcs_info } + +# Add vcs_info to precmd functions +precmd_functions+=( precmd_vcs_info ) + +# Manipulates cursor position: moves down by 2 lines, saves position, and restores cursor after an operation. +terminfo_down_sc=$terminfo[cud1]$terminfo[cuu1]$terminfo[sc]$terminfo[cud1] + +# Track last executed command for exit code display +typeset -g _last_executed_command="" +typeset -g _cmd_start_time=0 +typeset -g _cmd_end_time=0 +typeset -g _cmd_duration=0 +typeset -g _spinner_idx=0 +typeset -ga _spinner_frames=('⣾' '⣽' '⣻' '⢿' '⡿' '⣟' '⣯' '⣷') +typeset -g _cmd_is_running=0 +typeset -g _show_spinner=0 +typeset -g _SPINNER_DELAY=5 # Only show spinner after 5 seconds +typeset -g _FINISHED_DELAY=10 # Only show finished message after 10 seconds + +# Register the ZLE widget for spinner updates - do this early +zle -N update_spinner + +# Cache git information to avoid repeated expensive operations +typeset -g _git_cached_info="" +typeset -g _git_cache_timestamp=0 +typeset -g _git_cache_lifetime=2 # seconds before cache expires + +# Calculate how much space is available for the prompt components +function available_space() { + local width=${COLUMNS:-80} + echo $width +} + +# Check if we need to abbreviate git info +function need_to_abbreviate_git() { + local available=$(available_space) + local vi_mode_len=13 # Length of "-- INSERT --" + local prompt_base_len=20 # Base prompt elements length + local path_len=${#${PWD/#$HOME/\~}} + local git_full_len=0 + + # Try to estimate git info length if available + if git rev-parse --is-inside-work-tree &>/dev/null; then + local branch=$(git symbolic-ref --short HEAD 2>/dev/null) + git_full_len=${#branch} + + # Add length for status indicators + if [[ -n "$(git status --porcelain)" ]]; then + # Rough estimate for status text + git_full_len=$((git_full_len + 20)) + fi + fi + + # Calculate total space needed + local total_needed=$((vi_mode_len + prompt_base_len + path_len + git_full_len)) + + # Determine if we need to abbreviate + if [[ $total_needed -gt $available ]]; then + return 0 # Need to abbreviate + else + return 1 # Don't need to abbreviate + fi +} + +# Custom git branch coloring based on status +git_branch_test_color() { + local now=$(date +%s) + local cache_age=$((now - _git_cache_timestamp)) + + # Use cached value if available and not expired + if [[ -n "$_git_cached_info" && $cache_age -lt $_git_cache_lifetime ]]; then + echo "$_git_cached_info" + return + fi + + local ref=$(git symbolic-ref --short HEAD 2> /dev/null) + if [ -n "${ref}" ]; then + if [ -n "$(git status --porcelain)" ]; then + local gitstatuscolor='%F{green}' + else + local gitstatuscolor='%F{82}' + fi + _git_cached_info="${gitstatuscolor}${ref}" + _git_cache_timestamp=$now + echo "$_git_cached_info" + else + _git_cached_info="" + _git_cache_timestamp=$now + echo "" + fi +} + +# Git branch with dynamic abbreviation +git_branch_dynamic() { + local now=$(date +%s) + local cache_age=$((now - _git_cache_timestamp)) + + # Only query git if cache is expired + if [[ $cache_age -ge $_git_cache_lifetime ]]; then + local ref=$(git symbolic-ref --short HEAD 2> /dev/null) + if [ -n "${ref}" ]; then + if need_to_abbreviate_git; then + # Abbreviated version for small terminals + case "${ref}" in + "main") _git_cached_info="m" ;; + "master") _git_cached_info="m" ;; + "development") _git_cached_info="d" ;; + "develop") _git_cached_info="d" ;; + "feature/"*) _git_cached_info="f/${ref#feature/}" | cut -c 1-4 ;; + "release/"*) _git_cached_info="r/${ref#release/}" | cut -c 1-4 ;; + "hotfix/"*) _git_cached_info="h/${ref#hotfix/}" | cut -c 1-4 ;; + *) _git_cached_info="${ref}" | cut -c 1-5 ;; # Truncate to first 5 chars for other branches + esac + else + # Full branch name when there's room + _git_cached_info="${ref}" + fi + _git_cache_timestamp=$now + echo "$_git_cached_info" + else + _git_cached_info="" + _git_cache_timestamp=$now + echo "" + fi + else + echo "$_git_cached_info" + fi +} + +# VCS info styles (e.g., git) +zstyle ':vcs_info:*' check-for-changes true +zstyle ':vcs_info:*' enable git + +# Dynamically configure vcs_info formats based on available space +function configure_vcs_styles() { + if need_to_abbreviate_git; then + # Abbreviated versions + zstyle ':vcs_info:*' stagedstr ' +%F{15}s%f' + zstyle ':vcs_info:*' unstagedstr ' -%F{15}u%f' + else + # Full versions + zstyle ':vcs_info:*' stagedstr ' +%F{15}staged%f' + zstyle ':vcs_info:*' unstagedstr ' -%F{15}unstaged%f' + fi + + zstyle ':vcs_info:*' actionformats '%F{5}%F{2}%b%F{3}|%F{1}%a%F{5}%f ' + zstyle ':vcs_info:*' formats '%F{208} '$'\uE0A0'' %f$(git_branch_test_color)%f%F{76}%c%F{3}%u%f ' + zstyle ':vcs_info:git*+set-message:*' hooks git-untracked git-dynamic +} + +# Show "untracked" status in git - with conditional abbreviation ++vi-git-untracked() { + if [[ $(git rev-parse --is-inside-work-tree 2> /dev/null) == 'true' ]] && \ + git status --porcelain | grep '??' &> /dev/null ; then + + if need_to_abbreviate_git; then + hook_com[unstaged]+='%F{196} !%f%F{15}u%f' + else + hook_com[unstaged]+='%F{196} !%f%F{15}untracked%f' + fi + fi +} + +# Dynamic git branch hook ++vi-git-dynamic() { + hook_com[branch]=$(git_branch_dynamic) +} + +# SSH info with conditional abbreviation +ssh_name() { + if [[ -n $SSH_CONNECTION ]]; then + local ssh_info + + if need_to_abbreviate_git; then + # Abbreviated SSH info + ssh_info="ssh:%F{green}%n$nc%f" + else + ssh_info="ssh:%F{green}%n$nc%f" + if [[ -n $SSH_CONNECTION ]]; then + local ip_address + ip_address=$(echo $SSH_CONNECTION | awk '{print $3}') + ssh_info="$ssh_info@%F{green}$ip_address%f" + fi + fi + echo " ${ssh_info}" + fi +} + +# Job names (for job control) with conditional abbreviation +function job_name() { + job_name="" + job_length=0 + local available=$(available_space) + + # Only show jobs if we have reasonable space + if [ "${available}" -gt 60 ]; then + local job_count=$(jobs | wc -l) + if [ "${job_count}" -gt 0 ]; then + if need_to_abbreviate_git; then + job_name+="%F{green}j:${job_count}%f" + else + local title_jobs="jobs:" + job_name="${title_jobs}" + job_length=$((${available}-70)) + [ "${job_length}" -lt "0" ] && job_length=0 + + if [ "${job_length}" -gt 0 ]; then + job_name+="%F{green}$(jobs | grep + | tr -s " " | cut -d " " -f 4- | cut -b 1-${job_length} | sed "s/\(.*\)/\1/")%f" + else + job_name+="%F{green}${job_count}%f" + fi + fi + fi + fi + + echo "${job_name}" +} + +# Check if we should show the spinner based on elapsed time +function should_show_spinner() { + if [[ $_cmd_is_running -eq 1 ]]; then + local current_time=$(date +%s) + local elapsed=$((current_time - _cmd_start_time)) + + # Show spinner only after delay threshold + if [[ $elapsed -ge $_SPINNER_DELAY ]]; then + _show_spinner=1 + return 0 # Yes, show spinner + fi + fi + + _show_spinner=0 + return 1 # No, don't show spinner +} + +# Update spinner animation - simplified version +function update_spinner() { + # This function is now just a ZLE widget placeholder + # The actual spinner updates happen in the TRAPALRM handler + : +} + +# Start spinner timer when command runs longer than threshold +function start_spinner_timer() { + _spinner_idx=0 + _cmd_is_running=1 + _show_spinner=0 # Start with spinner hidden until delay passes + + # Set up the TRAPALRM for periodic updates - CRITICAL FIX + TMOUT=0.5 # Update spinner every 0.5 seconds + + # Define TRAPALRM function - this is key to the spinner working + TRAPALRM() { + if [[ $_cmd_is_running -eq 1 ]]; then + local current_time=$(date +%s) + local elapsed=$((current_time - _cmd_start_time)) + + # Show spinner only after delay threshold + if [[ $elapsed -ge $_SPINNER_DELAY ]]; then + _show_spinner=1 + _spinner_idx=$(( (_spinner_idx + 1) % ${#_spinner_frames[@]} )) + + # Force prompt refresh - critical for updating the spinner + if [[ -o zle ]]; then + zle reset-prompt 2>/dev/null || true + zle -R + fi + fi + fi + } +} + +# Stop spinner when command finishes +function stop_spinner_timer() { + _cmd_is_running=0 + _show_spinner=0 + + # Disable the alarm trap and timer + TRAPALRM() { : } + TMOUT=0 + + # Force prompt refresh to clear spinner + if [[ -o zle ]]; then + zle reset-prompt 2>/dev/null || true + zle -R + fi +} + +# Format time in a human-readable way +function format_time() { + local seconds=$1 + local result="" + + # Format time as hours:minutes:seconds for long durations + if [[ $seconds -ge 3600 ]]; then + local hours=$((seconds / 3600)) + local minutes=$(( (seconds % 3600) / 60 )) + local secs=$((seconds % 60)) + result="${hours}h${minutes}m${secs}s" + elif [[ $seconds -ge 60 ]]; then + local minutes=$((seconds / 60)) + local secs=$((seconds % 60)) + result="${minutes}m${secs}s" + else + result="${seconds}s" + fi + + echo "$result" +} + +# Error code display for RPROMPT with spinner - fixed version +function exit_code_info() { + local exit_code=$? + + # If a command is running and we should show spinner + if [[ $_cmd_is_running -eq 1 && $_show_spinner -eq 1 ]]; then + local spinner=${_spinner_frames[$_spinner_idx]} + local current_time=$(date +%s) + local elapsed=$((current_time - _cmd_start_time)) + echo "%F{yellow}${spinner} ${elapsed}s%f" + return + fi + + # Don't show error code when line editor is active (user is typing) + if [[ -o zle ]]; then + echo "" + return + fi + + # Show command finished message for completed commands that took longer than threshold + if [[ -n "$_last_executed_command" && $_cmd_duration -ge $_FINISHED_DELAY ]]; then + local duration_formatted=$(format_time $_cmd_duration) + + # Show error code along with finished message if there was an error + if [[ $exit_code -ne 0 ]]; then + # Show TSTP (148) as a suspension indicator instead of error + if [[ $exit_code -eq 148 ]]; then + echo "%F{cyan}finished ${duration_formatted}%f %F{yellow}⏸ TSTP%f" + return + fi + + local signal_name="" + # Check if it's a signal + if [[ $exit_code -gt 128 && $exit_code -le 165 ]]; then + local signal_num=$((exit_code - 128)) + signal_name=$(kill -l $signal_num 2>/dev/null) + if [[ -n "$signal_name" ]]; then + signal_name=" ($signal_name)" + fi + fi + + # Return formatted error code with finished message + echo "%F{cyan}finished ${duration_formatted}%f %F{red}✘ $exit_code$signal_name%f" + else + echo "%F{cyan}finished ${duration_formatted}%f %F{green}✓%f" + fi + return + fi + + # Don't show anything for exit code 0 (success) if this is first command + if [[ -z "$_last_executed_command" && $exit_code -eq 0 ]]; then + echo "" + return + fi + + # Show TSTP (148) as a suspension indicator instead of error + if [[ $exit_code -eq 148 ]]; then + echo "%F{yellow}⏸ TSTP%f" + return + fi + + if [[ $exit_code -ne 0 ]]; then + local signal_name="" + + # Check if it's a signal + if [[ $exit_code -gt 128 && $exit_code -le 165 ]]; then + local signal_num=$((exit_code - 128)) + signal_name=$(kill -l $signal_num 2>/dev/null) + if [[ -n "$signal_name" ]]; then + signal_name=" ($signal_name)" + fi + fi + + # Return formatted error code + echo "%F{red}✘ $exit_code$signal_name%f" + else + echo "%F{green}✓%f" # Success indicator + fi +} + +abbreviated_path() { + local full_path="${PWD/#$HOME/~}" # Replace $HOME with ~ + local available=$(available_space) + + # If path is root + if [[ "$full_path" == "/" ]]; then + echo "%F{4}/%f" + return + fi + + # If path is just ~ + if [[ "$full_path" == "~" ]]; then + echo "%F{4}~%f" + return + fi + + # If extremely small terminal, show nothing to avoid breaking prompt + if (( available < 20 )); then + echo "" + return + fi + + # For very narrow terminals, just show the current dir + if (( available < 30 )); then + echo "%F{4}%1~%f" + return + fi + + # For moderately narrow terminals, show last two components + if (( available < 40 )); then + echo "%F{4}%2~%f" + return + fi + + # For wide terminals, show full path + if (( available > 70 )); then + echo "%F{4}${full_path}%f" + return + fi + + # Otherwise, show abbreviated path (e.g. ~/d/p/n) + local parts=("${(s:/:)full_path}") + local result="" + local last_index=${#parts[@]} + + for i in {1..$((last_index - 1))}; do + [[ -n ${parts[i]} ]] && result+="/${parts[i]:0:1}" + done + + result+="/${parts[last_index]}" + echo "%F{4}${result}%f" +} + + +# Prompt variables +user="%n" +at="%F{15}at%{$reset_color%}" +machine="%F{4}%m%{$reset_color%}" +relative_home="%F{4}%~%{$reset_color%}" +carriage_return=""$'\n'"" +empty_line_bottom="" +chevron_right="" +color_reset="%{$(tput sgr0)%}" +color_yellow="%{$(tput setaf 226)%}" +color_blink="%{$(tput blink)%}" +prompt_symbol="$" +dollar_sign="${color_yellow}${color_blink}${prompt_symbol}${color_reset}" +dollar="%(?:%F{2}${dollar_sign}:%F{1}${dollar_sign})" +space=" " +#thin_space=$'\u2009' +thin_space=$'\u202F' +cmd_prompt="%(?:%F{2}${chevron_right} :%F{1}${chevron_right} )" +git_info="\$vcs_info_msg_0_" +v1="%{┌─[%}" +v2="%{]%}" +v3="└──[" +v4="]" +newline=$'\n' + +# Indicate INSERT mode for vi - NEVER truncate this +function insert-mode () { + echo "-- INSERT --" +} + +# Indicate NORMAL mode for vi - NEVER truncate this +function normal-mode () { + echo "-- NORMAL --" +} + +# Vi mode indicator +vi-mode-indicator () { + if [[ ${KEYMAP} == vicmd || ${KEYMAP} == vi-cmd-mode ]]; then + echo -ne '\e[1 q' + vi_mode=$(normal-mode) + elif [[ ${KEYMAP} == main || ${KEYMAP} == viins || ${KEYMAP} == '' ]]; then + echo -ne '\e[5 q' + vi_mode=$(insert-mode) + fi +} + +# Prompt function to ensure the prompt stays on one line, even in narrow terminals +function set-prompt() { + vi-mode-indicator + configure_vcs_styles # Dynamically set vcs styles based on available space + vcs_info # Refresh vcs info with new styles + + local available=$(available_space) + if (( available < 14 )); then + # Extremely narrow terminal — use minimal prompt + PS1="${carriage_return}${dollar}${space}${empty_line_bottom}" + RPROMPT='$(exit_code_info)' + + else + # Path display - always show something for path, but adapt based on space + local path_display="$(abbreviated_path)" + + # Git info - omit entirely if not enough space + local gitinfo="" + if [[ $available -gt 40 ]]; then + gitinfo="${vcs_info_msg_0_}" + fi + + # Jobs info + local jobs=" $(job_name)" + + # SSH info + local sshinfo="$(ssh_name)" + + # Vi mode is priority 1 - ALWAYS show it + mode="%F{145}%{$terminfo_down_sc$vi_mode$terminfo[rc]%f%}" + + # Right prompt for error codes or spinner + RPROMPT='$(exit_code_info)' + + PS1="${newline}${v1}${user}${v2} ${path_display}${gitinfo}${jobs}${sshinfo}${carriage_return}${mode}${v3}${dollar}${v4}${empty_line_bottom}" + fi +} + +# Pre-command hook to set prompt +my_precmd() { + # Calculate command duration if a command was run + if [[ -n "$_last_executed_command" && $_cmd_start_time -gt 0 ]]; then + _cmd_end_time=$(date +%s) + _cmd_duration=$((_cmd_end_time - _cmd_start_time)) + else + _cmd_duration=0 + fi + + stop_spinner_timer # Make sure spinner is stopped + vcs_info + set-prompt + vi-mode-indicator +} + +add-zsh-hook precmd my_precmd + +# Update mode file based on current mode +update-mode-file() { + set-prompt + local current_mode=$(cat ~/.vi-mode 2>/dev/null || echo "") + local new_mode="$vi_mode" + + if [[ "$new_mode" != "$current_mode" ]]; then + echo "$new_mode" >| ~/.vi-mode + fi + + # Ensure we're in an interactive shell and ZLE is active + if [[ -o zle ]] && zle -l &>/dev/null; then + zle reset-prompt 2>/dev/null || true + else + # If ZLE is not active, fallback and print the prompt manually + set-prompt + print -Pn "$PS1" + fi + + # Refresh tmux client if tmux is running + if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then + tmux refresh-client -S + fi +} + +# Check if nvim is running and update mode +function check-nvim-running() { + if pgrep -x "nvim" > /dev/null; then + vi_mode="" + update-mode-file + if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then + tmux refresh-client -S + fi + else + if [[ ${KEYMAP} == vicmd || ${KEYMAP} == vi-cmd-mode ]]; then + vi_mode=$(normal-mode) + elif [[ ${KEYMAP} == main || ${KEYMAP} == viins || ${KEYMAP} == '' ]]; then + vi_mode=$(insert-mode) + fi + update-mode-file + if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then + tmux refresh-client -S + fi + fi +} + +# ZLE line initialization hook +function zle-line-init() { + zle reset-prompt + vi-mode-indicator + case "${KEYMAP}" in + vicmd) + echo -ne '\e[1 q' + ;; + main|viins|*) + echo -ne '\e[5 q' + ;; + esac +} + +# ZLE keymap select hook +function zle-keymap-select() { + update-mode-file + zle reset-prompt + zle -R + vi-mode-indicator + case "${KEYMAP}" in + vicmd) + echo -ne '\e[1 q' + ;; + main|viins|*) + echo -ne '\e[5 q' + ;; + esac +} + +# Safer version of zle reset-prompt +function safe_reset_prompt() { + # Only reset if ZLE is active + if [[ -o zle ]] && zle -l &>/dev/null; then + zle reset-prompt 2>/dev/null || true + fi +} + +# Preexec hook for command execution - NO BACKGROUND JOBS VERSION +function preexec() { + # Store the command being executed + _last_executed_command=$1 + _cmd_start_time=$(date +%s) + _cmd_is_running=1 + _show_spinner=0 # Reset spinner flag + + # Start the spinner timer immediately + start_spinner_timer + + print -rn -- $terminfo[el] + echo -ne '\e[5 q' + vi-mode-indicator +} + +# Terminal resizing: resets the prompt if ZLE is active, updates the mode file. +TRAPWINCH() { + if [[ -o zle ]] && zle -l &>/dev/null; then + zle -R + zle reset-prompt 2>/dev/null || true + fi + update-mode-file 2>/dev/null +} + +# Register ZLE hooks +zle -N zle-line-init +zle -N zle-keymap-select +zle -N update_spinner + +# Register hooks +add-zsh-hook preexec preexec +add-zsh-hook precmd my_precmd + +set-prompt diff --git a/common/config/zsh/user/prompt_minimal.zsh b/common/config/zsh/user/prompt_minimal.zsh new file mode 100644 index 0000000..0389e7d --- /dev/null +++ b/common/config/zsh/user/prompt_minimal.zsh @@ -0,0 +1,295 @@ +# vim:ft=zsh ts=2 sw=2 sts=2 +#=#=#= +# simle_is_power theme +# folked from agnoster's Theme - https://gist.github.com/3712874 +# +# In order for this theme to render correctly, you will need a +# [Powerline-patched font](https://github.com/Lokaltog/powerline-fonts). +#=#= +#============================================================================== +# Color setting {{{ +#============================================================================== + +setopt prompt_subst + +bg_dir=240 +bg_dark=237 +fg_red=210 + +#===========================================================================}}} +# Segment drawing {{{ +#============================================================================== +# A few utility functions to make it easy and re-usable to draw segmented prompts + +CURRENT_BG='NONE' +# SEGMENT_SEPARATOR='' +SEGMENT_SEPARATOR='' +# SEGMENT_SEPARATOR='' +# SEGMENT_SEPARATOR='▒' +# SEGMENT_SEPARATOR='▓▒░' + +# Begin a segment +# Takes two arguments, background and foreground. Both can be omitted, +# rendering default background/foreground. +prompt_segment() { + local bg fg + [[ -n $1 ]] && bg="%K{$1}" || bg="%k" + [[ -n $2 ]] && fg="%F{$2}" || fg="%f" + if [[ $CURRENT_BG != 'NONE' && $1 != $CURRENT_BG ]]; then + echo -n " %{$bg%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR%{$fg%} " + else + echo -n "%{$bg%}%{$fg%} " + fi + CURRENT_BG=$1 + [[ -n $3 ]] && echo -n $3 +} + +# End the prompt, closing any open segments +prompt_end() { + if [[ -n $CURRENT_BG ]]; then + echo -n " %{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR" + else + echo -n "%{%k%}" + fi + echo -n "%{%f%}" + CURRENT_BG='' +} + +#===========================================================================}}} +# Prompt components {{{ +#============================================================================== +# Each component will draw itself, and hide itself if no information needs to be shown +#------------------------------------------------------------------------------ +# Init: {{{ +#------------------------------------------------------------------------------ + +prompt_init() { + echo -n "%{%F{240}%K{240}%}" +} + +#---------------------------------------------------------------------------}}} +# Status: {{{ +#------------------------------------------------------------------------------ +# - was there an error +# - am I root +# - are there background jobs? +# - am I in ranger subshell? + +prompt_status() { + local symbols + symbols=() + [[ $RETVAL -ne 0 ]] && symbols+="%{%F{${fg_red}}%}✞" + [[ $UID -eq 0 ]] && symbols+="%{%F{223}%}⚡" + [[ $(jobs -l | wc -l) -gt 0 ]] && symbols+="%{%F{cyan}%}⚙" + [[ -n ${RANGER_LEVEL} ]] && symbols+="%{%F{153}%}®" + + [[ -n "$symbols" ]] && prompt_segment ${bg_dark} NONE "$symbols" +} + +#---------------------------------------------------------------------------}}} +# Virtualenv: current working virtualenv {{{ +#------------------------------------------------------------------------------ + +prompt_virtualenv() { + local virtualenv_path="$VIRTUAL_ENV" + if [[ -n $virtualenv_path && -n $VIRTUAL_ENV_DISABLE_PROMPT ]]; then + prompt_segment green black "(`basename $virtualenv_path`)" + fi +} + +#---------------------------------------------------------------------------}}} +# Dir: current working directory {{{ +#------------------------------------------------------------------------------ + +prompt_dir() { + prompt_segment ${bg_dir} 231 '%~' +} + +#---------------------------------------------------------------------------}}} +# Git: branch/detached head, dirty status {{{ +#------------------------------------------------------------------------------ + +prompt_git() { + local ref dirty mode repo_path + repo_path=$(git rev-parse --git-dir 2>/dev/null) + + if $(git rev-parse --is-inside-work-tree >/dev/null 2>&1); then + # dirty=$(parse_git_dirty) + ref=$(git symbolic-ref HEAD 2> /dev/null) || ref="➔ $(git show-ref --head -s --abbrev |head -n1 2> /dev/null)" + # if [[ -n $dirty ]]; then + # prompt_segment ${bg_dark} 223 + # else + prompt_segment ${bg_dark} 153 + # fi + + if [[ -e "${repo_path}/BISECT_LOG" ]]; then + mode=" <B>" + elif [[ -e "${repo_path}/MERGE_HEAD" ]]; then + mode=" >M<" + elif [[ -e "${repo_path}/rebase" || -e "${repo_path}/rebase-apply" || -e "${repo_path}/rebase-merge" || -e "${repo_path}/../.dotest" ]]; then + mode=" >R>" + fi + + autoload -Uz vcs_info + + zstyle ':vcs_info:*' enable git + zstyle ':vcs_info:*' get-revision true + zstyle ':vcs_info:*' check-for-changes true + zstyle ':vcs_info:*' stagedstr '+' + zstyle ':vcs_info:git:*' unstagedstr '*' + zstyle ':vcs_info:*' formats ' %u%c' + zstyle ':vcs_info:*' actionformats ' %u%c' + vcs_info + echo -n "${ref/refs\/heads\// }${vcs_info_msg_0_%% }${mode}" + fi +} + +#---------------------------------------------------------------------------}}} +# Hg: prompt {{{ +#------------------------------------------------------------------------------ + +prompt_hg() { + local rev status + if $(hg id >/dev/null 2>&1); then + if $(hg prompt >/dev/null 2>&1); then + if [[ $(hg prompt "{status|unknown}") = "?" ]]; then + # if files are not added + prompt_segment ${fg_red} ${bg_dark} + st='±' + elif [[ -n $(hg prompt "{status|modified}") ]]; then + # if any modification + prompt_segment 223 ${bg_dark} + st='±' + else + # if working copy is clean + prompt_segment 153 ${bg_dark} + fi + echo -n $(hg prompt "☿ {rev}@{branch}") $st + else + st="" + rev=$(hg id -n 2>/dev/null | sed 's/[^-0-9]//g') + branch=$(hg id -b 2>/dev/null) + if `hg st | grep -q "^\?"`; then + prompt_segment ${fg_red} ${bg_dark} + st='±' + elif `hg st | grep -q "^(M|A)"`; then + prompt_segment 223 ${bg_dark} + st='±' + else + prompt_segment 153 ${bg_dark} + fi + echo -n "☿ $rev@$branch" $st + fi + fi +} + +#}}}========================================================================}}} +# Build main prompt {{{ +#============================================================================== + + +function vi-mode-indicator() { + local current_mode + current_mode=$(cat ~/.vi-mode 2>/dev/null || echo "") + + if [[ ${KEYMAP} == vicmd || ${KEYMAP} == vi-cmd-mode ]]; then + [[ "$current_mode" != "-- NORMAL --" ]] && echo "-- NORMAL --" >| ~/.vi-mode + elif [[ ${KEYMAP} == main || ${KEYMAP} == viins || ${KEYMAP} == '' ]]; then + [[ "$current_mode" != "-- INSERT --" ]] && echo "-- INSERT --" >| ~/.vi-mode + fi +} + +build_prompt() { + RETVAL=$? + vi-mode-indicator + prompt_init + prompt_virtualenv + prompt_dir + prompt_git + prompt_hg + prompt_status + prompt_end +} + +color_reset="%{$(tput sgr0)%}" +color_yellow="%{$(tput setaf 226)%}" +color_blink="%{$(tput blink)%}" +prompt_symbol="$" +dollar_sign="${color_yellow}${color_blink}${prompt_symbol}${color_reset}" +dollar="%(?:%F{2}${dollar_sign}:%F{1}${dollar_sign})" + +v1="%{┌─[%}" +v2="%{]%}" +v3="└─[" +v4="]" +user="%n" + +PROMPT="${v1}${user}%f%b%k${v2}$(build_prompt)$reset_color +${v3}${dollar}${v4}${empty_line_bottom}$reset_color" +#PROMPT='%n@%m:%~%# ' +#%{%F{240}%}\$ %{$reset_color%}' +#%{${dollar}%} %{$reset_color%}' +RPROMPT='' + +PROMPT2='%{%F{30}%}↪%{$reset_color%} ' +RPROMPT2='%{$fg_bold[green]%}%_%{$reset_color%}' + +function update-mode-file() { + local current_mode=$(cat ~/.vi-mode 2>/dev/null || echo "") + local new_mode="$vi_mode" + + # Check if the mode is different before updating + if [[ "$new_mode" != "$current_mode" ]]; then + echo "$new_mode" >| ~/.vi-mode + fi + + # Only call zle if ZLE is active + if [[ -o zle ]]; then + zle reset-prompt # Force refresh + fi + + # Ensure tmux client refresh only happens if tmux is running + if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then + tmux refresh-client -S + fi +} +function zle-line-init() { + zle reset-prompt + case "${KEYMAP}" in + vicmd) + echo -ne '\e[1 q' + ;; + main|viins|*) + echo -ne '\e[5 q' + ;; + esac +} +function zle-keymap-select() { + local current_keymap + current_keymap="${KEYMAP}" + + update-mode-file + zle reset-prompt + + case "$current_keymap" in + vicmd) + echo -ne '\e[1 q' + ;; + main|viins|*) + echo -ne '\e[5 q' + ;; + esac +} + +precmd () { + print -rP +} + +preexec () { + print -rn -- $terminfo[el] + echo -ne '\e[5 q' # Reset cursor shape +} +zle -N zle-line-init +zle -N zle-keymap-select + +#===========================================================================}}} diff --git a/common/config/zsh/user/prompt_new.zsh b/common/config/zsh/user/prompt_new.zsh new file mode 100644 index 0000000..78791ef --- /dev/null +++ b/common/config/zsh/user/prompt_new.zsh @@ -0,0 +1,863 @@ +# __ _ __ _| | _____ ______ _| | __ +# / _` |/ _` | |/ / _ \_ / _` | |/ / +# | (_| | (_| | < (_) / / (_| | < +# \__,_|\__, |_|\_\___/___\__,_|_|\_\ +# |___/ +# +# An asynchronous, dynamic color prompt for ZSH with Git, vi mode, and exit +# status indicators +# +# +# MIT License +# +# Copyright (c) 2017-2019 Alexandros Kozak +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# +# https://github.com/agkozak/agkozak-zsh-prompt +# + +# shellcheck disable=SC1090,SC2016,SC2034,SC2088,SC2148,SC2154,SC2190 + +# psvar[] Usage +# +# psvar Index Prompt String Equivalent Usage +# +# psvar[1] %1v Hostname/abbreviated hostname (only +# displayed for SSH connections) +# psvar[2] %2v Working directory or abbreviation +# thereof +# psvar[3] %3v Current working Git branch, along +# with indicator of changes made +# psvar[4] %4v Equals 'vicmd' when vi command mode +# is enabled; otherwise empty + +# Set AGKOZAK_PROMPT_DEBUG=1 to see debugging information +AGKOZAK_PROMPT_DEBUG=${AGKOZAK_PROMPT_DEBUG:-0} + +############################################################ +# Display a message on STDERR if debug mode is enabled +# +# Globals: +# AGKOZAK_PROMPT_DEBUG +# Arguments: +# $1 Message to send to STDERR +############################################################ +_agkozak_debug_print() { + (( AGKOZAK_PROMPT_DEBUG )) && print -- "agkozak-zsh-prompt: $1" >&2 +} + +if (( AGKOZAK_PROMPT_DEBUG )); then + autoload -Uz is-at-least + + setopt WARN_CREATE_GLOBAL + + if is-at-least 5.4.0; then + setopt WARN_NESTED_VAR + fi +fi + +# Set AGKOZAK_PROMPT_DIRTRIM to the desired number of directory elements to +# display, or set it to 0 for no directory trimming +typeset -g AGKOZAK_PROMPT_DIRTRIM=${AGKOZAK_PROMPT_DIRTRIM:-2} + +# Set AGKOZAK_MULTILINE to 0 to enable the legacy, single-line prompt +typeset -g AGKOZAK_MULTILINE=${AGKOZAK_MULTILINE:-1} + +# Set AGKOZAK_LEFT_PROMPT_ONLY to have the Git status appear in the left prompt +typeset -g AGKOZAK_LEFT_PROMPT_ONLY=${AGKOZAK_LEFT_PROMPT_ONLY:-0} + +# Set AGKOZAK_COLORS_* variables to any valid color +# AGKOZAK_COLORS_EXIT_STATUS changes the exit status color (default: red) +# AGKOZAK_COLORS_USER_HOST changes the username/hostname color (default: green) +# AGKOZAK_COLORS_PATH changes the path color (default: blue) +# AGKOZAK_COLORS_BRANCH_STATUS changes the branch status color (default: yellow) +# AGKOZAK_COLORS_PROMPT_CHAR changes the prompt character color (default: white) +typeset -g AGKOZAK_COLORS_EXIT_STATUS=${AGKOZAK_COLORS_EXIT_STATUS:-red} +typeset -g AGKOZAK_COLORS_USER_HOST=${AGKOZAK_COLORS_USER_HOST:-green} +typeset -g AGKOZAK_COLORS_PATH=${AGKOZAK_COLORS_PATH:-blue} +typeset -g AGKOZAK_COLORS_BRANCH_STATUS=${AGKOZAK_COLORS_BRANCH_STATUS:-yellow} + +setopt PROMPT_SUBST NO_PROMPT_BANG + +###################################################################### +# GENERAL FUNCTIONS +###################################################################### + +############################################################ +# Are colors available? +# +# Globals: +# AGKOZAK_HAS_COLORS +############################################################ +_agkozak_has_colors() { + if (( $+AGKOZAK_HAS_COLORS )); then + : + else + case $TERM in + *-256color) typeset -g AGKOZAK_HAS_COLORS=1 ;; + vt100|dumb) typeset -g AGKOZAK_HAS_COLORS=0 ;; + *) + local colors + case $OSTYPE in + freebsd*|dragonfly*) colors=$(tput Co) ;; + *) colors=$(tput colors) ;; + esac + typeset -g AGKOZAK_HAS_COLORS=$(( colors >= 8 )) + ;; + esac + fi + (( AGKOZAK_HAS_COLORS )) +} + +############################################################ +# Is the user connected via SSH? +# +# This function works perfectly for regular users. It is +# nearly impossible to detect with accuracy how a superuser +# is connected, so this prompt opts simply to display his or +# her username and hostname in inverse video. +############################################################ +_agkozak_is_ssh() { + [[ -n "${SSH_CONNECTION-}${SSH_CLIENT-}${SSH_TTY-}" ]] +} + +############################################################ +# Emulation of bash's PROMPT_DIRTRIM for ZSH +# +# Take PWD and substitute HOME with `~'. If the rest of PWD +# has more than a certain number of elements in its +# directory tree, keep the number specified by +# AGKOZAK_PROMPT_DIRTRIM (default: 2) and abbreviate the +# rest with `...'. (Set AGKOZAK_PROMPT_DIRTRIM=0 to disable +# directory trimming). For example, +# +# $HOME/dotfiles/polyglot/img +# +# will be displayed as +# +# ~/.../polyglot/img +# +# Named directories will by default be displayed using their +# aliases in the prompt (e.g. `~project'). Set +# AGKOZAK_NAMED_DIRS=0 to have them displayed just like any +# other directory. +# +# Globals: +# AGKOZAK_NAMED_DIRS +# Arguments: +# $@ [Optional] If `-v', store the function's output in +# psvar[2] instead of printing it to STDOUT +# $@ Number of directory elements to display (default: 2) +############################################################ +_agkozak_prompt_dirtrim() { + # Process arguments + local argument + for argument in $@; do + [[ $argument == '-v' ]] && local var=1 + done + until [[ $1 != '-v' ]]; do + shift + done + [[ $1 -ge 0 ]] || set 2 + + # Default behavior (when AGKOZAK_NAMED_DIRS is 1) + typeset -g AGKOZAK_NAMED_DIRS=${AGKOZAK_NAMED_DIRS:-1} + if (( AGKOZAK_NAMED_DIRS )); then + local zsh_pwd + print -Pnz '%~' + + # IF AGKOZAK_PROMPT_DIRTRIM is not 0, trim directory + if (( $1 )); then + read -rz zsh_pwd + case $zsh_pwd in + \~) print -Pnz $zsh_pwd ;; + \~/*) print -Pnz "%($(( $1 + 2 ))~|~/.../%${1}~|%~)" ;; + \~*) print -Pnz "%($(( $1 + 2 ))~|${zsh_pwd%%${zsh_pwd#\~*\/}}.../%${1}~|%~)" ;; + *) print -Pnz "%($(( $1 + 1 ))/|.../%${1}d|%d)" ;; + esac + fi + + # If AGKOZAK_NAMED_DIRS is 0 + else + local dir dir_count + case $HOME in + /) dir=${PWD} ;; + *) dir=${PWD#$HOME} ;; + esac + + # If AGKOZAK_PROMPT_DIRTRIM is not 0, trim the directory + if (( $1 > 0 )); then + + # The number of directory elements is the number of slashes in ${PWD#$HOME} + dir_count=$(( ${#dir} - ${#${dir//\//}} )) + if (( dir_count <= $1 )); then + case $PWD in + ${HOME}) print -nz '~' ;; + ${HOME}*) print -nz "~${dir}" ;; + *) print -nz "$PWD" ;; + esac + else + local lopped_path i + lopped_path=${dir} + i=0 + while (( i != $1 )); do + lopped_path=${lopped_path%\/*} + (( i++ )) + done + case $PWD in + ${HOME}*) print -nz "~/...${dir#${lopped_path}}" ;; + *) print -nz -f '...%s' "${PWD#${lopped_path}}" ;; + esac + fi + + # If AGKOZAK_PROMPT_DIRTRIM is 0 + else + case $PWD in + ${HOME}) print -nz '~' ;; + ${HOME}*) print -nz "~${dir}" ;; + *) print -nz "$PWD" ;; + esac + fi + fi + + local output + read -rz output + + # Argument -v stores the output to psvar[2]; otherwise send to STDOUT + if (( var )); then + psvar[2]=$output + else + print $output + fi +} + +############################################################ +# Display current branch name, followed by symbols +# representing changes to the working copy +############################################################ +_agkozak_branch_status() { + local ref branch + ref=$(command git symbolic-ref --quiet HEAD 2> /dev/null) + case $? in # See what the exit code is. + 0) ;; # $ref contains the name of a checked-out branch. + 128) return ;; # No Git repository here. + # Otherwise, see if HEAD is in detached state. + *) ref=$(command git rev-parse --short HEAD 2> /dev/null) || return ;; + esac + branch=${ref#refs/heads/} + + if [[ -n $branch ]]; then + local git_status symbols i=1 k + git_status="$(LC_ALL=C command git status 2>&1)" + + typeset -A messages + messages=( + '&*' ' have diverged,' + '&' 'Your branch is behind ' + '*' 'Your branch is ahead of ' + '+' 'new file: ' + 'x' 'deleted: ' + '!' 'modified: ' + '>' 'renamed: ' + '?' 'Untracked files:' + ) + + for k in '&*' '&' '*' '+' 'x' '!' '>' '?'; do + case $git_status in + *${messages[$k]}*) symbols+="${AGKOZAK_CUSTOM_SYMBOLS[$i]:-$k}" ;; + esac + (( i++ )) + done + + [[ -n $symbols ]] && symbols=" ${symbols}" + + printf '%s(%s%s)' "${AGKOZAK_BRANCH_STATUS_SEPARATOR- }" "$branch" "$symbols" + fi +} + +############################################################ +# Redraw the prompt when the vi mode changes. When the user +# enters vi command mode, the % or # in the prompt changes +# to a colon +############################################################ +zle-keymap-select() { + [[ $KEYMAP == 'vicmd' ]] && psvar[4]='vicmd' || psvar[4]='' + zle reset-prompt + zle -R +} + +############################################################ +# Redraw prompt when terminal size changes +############################################################ +TRAPWINCH() { + zle && zle -R +} + +############################################################ +# For legacy custom prompts: print a vi mode indicator +############################################################ +_agkozak_vi_mode_indicator() { + case $KEYMAP in + vicmd) print -n ':' ;; + *) print -n '%#' ;; + esac +} + +###################################################################### +# ASYNCHRONOUS FUNCTIONS +###################################################################### + +# Standarized $0 handling +# (See https://github.com/zdharma/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc) +0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}" +typeset -g AGKOZAK_PROMPT_DIR="${0:A:h}" + +############################################################ +# If zsh-async has not already been loaded, try to load it +# +# Globals: +# AGKOZAK_PROMPT_DEBUG +# AGKOZAK_PROMPT_DIR +############################################################ +_agkozak_load_async_lib() { + if ! whence -w async_init &> /dev/null; then # Don't load zsh-async twice + if (( AGKOZAK_PROMPT_DEBUG )); then + source "${AGKOZAK_PROMPT_DIR}/lib/async.zsh" + else + source "${AGKOZAK_PROMPT_DIR}/lib/async.zsh" &> /dev/null + fi + local success=$? + return $success + fi +} + +############################################################ +# If SIGUSR1 is available and not already in use by ZSH, use +# it; otherwise disable asynchronous mode +############################################################ +_agkozak_has_usr1() { + if whence -w TRAPUSR1 &> /dev/null; then + _agkozak_debug_print 'TRAPUSR1 already defined.' + return 1 + else + case $signals in # Array containing names of available signals + *USR1*) return 0 ;; + *) + _agkozak_debug_print 'SIGUSR1 not available.' + return 1 + ;; + esac + fi +} + +############################################################ +# If AGKOZAK_FORCE_ASYNC_METHOD is set to a valid value, +# set AGKOZAK_ASYNC_METHOD to that; otherwise, determine +# the optimal asynchronous method from the environment (usr1 +# for MSYS2/Cygwin, zsh-async for WSL, subst-async for +# everything else), with fallbacks being available. Define +# the necessary asynchronous functions (loading async.zsh +# when necessary). +# +# Globals: +# AGKOZAK_IS_WSL +# AGKOZAK_ASYNC_METHOD +# AGKOZAK_FORCE_ASYNC_METHOD +# AGKOZAK_TRAPUSR1_FUNCTION +############################################################ +_agkozak_async_init() { + + # WSL should have BG_NICE disabled, since it does not have a Linux kernel + setopt LOCAL_OPTIONS EXTENDED_GLOB + if [[ -e /proc/version ]]; then + if [[ -n ${(M)${(f)"$(</proc/version)"}:#*Microsoft*} ]]; then + unsetopt BG_NICE + typeset -g AGKOZAK_IS_WSL=1 # For later reference + fi + fi + + # If AGKOZAK_FORCE_ASYNC_METHOD is set, force the asynchronous method + [[ $AGKOZAK_FORCE_ASYNC_METHOD == 'zsh-async' ]] && _agkozak_load_async_lib + if [[ $AGKOZAK_FORCE_ASYNC_METHOD == (subst-async|zsh-async|usr1|none) ]]; then + typeset -g AGKOZAK_ASYNC_METHOD=$AGKOZAK_FORCE_ASYNC_METHOD + + # Otherwise, first provide for certain quirky systems + else + + if (( AGKOZAK_IS_WSL )) || [[ $OSTYPE == solaris* ]]; then + if [[ $ZSH_VERSION != '5.0.2' ]] &&_agkozak_load_async_lib; then + typeset -g AGKOZAK_ASYNC_METHOD='zsh-async' + elif _agkozak_has_usr1; then + typeset -g AGKOZAK_ASYNC_METHOD='usr1' + else + typeset -g AGKOZAK_ASYNC_METHOD='subst-async' + fi + + # SIGUSR1 method is still much faster on MSYS2, Cygwin, and ZSH v5.0.2 + elif [[ $ZSH_VERSION == '5.0.2' ]] || [[ $OSTYPE == (msys|cygwin) ]]; then + if _agkozak_has_usr1; then + typeset -g AGKOZAK_ASYNC_METHOD='usr1' + else + typeset -g AGKOZAK_ASYNC_METHOD='subst-async' + fi + + # Asynchronous methods don't work in Emacs shell mode (but they do in term + # and ansi-term) + elif [[ $TERM == 'dumb' ]]; then + typeset -g AGKOZAK_ASYNC_METHOD='none' + + # Otherwise use subst-async + else + typeset -g AGKOZAK_ASYNC_METHOD='subst-async' + fi + fi + + ############################################################ + # Process substitution async method + # + # Fork a background process to fetch the Git status and feed + # it asynchronously to a file descriptor. Install a callback + # handler to process input from the file descriptor. + # + # Globals: + # AGKOZAK_ASYNC_FD + # AGKOZAK_IS_WSL + ############################################################ + _agkozak_subst_async() { + setopt LOCAL_OPTIONS NO_IGNORE_BRACES + typeset -g AGKOZAK_ASYNC_FD=13371 + + # Workaround for buggy behavior in MSYS2, Cygwin, and Solaris + if [[ $OSTYPE == (msys|cygwin|solaris*) ]]; then + exec {AGKOZAK_ASYNC_FD}< <(_agkozak_branch_status; command true) + # Prevent WSL from locking up when using X; also workaround for ZSH v5.0.2 + elif (( AGKOZAK_IS_WSL )) && (( $+DISPLAY )) \ + || [[ $ZSH_VERSION == '5.0.2' ]]; then + exec {AGKOZAK_ASYNC_FD}< <(_agkozak_branch_status) + command sleep 0.01 + else + exec {AGKOZAK_ASYNC_FD}< <(_agkozak_branch_status) + fi + + # Bug workaround; see http://www.zsh.org/mla/workers/2018/msg00966.html + command true + + zle -F "$AGKOZAK_ASYNC_FD" _agkozak_zsh_subst_async_callback + } + + ############################################################ + # ZLE callback handler + # + # Read Git status from file descriptor and set psvar[3] + # + # Arguments: + # $1 File descriptor + ############################################################ + _agkozak_zsh_subst_async_callback() { + setopt LOCAL_OPTIONS NO_IGNORE_BRACES + + local FD="$1" response + + # Read data from $FD descriptor + IFS='' builtin read -rs -d $'\0' -u "$FD" response + + # Withdraw callback and close the file descriptor + zle -F ${FD}; exec {FD}<&- + + # Make the changes visible + psvar[3]="$response" + zle && zle reset-prompt + } + + case $AGKOZAK_ASYNC_METHOD in + + zsh-async) + + ############################################################ + # Create zsh-async worker + ############################################################ + _agkozak_zsh_async() { + async_start_worker agkozak_git_status_worker -n + async_register_callback agkozak_git_status_worker _agkozak_zsh_async_callback + async_job agkozak_git_status_worker _agkozak_branch_status + } + + ############################################################ + # Set RPROMPT and stop worker + ############################################################ + _agkozak_zsh_async_callback() { + psvar[3]=$3 + zle && zle reset-prompt + async_stop_worker agkozak_git_status_worker -n + } + ;; + + usr1) + + ############################################################ + # Launch async workers to calculate Git status. TRAPUSR1 + # actually displays the status; if some other script + # redefines TRAPUSR1, drop the prompt into synchronous mode. + # + # Globals: + # AGKOZAK_TRAPUSR1_FUNCTION + # AGKOZAK_USR1_ASYNC_WORKER + # AGKOZAK_ASYNC_METHOD + ############################################################ + _agkozak_usr1_async() { + if [[ "$(builtin which TRAPUSR1)" = "$AGKOZAK_TRAPUSR1_FUNCTION" ]]; then + # Kill running child process if necessary + if (( AGKOZAK_USR1_ASYNC_WORKER )); then + kill -s HUP "$AGKOZAK_USR1_ASYNC_WORKER" &> /dev/null || : + fi + + # Start background computation of Git status + _agkozak_usr1_async_worker &! + typeset -g AGKOZAK_USR1_ASYNC_WORKER=$! + else + _agkozak_debug_print 'TRAPUSR1 has been redefined. Switching to subst-async mode.' + typeset -g AGKOZAK_ASYNC_METHOD='subst-async' + psvar[3]="$(_agkozak_branch_status)" + fi + } + + ############################################################ + # Calculate Git status and store it in a temporary file; + # then kill own process, sending SIGUSR1 + # + # Globals: + # AGKOZAK_PROMPT_DEBUG + ############################################################ + _agkozak_usr1_async_worker() { + # Save Git branch status to temporary file + _agkozak_branch_status >| /tmp/agkozak_zsh_prompt_$$ + + # Signal parent process + if (( AGKOZAK_PROMPT_DEBUG )); then + kill -s USR1 $$ + else + kill -s USR1 $$ &> /dev/null + fi + } + + ############################################################ + # On SIGUSR1, fetch Git status from temprary file and store + # it in psvar[3]. This function caches its own code in + # AGKOZAK_TRAPUSR1_FUNCTION so that it can tell if it has + # been redefined by another script. + # + # Globals: + # AGKOZAK_USR1_ASYNC_WORKER + # AGKOZAK_TRAPUSR1_FUNCTION + ############################################################ + TRAPUSR1() { + # Set prompt from contents of temporary file + psvar[3]=$(print -n -- "$(< /tmp/agkozak_zsh_prompt_$$)") + + # Reset asynchronous process number + typeset -g AGKOZAK_USR1_ASYNC_WORKER=0 + + # Redraw the prompt + zle && zle reset-prompt + } + + typeset -g AGKOZAK_TRAPUSR1_FUNCTION="$(builtin which TRAPUSR1)" + ;; + esac +} + +###################################################################### +# THE PROMPT +###################################################################### + +############################################################ +# Strip color codes from a prompt string +# +# Arguments: +# $1 The prompt string +############################################################ +_agkozak_strip_colors() { + + local prompt=$1 + local open_braces + + while [[ -n $prompt ]]; do + case $prompt in + %F\{*|%K\{*) + (( open_braces++ )) + prompt=${prompt#%[FK]\{} + while (( open_braces )); do + case ${prompt:0:1} in + \{) (( open_braces++ )) ;; + \}) (( open_braces-- )) ;; + esac + prompt=${prompt#?} + done + ;; + %f*|%k*) prompt=${prompt#%[fk]} ;; + *) + print -n -- "${prompt:0:1}" + prompt=${prompt#?} + ;; + esac + done +} + +############################################################ +# Runs right before each prompt is displayed; hooks into +# precmd +# +# 1) Redisplays path ($psvar[2]) whenever the value of +# AGKOZAK_PROMPT_DIRTRIM or AGKOZAK_NAMED_DIRS changes +# 2) If AGKOZAK_MULTILINE is changed to 0, set +# AGKOZAK_LEFT_PROMPT_ONLY=0 +# 3) If AGKOZAK_LEFT_PROMPT_ONLY is changed, updated both +# prompt strings +# 4) Resets Git status and vi mode display +# 5) Begins to calculate Git status +# 6) Sets AGKOZAK_PROMPT_WHITESPACE based on value of +# AGKOZAK_MULTILINE +# 7) Optionally display a blank line (AGKOZAK_BLANK_LINES), +# while avoiding a blank line when the shell is first +# loaded +# 8) If custom prompts are defined, update the prompt +# strings +# +# TODO: Consider making AGKOZAK_PROMPT_WHITESPACE a psvar +# +# Globals: +# AGKOZAK_PROMPT_DIRTRIM +# AGKOZAK_OLD_PROMPT_DIRTRIM +# AGKOZAK_NAMED_DIRS +# AGKOZAK_OLD_NAMED_DIRS +# AGKOZAK_MULTILINE +# AGKOZAK_OLD_MULTILINE +# AGKOZAK_LEFT_PROMPT_ONLY +# AGKOZAK_OLD_LEFT_PROMPT_ONLY +# AGKOZAK_ASYNC_METHOD +# AGKOZAK_PROMPT_WHITESPACE +# AGKOZAK_BLANK_LINES +# AGKOZAK_FIRST_PROMPT_PRINTED +# AGKOZAK_CUSTOM_PROMPT +# AGKOZAK_CURRENT_CUSTOM_PROMPT +# AGKOZAK_CUSTOM_RPROMPT +# AGKOZAK_CURRENT_CUSTOM_RPROMPT +############################################################ +_agkozak_precmd() { + # Update displayed directory when AGKOZAK_PROMPT_DIRTRIM or AGKOZAK_NAMED_DIRS + # changes or when first sourcing this script + if (( AGKOZAK_PROMPT_DIRTRIM != AGKOZAK_OLD_PROMPT_DIRTRIM )) \ + || (( AGKOZAK_NAMED_DIRS != AGKOZAK_OLD_NAMED_DIRS )) \ + || (( ! $+psvar[2] )); then + _agkozak_prompt_dirtrim -v $AGKOZAK_PROMPT_DIRTRIM + typeset -g AGKOZAK_OLD_PROMPT_DIRTRIM=$AGKOZAK_PROMPT_DIRTRIM + typeset -g AGKOZAK_OLD_NAMED_DIRS=$AGKOZAK_NAMED_DIRS + fi + + if (( AGKOZAK_MULTILINE != AGKOZAK_OLD_MULTILINE )); then + (( AGKOZAK_MULTILINE == 0 )) && AGKOZAK_LEFT_PROMPT_ONLY=0 + typeset -g AGKOZAK_OLD_MULTILINE=$AGKOZAK_MULTILINE + fi + + if (( AGKOZAK_LEFT_PROMPT_ONLY != AGKOZAK_OLD_LEFT_PROMPT_ONLY )); then + unset AGKOZAK_CUSTOM_PROMPT AGKOZAK_CUSTOM_RPROMPT + typeset -g AGKOZAK_OLD_LEFT_PROMPT_ONLY=$AGKOZAK_LEFT_PROMPT_ONLY + _agkozak_prompt_string + fi + + psvar[3]='' + psvar[4]='' + + case $AGKOZAK_ASYNC_METHOD in + 'subst-async') _agkozak_subst_async ;; + 'zsh-async') _agkozak_zsh_async ;; + 'usr1') _agkozak_usr1_async ;; + *) psvar[3]="$(_agkozak_branch_status)" ;; + esac + + if (( AGKOZAK_MULTILINE == 0 )) && (( ! AGKOZAK_LEFT_PROMPT_ONLY )) \ + && [[ -z $INSIDE_EMACS ]]; then + typeset -g AGKOZAK_PROMPT_WHITESPACE=' ' + else + typeset -g AGKOZAK_PROMPT_WHITESPACE=$'\n' + fi + + if (( AGKOZAK_BLANK_LINES )); then + if (( AGKOZAK_FIRST_PROMPT_PRINTED )); then + print + fi + typeset -g AGKOZAK_FIRST_PROMPT_PRINTED=1 + fi + + # If AGKOZAK_CUSTOM_PROMPT or AGKOZAK_CUSTOM_RPROMPT changes, the + # corresponding prompt is updated + + if [[ ${AGKOZAK_CUSTOM_PROMPT} != "${AGKOZAK_CURRENT_CUSTOM_PROMPT}" ]]; then + typeset -g AGKOZAK_CURRENT_CUSTOM_PROMPT=${AGKOZAK_CUSTOM_PROMPT} + PROMPT=${AGKOZAK_CUSTOM_PROMPT} + if ! _agkozak_has_colors; then + PROMPT=$(_agkozak_strip_colors "${PROMPT}") + fi + fi + + if [[ ${AGKOZAK_CUSTOM_RPROMPT} != "${AGKOZAK_CURRENT_CUSTOM_RPROMPT}" ]]; then + typeset -g AGKOZAK_CURRENT_CUSTOM_RPROMPT=${AGKOZAK_CUSTOM_RPROMPT} + RPROMPT=${AGKOZAK_CUSTOM_RPROMPT} + if ! _agkozak_has_colors; then + RPROMPT=$(_agkozak_strip_colors "${RPROMPT}") + fi + fi +} + +############################################################ +# Set the prompt strings +# +# Globals: +# AGKOZAK_CUSTOM_PROMPT +# AGKOZAK_COLORS_EXIT_STATUS +# AGKOZAK_COLORS_USER_HOST +# AGKOZAK_COLORS_PATH +# AGKOZAK_PROMPT_WHITESPACE +# AGKOZAK_COLORS_PROMPT_CHAR +# AGKOZAK_PROMPT_CHAR +# AGKOZAK_CURRENT_CUSTOM_PROMPT +# AGKOZAK_CUSTOM_RPROMPT +# AGKOZAK_COLORS_BRANCH_STATUS +# AGKOZAK_CURRENT_CUSTOM_RPROMPT +############################################################ +_agkozak_prompt_string () { + if (( $+AGKOZAK_CUSTOM_PROMPT )); then + PROMPT=${AGKOZAK_CUSTOM_PROMPT} + else + # The color left prompt + PROMPT='%(?..%B%F{${AGKOZAK_COLORS_EXIT_STATUS}}(%?%)%f%b )' + PROMPT+='%(!.%S%B.%B%F{${AGKOZAK_COLORS_USER_HOST}})%n%1v%(!.%b%s.%f%b) ' + PROMPT+='%B%F{${AGKOZAK_COLORS_PATH}}%2v%f%b' + if (( AGKOZAK_LEFT_PROMPT_ONLY )); then + PROMPT+='%(3V.%F{${AGKOZAK_COLORS_BRANCH_STATUS}}%3v%f.)' + fi + PROMPT+='${AGKOZAK_PROMPT_WHITESPACE}' + PROMPT+='${AGKOZAK_COLORS_PROMPT_CHAR:+%F{${AGKOZAK_COLORS_PROMPT_CHAR}\}}' + PROMPT+='%(4V.${AGKOZAK_PROMPT_CHAR[3]:-:}.%(!.${AGKOZAK_PROMPT_CHAR[2]:-%#}.${AGKOZAK_PROMPT_CHAR[1]:-%#}))' + PROMPT+='${AGKOZAK_COLORS_PROMPT_CHAR:+%f} ' + + typeset -g AGKOZAK_CUSTOM_PROMPT=${PROMPT} + typeset -g AGKOZAK_CURRENT_CUSTOM_PROMPT=${AGKOZAK_CUSTOM_PROMPT} + fi + + if (( $+AGKOZAK_CUSTOM_RPROMPT )); then + RPROMPT=${AGKOZAK_CUSTOM_RPROMPT} + else + # The color right prompt + if (( ! AGKOZAK_LEFT_PROMPT_ONLY )); then + typeset -g RPROMPT='%(3V.%F{${AGKOZAK_COLORS_BRANCH_STATUS}}%3v%f.)' + else + typeset -g RPROMPT='' + fi + + typeset -g AGKOZAK_CUSTOM_RPROMPT=${RPROMPT} + typeset -g AGKOZAK_CURRENT_CUSTOM_RPROMPT=${RPROMPT} + fi + + if ! _agkozak_has_colors; then + PROMPT=$(_agkozak_strip_colors "$PROMPT") + RPROMPT=$(_agkozak_strip_colors "$RPROMPT") + fi +} + +############################################################ +# Prompt setup +# +# Globals: +# AGKOZAK_ASYNC_METHOD +# AGKOZAK_USR1_ASYNC_WORKER +# AGKOZAK_PROMPT_DIRTRIM +############################################################ +() { + + _agkozak_async_init + + case $AGKOZAK_ASYNC_METHOD in + 'subst-async') ;; + 'zsh-async') async_init ;; + 'usr1') typeset -g AGKOZAK_USR1_ASYNC_WORKER=0 ;; + esac + + zle -N zle-keymap-select + + # Don't use ZSH hooks in Emacs classic shell + if (( $+INSIDE_EMACS )) && [[ $TERM == 'dumb' ]]; then + : + else + autoload -Uz add-zsh-hook + add-zsh-hook precmd _agkozak_precmd + + ############################################################ + # Update the displayed directory when the PWD changes + ############################################################ + _agkozak_chpwd() { + _agkozak_prompt_dirtrim -v $AGKOZAK_PROMPT_DIRTRIM + } + + add-zsh-hook chpwd _agkozak_chpwd + fi + + # Only display the HOSTNAME for an SSH connection or for a superuser + if _agkozak_is_ssh || (( EUID == 0 )); then + psvar[1]="@${HOST%%.*}" + else + psvar[1]='' + fi + + # The DragonFly BSD console and Emacs shell can't handle bracketed paste. + # Avoid the ugly ^[[?2004 control sequence. + if [[ $TERM == 'cons25' ]] || [[ $TERM == 'dumb' ]]; then + unset zle_bracketed_paste + fi + + # The Emacs shell has only limited support for some ZSH features, so use a + # more limited prompt. + if [[ $TERM == 'dumb' ]]; then + PROMPT='%(?..(%?%) )' + PROMPT+='%n%1v ' + PROMPT+='$(_agkozak_prompt_dirtrim "$AGKOZAK_PROMPT_DIRTRIM")' + PROMPT+='$(_agkozak_branch_status) ' + PROMPT+='%# ' + else + # Avoid continuation lines in Emacs term and ansi-term + (( $+INSIDE_EMACS )) && ZLE_RPROMPT_INDENT=3 + + # When VSCode is using the DOM renderer, the right prompt overflows off the + # side of the screen + (( $+VSCODE_PID )) && ZLE_RPROMPT_INDENT=6 + + _agkozak_prompt_string + + fi + + _agkozak_debug_print "Using async method: $AGKOZAK_ASYNC_METHOD" +} + +# Clean up environment +unfunction _agkozak_load_async_lib _agkozak_has_usr1 _agkozak_is_ssh \ + _agkozak_async_init + +# vim: ts=2:et:sts=2:sw=2:ROMPT='%~%<< $(git_prompt_info)${PR_BOLD_WHITE}>%{${reset_color}%} ' diff --git a/common/config/zsh/user/prompt_simple.zsh b/common/config/zsh/user/prompt_simple.zsh new file mode 100644 index 0000000..0bbad44 --- /dev/null +++ b/common/config/zsh/user/prompt_simple.zsh @@ -0,0 +1,227 @@ +# vim:ft=zsh ts=2 sw=2 sts=2 +# +### Segment drawing +# A few utility functions to make it easy and re-usable to draw segmented prompts +CURRENT_BG='NONE' + +case ${SOLARIZED_THEME:-dark} in + light) CURRENT_FG='white';; + *) CURRENT_FG='black';; +esac + +# Segments +() { + local LC_ALL="" LC_CTYPE="en_US.UTF-8" + SEGMENT_SEPARATOR= +} + +# Begin a segment +# Takes two arguments, background and foreground. Both can be omitted, +# rendering default background/foreground. +prompt_segment() { + local bg fg + [[ -n $2 ]] && fg="$FG[254]" || fg="%f" + if [[ $CURRENT_BG != 'NONE' && $1 != $CURRENT_BG ]]; then + echo -n " %{$bg%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR%{$fg%} " + else + echo -n "%{$bg%}%{$fg%}" + fi + CURRENT_BG=$1 + [[ -n $3 ]] && echo -n $3 +} + +# End the prompt, closing any open segments +prompt_end() { + if [[ -n $CURRENT_BG ]]; then + echo -n " %{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR" + else + echo -n "%{%k%}" + fi + echo -n "%{%f%}" + CURRENT_BG='' +} + +### Prompt components +# Each component will draw itself, and hide itself if no information needs to be shown + +# Context: user@hostname (who am I and where am I) +prompt_context() { } + +parse_git_dirty() { + local -a git_status + git_status=($(git status --porcelain 2>/dev/null)) + if [[ ${#git_status[@]} -gt 0 ]]; then + echo "±" + fi +} + +# Git: branch/detached head, dirty status +prompt_git() { + (( $+commands[git] )) || return + if [[ "$(git config --get oh-my-zsh.hide-status 2>/dev/null)" = 1 ]]; then + return + fi + local PL_BRANCH_CHAR + () { + local LC_ALL="" LC_CTYPE="en_US.UTF-8" + PL_BRANCH_CHAR=$'' # + } + local ref dirty mode repo_path + + if [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]]; then + repo_path=$(git rev-parse --git-dir 2>/dev/null) + dirty=$(parse_git_dirty) + ref=$(git symbolic-ref HEAD 2> /dev/null) || ref="➦ $(git rev-parse --short HEAD 2> /dev/null)" + if [[ -n $dirty ]]; then + prompt_segment yellow black + PL_BRANCH_CHAR=%{%F{yellow}%}'' + else + prompt_segment green $CURRENT_FG + fi + + if [[ -e "${repo_path}/BISECT_LOG" ]]; then + mode=" <B>" + elif [[ -e "${repo_path}/MERGE_HEAD" ]]; then + mode=" >M<" + elif [[ -e "${repo_path}/rebase" || -e "${repo_path}/rebase-apply" || -e "${repo_path}/rebase-merge" || -e "${repo_path}/../.dotest" ]]; then + mode=" >R>" + fi + + setopt promptsubst + autoload -Uz vcs_info + + zstyle ':vcs_info:*' enable git + zstyle ':vcs_info:*' get-revision true + zstyle ':vcs_info:*' check-for-changes true + zstyle ':vcs_info:*' stagedstr '✚' + zstyle ':vcs_info:*' unstagedstr '± ' + zstyle ':vcs_info:*' formats ' %u%c' + zstyle ':vcs_info:*' actionformats ' %u%c' + vcs_info + echo -n "${ref/refs\/heads\//$PL_BRANCH_CHAR }${vcs_info_msg_0_%% }${mode}" + fi +} + +prompt_bzr() { + (( $+commands[bzr] )) || return + + # Test if bzr repository in directory hierarchy + local dir="$PWD" + while [[ ! -d "$dir/.bzr" ]]; do + [[ "$dir" = "/" ]] && return + dir="${dir:h}" + done + + local bzr_status status_mod status_all revision + if bzr_status=$(bzr status 2>&1); then + status_mod=$(echo -n "$bzr_status" | head -n1 | grep "modified" | wc -m) + status_all=$(echo -n "$bzr_status" | head -n1 | wc -m) + revision=$(bzr log -r-1 --log-format line | cut -d: -f1) + if [[ $status_mod -gt 0 ]] ; then + prompt_segment yellow black "bzr@$revision ✚" + else + if [[ $status_all -gt 0 ]] ; then + prompt_segment yellow black "bzr@$revision" + else + prompt_segment green black "bzr@$revision" + fi + fi + fi +} + +prompt_hg() { + (( $+commands[hg] )) || return + local rev st branch + if $(hg id >/dev/null 2>&1); then + if $(hg prompt >/dev/null 2>&1); then + if [[ $(hg prompt "{status|unknown}") = "?" ]]; then + # if files are not added + prompt_segment red white + st='±' + elif [[ -n $(hg prompt "{status|modified}") ]]; then + # if any modification + prompt_segment yellow black + st='±' + else + # if working copy is clean + prompt_segment green $CURRENT_FG + fi + echo -n $(hg prompt "☿ {rev}@{branch}") $st + else + st="" + rev=$(hg id -n 2>/dev/null | sed 's/[^-0-9]//g') + branch=$(hg id -b 2>/dev/null) + if `hg st | grep -q "^\?"`; then + prompt_segment red black + st='±' + elif `hg st | grep -q "^[MA]"`; then + prompt_segment yellow black + st='±' + else + prompt_segment green $CURRENT_FG + fi + echo -n "☿ $rev@$branch" $st + fi + fi +} + +# Change prompt for HOME dir +prompt_dir () { + if [[ "$PWD" == "$HOME" ]]; then + prompt_segment blue $CURRENT_FG '' + else + prompt_segment blue CURRENT_FG '%2~' + fi +} + +# Virtualenv: current working virtualenv +prompt_virtualenv() { + local virtualenv_path="$VIRTUAL_ENV" + if [[ -n $virtualenv_path && -n $VIRTUAL_ENV_DISABLE_PROMPT ]]; then + prompt_segment blue black "(`basename $virtualenv_path`)" + fi +} + +# Status: +# - was there an error +# - am I root +# - are there background jobs? +prompt_status() { + local -a symbols + + [[ $RETVAL -ne 0 ]] && symbols+=" %{%F{red}%}" + [[ $UID -eq 0 ]] && symbols+="%{%F{yellow}%}⚡" + [[ $(jobs -l | wc -l) -gt 0 ]] && symbols+="%{%F{cyan}%}⚙" + + [[ -n "$symbols" ]] && prompt_segment black default "$symbols" +} + +#AWS Profile: +# - display current AWS_PROFILE name +# - displays yellow on red if profile name contains 'production' or +# ends in '-prod' +# - displays black on green otherwise +prompt_aws() { + [[ -z "$AWS_PROFILE" || "$SHOW_AWS_PROMPT" = false ]] && return + case "$AWS_PROFILE" in + *-prod|*production*) prompt_segment red yellow "AWS: $AWS_PROFILE" ;; + *) prompt_segment green black "AWS: $AWS_PROFILE" ;; + esac +} + +## Main prompt +build_prompt() { + RETVAL=$? + prompt_status + prompt_virtualenv + prompt_aws + prompt_context + prompt_dir + prompt_git + prompt_bzr + prompt_hg + prompt_end +} + +PROMPT='%{%F{blue}%} %{%f%b%k%}$(build_prompt) ' +bindkey -M vicmd '\e[C' vi-forward-char # ESC + right arrow diff --git a/common/install.sh b/common/install.sh new file mode 100755 index 0000000..4a2b209 --- /dev/null +++ b/common/install.sh @@ -0,0 +1,3715 @@ +#!/usr/bin/env bash + +# Created By: srdusr +# Created On: Tue 06 Sep 2025 16:20:52 PM CAT +# Project: Dotfiles installation script + +# TODO: allow optional change user/password, also optional change root password, first check if they are the same (auto) + +# Dependencies: git, curl + +set -euo pipefail # Exit on error, undefined vars, pipe failures + +#====================================== +# Variables & Configuration +#====================================== + +# Color definitions +NOCOLOR='\033[0m' +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +WHITE='\033[0;37m' +BOLD='\033[1m' + +# Dotfiles configuration +DOTFILES_URL='https://github.com/srdusr/dotfiles.git' +DOTFILES_DIR="$HOME/.cfg" +LOG_FILE="$HOME/.local/share/dotfiles_install.log" +STATE_FILE="$HOME/.local/share/dotfiles_install_state" +BACKUP_DIR="$HOME/.dotfiles-backup-$(date +%Y%m%d-%H%M%S)" +PACKAGES_FILE="packages.yml" + +# Network connectivity check +CONNECTIVITY_CHECKED=false +INTERNET_AVAILABLE=false + +# Installation tracking +INSTALL_SUMMARY=() + +FAILED_ITEMS=() +SKIPPED_ITEMS=() +COMPLETED_STEPS=() + +# Script options +RESUME_MODE=false +UPDATE_MODE=false +VERBOSE_MODE=false +DRY_RUN=false +FORCE_MODE=false +ASK_MODE=false # New: ask for each step +INSTALL_MODE="ask" # ask, essentials, full, profile + +# Global variables for system detection +CFG_OS="" +DISTRO="" +PACKAGE_MANAGER="" +PACKAGE_UPDATE_CMD="" +PACKAGE_INSTALL_CMD="" +PRIVILEGE_TOOL="" +PRIVILEGE_CACHED=false + +# Essential tools needed by this script +ESSENTIAL_TOOLS=("git" "curl" "wget") +PACKAGE_TOOLS=("yq" "jq") + +# Config command tracking +CONFIG_COMMAND_AVAILABLE=false +CONFIG_COMMAND_FILE="" + +# Steps can be skipped by providing a comma-separated list in SKIP_STEPS +SKIP_STEPS="${SKIP_STEPS:-}" + +# Run control: run only a specific step, or start from a specific step +RUN_ONLY_STEP="${RUN_ONLY_STEP:-}" +RUN_FROM_STEP="${RUN_FROM_STEP:-}" +__RUN_FROM_STARTED=false + +# Interactive per-step prompt even without --ask (opt-in) +# Set INTERACTIVE_SKIP=true to be prompted for non-essential steps. +INTERACTIVE_SKIP="${INTERACTIVE_SKIP:-false}" + +# Steps considered essential (should rarely be skipped) +ESSENTIAL_STEPS=( + setup_environment + check_connectivity + detect_package_manager + install_dependencies +) + +is_step_skipped() { + local step="$1" + [[ ",${SKIP_STEPS}," == *",${step},"* ]] +} + +skip_step_if_requested() { + local step="$1" + if is_step_skipped "$step"; then + print_skip "Skipping step by request: $step" + mark_step_completed "$step" + return 1 + fi + return 0 +} + +should_run_step() { + local step="$1" + # If RUN_ONLY_STEP is set, only run that exact step + if [[ -n "$RUN_ONLY_STEP" && "$step" != "$RUN_ONLY_STEP" ]]; then + print_skip "Skipping step (RUN_ONLY_STEP=$RUN_ONLY_STEP): $step" + return 1 + fi + # If RUN_FROM_STEP is set, skip until we reach it, then run subsequent steps + if [[ -n "$RUN_FROM_STEP" && "$__RUN_FROM_STARTED" != true ]]; then + if [[ "$step" == "$RUN_FROM_STEP" ]]; then + __RUN_FROM_STARTED=true + else + print_skip "Skipping step until RUN_FROM_STEP=$RUN_FROM_STEP: $step" + return 1 + fi + fi + return 0 +} + +# Installation profiles +declare -A INSTALLATION_PROFILES=( + ["essentials"]="Essential packages only (git, curl, wget, vim, zsh)" + ["minimal"]="Minimal setup for basic development" + ["dev"]="Full development environment" + ["server"]="Server configuration" + ["full"]="Complete installation with all packages" +) + +# Installation steps configuration +declare -A INSTALLATION_STEPS=( + ["setup_environment"]="Setup installation environment" + ["check_connectivity"]="Check internet connectivity" + ["detect_package_manager"]="Detect or configure package manager" + ["install_dependencies"]="Install dependencies" + ["install_dotfiles"]="Install dotfiles repository" + ["setup_user_dirs"]="Setup user directories" + ["install_essentials"]="Install essential tools" + ["install_packages"]="Install system packages" + ["setup_shell"]="Setup shell environment" + ["setup_ssh"]="Setup SSH configuration" + ["configure_services"]="Configure system services" + ["setup_development_environment"]="Setup development environment" + ["apply_tweaks"]="Apply system tweaks" + ["deploy_config"]="Deploy config command and dotfiles" +) + +# Step order +STEP_ORDER=( + "setup_environment" + "check_connectivity" + "detect_package_manager" + "install_dependencies" + "install_dotfiles" + "deploy_config" + "setup_user_dirs" + "install_essentials" + "install_packages" + "setup_shell" + "setup_ssh" + "configure_services" + "setup_development_environment" + "apply_tweaks" +) + +#====================================== +# State Management Functions +#====================================== + +save_state() { + local current_step="$1" + local status="$2" + + mkdir -p "$(dirname "$STATE_FILE")" + + { + echo "LAST_STEP=$current_step" + echo "STEP_STATUS=$status" + echo "TIMESTAMP=$(date +%s)" + echo "RESUME_AVAILABLE=true" + echo "PRIVILEGE_CACHED=$PRIVILEGE_CACHED" + echo "INSTALL_MODE=$INSTALL_MODE" + echo "COMPLETED_STEPS=(${COMPLETED_STEPS[*]})" + echo "CFG_OS=$CFG_OS" + echo "DISTRO=${DISTRO:-}" + echo "PACKAGE_MANAGER=${PACKAGE_MANAGER:-}" + echo "PRIVILEGE_TOOL=${PRIVILEGE_TOOL:-}" + echo "CONNECTIVITY_CHECKED=$CONNECTIVITY_CHECKED" + echo "INTERNET_AVAILABLE=$INTERNET_AVAILABLE" + } > "$STATE_FILE" +} + +load_state() { + if [[ -f "$STATE_FILE" ]]; then + source "$STATE_FILE" + return 0 + else + return 1 + fi +} + +clear_state() { + [[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE" +} + +is_step_completed() { + local step="$1" + [[ " ${COMPLETED_STEPS[*]} " =~ " ${step} " ]] +} + +mark_step_completed() { + local step="$1" + if ! is_step_completed "$step"; then + COMPLETED_STEPS+=("$step") + fi + save_state "$step" "completed" +} + +mark_step_failed() { + local step="$1" + save_state "$step" "failed" +} + +#====================================== +# UI Functions +#====================================== + +print_color() { + local color="$1" + local message="$2" + echo -e "${color}${message}${NOCOLOR}" + + if [[ -n "${LOG_FILE:-}" && -f "$LOG_FILE" ]]; then + echo "$(date +'%Y-%m-%d %H:%M:%S') - $message" >> "$LOG_FILE" + fi +} + +print_header() { + local title="$1" + local border_char="=" + local border_length=60 + + echo + print_color "$CYAN" "$(printf '%*s' $border_length '' | tr ' ' "$border_char")" + print_color "$CYAN$BOLD" "$(printf '%*s' $(((border_length + ${#title}) / 2)) "$title")" + print_color "$CYAN" "$(printf '%*s' $border_length '' | tr ' ' "$border_char")" + echo +} + +print_section() { + local title="$1" + echo + print_color "$BLUE$BOLD" "▶ $title" + print_color "$BLUE" "$(printf '%*s' $((${#title} + 2)) '' | tr ' ' '-')" +} + +print_success() { + local message="$1" + print_color "$GREEN" "✓ $message" + INSTALL_SUMMARY+=("✓ $message") +} + +print_error() { + local message="$1" + print_color "$RED" "✗ $message" >&2 + FAILED_ITEMS+=("✗ $message") +} + +print_warning() { + local message="$1" + print_color "$YELLOW" "⚠ $message" +} + +print_info() { + local message="$1" + if [[ "$VERBOSE_MODE" == true ]] || [[ "${2:-}" == "always" ]]; then + print_color "$CYAN" "ℹ $message" + fi +} + +print_skip() { + local message="$1" + print_color "$YELLOW" "⏭ $message" + SKIPPED_ITEMS+=("⏭ $message") +} + +print_dry_run() { + local message="$1" + print_color "$CYAN" "[DRY RUN] $message" +} + +#====================================== +# Network Connectivity Functions +#====================================== + +check_internet_connectivity() { + if [[ "$CONNECTIVITY_CHECKED" == true ]]; then + return $([[ "$INTERNET_AVAILABLE" == true ]] && echo 0 || echo 1) + fi + + print_section "Checking Internet Connectivity" + + local test_urls=("8.8.8.8" "1.1.1.1" "google.com" "github.com") + + for url in "${test_urls[@]}"; do + if ping -c 1 -W 2 "$url" &>/dev/null || curl -s --connect-timeout 5 "https://$url" &>/dev/null; then + INTERNET_AVAILABLE=true + CONNECTIVITY_CHECKED=true + print_success "Internet connectivity confirmed" + return 0 + fi + done + + INTERNET_AVAILABLE=false + CONNECTIVITY_CHECKED=true + print_error "No internet connectivity detected" + + # Try to connect to WiFi or prompt user + attempt_network_connection + + return 1 +} + +attempt_network_connection() { + print_warning "Attempting to establish network connection..." + + # Try NetworkManager + if command_exists nmcli; then + print_info "Available WiFi networks:" + nmcli device wifi list 2>/dev/null || print_warning "Could not list WiFi networks" + + if prompt_user "Would you like to connect to a WiFi network?"; then + print_color "$YELLOW" "Enter WiFi network name (SSID): " + read -r wifi_ssid + if [[ -n "$wifi_ssid" ]]; then + print_color "$YELLOW" "Enter WiFi password: " + read -rs wifi_password + echo + + if execute_with_privilege "nmcli device wifi connect '$wifi_ssid' password '$wifi_password'"; then + print_success "Connected to WiFi network: $wifi_ssid" + # Re-check connectivity + CONNECTIVITY_CHECKED=false + check_internet_connectivity + return $? + else + print_error "Failed to connect to WiFi network" + fi + fi + fi + fi + + # Try other connection methods + if command_exists iwctl; then + print_info "You can also connect manually using iwctl" + fi + + return 1 +} + +#====================================== +# System Detection Functions +#====================================== + +detect_os() { + case "$(uname -s)" in + Linux) CFG_OS="linux" ;; + Darwin) CFG_OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) CFG_OS="windows" ;; + *) CFG_OS="unknown" ;; + esac + + print_info "Detected OS: $CFG_OS" "always" +} + +detect_privilege_tools() { + if [[ "$(id -u)" -eq 0 ]]; then + PRIVILEGE_TOOL="" + print_info "Running as root, no privilege escalation needed" + return 0 + fi + + for tool in sudo doas pkexec; do + if command -v "$tool" &>/dev/null; then + PRIVILEGE_TOOL="$tool" + print_success "Using privilege escalation tool: $PRIVILEGE_TOOL" + return 0 + fi + done + + print_warning "No privilege escalation tool found (sudo, doas, pkexec)" + PRIVILEGE_TOOL="" + return 1 +} + +test_privilege_access() { + if [[ "$PRIVILEGE_CACHED" == true ]]; then + return 0 + fi + + if [[ -z "$PRIVILEGE_TOOL" ]]; then + return 0 # Running as root or no privilege needed + fi + + print_info "Testing privilege access..." + if "$PRIVILEGE_TOOL" -v &>/dev/null || echo "test" | "$PRIVILEGE_TOOL" -S true &>/dev/null; then + PRIVILEGE_CACHED=true + print_success "Privilege access confirmed" + return 0 + else + print_error "Failed to obtain privilege access" + return 1 + fi +} + +detect_package_manager() { + print_section "Detecting Package Manager" + save_state "detect_package_manager" "started" + + # First try to detect from OS release files + if [[ "$CFG_OS" == "linux" && -f /etc/os-release ]]; then + source /etc/os-release + case "$ID" in + arch|manjaro|endeavouros|artix) + DISTRO="$ID" + PACKAGE_MANAGER="pacman" + PACKAGE_UPDATE_CMD="pacman -Sy" + PACKAGE_INSTALL_CMD="pacman -S --noconfirm" + ;; + debian|ubuntu|mint|pop|elementary|zorin) + DISTRO="$ID" + PACKAGE_MANAGER="apt" + PACKAGE_UPDATE_CMD="apt-get update" + PACKAGE_INSTALL_CMD="apt-get install -y" + ;; + fedora|rhel|centos|rocky|almalinux) + DISTRO="$ID" + PACKAGE_MANAGER="dnf" + PACKAGE_UPDATE_CMD="dnf check-update" + PACKAGE_INSTALL_CMD="dnf install -y" + ;; + opensuse*|sles) + DISTRO="$ID" + PACKAGE_MANAGER="zypper" + PACKAGE_UPDATE_CMD="zypper refresh" + PACKAGE_INSTALL_CMD="zypper install -y" + ;; + gentoo) + DISTRO="$ID" + PACKAGE_MANAGER="portage" + PACKAGE_UPDATE_CMD="emerge --sync" + PACKAGE_INSTALL_CMD="emerge" + ;; + alpine) + DISTRO="$ID" + PACKAGE_MANAGER="apk" + PACKAGE_UPDATE_CMD="apk update" + PACKAGE_INSTALL_CMD="apk add" + ;; + void) + DISTRO="$ID" + PACKAGE_MANAGER="xbps" + PACKAGE_UPDATE_CMD="xbps-install -S" + PACKAGE_INSTALL_CMD="xbps-install -y" + ;; + nixos) + DISTRO="$ID" + PACKAGE_MANAGER="nix" + PACKAGE_UPDATE_CMD="nix-channel --update" + PACKAGE_INSTALL_CMD="nix-env -iA nixpkgs." + ;; + esac + elif [[ "$CFG_OS" == "macos" ]]; then + DISTRO="macos" + if command -v brew &>/dev/null; then + PACKAGE_MANAGER="brew" + PACKAGE_UPDATE_CMD="brew update" + PACKAGE_INSTALL_CMD="brew install" + else + PACKAGE_MANAGER="brew-install" + fi + fi + + # Fallback: detect by available commands + if [[ -z "$PACKAGE_MANAGER" ]]; then + local managers=( + "pacman:pacman:pacman -Sy:pacman -S --noconfirm" + "apt:apt:apt-get update:apt-get install -y" + "dnf:dnf:dnf check-update:dnf install -y" + "yum:yum:yum check-update:yum install -y" + "zypper:zypper:zypper refresh:zypper install -y" + "emerge:portage:emerge --sync:emerge" + "apk:apk:apk update:apk add" + "xbps-install:xbps:xbps-install -S:xbps-install -y" + "nix-env:nix:nix-channel --update:nix-env -iA nixpkgs." + "pkg:pkg:pkg update:pkg install -y" + "brew:brew:brew update:brew install" + ) + + for manager in "${managers[@]}"; do + local cmd="${manager%%:*}" + local name="${manager#*:}"; name="${name%%:*}" + local update_cmd="${manager#*:*:}"; update_cmd="${update_cmd%%:*}" + local install_cmd="${manager##*:}" + + if command -v "$cmd" &>/dev/null; then + PACKAGE_MANAGER="$name" + PACKAGE_UPDATE_CMD="$update_cmd" + PACKAGE_INSTALL_CMD="$install_cmd" + break + fi + done + fi + + if [[ -n "$PACKAGE_MANAGER" ]]; then + print_success "Detected package manager: $PACKAGE_MANAGER" + [[ -n "$DISTRO" ]] && print_info "Distribution: $DISTRO" + + # Try to override commands from packages.yml -> package_managers + # Find packages.yml in standard locations + local original_dir="$PWD" + cd "$HOME" 2>/dev/null || true + local packages_files=("$PACKAGES_FILE" "common/$PACKAGES_FILE" ".cfg/common/$PACKAGES_FILE") + local found_packages_file="" + for pf in "${packages_files[@]}"; do + if [[ -f "$pf" ]]; then + found_packages_file="$pf" + break + fi + done + cd "$original_dir" 2>/dev/null || true + + if command_exists yq && [[ -n "$found_packages_file" ]]; then + # Prefer distro block, fallback to manager block + # Initialize to avoid set -u (nounset) issues before assignment + local pm_update="" pm_install="" + if [[ -n "$DISTRO" ]]; then + pm_update=$(yq eval ".package_managers.${DISTRO}.update" "$found_packages_file" 2>/dev/null | grep -v "^null$" || echo "") + pm_install=$(yq eval ".package_managers.${DISTRO}.install" "$found_packages_file" 2>/dev/null | grep -v "^null$" || echo "") + fi + if [[ -z "$pm_update" || -z "$pm_install" ]]; then + pm_update=$(yq eval ".package_managers.${PACKAGE_MANAGER}.update" "$found_packages_file" 2>/dev/null | grep -v "^null$" || echo "") + pm_install=$(yq eval ".package_managers.${PACKAGE_MANAGER}.install" "$found_packages_file" 2>/dev/null | grep -v "^null$" || echo "") + fi + if [[ -n "$pm_update" && -n "$pm_install" ]]; then + PACKAGE_UPDATE_CMD="$pm_update" + PACKAGE_INSTALL_CMD="$pm_install" + print_info "Using package manager commands from packages.yml" + fi + fi + + # Export for compatibility with packages.yml custom commands that reference CFG_DISTRO + export CFG_DISTRO="$DISTRO" + + mark_step_completed "detect_package_manager" + return 0 + else + print_error "Could not detect package manager" + manual_package_manager_setup + return $? + fi +} + +manual_package_manager_setup() { + print_warning "No supported package manager detected automatically" + print_info "Please provide package manager commands manually:" + + while true; do + print_color "$YELLOW" "Enter package update command (e.g., 'apt-get update'): " + read -r PACKAGE_UPDATE_CMD + [[ -n "$PACKAGE_UPDATE_CMD" ]] && break + print_warning "Update command cannot be empty" + done + + while true; do + print_color "$YELLOW" "Enter package install command (e.g., 'apt-get install -y'): " + read -r PACKAGE_INSTALL_CMD + [[ -n "$PACKAGE_INSTALL_CMD" ]] && break + print_warning "Install command cannot be empty" + done + + PACKAGE_MANAGER="manual" + print_success "Manual package manager configuration set" + print_info "Update command: $PACKAGE_UPDATE_CMD" + print_info "Install command: $PACKAGE_INSTALL_CMD" + + mark_step_completed "detect_package_manager" + return 0 +} + +#====================================== +# Utility Functions +#====================================== + + +command_exists() { + command -v "$1" &>/dev/null +} + +execute_command() { + local cmd="$*" + + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "$cmd" + return 0 + fi + + if [[ "$VERBOSE_MODE" == true ]]; then + print_info "Running: $cmd" + fi + + eval "$cmd" +} + +execute_with_privilege() { + local cmd="$*" + + if [[ "$DRY_RUN" == true ]]; then + if [[ -n "$PRIVILEGE_TOOL" ]]; then + print_dry_run "$PRIVILEGE_TOOL $cmd" + else + print_dry_run "$cmd" + fi + return 0 + fi + + if [[ -n "$PRIVILEGE_TOOL" ]]; then + if [[ "$PRIVILEGE_CACHED" != true ]]; then + test_privilege_access || return 1 + fi + eval "$PRIVILEGE_TOOL $cmd" + else + eval "$cmd" + fi +} + +prompt_user() { + local question="$1" + local default="${2:-Y}" + local response + + if [[ "$FORCE_MODE" == true ]]; then + print_info "Auto-answering '$question' with: $default" + [[ "$default" =~ ^[Yy] ]] && return 0 || return 1 + fi + + while true; do + if [[ "$default" == "Y" ]]; then + printf "%b%s%b" "$YELLOW" "$question [Y/n]: " "$NOCOLOR" + else + printf "%b%s%b" "$YELLOW" "$question [y/N]: " "$NOCOLOR" + fi + + read -r response + + if [[ -z "$response" ]]; then + response="$default" + fi + + case "${response^^}" in + Y|YES) echo; return 0 ;; + N|NO) echo; return 1 ;; + *) echo; print_warning "Please answer Y/yes or N/no" ;; + esac + done +} + +create_dir() { + local dir="$1" + local permissions="${2:-755}" + + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "Create directory: $dir (mode: $permissions)" + return 0 + fi + + if [[ ! -d "$dir" ]]; then + mkdir -p "$dir" || { + print_error "Failed to create directory: $dir" + return 1 + } + chmod "$permissions" "$dir" + print_success "Created directory: $dir" + else + print_info "Directory already exists: $dir" + fi +} + +setup_logging() { + local log_dir + log_dir="$(dirname "$LOG_FILE")" + + if [[ ! -d "$log_dir" ]]; then + mkdir -p "$log_dir" || { + print_error "Failed to create log directory: $log_dir" + exit 1 + } + fi + + { + echo "=======================================" + echo "Dotfiles Installation Log" + echo "Date: $(date)" + echo "User: $USER" + echo "Host: ${HOSTNAME:-$(hostname)}" + echo "OS: $(uname -s)" + echo "Install Mode: $INSTALL_MODE" + echo "=======================================" + echo + } > "$LOG_FILE" + + print_info "Log file initialized: $LOG_FILE" "always" +} + +get_package_names() { + local package="$1" + local packages_file="${2:-}" + + # If packages.yml is available, check for distribution-specific mappings + if [[ -n "$packages_file" ]] && [[ -f "$packages_file" ]] && command_exists yq; then + local distro_packages="" + + # Try to get package name(s) for current distribution + case "$DISTRO" in + arch|manjaro|endeavouros|artix) + distro_packages=$(yq eval ".arch.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + debian|ubuntu|mint|pop|elementary|zorin) + distro_packages=$(yq eval ".debian.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + fedora|rhel|centos|rocky|almalinux) + distro_packages=$(yq eval ".rhel.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + opensuse*|sles) + distro_packages=$(yq eval ".opensuse.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + gentoo) + distro_packages=$(yq eval ".gentoo.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + alpine) + distro_packages=$(yq eval ".alpine.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + void) + distro_packages=$(yq eval ".void.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + macos) + # macOS uses array format, check if package exists in the list + if yq eval ".macos[]" "$packages_file" 2>/dev/null | grep -q "^$package$"; then + distro_packages="$package" + fi + ;; + esac + + # Return the distribution-specific package name(s) if found + if [[ -n "$distro_packages" ]]; then + echo "$distro_packages" + return 0 + fi + fi + + # Fallback to original package name + echo "$package" +} + +get_package_use_flags() { + local package="$1" + local packages_file="${2:-}" + + # Only relevant for Gentoo/Portage + if [[ "$PACKAGE_MANAGER" != "portage" ]]; then + echo "" + return 0 + fi + + if [[ -n "$packages_file" ]] && [[ -f "$packages_file" ]] && command_exists yq; then + local use_flags + use_flags=$(yq eval ".gentoo_use_flags.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + echo "$use_flags" + else + echo "" + fi +} + +#====================================== +# Dependency Installation Functions +#====================================== + +install_dependencies_if_missing() { + print_section "Checking for dependencies git, wget/curl" + save_state "install_dependencies" "started" + + local missing_deps=() + local failed_deps=() + + # Check for missing essential tools + for tool in "${ESSENTIAL_TOOLS[@]}"; do + if ! command_exists "$tool"; then + missing_deps+=("$tool") + fi + done + + # If everything is already present, skip with a clear message + if [[ ${#missing_deps[@]} -eq 0 ]]; then + print_skip "All required dependencies are already installed" + mark_step_completed "install_dependencies" + return 0 + fi + + # If no internet and dependencies are missing, try offline packages + if [[ "$INTERNET_AVAILABLE" != true ]] && [[ ${#missing_deps[@]} -gt 0 ]]; then + print_warning "No internet connection available" + print_info "Attempting to install dependencies from local packages..." + + # Try to install from local package cache + for tool in "${missing_deps[@]}"; do + if install_package_offline "$tool"; then + print_success "Installed $tool from local cache" + else + failed_deps+=("$tool") + fi + done + elif [[ ${#missing_deps[@]} -gt 0 ]]; then + # Online installation + print_info "Installing missing dependencies: ${missing_deps[*]}" + update_package_database + + for tool in "${missing_deps[@]}"; do + if install_single_package "$tool" "dependency"; then + print_success "Installed dependency: $tool" + else + failed_deps+=("$tool") + fi + done + fi + + if [[ ${#failed_deps[@]} -gt 0 ]]; then + print_error "Failed to install dependencies: ${failed_deps[*]}" + mark_step_failed "install_dependencies" + return 1 + else + print_success "Dependencies satisfied: ${missing_deps[*]}" + mark_step_completed "install_dependencies" + return 0 + fi +} + +install_package_offline() { + local package="$1" + + case "$PACKAGE_MANAGER" in + pacman) + # Check if package is in cache + if execute_with_privilege "pacman -U /var/cache/pacman/pkg/${package}-*.pkg.tar.*" 2>/dev/null; then + return 0 + fi + ;; + apt) + # Try from local cache + if execute_with_privilege "apt-get install --no-download '$package'" 2>/dev/null; then + return 0 + fi + ;; + esac + + return 1 +} + +#====================================== +# Package Management Functions +#====================================== + + +install_single_package() { + local package="$1" + local package_type="${2:-system}" + local packages_file="${3:-}" + + # Get the correct package name(s) for this distro - can be multiple packages + local pkg_names + pkg_names=$(get_package_names "$package" "$packages_file") + + # Get USE flags for Gentoo + local use_flags + use_flags=$(get_package_use_flags "$package" "$packages_file") + + print_info "Installing $package_type package: $package -> $pkg_names" + + # Handle multiple packages + local install_success=true + for pkg_name in $pkg_names; do + print_info "Installing: $pkg_name" + + case "$PACKAGE_MANAGER" in + pacman) + execute_with_privilege "$PACKAGE_INSTALL_CMD '$pkg_name'" || install_success=false + ;; + apt) + execute_with_privilege "$PACKAGE_INSTALL_CMD '$pkg_name'" || install_success=false + ;; + dnf|yum) + execute_with_privilege "$PACKAGE_INSTALL_CMD '$pkg_name'" || install_success=false + ;; + zypper) + execute_with_privilege "$PACKAGE_INSTALL_CMD '$pkg_name'" || install_success=false + ;; + portage) + local emerge_cmd="$PACKAGE_INSTALL_CMD" + if [[ -n "$use_flags" ]]; then + emerge_cmd="USE='$use_flags' $PACKAGE_INSTALL_CMD" + print_info "Using USE flags for $pkg_name: $use_flags" + fi + execute_with_privilege "$emerge_cmd '$pkg_name'" || install_success=false + ;; + apk) + execute_with_privilege "$PACKAGE_INSTALL_CMD '$pkg_name'" || install_success=false + ;; + xbps) + execute_with_privilege "$PACKAGE_INSTALL_CMD '$pkg_name'" || install_success=false + ;; + nix) + execute_command "$PACKAGE_INSTALL_CMD$pkg_name" || install_success=false + ;; + brew) + execute_command "$PACKAGE_INSTALL_CMD '$pkg_name'" || install_success=false + ;; + brew-install) + print_error "Homebrew not installed. Please install it first." + return 1 + ;; + manual) + execute_with_privilege "$PACKAGE_INSTALL_CMD '$pkg_name'" || install_success=false + ;; + *) + print_error "Package manager '$PACKAGE_MANAGER' not supported" + return 1 + ;; + esac + done + + return $([[ "$install_success" == true ]] && echo 0 || echo 1) +} + +update_package_database() { + print_info "Updating package database..." + + case "$PACKAGE_MANAGER" in + pacman) + execute_with_privilege "$PACKAGE_UPDATE_CMD" ;; + apt) + execute_with_privilege "$PACKAGE_UPDATE_CMD" ;; + dnf|yum) + execute_with_privilege "$PACKAGE_UPDATE_CMD" || true ;; + zypper) + execute_with_privilege "$PACKAGE_UPDATE_CMD" ;; + portage) + execute_with_privilege "$PACKAGE_UPDATE_CMD" ;; + apk) + execute_with_privilege "$PACKAGE_UPDATE_CMD" ;; + xbps) + execute_with_privilege "$PACKAGE_UPDATE_CMD" ;; + brew) + execute_command "$PACKAGE_UPDATE_CMD" ;; + manual) + execute_with_privilege "$PACKAGE_UPDATE_CMD" ;; + *) + print_info "Package database update not needed for $PACKAGE_MANAGER" ;; + esac +} + +install_homebrew() { + if command_exists brew; then + print_info "Homebrew already installed" + return 0 + fi + + print_info "Installing Homebrew..." + if execute_command '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'; then + print_success "Homebrew installed" + PACKAGE_MANAGER="brew" + + # Add to PATH for current session + if [[ -f "/opt/homebrew/bin/brew" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + elif [[ -f "/usr/local/bin/brew" ]]; then + eval "$(/usr/local/bin/brew shellenv)" + fi + return 0 + else + print_error "Failed to install Homebrew" + return 1 + fi +} + +install_yq() { + if command_exists yq; then + print_info "yq already installed" + return 0 + fi + + print_info "Installing yq..." + + local bin_dir="$HOME/.local/bin" + create_dir "$bin_dir" + + local yq_path="$bin_dir/yq" + local yq_url="" + + case "$(uname -m)" in + x86_64|amd64) + yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" ;; + aarch64|arm64) + yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64" ;; + armv7l) + yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm" ;; + *) + print_error "Unsupported architecture: $(uname -m)" + return 1 ;; + esac + + if execute_command "curl -L '$yq_url' -o '$yq_path'"; then + execute_command "chmod +x '$yq_path'" + + # Add to PATH if not already there + if [[ ":$PATH:" != *":$bin_dir:"* ]]; then + export PATH="$bin_dir:$PATH" + fi + + print_success "yq installed successfully" + return 0 + else + print_error "Failed to install yq" + return 1 + fi +} + +parse_packages_from_yaml() { + local packages_file="$1" + local section="$2" + local packages=() + + if [[ ! -f "$packages_file" ]]; then + print_warning "Package file not found: $packages_file" + return 1 + fi + + if ! command_exists yq; then + print_error "yq not available for parsing packages.yml" + return 1 + fi + + # Try to parse packages from the specified section + if yq eval ".$section" "$packages_file" &>/dev/null; then + mapfile -t packages < <(yq eval ".$section[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + fi + + # Output packages (one per line) + printf '%s\n' "${packages[@]}" +} + +get_profile_package_groups() { + local packages_file="$1" + local profile="$2" + local groups=() + + if [[ ! -f "$packages_file" ]]; then + print_warning "Package file not found: $packages_file" + return 1 + fi + + # Get package groups for the profile from the profiles section + if yq eval ".profiles.$profile.packages" "$packages_file" &>/dev/null; then + mapfile -t groups < <(yq eval ".profiles.$profile.packages[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + fi + + # Fallback to old method if profiles section doesn't exist + if [[ ${#groups[@]} -eq 0 ]]; then + case "$profile" in + essentials) + groups=("common" "essentials") ;; + minimal) + groups=("common" "essentials" "minimal") ;; + dev) + groups=("common" "essentials" "minimal" "dev") ;; + server) + groups=("common" "essentials" "minimal" "server") ;; + full) + groups=("common" "essentials" "minimal" "dev" "server" "desktop" "wm" "media" "fonts") ;; + *) + print_error "Unknown profile: $profile" + return 1 + ;; + esac + fi + + printf '%s\n' "${groups[@]}" +} + +install_packages_from_yaml() { + local packages_file="$1" + local profile="${2:-essentials}" + local failed_packages=() + local installed_count=0 + + print_section "Installing Packages (Profile: $profile)" + + if [[ ! -f "$packages_file" ]]; then + print_warning "Package file not found: $packages_file, skipping package installation" + return 0 + fi + + # Get package groups to install based on profile + local groups + mapfile -t groups < <(get_profile_package_groups "$packages_file" "$profile") + + if [[ ${#groups[@]} -eq 0 ]]; then + print_error "No package groups found for profile: $profile" + return 1 + fi + + print_info "Installing package groups for $profile: ${groups[*]}" + + # Install packages from each group + for group in "${groups[@]}"; do + print_info "Installing packages from group: $group" + + local packages + mapfile -t packages < <(parse_packages_from_yaml "$packages_file" "$group") + + if [[ ${#packages[@]} -eq 0 ]]; then + print_info "No packages found in group: $group" + continue + fi + + print_info "Found ${#packages[@]} packages in group $group: ${packages[*]}" + + for package in "${packages[@]}"; do + [[ -z "$package" ]] && continue + + if install_single_package "$package" "$group" "$packages_file"; then + print_success "Installed: $package" + ((installed_count++)) + else + print_error "Failed to install: $package" + failed_packages+=("$package") + fi + done + done + + # Handle development environment setup + if yq eval ".profiles.$profile.enable_development" "$packages_file" 2>/dev/null | grep -q "true"; then + setup_development_environment "$packages_file" + fi + + print_info "Package installation summary:" + print_color "$GREEN" " Installed: $installed_count" + print_color "$RED" " Failed: ${#failed_packages[@]}" + + if [[ ${#failed_packages[@]} -gt 0 ]]; then + print_warning "Failed packages: ${failed_packages[*]}" + print_info "Failed packages will be listed in the final summary" + return 0 + else + print_success "All packages installed successfully" + return 0 + fi +} + +#====================================== +# Dotfiles Management System (Config Command) +#====================================== + +check_existing_config_command() { + print_info "Checking for existing config command..." + + # Known function files where config might already be defined + local function_files=( + "$HOME/.config/zsh/user/functions.zsh" + "$HOME/.config/zsh/.zshrc" + "$HOME/.zshrc" + "$HOME/.bashrc" + "$HOME/.profile" + ) + + # Check if config command is already available in current shell + if type config >/dev/null 2>&1; then + CONFIG_COMMAND_AVAILABLE=true + print_success "Config command already available in current shell" + return 0 + fi + + # Check files for existing config function definition + for f in "${function_files[@]}"; do + if [[ -f "$f" ]]; then + if grep -q '^\s*config\s*()' "$f" || grep -q '# Dotfiles Management System' "$f"; then + CONFIG_COMMAND_AVAILABLE=true + CONFIG_COMMAND_FILE="$f" + print_success "Config command found in: $f" + return 0 + fi + fi + done + + CONFIG_COMMAND_AVAILABLE=false + print_info "No existing config command found" + return 1 +} + +install_config_command() { + print_section "Installing Config Command" + + if check_existing_config_command; then + if [[ "$FORCE_MODE" == true ]]; then + print_info "Force mode: reinstalling config command" + else + return 0 + fi + fi + + # Determine current shell and profile file + local current_shell + current_shell=$(basename "$SHELL") + + local profile_file="" + case "$current_shell" in + bash) + if [[ -f "$HOME/.bashrc" ]]; then + profile_file="$HOME/.bashrc" + else + profile_file="$HOME/.bashrc" + touch "$profile_file" + fi + ;; + zsh) + if [[ -f "$HOME/.config/zsh/user/functions.zsh" ]]; then + profile_file="$HOME/.config/zsh/user/functions.zsh" + elif [[ -f "$HOME/.config/zsh/.zshrc" ]]; then + profile_file="$HOME/.config/zsh/.zshrc" + elif [[ -f "$HOME/.zshrc" ]]; then + profile_file="$HOME/.zshrc" + else + profile_file="$HOME/.zshrc" + touch "$profile_file" + fi + ;; + *) + if [[ -f "$HOME/.profile" ]]; then + profile_file="$HOME/.profile" + else + profile_file="$HOME/.profile" + touch "$profile_file" + fi + ;; + esac + + if [[ ! -w "$profile_file" ]]; then + print_error "Cannot write to profile file: $profile_file" + return 1 + fi + + # Check if config function already exists in the target file + if grep -q "# Dotfiles Management System" "$profile_file" 2>/dev/null; then + print_info "Config function already exists in $profile_file" + CONFIG_COMMAND_AVAILABLE=true + CONFIG_COMMAND_FILE="$profile_file" + return 0 + fi + + print_info "Adding config function to: $profile_file" + + # Add the config function + cat >> "$profile_file" << 'EOF' + +# Dotfiles Management System +if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then + # Core git wrapper with repository as work-tree + _config() { + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" "$@" + } + + # Detect OS + case "$(uname -s)" in + Linux) CFG_OS="linux" ;; + Darwin) CFG_OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) CFG_OS="windows" ;; + *) CFG_OS="other" ;; + esac + + # Map system path to repository path + _repo_path() { + local f="$1" + + # If it's an absolute path that's not in HOME, handle it specially + if [[ "$f" == /* && "$f" != "$HOME/"* ]]; then + echo "$CFG_OS/${f#/}" + return + fi + + # Check for paths that should go to the repository root + case "$f" in + common/*|linux/*|macos/*|windows/*|profile/*|README.md) + echo "$f" + return + ;; + "$HOME/"*) + f="${f#$HOME/}" + ;; + esac + + # Default: put under OS-specific home + echo "$CFG_OS/home/$f" + } + + _sys_path() { + local repo_path="$1" + local os_path_pattern="$CFG_OS/" + + # Handle OS-specific files that are not in the home subdirectory + if [[ "$repo_path" == "$os_path_pattern"* && "$repo_path" != */home/* ]]; then + echo "/${repo_path#$os_path_pattern}" + return + fi + + case "$repo_path" in + # Common configs → OS-specific config dirs + common/config/*) + case "$CFG_OS" in + linux) + local base="${XDG_CONFIG_HOME:-$HOME/.config}" + echo "$base/${repo_path#common/config/}" + ;; + macos) + echo "$HOME/Library/Application Support/${repo_path#common/config/}" + ;; + windows) + echo "$LOCALAPPDATA\\${repo_path#common/config/}" + ;; + *) + echo "$HOME/.config/${repo_path#common/config/}" + ;; + esac + ;; + + # Common assets → stay in repo + common/assets/*) + echo "$HOME/.cfg/$repo_path" + ;; + + # Other common files (dotfiles like .bashrc, .gitconfig, etc.) → $HOME + common/*) + echo "$HOME/${repo_path#common/}" + ;; + + # OS-specific home + */home/*) + echo "$HOME/${repo_path#*/home/}" + ;; + + # Profile configs and README → stay in repo + profile/*|README.md) + echo "$HOME/.cfg/$repo_path" + ;; + + # Default fallback + *) + echo "$HOME/.cfg/$repo_path" + ;; + + esac + } + + # Prompts for sudo if needed and runs the command + _sudo_prompt() { + if [[ $EUID -eq 0 ]]; then + "$@" + else + if command -v sudo >/dev/null; then + sudo "$@" + elif command -v doas >/dev/null; then + doas "$@" + elif command -v pkexec >/dev/null; then + pkexec "$@" + else + echo "Error: No privilege escalation tool found." + return 1 + fi + fi + } + + # Main config command + config() { + local cmd="$1"; shift + local target_dir="" + + # Parse optional --target flag for add + if [[ "$cmd" == "add" ]]; then + while [[ "$1" == --* ]]; do + case "$1" in + --target|-t) + target_dir="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + return 1 + ;; + esac + done + fi + + case "$cmd" in + add) + local file_path + for file_path in "$@"; do + local repo_path + if [[ -n "$target_dir" ]]; then + local rel_path + if [[ "$file_path" == /* ]]; then + rel_path="$(basename "$file_path")" + else + rel_path="$file_path" + fi + repo_path="$target_dir/$rel_path" + else + repo_path="$(_repo_path "$file_path")" + fi + + local full_repo_path="$HOME/.cfg/$repo_path" + mkdir -p "$(dirname "$full_repo_path")" + cp -a "$file_path" "$full_repo_path" + + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" add "$repo_path" + + echo "Added: $file_path -> $repo_path" + done + ;; + rm) + local rm_opts="" + local file_path_list=() + + for arg in "$@"; do + if [[ "$arg" == "-"* ]]; then + rm_opts+=" $arg" + else + file_path_list+=("$arg") + fi + done + + for file_path in "${file_path_list[@]}"; do + local repo_path="$(_repo_path "$file_path")" + + if [[ "$rm_opts" == *"-r"* ]]; then + _config rm --cached -r "$repo_path" + else + _config rm --cached "$repo_path" + fi + + eval "rm $rm_opts \"$file_path\"" + echo "Removed: $file_path" + done + ;; + sync) + local direction="${1:-to-repo}"; shift + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ "$direction" == "to-repo" ]]; then + if [[ -e "$sys_file" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + cp -a "$sys_file" "$full_repo_path" + echo "Synced to repo: $sys_file" + fi + elif [[ "$direction" == "from-repo" ]]; then + if [[ -e "$full_repo_path" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + local dest_dir="$(dirname "$sys_file")" + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + echo "Synced from repo: $sys_file" + fi + fi + done + ;; + status) + local auto_synced=() + while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ -e "$sys_file" && -e "$full_repo_path" ]]; then + if ! diff -q "$full_repo_path" "$sys_file" >/dev/null 2>&1; then + cp -fa "$sys_file" "$full_repo_path" + auto_synced+=("$repo_file") + fi + fi + done < <(_config ls-files) + if [[ ${#auto_synced[@]} -gt 0 ]]; then + echo "=== Auto-synced Files ===" + for repo_file in "${auto_synced[@]}"; do + echo "synced: $(_sys_path "$repo_file") -> $repo_file" + done + echo + fi + _config status + echo + ;; + deploy) + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" + + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if needed + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Deployed: $repo_file -> $sys_file" + fi + done + ;; + checkout) + echo "Checking out dotfiles from .cfg..." + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" + + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if it doesn't exist + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Checked out: $repo_file -> $sys_file" + fi + done + ;; + backup) + local timestamp=$(date +%Y%m%d%H%M%S) + local backup_dir="$HOME/.dotfiles_backup/$timestamp" + echo "Backing up existing dotfiles to $backup_dir..." + + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + if [[ -e "$sys_file" ]]; then + local dest_dir_full="$backup_dir/$(dirname "$repo_file")" + mkdir -p "$dest_dir_full" + cp -a "$sys_file" "$backup_dir/$repo_file" + fi + done + echo "Backup complete. To restore, copy files from $backup_dir to their original locations." + ;; + *) + _config "$cmd" "$@" + ;; + esac + } +fi +EOF + + if [[ $? -eq 0 ]]; then + print_success "Config command added to: $profile_file" + CONFIG_COMMAND_AVAILABLE=true + CONFIG_COMMAND_FILE="$profile_file" + + # Source the file to make config command available immediately + # shellcheck disable=SC1090 + source "$profile_file" 2>/dev/null || print_warning "Failed to source $profile_file" + + return 0 + else + print_error "Failed to add config command to $profile_file" + return 1 + fi +} + +deploy_config() { + print_section "Deploying Configuration" + save_state "deploy_config" "started" + + # Ensure config command is available + if [[ "$CONFIG_COMMAND_AVAILABLE" != true ]]; then + install_config_command || { + print_error "Failed to install config command" + mark_step_failed "deploy_config" + return 1 + } + fi + + # Deploy dotfiles from repository to system + if [[ -d "$DOTFILES_DIR" ]]; then + print_info "Checking out dotfiles from repository..." + + # First, checkout files from the bare repository to restore directory structure + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "config checkout" + else + # Source the config function if available + if type config >/dev/null 2>&1; then + print_info "Using config command to checkout files..." + if config checkout; then + print_success "Files checked out from repository" + else + print_warning "Some files may have failed to checkout, trying force checkout..." + config checkout -f || print_warning "Force checkout also had issues" + fi + else + # Fallback: use git directly + print_info "Using git directly to checkout files..." + # IMPORTANT: use $HOME/.cfg as work-tree, never the bare repo path + if git --git-dir="$DOTFILES_DIR" --work-tree="$HOME/.cfg" checkout HEAD -- . 2>/dev/null; then + print_success "Files checked out using git directly" + else + print_warning "Git checkout had issues, continuing anyway..." + fi + fi + fi + + # Backup existing files prior to deployment (prompt, allow skip) + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "Backup existing dotfiles prior to deployment" + else + if [[ "$FORCE_MODE" == true ]]; then + # In force mode, perform backup without prompting + backup_existing_dotfiles || print_warning "Backup encountered issues (continuing)" + else + if prompt_user "Backup existing dotfiles before deployment?"; then + backup_existing_dotfiles || print_warning "Backup encountered issues (continuing)" + else + print_skip "User chose to skip backup before deployment" + fi + fi + fi + + print_info "Deploying dotfiles from repository to system locations..." + + # Verify config command is working + if ! verify_config_command; then + print_warning "Config command not working properly, using manual deployment" + manual_deploy_dotfiles + else + print_info "Config command available, deploying files..." + + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "config deploy" + else + # Use the config function to deploy files + if config deploy; then + print_success "Dotfiles deployed successfully" + else + print_warning "Some files may have failed to deploy" + fi + fi + fi + + # Set appropriate permissions + set_dotfile_permissions + + else + print_warning "Dotfiles directory not found, skipping deployment" + fi + + mark_step_completed "deploy_config" +} + +verify_config_command() { + # Always verify the function is actually available in this shell + if type config >/dev/null 2>&1; then + CONFIG_COMMAND_AVAILABLE=true + print_success "Config command is available and working" + return 0 + fi + # Try sourcing the detected profile file if known + if [[ -n "$CONFIG_COMMAND_FILE" && -f "$CONFIG_COMMAND_FILE" ]]; then + # shellcheck disable=SC1090 + source "$CONFIG_COMMAND_FILE" 2>/dev/null || true + if type config >/dev/null 2>&1; then + CONFIG_COMMAND_AVAILABLE=true + print_success "Config command is available and working" + return 0 + fi + fi + print_warning "Config command not available" + return 1 +} + +# Manual deployment function (fallback when config command not available) +manual_deploy_dotfiles() { + print_info "Using manual deployment method..." + + if [[ ! -d "$DOTFILES_DIR" ]]; then + print_error "Dotfiles directory not found: $DOTFILES_DIR" + return 1 + fi + + # Source locations are always within the checked-out work-tree ($HOME/.cfg) + local os_dir="$HOME/.cfg/$CFG_OS" + local common_dir="$HOME/.cfg/common" + + deploy_file() { + local repo_file="$1" + local rel_path sys_file sys_dir base + + # Determine destination based on repo path + rel_path="${repo_file#$DOTFILES_DIR/}" + + # OS-specific files outside home + if [[ "$rel_path" == "$CFG_OS/"* && "$rel_path" != */home/* ]]; then + sys_file="/${rel_path#$CFG_OS/}" + else + case "$rel_path" in + common/config/*) + case "$CFG_OS" in + linux) + base="${XDG_CONFIG_HOME:-$HOME/.config}" + sys_file="$base/${rel_path#common/config/}" + ;; + macos) + sys_file="$HOME/Library/Application Support/${rel_path#common/config/}" + ;; + windows) + sys_file="$LOCALAPPDATA\\${rel_path#common/config/}" + ;; + *) + sys_file="$HOME/.config/${rel_path#common/config/}" + ;; + esac + ;; + common/assets/*) + # Assets are repo-internal; do not deploy to filesystem + return 0 + ;; + common/*) + sys_file="$HOME/${rel_path#common/}" + ;; + */home/*) + sys_file="$HOME/${rel_path#*/home/}" + ;; + profile/*|README.md) + sys_file="$HOME/.cfg/$rel_path" + ;; + *) + sys_file="$HOME/.cfg/$rel_path" + ;; + esac + fi + + sys_dir="$(dirname "$sys_file")" + mkdir -p "$sys_dir" + + # Avoid copying if source and destination resolve to the same file + local src_real dst_real + src_real=$(readlink -f -- "$repo_file" 2>/dev/null || echo "$repo_file") + dst_real=$(readlink -f -- "$sys_file" 2>/dev/null || echo "$sys_file") + if [[ -n "$dst_real" && "$src_real" == "$dst_real" ]]; then + print_skip "Skipping self-copy: $rel_path" + return 0 + fi + + # Copy with privilege if path is system (/etc, /usr, etc.) + if [[ "$sys_file" == /* ]]; then + # If we lack a privilege tool and are not root, skip with clear message + if [[ -z "$PRIVILEGE_TOOL" && "$EUID" -ne 0 ]]; then + print_skip "Skipping privileged deploy (no sudo/doas): $rel_path -> $sys_file" + else + execute_with_privilege "cp -a '$repo_file' '$sys_file'" \ + && print_info "Deployed (privileged): $rel_path" \ + || print_error "Failed to deploy (privileged): $rel_path" + fi + else + cp -a "$repo_file" "$sys_file" \ + && print_info "Deployed: $rel_path" \ + || print_error "Failed to deploy: $rel_path" + fi + } + + # Deploy all files in OS dir + if [[ -d "$os_dir" ]]; then + find "$os_dir" -type f | while read -r f; do + deploy_file "$f" + done + fi + + # Deploy all files in common dir + if [[ -d "$common_dir" ]]; then + find "$common_dir" -type f | while read -r f; do + deploy_file "$f" + done + fi +} + +# Set appropriate file permissions +set_dotfile_permissions() { + print_info "Setting appropriate file permissions..." + + # SSH directory permissions + if [[ -d "$HOME/.ssh" ]]; then + chmod 700 "$HOME/.ssh" + find "$HOME/.ssh" -name "id_*" -not -name "*.pub" -exec chmod 600 {} \; 2>/dev/null || true + find "$HOME/.ssh" -name "*.pub" -exec chmod 644 {} \; 2>/dev/null || true + find "$HOME/.ssh" -name "config" -exec chmod 600 {} \; 2>/dev/null || true + print_info "SSH permissions set" + fi + + # GPG directory permissions + if [[ -d "$HOME/.gnupg" ]]; then + chmod 700 "$HOME/.gnupg" + find "$HOME/.gnupg" -type f -exec chmod 600 {} \; 2>/dev/null || true + print_info "GPG permissions set" + fi + + # Make scripts executable + if [[ -d "$HOME/.local/bin" ]]; then + find "$HOME/.local/bin" -type f -exec chmod +x {} \; 2>/dev/null || true + print_info "Script permissions set" + fi + + if [[ -d "$HOME/.scripts" ]]; then + find "$HOME/.scripts" -type f -name "*.sh" -exec chmod +x {} \; 2>/dev/null || true + print_info "Shell script permissions set" + fi +} + +#====================================== +# Installation Step Functions +#====================================== + +setup_environment() { + print_section "Setting Up Environment" + save_state "setup_environment" "started" + + detect_os + detect_privilege_tools + detect_package_manager || { + print_error "Cannot proceed without a supported package manager" + mark_step_failed "setup_environment" + return 1 + } + + if [[ -n "$PRIVILEGE_TOOL" ]]; then + test_privilege_access || { + print_error "Cannot obtain necessary privileges" + mark_step_failed "setup_environment" + return 1 + } + fi + + mark_step_completed "setup_environment" +} + +check_connectivity() { + print_section "Checking Connectivity" + save_state "check_connectivity" "started" + + if check_internet_connectivity; then + mark_step_completed "check_connectivity" + return 0 + else + print_warning "Limited internet connectivity - some features may be unavailable" + mark_step_completed "check_connectivity" + return 0 # Don't fail completely + fi +} + +install_dependencies() { + print_section "Installing Dependencies" + save_state "install_dependencies" "started" + + if install_dependencies_if_missing; then + mark_step_completed "install_dependencies" + return 0 + else + mark_step_failed "install_dependencies" + return 1 + fi +} + +install_dotfiles() { + print_section "Installing Dotfiles" + save_state "install_dotfiles" "started" + + local update=false + + # Check internet connectivity for git operations + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping dotfiles installation" + mark_step_completed "install_dotfiles" + return 0 + fi + + if [[ -d "$DOTFILES_DIR" ]]; then + if [[ "$UPDATE_MODE" == true ]] || prompt_user "Dotfiles repository already exists. Update it?"; then + print_info "Updating existing dotfiles..." + # Detect ahead/behind before pulling to avoid unexpected fast-forwards + execute_command "git --git-dir='$DOTFILES_DIR' fetch origin main" || true + local ahead behind ab_line + ahead=0; behind=0 + ab_line=$(git --git-dir="$DOTFILES_DIR" rev-list --left-right --count HEAD...origin/main 2>/dev/null || true) + # Expected format: "<ahead>\t<behind>"; parse safely + if [[ "$ab_line" =~ ^([0-9]+)[[:space:]]+([0-9]+)$ ]]; then + ahead="${BASH_REMATCH[1]}" + behind="${BASH_REMATCH[2]}" + fi + if [[ ${ahead:-0} -gt 0 && ${behind:-0} -eq 0 ]]; then + print_warning "Your local dotfiles are ahead of origin/main by $ahead commit(s)." + while true; do + echo + print_color "$YELLOW" "Choose an action for local-ahead state:" + echo " [k] Keep local (skip pull)" + echo " [p] Push local commits" + echo " [c] Commit new changes and push" + echo " [s] Stash uncommitted changes (if any) and pull" + echo " [a] Abort" + printf "%b%s%b" "$YELLOW" "Enter choice [k/p/c/s/a]: " "$NOCOLOR" + read -r choice + case "${choice,,}" in + k) + print_warning "Keeping local commits; skipping pull" + break + ;; + p) + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' push origin HEAD:main"; then + print_success "Pushed local commits" + else + print_error "Push failed" + fi + break + ;; + c) + print_info "Committing changes before push..." + printf "%b%s%b" "$YELLOW" "Commit message (default: 'WIP local changes via installer'): " "$NOCOLOR" + read -r commit_msg + [[ -z "$commit_msg" ]] && commit_msg="WIP local changes via installer" + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' add -A" \ + && execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' commit -m \"$commit_msg\"" \ + && execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' push origin HEAD:main"; then + print_success "Committed and pushed" + else + print_error "Commit/push failed" + fi + break + ;; + s) + print_info "Stashing local (including untracked) before pull..." + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' stash push -u -m 'installer-stash'"; then + print_success "Stashed local changes" + else + print_error "Stash failed" + fi + break + ;; + a) + print_error "Aborted by user" + mark_step_failed "install_dotfiles" + return 1 + ;; + *) + print_warning "Invalid choice. Please enter k/p/c/s/a." + ;; + esac + done + fi + # If remote is ahead (fast-forward), ask the user before pulling + if [[ ${behind:-0} -gt 0 && ${ahead:-0} -eq 0 ]]; then + print_warning "Origin/main is ahead by $behind commit(s)." + if ! prompt_user "Fast-forward to origin/main now?"; then + print_skip "User chose not to fast-forward; skipping pull" + # Skip pull entirely + goto_after_pull=true + fi + fi + if [[ "${goto_after_pull:-false}" == true ]] || execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' pull origin main"; then + update=true + print_success "Dotfiles updated successfully" + else + print_error "Failed to pull updates" + # Interactive resolution for local changes + while true; do + echo + print_color "$YELLOW" "Local changes detected. Choose an action:" + echo " [c] Commit local changes" + echo " [s] Stash local changes" + echo " [k] Keep local changes (skip pulling)" + echo " [a] Abort" + printf "%b%s%b" "$YELLOW" "Enter choice [c/s/k/a]: " "$NOCOLOR" + read -r choice + case "${choice,,}" in + c) + print_info "Committing local changes..." + printf "%b%s%b" "$YELLOW" "Commit message (default: 'WIP local changes via installer'): " "$NOCOLOR" + read -r commit_msg + [[ -z "$commit_msg" ]] && commit_msg="WIP local changes via installer" + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' add -A" \ + && execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' commit -m \"$commit_msg\""; then + print_success "Committed local changes" + print_info "Retrying pull..." + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' pull origin main"; then + update=true; print_success "Dotfiles updated successfully"; break + else + print_error "Pull failed again after commit. You may resolve manually or choose another option." + fi + else + print_error "Commit failed. Try another option." + fi + ;; + s) + print_info "Stashing local changes..." + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' stash push -u -m 'installer-stash'"; then + print_success "Stashed local changes" + print_info "Retrying pull..." + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' pull origin main"; then + update=true; print_success "Dotfiles updated successfully"; break + else + print_error "Pull failed again after stash. You may resolve manually or choose another option." + fi + else + print_error "Stash failed. Try another option." + fi + ;; + k) + print_warning "Keeping local changes and skipping pull" + break + ;; + a) + print_error "Aborted by user" + mark_step_failed "install_dotfiles" + return 1 + ;; + *) + print_warning "Invalid choice. Please enter c/s/k/a." + ;; + esac + done + fi + else + print_skip "Skipping dotfiles update" + mark_step_completed "install_dotfiles" + return 0 + fi + else + print_info "Cloning dotfiles repository..." + if execute_command "git clone --bare '$DOTFILES_URL' '$DOTFILES_DIR'"; then + print_success "Dotfiles repository cloned" + else + print_error "Failed to clone dotfiles repository" + mark_step_failed "install_dotfiles" + return 1 + fi + fi + + # Configure the repository + execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' config status.showUntrackedFiles no" + + mark_step_completed "install_dotfiles" + print_success "Dotfiles installed successfully" +} + +setup_user_dirs() { + print_section "Setting Up User Directories" + save_state "setup_user_dirs" "started" + + local directories=('.cache' '.config' '.local/bin' '.local/share' '.scripts') + + for dir in "${directories[@]}"; do + create_dir "$HOME/$dir" + done + + # Set up XDG directories (ensure existence; no deletions) + if command_exists xdg-user-dirs-update; then + # Suppress tool output to avoid misleading terms like "removed"; we only ensure presence. + execute_command "xdg-user-dirs-update >/dev/null 2>&1 || true" + print_success "Ensured XDG user directories exist" + fi + + mark_step_completed "setup_user_dirs" +} + +install_essentials() { + print_section "Installing Essential Tools" + save_state "install_essentials" "started" + + # Fast-path: determine if any package tools are actually missing + local missing_tools=() + for tool in "${PACKAGE_TOOLS[@]}"; do + if [[ "$tool" == "yq" ]]; then + if command_exists yq || [[ -x "$HOME/.local/bin/yq" ]]; then + continue + fi + elif [[ "$tool" == "jq" ]]; then + if command_exists jq || is_package_installed jq; then + continue + fi + fi + if ! command_exists "$tool"; then + missing_tools+=("$tool") + fi + done + + if [[ ${#missing_tools[@]} -eq 0 ]]; then + print_skip "All essential tools are already installed" + mark_step_completed "install_essentials" + return 0 + fi + + # Install package processing tools first + for tool in "${PACKAGE_TOOLS[@]}"; do + if [[ "$tool" == "yq" ]]; then + if command_exists yq || [[ -x "$HOME/.local/bin/yq" ]]; then + print_info "Package tool already available: yq" + continue + fi + elif [[ "$tool" == "jq" ]]; then + if command_exists jq || is_package_installed jq; then + print_info "Package tool already available: jq" + continue + fi + fi + + if ! command_exists "$tool"; then + case "$tool" in + yq) + if install_yq; then + print_success "Installed package tool: $tool" + else + print_error "Failed to install package tool: $tool" + mark_step_failed "install_essentials" + return 1 + fi + ;; + jq) + if command_exists jq || is_package_installed jq; then + print_info "Package tool already available: jq" + elif install_single_package "jq" "essential"; then + print_success "Installed package tool: $tool" + else + print_error "Failed to install package tool: $tool" + mark_step_failed "install_essentials" + return 1 + fi + ;; + esac + else + print_info "Package tool already available: $tool" + fi + done + + mark_step_completed "install_essentials" +} + +install_packages() { + print_section "Installing Packages" + save_state "install_packages" "started" + + # Skip if essentials-only mode + if [[ "$INSTALL_MODE" == "essentials" ]]; then + print_skip "Package installation (essentials-only mode)" + mark_step_completed "install_packages" + return 0 + fi + + # Skip if no internet and packages require download + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping package installation" + mark_step_completed "install_packages" + return 0 + fi + + # Change to home directory to find packages.yml + local original_dir="$PWD" + cd "$HOME" 2>/dev/null || true + + # Look for packages.yml in common locations + local packages_files=("$PACKAGES_FILE" "common/$PACKAGES_FILE" ".cfg/common/$PACKAGES_FILE") + local found_packages_file="" + + for pf in "${packages_files[@]}"; do + if [[ -f "$pf" ]]; then + found_packages_file="$pf" + break + fi + done + + if [[ -n "$found_packages_file" ]]; then + # Handle custom installs first + handle_custom_installs "$found_packages_file" + + # Install packages + if install_packages_from_yaml "$found_packages_file" "$INSTALL_MODE"; then + mark_step_completed "install_packages" + else + print_warning "Some packages failed to install, but continuing..." + mark_step_completed "install_packages" + fi + else + print_warning "packages.yml not found, attempting to download from GitHub..." + + # Derive raw URL from DOTFILES_URL + # Supports formats like: + # https://github.com/<owner>/<repo>.git + # git@github.com:<owner>/<repo>.git + # https://github.com/<owner>/<repo> + local owner repo branch + branch="main" + case "$DOTFILES_URL" in + git@github.com:*) + owner="${DOTFILES_URL#git@github.com:}" + owner="${owner%.git}" + repo="${owner#*/}" + owner="${owner%%/*}" + ;; + https://github.com/*) + owner="${DOTFILES_URL#https://github.com/}" + owner="${owner%.git}" + repo="${owner#*/}" + owner="${owner%%/*}" + ;; + *) + owner="" + repo="" + ;; + esac + + local packages_url="" + if [[ -n "$owner" && -n "$repo" ]]; then + packages_url="https://raw.githubusercontent.com/$owner/$repo/$branch/common/packages.yml" + fi + local temp_packages="/tmp/packages.yml" + + if command_exists curl && [[ -n "$packages_url" ]]; then + if curl -fsSL "$packages_url" -o "$temp_packages" 2>/dev/null; then + # Create common directory if it doesn't exist + mkdir -p "$HOME/.cfg/common" 2>/dev/null || mkdir -p "$HOME/common" 2>/dev/null + + # Move to appropriate location + if [[ -d "$HOME/.cfg/common" ]]; then + mv "$temp_packages" "$HOME/.cfg/common/packages.yml" + found_packages_file="$HOME/.cfg/common/packages.yml" + elif [[ -d "$HOME/common" ]]; then + mv "$temp_packages" "$HOME/common/packages.yml" + found_packages_file="$HOME/common/packages.yml" + else + mv "$temp_packages" "$HOME/packages.yml" + found_packages_file="$HOME/packages.yml" + fi + + print_success "Downloaded packages.yml from GitHub" + + # Now install packages with the downloaded file + handle_custom_installs "$found_packages_file" + if install_packages_from_yaml "$found_packages_file" "$INSTALL_MODE"; then + mark_step_completed "install_packages" + else + print_warning "Some packages failed to install, but continuing..." + mark_step_completed "install_packages" + fi + else + print_warning "Failed to download packages.yml, skipping package installation" + mark_step_completed "install_packages" + fi + else + print_warning "curl not available and packages.yml not found, skipping package installation" + mark_step_completed "install_packages" + fi + fi + + cd "$original_dir" 2>/dev/null || true +} + +setup_shell() { + print_section "Setting Up Shell Environment" + save_state "setup_shell" "started" + + # Ensure config command is available before changing shells + if [[ "$CONFIG_COMMAND_AVAILABLE" != true ]]; then + print_warning "Config command not available, installing it first..." + install_config_command || { + print_error "Failed to install config command before shell setup" + mark_step_failed "setup_shell" + return 1 + } + fi + + if command_exists zsh; then + local zsh_path + zsh_path="$(command -v zsh)" + + if [[ "$FORCE_MODE" == true ]]; then + print_info "FORCE mode: changing default shell to Zsh without prompting" + if execute_with_privilege "chsh -s '$zsh_path' '$USER'"; then + print_success "Default shell changed to Zsh" + print_warning "Please log out and log back in to apply changes" + else + print_error "Failed to change default shell" + fi + elif [[ "$ASK_MODE" == true ]]; then + if prompt_user "Change default shell to Zsh?" "N"; then + if execute_with_privilege "chsh -s '$zsh_path' '$USER'"; then + print_success "Default shell changed to Zsh" + print_warning "Please log out and log back in to apply changes" + else + print_error "Failed to change default shell" + fi + else + print_skip "Default shell change (user chose No)" + fi + else + print_info "Skipping shell change (non-interactive mode). Use --ask to be prompted or --force to auto-change." + fi + else + print_warning "Zsh not installed, skipping shell setup" + fi + + # Zsh plugins are managed via packages.yml custom_installs (zsh_plugins) + # No direct plugin installation here to avoid duplication. + + mark_step_completed "setup_shell" +} + +## install_zsh_plugins deprecated; handled via packages.yml + +setup_ssh() { + print_section "Setting Up SSH" + save_state "setup_ssh" "started" + + local ssh_dir="$HOME/.ssh" + + if [[ ! -f "$ssh_dir/id_rsa" && ! -f "$ssh_dir/id_ed25519" ]]; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Generate SSH key pair?"; then + create_dir "$ssh_dir" 700 + + local email="${USER}@${HOSTNAME:-$(hostname)}" + local key_file="$ssh_dir/id_ed25519" + + if execute_command "ssh-keygen -t ed25519 -f '$key_file' -N '' -C '$email'"; then + print_success "SSH key pair generated (Ed25519)" + execute_command "chmod 600 '$key_file'" + execute_command "chmod 644 '$key_file.pub'" + + if [[ "$DRY_RUN" != true ]] && [[ -f "$key_file.pub" ]]; then + print_info "Your public key:" + print_color "$GREEN" "$(cat "$key_file.pub")" + print_info "Copy this key to your Git hosting service" + fi + else + print_error "Failed to generate SSH key" + mark_step_failed "setup_ssh" + return 1 + fi + fi + else + print_info "SSH key already exists" + fi + + mark_step_completed "setup_ssh" +} + +# Helper function to detect the init system +detect_init_system() { + if [ -d /run/systemd/system ]; then + echo "systemd" + elif command -v rc-service &>/dev/null; then + echo "openrc" + elif [ -d /etc/sv ]; then + echo "runit" + elif command -v service &>/dev/null; then + echo "sysvinit" + else + echo "unknown" + fi +} + +# Helper function to manage a service (enable/start) +manage_service() { + local action="$1" + local service="$2" + local init_system="$3" + # use numeric success code: 0=success, 1=failure + local success=1 + + case "$init_system" in + systemd) + # Resolve common generic service names to distro-specific systemd unit names + local svc_candidates=() + local lower_service + lower_service="${service,,}" + case "$lower_service" in + networkmanager) + svc_candidates+=("NetworkManager" "NetworkManager.service" "network-manager") + ;; + sshd) + # Debian uses 'ssh' service, others commonly use 'sshd' + svc_candidates+=("sshd" "ssh" "sshd.service" "ssh.service") + ;; + *) + svc_candidates+=("$service") + ;; + esac + + local tried=false + local rc=1 + for svc in "${svc_candidates[@]}"; do + tried=true + if [ "$action" == "enable" ]; then + # Prefer enabling and starting in one go when possible + if ! execute_command "$PRIVILEGE_TOOL systemctl enable --now '$svc'"; then + execute_command "$PRIVILEGE_TOOL systemctl enable '$svc'" + fi + rc=$? + elif [ "$action" == "start" ]; then + execute_command "$PRIVILEGE_TOOL systemctl start '$svc'" + rc=$? + else + rc=1 + fi + if [[ $rc -eq 0 ]]; then + success=0 + break + fi + print_warning "Failed to $action service candidate: $svc" + done + # If we didn't have a special mapping, fall back to original name once + if [[ "$tried" == false ]]; then + if [ "$action" == "enable" ]; then + execute_command "$PRIVILEGE_TOOL systemctl enable '$service'" + rc=$? + elif [ "$action" == "start" ]; then + execute_command "$PRIVILEGE_TOOL systemctl start '$service'" + rc=$? + fi + [[ $rc -eq 0 ]] && success=0 + fi + ;; + openrc) + if [ "$action" == "enable" ]; then + execute_command "$PRIVILEGE_TOOL rc-update add '$service' default" + success=$? + elif [ "$action" == "start" ]; then + execute_command "$PRIVILEGE_TOOL rc-service '$service' start" + success=$? + fi + ;; + runit) + if [ "$action" == "enable" ]; then + # Runit services are enabled by creating a symlink in the run level directory + execute_command "$PRIVILEGE_TOOL ln -sf /etc/sv/'$service' /var/service/" + success=$? + elif [ "$action" == "start" ]; then + # The 'start' action is usually implied by the symlink, but you can + # manually start it if needed + execute_command "$PRIVILEGE_TOOL sv start '$service'" + success=$? + fi + ;; + sysvinit|unknown) + # Use the generic 'service' command + if [ "$action" == "start" ]; then + execute_command "$PRIVILEGE_TOOL service '$service' start" + success=$? + fi + # Enabling is system-dependent for sysvinit/unknown; we'll check for chkconfig + if [ "$action" == "enable" ]; then + if command -v chkconfig &>/dev/null; then + execute_command "$PRIVILEGE_TOOL chkconfig '$service' on" + success=$? + else + success=0 + fi + fi + ;; + *) + print_error "Unknown init system: $init_system. Cannot $action service '$service'." + return 1 + ;; + esac + + return $success +} + +#====================================== +# Service Management Functions +#====================================== + +configure_services_from_yaml() { + local packages_file="$1" + local profile="$2" + + print_section "Configuring System Services" + save_state "configure_services" "started" + + if [[ "$CFG_OS" != "linux" ]]; then + print_skip "Service configuration (not supported on $CFG_OS)" + mark_step_completed "configure_services" + return 0 + fi + + if [[ ! -f "$packages_file" ]]; then + print_warning "Package file not found, skipping service configuration" + mark_step_completed "configure_services" + return 0 + fi + + # Detect the init system + local INIT_SYSTEM=$(detect_init_system) + print_info "Detected Init System: $INIT_SYSTEM" + + # Get services to enable for all profiles + local services_all + mapfile -t services_all < <(yq eval ".services.enable.all[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + + # Get services to enable for specific profile + local services_profile + mapfile -t services_profile < <(yq eval ".services.enable.$profile[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + + # Get services to disable for specific profile + local services_disable + mapfile -t services_disable < <(yq eval ".services.disable.$profile[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + + # Enable services + for service in "${services_all[@]}" "${services_profile[@]}"; do + [[ -z "$service" ]] && continue + if [[ "$FORCE_MODE" == true ]] || prompt_user "Enable $service service?"; then + if manage_service "enable" "$service" "$INIT_SYSTEM"; then + manage_service "start" "$service" "$INIT_SYSTEM" + print_success "Enabled and started $service" + else + print_error "Failed to enable $service" + fi + fi + done + + # Disable services + for service in "${services_disable[@]}"; do + [[ -z "$service" ]] && continue + if [[ "$FORCE_MODE" == true ]] || prompt_user "Disable $service service?"; then + if manage_service "stop" "$service" "$INIT_SYSTEM"; then + manage_service "disable" "$service" "$INIT_SYSTEM" + print_success "Stopped and disabled $service" + else + print_error "Failed to disable $service" + fi + fi + done + + mark_step_completed "configure_services" +} + +configure_services() { + # Change to home directory to find packages.yml + local original_dir="$PWD" + cd "$HOME" 2>/dev/null || true + + local packages_files=("$PACKAGES_FILE" "common/$PACKAGES_FILE" ".cfg/common/$PACKAGES_FILE") + local found_packages_file="" + + for pf in "${packages_files[@]}"; do + if [[ -f "$pf" ]]; then + found_packages_file="$pf" + break + fi + done + + if [[ -n "$found_packages_file" ]]; then + configure_services_from_yaml "$found_packages_file" "$INSTALL_MODE" + else + # Fallback to original configure_services logic + print_section "Configuring System Services" + save_state "configure_services" "started" + + if [[ "$CFG_OS" != "linux" ]]; then + print_skip "Service configuration (not supported on $CFG_OS)" + mark_step_completed "configure_services" + return 0 + fi + + # Original service configuration logic here... + mark_step_completed "configure_services" + fi + + cd "$original_dir" 2>/dev/null || true +} + +setup_tmux_plugins() { + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping Tmux plugins installation" + return 0 + fi + + local tpm_dir="$HOME/.config/tmux/plugins/tpm" + local plugins_dir="$HOME/.config/tmux/plugins" + + if [[ ! -f "$HOME/.tmux.conf" && ! -f "$HOME/.config/tmux/tmux.conf" ]]; then + print_info "Tmux config not found, skipping plugin setup" + return 0 + fi + + print_info "Setting up Tmux plugins..." + create_dir "$plugins_dir" + + if [[ ! -d "$tpm_dir" || ! "$(ls -A "$tpm_dir" 2>/dev/null)" ]]; then + print_info "Installing Tmux Plugin Manager (TPM)..." + if execute_command "git clone https://github.com/tmux-plugins/tpm '$tpm_dir'"; then + print_success "TPM installed successfully" + print_info "Run 'tmux' and press 'prefix + I' to install plugins" + else + print_error "Failed to install TPM" + fi + else + print_info "TPM already installed" + fi +} + +#====================================== +# Development Environment Setup +#====================================== + + +setup_development_environment() { + # Accept optional packages_file argument. If missing, try to locate a default. + local packages_file="${1:-}" + if [[ -z "$packages_file" ]]; then + local candidates=("$HOME/$PACKAGES_FILE" "$HOME/common/$PACKAGES_FILE" "$HOME/.cfg/common/$PACKAGES_FILE") + for pf in "${candidates[@]}"; do + if [[ -f "$pf" ]]; then + packages_file="$pf" + break + fi + done + fi + + print_info "Setting up development environment" + + if [[ -z "$packages_file" || ! -f "$packages_file" ]]; then + print_warning "Package file not found, skipping development setup" + return 0 + fi + + # Apply git configuration + local git_configs + if command_exists yq; then + mapfile -t git_configs < <(yq eval ".development.git_config[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + else + git_configs=() + fi + + if [[ ${#git_configs[@]} -gt 0 ]] && command_exists git; then + print_info "Applying git configuration" + for config in "${git_configs[@]}"; do + [[ -z "$config" ]] && continue + print_info "Running: $config" + execute_command "$config" + done + fi +} + +# Backup existing files that will be affected by deployment +backup_existing_dotfiles() { + local backup_root="$BACKUP_DIR/pre-deploy" + local os_dir="$DOTFILES_DIR/$CFG_OS" + local common_dir="$DOTFILES_DIR/common" + + print_info "Creating backup at: $backup_root" + mkdir -p "$backup_root" 2>/dev/null || true + + # Helper to compute destination path similar to manual_deploy_dotfiles + _compute_dest_path() { + local repo_file="$1" + local rel_path sys_file base + rel_path="${repo_file#$DOTFILES_DIR/}" + + if [[ "$rel_path" == "$CFG_OS/"* && "$rel_path" != */home/* ]]; then + sys_file="/${rel_path#$CFG_OS/}" + else + case "$rel_path" in + common/config/*) + case "$CFG_OS" in + linux) + base="${XDG_CONFIG_HOME:-$HOME/.config}" + sys_file="$base/${rel_path#common/config/}" + ;; + macos) + sys_file="$HOME/Library/Application Support/${rel_path#common/config/}" + ;; + windows) + sys_file="$LOCALAPPDATA\\${rel_path#common/config/}" + ;; + *) + sys_file="$HOME/.config/${rel_path#common/config/}" + ;; + esac + ;; + common/assets/*) + sys_file="$HOME/.cfg/$rel_path" + ;; + common/*) + sys_file="$HOME/${rel_path#common/}" + ;; + */home/*) + sys_file="$HOME/${rel_path#*/home/}" + ;; + profile/*|README.md) + sys_file="$HOME/.cfg/$rel_path" + ;; + *) + sys_file="$HOME/.cfg/$rel_path" + ;; + esac + fi + + echo "$sys_file" + } + + _backup_one() { + local repo_file="$1" + local dest + dest=$(_compute_dest_path "$repo_file") + [[ -z "$dest" ]] && return 0 + + if [[ -e "$dest" ]]; then + local rel_path="${repo_file#$DOTFILES_DIR/}" + local backup_path="$backup_root/$rel_path" + local backup_dir + backup_dir="$(dirname "$backup_path")" + mkdir -p "$backup_dir" 2>/dev/null || true + + if [[ "$dest" == /* ]]; then + execute_with_privilege "cp -a '$dest' '$backup_path'" \ + && print_info "Backed up (privileged): $rel_path" \ + || print_warning "Failed to backup (privileged): $rel_path" + else + cp -a "$dest" "$backup_path" \ + && print_info "Backed up: $rel_path" \ + || print_warning "Failed to backup: $rel_path" + fi + fi + } + + # Backup files from OS dir + if [[ -d "$os_dir" ]]; then + find "$os_dir" -type f | while read -r f; do + _backup_one "$f" + done + fi + + # Backup files from common dir + if [[ -d "$common_dir" ]]; then + find "$common_dir" -type f | while read -r f; do + _backup_one "$f" + done + fi + + print_success "Backup completed at: $backup_root" +} + +install_rust_development() { + local packages_file="$1" + + if ! command_exists rustc; then + install_rust + fi + + if command_exists cargo; then + print_info "Installing Rust components" + local components + mapfile -t components < <(yq eval ".development.rust.components[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + + for component in "${components[@]}"; do + [[ -z "$component" ]] && continue + execute_command "rustup component add $component" + done + fi +} + +install_nodejs_development() { + local packages_file="$1" + + if ! command_exists node; then + install_nvm + install_node + fi + + if command_exists npm; then + print_info "Installing global Node.js packages" + local packages + mapfile -t packages < <(yq eval ".development.nodejs.global_packages[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + + for package in "${packages[@]}"; do + [[ -z "$package" ]] && continue + execute_command "npm install -g $package" + done + fi +} + +install_python_development() { + local packages_file="$1" + + if command_exists pip || command_exists pip3; then + print_info "Installing global Python packages" + local packages + mapfile -t packages < <(yq eval ".development.python.global_packages[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + + local pip_cmd="pip3" + command_exists pip3 || pip_cmd="pip" + + for package in "${packages[@]}"; do + [[ -z "$package" ]] && continue + execute_command "$pip_cmd install --user $package" + done + fi +} + +get_git_email_guess() { + local email_guess="" + + # Try to get email from existing git config + if command_exists git; then + email_guess=$(git config --global user.email 2>/dev/null || echo "") + if [[ -n "$email_guess" ]]; then + echo "$email_guess" + return 0 + fi + fi + + # Try to extract from common email-related environment variables + for var in EMAIL MAIL USER_EMAIL GIT_AUTHOR_EMAIL GIT_COMMITTER_EMAIL; do + if [[ -n "${!var:-}" ]]; then + echo "${!var}" + return 0 + fi + done + + # Check for email in /etc/passwd gecos field + if [[ -f /etc/passwd ]]; then + local gecos + gecos=$(getent passwd "$USER" 2>/dev/null | cut -d: -f5 | cut -d, -f1) + if [[ "$gecos" == *@* ]]; then + echo "$gecos" + return 0 + fi + fi + + # Try to guess based on common patterns + local domain="" + + # Check if we can determine domain from hostname + if command_exists hostname; then + local fqdn + fqdn=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "") + if [[ "$fqdn" == *.* ]]; then + domain="${fqdn#*.}" + fi + fi + + # Fallback domain guessing + if [[ -z "$domain" ]]; then + if [[ -f /etc/mailname ]]; then + domain=$(cat /etc/mailname 2>/dev/null || echo "") + elif [[ -f /etc/hostname ]]; then + local hostname_file + hostname_file=$(cat /etc/hostname 2>/dev/null || echo "") + if [[ "$hostname_file" == *.* ]]; then + domain="${hostname_file#*.}" + fi + fi + fi + + # Final fallback + if [[ -z "$domain" ]]; then + domain="localhost" + fi + + echo "${USER}@${domain}" +} + +configure_git() { + local git_name="${USER}" + local git_email + git_email=$(get_git_email_guess) + + if [[ "$FORCE_MODE" != true ]]; then + print_color "$YELLOW" "Enter your Git username [$git_name]: " + read -r input_name + [[ -n "$input_name" ]] && git_name="$input_name" + + print_color "$YELLOW" "Enter your Git email [$git_email]: " + read -r input_email + [[ -n "$input_email" ]] && git_email="$input_email" + fi + + execute_command "git config --global user.name '$git_name'" + execute_command "git config --global user.email '$git_email'" + execute_command "git config --global init.defaultBranch main" + execute_command "git config --global pull.rebase false" + print_success "Git configured with name: $git_name, email: $git_email" +} + +install_development_tools() { + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping development tools installation" + return 0 + fi + + print_info "Installing development tools..." + + # Install Rust if not present + if ! command_exists rustc; then + install_rust + fi + + # Install Node.js via NVM if not present + if ! command_exists node; then + install_nvm + install_node + fi + + # Install Yarn if Node.js is available + if command_exists npm && ! command_exists yarn; then + install_yarn + fi +} + +install_rust() { + print_info "Installing Rust via rustup..." + + if command_exists rustup; then + print_info "Rust already installed" + return 0 + fi + + local cargo_home="${XDG_DATA_HOME:-$HOME/.local/share}/cargo" + local rustup_home="${XDG_DATA_HOME:-$HOME/.local/share}/rustup" + + create_dir "$(dirname "$cargo_home")" + + if execute_command "CARGO_HOME='$cargo_home' RUSTUP_HOME='$rustup_home' curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"; then + print_success "Rust installed successfully" + + # Add to PATH for current session + if [[ -f "$cargo_home/env" ]]; then + source "$cargo_home/env" + fi + + return 0 + else + print_error "Failed to install Rust" + return 1 + fi +} + +install_nvm() { + local nvm_dir="$HOME/.config/nvm" + + if [[ -d "$nvm_dir" && -f "$nvm_dir/nvm.sh" ]]; then + print_info "NVM already installed" + return 0 + fi + + print_info "Installing Node Version Manager (NVM)..." + create_dir "$nvm_dir" + + if execute_command "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | NVM_DIR='$nvm_dir' bash"; then + export NVM_DIR="$nvm_dir" + if [[ -s "$NVM_DIR/nvm.sh" ]]; then + source "$NVM_DIR/nvm.sh" + print_success "NVM installed successfully" + return 0 + else + print_error "NVM installation failed - script not found" + return 1 + fi + else + print_error "Failed to install NVM" + return 1 + fi +} + +install_node() { + if command_exists node; then + print_info "Node.js already installed" + return 0 + fi + + print_info "Installing Node.js..." + + # Source NVM if available + local nvm_dir="$HOME/.config/nvm" + if [[ -s "$nvm_dir/nvm.sh" ]]; then + export NVM_DIR="$nvm_dir" + source "$NVM_DIR/nvm.sh" + fi + + if command_exists nvm; then + if execute_command "nvm install --lts" && execute_command "nvm use --lts" && execute_command "nvm alias default lts/*"; then + print_success "Node.js installed successfully" + return 0 + else + print_error "Failed to install Node.js via NVM" + return 1 + fi + else + print_error "NVM not available for Node.js installation" + return 1 + fi +} + +install_yarn() { + print_info "Installing Yarn..." + + if execute_command "curl -o- -L https://yarnpkg.com/install.sh | bash"; then + print_success "Yarn installed successfully" + + # Add to PATH for current session + local yarn_bin="$HOME/.yarn/bin" + if [[ -d "$yarn_bin" && ":$PATH:" != *":$yarn_bin:"* ]]; then + export PATH="$yarn_bin:$PATH" + fi + + return 0 + else + print_error "Failed to install Yarn" + return 1 + fi +} + +#====================================== +# System Tweaks Functions +#====================================== + +apply_system_tweaks() { + local packages_file="$1" + + print_section "Applying System Tweaks" + + if [[ ! -f "$packages_file" ]]; then + print_warning "Package file not found, skipping system tweaks" + return 0 + fi + + # Detect desktop environment and apply appropriate tweaks + local desktop_env="" + if [[ "$XDG_CURRENT_DESKTOP" == *"GNOME"* ]] || command_exists gnome-shell; then + desktop_env="gnome" + elif [[ "$XDG_CURRENT_DESKTOP" == *"KDE"* ]] || command_exists plasmashell; then + desktop_env="kde" + fi + + if [[ -n "$desktop_env" ]]; then + print_info "Applying $desktop_env tweaks" + + # Get tweak commands for the desktop environment + local tweaks + mapfile -t tweaks < <(yq eval ".system_tweaks.$desktop_env[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + + for tweak in "${tweaks[@]}"; do + [[ -z "$tweak" ]] && continue + print_info "Applying tweak: $tweak" + if execute_command "$tweak"; then + print_success "Applied: $tweak" + else + print_warning "Failed to apply: $tweak" + fi + done + else + print_info "No supported desktop environment detected for tweaks" + fi + +} + +apply_tweaks() { + print_section "Applying System Tweaks" + save_state "apply_tweaks" "started" + + # Change to home directory to find packages.yml + local original_dir="$PWD" + cd "$HOME" 2>/dev/null || true + + local packages_files=("$PACKAGES_FILE" "common/$PACKAGES_FILE" ".cfg/common/$PACKAGES_FILE") + local found_packages_file="" + + for pf in "${packages_files[@]}"; do + if [[ -f "$pf" ]]; then + found_packages_file="$pf" + break + fi + done + + if [[ -n "$found_packages_file" ]]; then + apply_system_tweaks "$found_packages_file" + else + case "$CFG_OS" in + linux) + apply_linux_tweaks + ;; + macos) + apply_macos_tweaks + ;; + *) + print_info "No system tweaks defined for $CFG_OS" + ;; + esac + fi + + cd "$original_dir" 2>/dev/null || true + mark_step_completed "apply_tweaks" +} + +apply_linux_tweaks() { + # --- Locale tweak --- + if command -v localectl >/dev/null 2>&1; then + local current_locale + current_locale=$(localectl status | grep "System Locale" | cut -d= -f2 | cut -d, -f1) + if [[ "$current_locale" != "en_US.UTF-8" ]]; then + if prompt_user "Set system locale to en_US.UTF-8?"; then + if execute_with_privilege "localectl set-locale LANG=en_US.UTF-8"; then + print_success "Locale set to en_US.UTF-8" + else + print_error "Failed to set locale" + fi + fi + fi + fi + + # Desktop environment tweaks should be declared in packages.yml under system_tweaks. + print_info "Linux system tweaks applied (core). Desktop tweaks come from packages.yml." +} + +apply_macos_tweaks() { + print_info "macOS system tweaks applied (placeholder)" +} + +#====================================== +# Custom Installation Functions +#====================================== + +handle_custom_installs() { + local packages_file="$1" + + if [[ ! -f "$packages_file" ]] || ! command_exists yq; then + return 0 + fi + + print_info "Processing custom installations..." + + # Get custom install commands + local custom_installs + mapfile -t custom_installs < <(yq eval ".custom_installs | keys | .[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + + for install_name in "${custom_installs[@]}"; do + [[ -z "$install_name" ]] && continue + + # Check condition + local condition + condition=$(yq eval ".custom_installs.$install_name.condition" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + + if [[ -n "$condition" ]]; then + # Evaluate condition safely even under set -u (nounset) + local -i _had_nounset=0 + if set -o | grep -q "nounset\s*on"; then + _had_nounset=1 + set +u + fi + if ! eval "$condition" 2>/dev/null; then + if [[ $_had_nounset -eq 1 ]]; then set -u; fi + print_info "Skipping $install_name (condition not met)" + continue + fi + if [[ $_had_nounset -eq 1 ]]; then set -u; fi + fi + + # Get OS-specific command + local install_cmd="" + case "$CFG_OS" in + linux) + install_cmd=$(yq eval ".custom_installs.$install_name.linux" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + macos) + install_cmd=$(yq eval ".custom_installs.$install_name.macos" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + windows) + install_cmd=$(yq eval ".custom_installs.$install_name.windows" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + esac + + # Fallback to generic command + if [[ -z "$install_cmd" ]]; then + install_cmd=$(yq eval ".custom_installs.$install_name.command" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + fi + + if [[ -n "$install_cmd" ]]; then + print_info "Running custom install: $install_name" + if execute_command "$install_cmd"; then + print_success "Custom install completed: $install_name" + # If yq was installed into ~/.local/bin via custom install, ensure PATH includes it for current session + if [[ "$install_name" == "yq" && -x "$HOME/.local/bin/yq" ]]; then + if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" + print_info "Added $HOME/.local/bin to PATH for current session" + fi + fi + else + print_error "Custom install failed: $install_name" + fi + else + print_warning "No install command found for $install_name on $CFG_OS" + fi + done +} + +#====================================== +# Installation Mode Selection +#====================================== + + +detect_installation_mode() { + if [[ "$INSTALL_MODE" != "ask" ]]; then + return 0 # Mode already set via command line + fi + + # Check if this is a re-run + if [[ -d "$DOTFILES_DIR" && ! "$UPDATE_MODE" == true ]]; then + print_section "Existing Installation Detected" + print_info "Dotfiles repository already exists at: $DOTFILES_DIR" + + if [[ "$FORCE_MODE" == true ]]; then + print_info "Force mode: proceeding with update" + UPDATE_MODE=true + INSTALL_MODE="essentials" # Default to essentials for updates + else + while true; do + print_color "$YELLOW" "What would you like to do?" + print_color "$CYAN" "1. Update existing dotfiles and system" + print_color "$CYAN" "2. Full reinstallation" + print_color "$CYAN" "3. Exit" + print_color "$YELLOW" "Select option [1-3]: " + read -r response + + case "$response" in + 1) + UPDATE_MODE=true + INSTALL_MODE="essentials" + print_success "Update mode selected" + break + ;; + 2) + print_warning "This will backup and reinstall everything" + if prompt_user "Continue with full reinstallation?"; then + # Backup existing installation + local backup_timestamp=$(date +%Y%m%d-%H%M%S) + local backup_location="$HOME/.dotfiles-backup-$backup_timestamp" + print_info "Backing up existing installation to: $backup_location" + cp -r "$DOTFILES_DIR" "$backup_location" 2>/dev/null || true + break + else + continue + fi + ;; + 3) + print_info "Installation cancelled by user" + exit 0 + ;; + *) + print_warning "Invalid selection. Please enter 1-3" + ;; + esac + done + fi + fi + + # If still asking, show installation mode selection + if [[ "$INSTALL_MODE" == "ask" ]]; then + select_installation_mode + fi +} + +select_installation_mode() { + print_header "Installation Mode Selection" + + print_color "$CYAN" "Available installation modes:" + echo + + local mode_number=1 + local mode_options=() + + for mode in essentials minimal dev server full; do + local description="${INSTALLATION_PROFILES[$mode]}" + print_color "$YELLOW" "$mode_number. $mode - $description" + mode_options+=("$mode") + ((mode_number++)) + done + + echo + print_color "$CYAN" "You can also specify a custom profile from the profiles/ directory" + echo + + while true; do + print_color "$YELLOW" "Select installation mode [1-5] or enter profile name [dev]: " + read -r response + + if [[ -z "$response" ]]; then + INSTALL_MODE="dev" + break + elif [[ "$response" =~ ^[1-5]$ ]]; then + INSTALL_MODE="${mode_options[$((response-1))]}" + break + elif [[ "$response" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]; then + # Check if it's a valid profile + if [[ -f "profiles/$response.yml" ]] || [[ "${INSTALLATION_PROFILES[$response]:-}" ]]; then + INSTALL_MODE="$response" + break + else + print_warning "Profile '$response' not found" + fi + else + print_warning "Invalid selection. Please enter 1-5 or a profile name" + fi + done + + print_success "Selected installation mode: $INSTALL_MODE" + print_info "Description: ${INSTALLATION_PROFILES[$INSTALL_MODE]:-Custom profile}" +} + +#====================================== +# Ask Mode Implementation +#====================================== + +should_run_step() { + local step="$1" + local description="${INSTALLATION_STEPS[$step]}" + + # Respect explicit skip list + if is_step_skipped "$step"; then + return 1 + fi + + # Run-only and run-from controls + if [[ -n "$RUN_ONLY_STEP" && "$step" != "$RUN_ONLY_STEP" ]]; then + return 1 + fi + if [[ -n "$RUN_FROM_STEP" && "$__RUN_FROM_STARTED" != true ]]; then + if [[ "$step" == "$RUN_FROM_STEP" ]]; then + __RUN_FROM_STARTED=true + else + return 1 + fi + fi + + # Skip already completed steps unless forced + if is_step_completed "$step" && [[ "$FORCE_MODE" != true ]]; then + return 1 + fi + + # Ask mode prompt + if [[ "$ASK_MODE" == true ]]; then + prompt_user "Run step: $description?" && return 0 || return 1 + fi + + # Interactive skip even when not in ask mode (non-essential steps) + if [[ "$INTERACTIVE_SKIP" == true ]]; then + local is_essential=false + for es in "${ESSENTIAL_STEPS[@]}"; do [[ "$es" == "$step" ]] && is_essential=true && break; done + if [[ "$is_essential" != true ]]; then + if ! prompt_user "Proceed with: $description? (Choose No to skip)"; then + return 1 + fi + fi + fi + + return 0 +} + +#====================================== +# Command Line Argument Parsing +#====================================== + +show_help() { + cat << EOF +Dotfiles Installation Script + +USAGE: + $0 [OPTIONS] + +OPTIONS: + -h, --help Show this help message + -r, --resume Resume from last failed step + -u, --update Update existing dotfiles and packages + -v, --verbose Enable verbose output + -n, --dry-run Show what would be done without executing + -f, --force Force reinstallation and skip prompts + -a, --ask Ask before running each step + -m, --mode MODE Installation mode (essentials|minimal|dev|server|full|PROFILE) + +INSTALLATION MODES: + essentials Install only essential packages (git, curl, etc.) + minimal Minimal setup for basic development + dev Full development environment (default) + server Server configuration + full Complete installation with all packages + PROFILE Custom profile from profiles/ directory + +EXAMPLES: + $0 # Interactive installation (asks for mode) + $0 --mode essentials # Install essentials only + $0 --mode dev # Development environment + $0 --resume # Resume from last failed step + $0 --update --mode full # Update and install all packages + $0 --dry-run --mode dev # Preview development installation + $0 --ask --mode minimal # Ask before each step in minimal mode + +NOTES: + • Running without arguments on an existing installation will default to update mode + • Use --force to override existing installations + • Use --ask to have control over each installation step + • Configuration files are backed up before modification + +EOF +} + +parse_arguments() { + while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -r|--resume) + RESUME_MODE=true + shift + ;; + -u|--update) + UPDATE_MODE=true + shift + ;; + -v|--verbose) + VERBOSE_MODE=true + shift + ;; + -n|--dry-run) + DRY_RUN=true + shift + ;; + -f|--force) + FORCE_MODE=true + shift + ;; + -a|--ask) + ASK_MODE=true + shift + ;; + -m|--mode) + INSTALL_MODE="$2" + shift 2 + ;; + *) + print_error "Unknown option: $1" + show_help + exit 1 + ;; + esac + done + + # Validate install mode + if [[ "$INSTALL_MODE" != "ask" ]]; then + if [[ ! "${INSTALLATION_PROFILES[$INSTALL_MODE]:-}" ]] && [[ ! -f "profiles/$INSTALL_MODE.yml" ]]; then + print_error "Invalid installation mode: $INSTALL_MODE" + print_info "Available modes: ${!INSTALLATION_PROFILES[*]}" + exit 1 + fi + fi +} + +#====================================== +# Summary Functions +#====================================== + +print_installation_summary() { + print_header "Installation Summary" + + local total_steps=${#STEP_ORDER[@]} + local completed_count=${#COMPLETED_STEPS[@]} + local failed_count=${#FAILED_ITEMS[@]} + + print_section "Progress Overview" + print_color "$CYAN" "Installation Mode: $INSTALL_MODE" + print_color "$CYAN" "Total Steps: $total_steps" + print_color "$GREEN" "Completed: $completed_count" + print_color "$RED" "Failed: $failed_count" + + if [[ ${#INSTALL_SUMMARY[@]} -gt 0 ]]; then + print_section "Successful Operations" + printf '%s\n' "${INSTALL_SUMMARY[@]}" + fi + + if [[ ${#FAILED_ITEMS[@]} -gt 0 ]]; then + print_section "Failed Operations" + printf '%s\n' "${FAILED_ITEMS[@]}" + print_warning "Check the log file: $LOG_FILE" + fi + + if [[ ${#SKIPPED_ITEMS[@]} -gt 0 ]]; then + print_section "Skipped Operations" + printf '%s\n' "${SKIPPED_ITEMS[@]}" + fi + + echo + print_color "$GREEN$BOLD" "Installation completed!" + print_info "Log file: $LOG_FILE" "always" +} + +#====================================== +# Main Installation Flow +#====================================== + +execute_step() { + local step_name="$1" + local step_desc="${INSTALLATION_STEPS[$step_name]}" + + if is_step_completed "$step_name" && [[ "$FORCE_MODE" != true ]]; then + print_success "$step_desc (already completed)" + return 0 + fi + + if "$step_name"; then + print_success "$step_desc completed" + mark_step_completed "$step_name" + return 0 + else + print_error "$step_desc failed" + mark_step_failed "$step_name" + return 1 + fi +} + +main() { + parse_arguments "$@" + setup_logging + + print_header "Dotfiles Installation" + + if [[ "$DRY_RUN" == true ]]; then + print_warning "DRY RUN MODE - No changes will be made" + echo + fi + + if [[ "$ASK_MODE" == true ]]; then + print_warning "ASK MODE - You will be prompted for each step" + echo + fi + + print_info "Starting installation for user: $USER" "always" + print_info "Log file: $LOG_FILE" "always" + + # Handle resume mode + if [[ "$RESUME_MODE" == true ]]; then + if load_state; then + print_info "Resuming from previous installation..." "always" + print_info "Last step: ${LAST_STEP:-unknown}" "always" + if [[ -n "${COMPLETED_STEPS:-}" ]]; then + eval "COMPLETED_STEPS=(${COMPLETED_STEPS:-})" + fi + else + print_warning "No previous installation state found" + print_info "Starting fresh installation..." + RESUME_MODE=false + fi + fi + + # Detect installation mode (handles re-runs and updates) + detect_installation_mode + + # Show installation plan + echo + print_color "$YELLOW$BOLD" "Installation Plan (Mode: $INSTALL_MODE):" + local step_number=1 + for step in "${STEP_ORDER[@]}"; do + local step_desc="${INSTALLATION_STEPS[$step]}" + if is_step_completed "$step" && [[ "$FORCE_MODE" != true ]]; then + print_color "$GREEN" "$step_number. $step_desc (✓ completed)" + else + print_color "$CYAN" "$step_number. $step_desc" + fi + step_number=$((step_number + 1)) + done + + echo + if [[ "$FORCE_MODE" != true ]] && [[ "$DRY_RUN" != true ]] && [[ "$ASK_MODE" != true ]]; then + if ! prompt_user "Continue with installation?"; then + print_info "Installation cancelled by user" + exit 0 + fi + fi + + # Execute installation steps + local failed_steps=() + local step_number=1 + local total_steps=${#STEP_ORDER[@]} + + for step in "${STEP_ORDER[@]}"; do + echo + print_color "$CYAN$BOLD" "[$step_number/$total_steps] ${INSTALLATION_STEPS[$step]}" + + # Check if we should run this step (ask mode) + if ! should_run_step "$step"; then + print_skip "${INSTALLATION_STEPS[$step]} (user choice)" + step_number=$((step_number + 1)) + continue + fi + + if execute_step "$step"; then + print_info "Step completed successfully: $step" + else + failed_steps+=("$step") + print_error "Step failed: $step" + + if [[ "$FORCE_MODE" != true ]] && [[ "$DRY_RUN" != true ]]; then + echo + if ! prompt_user "Step '$step' failed. Continue with remaining steps?" "Y"; then + print_info "Installation stopped by user" + break + fi + fi + fi + + step_number=$((step_number + 1)) + done + + # Post-installation + if [[ ${#failed_steps[@]} -eq 0 ]]; then + print_success "All installation steps completed successfully!" + clear_state + else + print_warning "${#failed_steps[@]} steps failed: ${failed_steps[*]}" + if [[ "${failed_steps[-1]:-}" != "" ]]; then + save_state "${failed_steps[-1]}" "failed" + fi + fi + + print_installation_summary + + # Final recommendations + if [[ "$DRY_RUN" != true ]]; then + echo + print_section "Post-Installation Recommendations" + print_color "$CYAN" "• Restart your terminal or run: source ~/.bashrc (or ~/.zshrc)" + print_color "$CYAN" "• Review your dotfiles configuration in: $DOTFILES_DIR" + print_color "$CYAN" "• Use the 'config' command to manage your dotfiles" + print_color "$CYAN" "• Test the config command: config status" + + if [[ ${#failed_steps[@]} -gt 0 ]]; then + print_color "$YELLOW" "• Run '$0 --resume' to retry failed steps" + print_color "$YELLOW" "• Check the log file for detailed error information: $LOG_FILE" + fi + + echo + print_color "$GREEN$BOLD" "Thank you for using the Dotfiles Installation Script!" + fi + + [[ ${#failed_steps[@]} -eq 0 ]] && exit 0 || exit 1 +} + +#====================================== +# Script Entry Point +#====================================== + +cleanup_on_exit() { + local exit_code=$? + + if [[ $exit_code -ne 0 ]] && [[ "$DRY_RUN" != true ]]; then + print_error "Installation interrupted (exit code: $exit_code)" + + if [[ -n "${current_step:-}" ]]; then + save_state "$current_step" "interrupted" + print_info "State saved. Run with --resume to continue" + fi + fi +} + +handle_interrupt() { + print_warning "Installation interrupted by user" + exit 130 +} + +trap cleanup_on_exit EXIT +trap handle_interrupt INT + +# Execute main if script is run directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + # Check basic requirements + for req in git curl; do + if ! command_exists "$req"; then + print_error "$req is required but not installed" + exit 1 + fi + done + + main "$@" +fi diff --git a/common/packages.yml b/common/packages.yml new file mode 100644 index 0000000..0fce986 --- /dev/null +++ b/common/packages.yml @@ -0,0 +1,1066 @@ +# Dotfiles Installation Packages Configuration +# This file defines packages to install based on installation profiles and distribution-specific mappings + +#====================================== +# Installation Profiles +#====================================== + +# Core packages needed by all installations +common: + - git + - curl + - wget + +# Essential packages for basic functionality +essentials: + - zsh + - bash + - vim + - openssh + - sudo + - man + - bc + - time + - rsync + - tree + +# Minimal development environment +minimal: + - gcc + - make + - python + - jq + - fzf + - neovim + - tmux + +# Full development environment +dev: + - clang + - meson + - gdb + - cmake + - go + - ninja + - ripgrep + - fd + - nodejs + - emacs + - vscode + - ansible + +# Server-focused packages +server: + - ufw + - net-tools + - htop + - btop + - powertop + - clamav + - ntp + - networkmanager + - smartmontools + - hdparm + - acpi + - parted + - sysstat + - hwinfo + - ansible + +# Desktop environment packages +desktop: + - xorg + - wayland + - xclip + - xterm + - gtk + - firefox + - mpv + - discord + - libinput + - nnn + - ranger + - obs-studio + - unrar + - unzip + - p7zip + - imagemagick + - ffmpeg + - wezterm + - ncdu + - picom + - rofi + - udiskie + - brightnessctl + - wl-clipboard + - nemo + - blueman + - bluez + +# Window managers +wm: + - hyprland + - bspwm + - sxhkd + - polybar + +# Audio/Media packages +media: + - mpd + - pipewire + - ncmpcpp + +# Gaming +gaming: + - wine + - steam + +# Virtualization +virtualization: + - libvirt + - qemu + +# Fonts +fonts: + - hack-font + - nerd-fonts + - font-awesome + - dejavu-fonts + +#====================================== +# Distribution-specific package mappings +# Format: generic_name -> distro_specific_name +#====================================== + +arch: + # Core tools + python: python + nodejs: nodejs + man: man-pages man-db + tree: tree + ntp: ntpsec + hack-font: ttf-hack + nerd-fonts: ttf-nerd-fonts-symbols-mono + font-awesome: ttf-font-awesome + dejavu-fonts: ttf-dejavu + networkmanager: networkmanager + qemu: qemu-full + vscode: code + + # Desktop specific + xorg: xorg xorg-server + wayland: wayland xorg-xwayland + gtk: gtk3 gtk4 + libinput: libinput xf86-input-libinput + bluez: bluez bluez-utils bluez-tools + + # Media + pipewire: pipewire wireplumber + +debian: + # Core tools + python: python3 python3-pip + nodejs: nodejs npm + man: man-pages-dev man-db + tree: tree + ntp: ntp + hack-font: fonts-hack + nerd-fonts: fonts-nerd-font-symbols + font-awesome: fonts-font-awesome + dejavu-fonts: fonts-dejavu + networkmanager: network-manager + qemu: qemu-system + vscode: code + fd: fd-find + openssh: openssh-server + ansible: ansible + +fedora: + # Core tools + python: python3 python3-pip + nodejs: nodejs npm + man: man-pages man-db + tree: tree + ntp: chrony + hack-font: adobe-source-code-pro-fonts + nerd-fonts: powerline-fonts + font-awesome: fontawesome-fonts + dejavu-fonts: dejavu-fonts-common + networkmanager: NetworkManager + qemu: qemu-kvm + vscode: code-oss + fd: fd-find + openssh: openssh-server + ansible: ansible + ninja: ninja-build + + # Desktop specific + xorg: xorg xserver-xorg + wayland: libwayland-dev xwayland + gtk: libgtk-3-dev libgtk-4-dev + libinput: libinput10 xserver-xorg-input-libinput + bluez: bluez bluez-tools + + # Media + pipewire: pipewire wireplumber + + # System tools + ufw: ufw + net-tools: net-tools + btop: btop + powertop: powertop + clamav: clamav + smartmontools: smartmontools + hdparm: hdparm + acpi: acpi + parted: parted + cups: cups + sysstat: sysstat + hwinfo: hwinfo + +rhel: + # Core tools + python: python3 python3-pip + nodejs: nodejs npm + man: man-pages man-db + tree: tree + ntp: chrony + hack-font: adobe-source-code-pro-fonts + nerd-fonts: powerline-fonts + font-awesome: fontawesome-fonts + dejavu-fonts: dejavu-fonts-common + networkmanager: NetworkManager + qemu: qemu-kvm + vscode: code + fd: fd-find + openssh: openssh-server + ansible: ansible + ninja: ninja-build + + # Desktop specific + xorg: xorg-x11-server-Xorg + wayland: wayland-devel xorg-x11-server-Xwayland + gtk: gtk3-devel gtk4-devel + libinput: libinput + bluez: bluez bluez-tools + + # System tools + ufw: firewalld + net-tools: net-tools + btop: htop + powertop: powertop + clamav: clamav + smartmontools: smartmontools + hdparm: hdparm + acpi: acpi + parted: parted + cups: cups + sysstat: sysstat + +opensuse: + # Core tools + python: python3 python3-pip + nodejs: nodejs16 npm16 + man: man-pages man + tree: tree + ntp: chrony + hack-font: adobe-sourcecodepro-fonts + nerd-fonts: powerline-fonts + font-awesome: fontawesome-fonts + dejavu-fonts: dejavu-fonts + networkmanager: NetworkManager + qemu: qemu + vscode: code + openssh: openssh + ansible: ansible + +gentoo: + # Core tools with full package paths + git: dev-vcs/git + curl: net-misc/curl + wget: net-misc/wget + zsh: app-shells/zsh + bash: app-shells/bash + vim: app-editors/vim + neovim: app-editors/neovim + tmux: app-misc/tmux + openssh: net-misc/openssh + sudo: app-admin/sudo + man: sys-apps/man-pages sys-apps/man-db + bc: sys-devel/bc + time: sys-process/time + rsync: net-misc/rsync + tree: app-text/tree + gcc: sys-devel/gcc + clang: sys-devel/clang + make: sys-devel/make + cmake: dev-util/cmake + meson: dev-util/meson + gdb: sys-devel/gdb + ninja: dev-util/ninja + ripgrep: sys-apps/ripgrep + fd: sys-apps/fd + python: dev-lang/python + nodejs: net-libs/nodejs + jq: app-misc/jq + fzf: app-shells/fzf + emacs: app-editors/emacs + vscode: app-editors/vscode + go: dev-lang/go + htop: sys-process/htop + ufw: net-firewall/ufw + net-tools: sys-apps/net-tools + btop: sys-process/btop + powertop: sys-power/powertop + clamav: app-antivirus/clamav + ntp: net-misc/chrony + networkmanager: net-misc/networkmanager + smartmontools: sys-apps/smartmontools + hdparm: sys-apps/hdparm + acpi: sys-power/acpi + parted: sys-block/parted + cups: net-print/cups + sysstat: app-admin/sysstat + hwinfo: sys-apps/hwinfo + hack-font: media-fonts/hack + nerd-fonts: media-fonts/nerd-fonts + font-awesome: media-fonts/fontawesome + dejavu-fonts: media-fonts/dejavu + + # Desktop + xorg: x11-base/xorg-server + wayland: dev-libs/wayland x11-base/xwayland + xclip: x11-misc/xclip + xterm: x11-terms/xterm + gtk: x11-libs/gtk+ + firefox: www-client/firefox + mpv: media-video/mpv + discord: net-im/discord-bin + libinput: dev-libs/libinput x11-drivers/xf86-input-libinput + nnn: app-misc/nnn + ranger: app-misc/ranger + obs-studio: media-video/obs-studio + unrar: app-arch/unrar + unzip: app-arch/unzip + p7zip: app-arch/p7zip + imagemagick: media-gfx/imagemagick + ffmpeg: media-video/ffmpeg + wezterm: x11-terms/wezterm + ncdu: sys-fs/ncdu + picom: x11-misc/picom + rofi: x11-misc/rofi + udiskie: sys-fs/udiskie + brightnessctl: app-misc/brightnessctl + wl-clipboard: gui-apps/wl-clipboard + nemo: gnome-extra/nemo + blueman: net-wireless/blueman + bluez: net-wireless/bluez + + # Window managers + hyprland: gui-wm/hyprland + bspwm: x11-wm/bspwm + sxhkd: x11-misc/sxhkd + polybar: x11-misc/polybar + + # Media + mpd: media-sound/mpd + pipewire: media-video/pipewire media-video/wireplumber + ncmpcpp: media-sound/ncmpcpp + + # Gaming + wine: app-emulation/wine-vanilla + steam: games-util/steam-launcher + + # Virtualization + libvirt: app-emulation/libvirt + qemu: app-emulation/qemu + +alpine: + python: python3 py3-pip + nodejs: nodejs npm + man: man-pages man-db + ntp: chrony + htop: htop + openssh: openssh + ansible: ansible + +void: + python: python3 python3-pip + nodejs: nodejs + man: man-pages + ntp: chrony + openssh: openssh + ripgrep: ripgrep + fd: fd + btop: btop + networkmanager: NetworkManager + ansible: ansible + +macos: + # Homebrew packages + - git + - curl + - wget + - zsh + - bash + - vim + - neovim + - tmux + - openssh + - python3 + - node + - jq + - fzf + - ripgrep + - fd + - bat + - htop + - rsync + - cmake + - ninja + - go + - emacs + - visual-studio-code + - ansible + +windows: + - git + - ripgrep + - fd + - sudo + - win32yank + - microsoft-windows-terminal + - wsl + - firefox + - setdefaultbrowser + - nodejs + - bat + - 7zip + - python + - javaruntime + - autohotkey + - bitwarden + - notepadplusplus + - neovim + +bloatware: + # - Anytime + - BioEnrollment + # - Browser + - ContactSupport + - Cortana + # - Defender + - Feedback + - Flash + # - Gaming # Breaks Xbox Live Account Login + # - Holo + # - InternetExplorer + - Maps + # - MiracastView + - OneDrive + # - SecHealthUI + - Wallet + # - Xbox # Causes a bootloop since upgrade 1511? + +default: + # default Windows 10 apps + # - Microsoft.3DBuilder + - Microsoft.Appconnector + - Microsoft.BingFinance + - Microsoft.BingNews + - Microsoft.BingSports + - Microsoft.BingTranslator + - Microsoft.BingWeather + # - Microsoft.FreshPaint + # - Microsoft.Microsoft3DViewer + - Microsoft.MicrosoftOfficeHub + - Microsoft.MicrosoftSolitaireCollection + - Microsoft.MicrosoftPowerBIForWindows + - Microsoft.MinecraftUWP + # - Microsoft.MicrosoftStickyNotes + # - Microsoft.NetworkSpeedTest + - Microsoft.Office.OneNote + # - Microsoft.OneConnect + - Microsoft.People + # - Microsoft.Print3D + - Microsoft.SkypeApp + - Microsoft.Wallet + # - Microsoft.Windows.Photos + # - Microsoft.WindowsAlarms + # - Microsoft.WindowsCalculator + - Microsoft.WindowsCamera + - microsoft.windowscommunicationsapps + - Microsoft.WindowsMaps + - Microsoft.WindowsPhone + - Microsoft.WindowsSoundRecorder + - Microsoft.WindowsStore + # - Microsoft.XboxApp + # - Microsoft.XboxGameOverlay + # - Microsoft.XboxIdentityProvider + # - Microsoft.XboxSpeechToTextOverlay + - Microsoft.ZuneMusic + - Microsoft.ZuneVideo + + # Threshold 2 apps + - Microsoft.CommsPhone + - Microsoft.ConnectivityStore + - Microsoft.GetHelp + - Microsoft.Getstarted + - Microsoft.Messaging + - Microsoft.Office.Sway + - Microsoft.OneConnect + - Microsoft.WindowsFeedbackHub + + # Redstone apps + - Microsoft.BingFoodAndDrink + - Microsoft.BingTravel + - Microsoft.BingHealthAndFitness + - Microsoft.WindowsReadingList + + # non-Microsoft + - king.com.CandyCrushSaga + - king.com.CandyCrushSodaSaga + - king.com.* + - Facebook.Facebook + + # apps which cannot be removed using Remove-AppxPackage + # - Microsoft.BioEnrollment + # - Microsoft.MicrosoftEdge + # - Microsoft.Windows.Cortana + # - Microsoft.WindowsFeedback + # - Microsoft.XboxGameCallableUI + # - Microsoft.XboxIdentityProvider + # - Windows.ContactSupport + +#====================================== +# Gentoo USE flags configuration +#====================================== +gentoo_use_flags: + git: "curl gpg perl python" + curl: "ssl http2 ipv6" + wget: "ssl ipv6 nls" + zsh: "unicode pcre gdbm" + bash: "net nls readline" + tmux: "vim-syntax" + vim: "python lua ruby perl cscope" + neovim: "lua python ruby" + emacs: "gtk jpeg png svg tiff xpm cairo dbus json ssl xml" + gcc: "cxx fortran graphite jit nptl openmp pch pie ssp" + clang: "static-analyzer" + python: "sqlite ssl readline ncurses xml" + nodejs: "ssl" + htop: "unicode lm-sensors" + openssh: "ssl kerberos ldap pam" + firefox: "dbus gtk3 pulseaudio startup-notification wifi" + mpv: "alsa pulseaudio lua drm wayland X" + gtk: "wayland X cups introspection" + pipewire: "alsa bluetooth jack pulseaudio sound-server" + ffmpeg: "alsa encode mp3 opus pulseaudio theora vorbis webp x264 x265" + networkmanager: "bluetooth dhclient introspection wifi" + bluez: "alsa cups obex readline" + qemu: "aio alsa bluetooth curl gtk jpeg ncurses opengl png pulseaudio sdl spice ssh usb vnc" + libvirt: "firewalld libssh nfs numa parted qemu sasl udev" + +#====================================== +# System tweaks and configurations +#====================================== +system_tweaks: + gnome: + # Power management settings + - gsettings set org.gnome.desktop.session idle-delay 0 + - gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' + - gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type 'nothing' + # Interface tweaks + - gsettings set org.gnome.desktop.interface clock-show-weekday true + - gsettings set org.gnome.desktop.interface show-battery-percentage true + + kde: + # Power management + - kwriteconfig5 --file powermanagementprofilesrc --group AC --group DimDisplay --key idleTime 300000 + - kwriteconfig5 --file powermanagementprofilesrc --group AC --group DPMSControl --key idleTime 600000 + + windows: + registry: + # Explorer settings + - path: "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced" + name: "Hidden" + value: 1 + type: "DWORD" + description: "Show hidden files" + - path: "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced" + name: "HideFileExt" + value: 0 + type: "DWORD" + description: "Show file extensions" + - path: "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced" + name: "TaskbarGlomLevel" + value: 2 + type: "DWORD" + description: "Never combine taskbar buttons" + - path: "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced" + name: "TaskbarSmallIcons" + value: 1 + type: "DWORD" + description: "Use small taskbar icons" + + # Dark mode + - path: "HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize" + name: "AppsUseLightTheme" + value: 0 + type: "DWORD" + description: "Use dark theme for apps" + - path: "HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize" + name: "SystemUsesLightTheme" + value: 0 + type: "DWORD" + description: "Use dark theme for system" + + # Search settings + - path: "HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Search" + name: "SearchBoxTaskbarMode" + value: 0 + type: "DWORD" + description: "Hide search box from taskbar" + + features: + - name: "Microsoft-Windows-Subsystem-Linux" + description: "Windows Subsystem for Linux" + requires_admin: true + - name: "VirtualMachinePlatform" + description: "Virtual Machine Platform" + requires_admin: true + + hardening: + registry: + # Security hardening registry settings + - path: "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System" + name: "EnableLUA" + value: 1 + type: "DWORD" + description: "Enable User Account Control" + - path: "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System" + name: "ConsentPromptBehaviorAdmin" + value: 2 + type: "DWORD" + description: "UAC prompt for administrators" + - path: "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" + name: "DisablePasswordCaching" + value: 1 + type: "DWORD" + description: "Disable password caching" + - path: "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa" + name: "LimitBlankPasswordUse" + value: 1 + type: "DWORD" + description: "Limit blank password use" + - path: "HKLM:\\SYSTEM\\CurrentControlSet\\Services\\lanmanserver\\parameters" + name: "AutoDisconnectTimeout" + value: 15 + type: "DWORD" + description: "Auto disconnect timeout" + - path: "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System" + name: "DontDisplayLastUserName" + value: 1 + type: "DWORD" + description: "Don't display last username" + + services: + disable: + - "Fax" + - "TelnetD" + - "RemoteRegistry" + - "Messenger" + - "NetMeeting Remote Desktop Sharing" + - "Remote Desktop Help Session Manager" + - "Routing and Remote Access" + - "Simple TCP/IP Services" + - "SNMP Service" + + firewall: + - "netsh advfirewall set allprofiles state on" + - "netsh advfirewall firewall set rule group=\"File and Printer Sharing\" new enable=No" + - "netsh advfirewall firewall set rule group=\"Network Discovery\" new enable=No" + + linux: + hardening: + sysctl: + # Network security + - net.ipv4.ip_forward = 0 + - net.ipv4.conf.all.send_redirects = 0 + - net.ipv4.conf.default.send_redirects = 0 + - net.ipv4.conf.all.accept_source_route = 0 + - net.ipv4.conf.default.accept_source_route = 0 + - net.ipv4.conf.all.accept_redirects = 0 + - net.ipv4.conf.default.accept_redirects = 0 + - net.ipv4.conf.all.secure_redirects = 0 + - net.ipv4.conf.default.secure_redirects = 0 + - net.ipv4.conf.all.log_martians = 1 + - net.ipv4.conf.default.log_martians = 1 + - net.ipv4.icmp_echo_ignore_broadcasts = 1 + - net.ipv4.icmp_ignore_bogus_error_responses = 1 + - net.ipv4.conf.all.rp_filter = 1 + - net.ipv4.conf.default.rp_filter = 1 + - net.ipv4.tcp_syncookies = 1 + - net.ipv6.conf.all.accept_ra = 0 + - net.ipv6.conf.default.accept_ra = 0 + - net.ipv6.conf.all.accept_redirects = 0 + - net.ipv6.conf.default.accept_redirects = 0 + + # Kernel security + - kernel.dmesg_restrict = 1 + - kernel.kptr_restrict = 2 + - kernel.yama.ptrace_scope = 1 + - kernel.kexec_load_disabled = 1 + - kernel.unprivileged_bpf_disabled = 1 + - net.core.bpf_jit_harden = 2 + + # Memory protection + - kernel.randomize_va_space = 2 + - vm.mmap_min_addr = 65536 + + packages: + security: + - fail2ban + - ufw + - rkhunter + - chkrootkit + - lynis + - aide + - apparmor + - apparmor-utils + + services: + disable: + - avahi-daemon + - cups + - bluetooth + - whoopsie + - apport + enable: + - ufw + - fail2ban + - apparmor + + filesystem: + - "chmod 700 /root" + - "chmod 644 /etc/passwd" + - "chmod 600 /etc/shadow" + - "chmod 644 /etc/group" + - "chmod 600 /etc/gshadow" + - "find /home -name '.netrc' -delete" + - "find /home -name '.rhosts' -delete" + + macos: + hardening: + defaults: + # Security settings + - domain: "com.apple.screensaver" + key: "askForPassword" + value: 1 + type: "int" + description: "Require password after screensaver" + - domain: "com.apple.screensaver" + key: "askForPasswordDelay" + value: 0 + type: "int" + description: "Require password immediately" + - domain: "com.apple.Safari" + key: "SendDoNotTrackHTTPHeader" + value: 1 + type: "bool" + description: "Enable Do Not Track" + - domain: "com.apple.Safari" + key: "AutoFillPasswords" + value: 0 + type: "bool" + description: "Disable password autofill" + - domain: "com.apple.loginwindow" + key: "GuestEnabled" + value: 0 + type: "bool" + description: "Disable guest account" + - domain: "com.apple.loginwindow" + key: "SHOWFULLNAME" + value: 1 + type: "bool" + description: "Show full name in login window" + + system: + - "sudo spctl --master-enable" # Enable Gatekeeper + - "sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1" # Enable firewall + - "sudo launchctl load /System/Library/LaunchDaemons/com.apple.locate.plist" # Enable locate database + + services: + disable: + - "com.apple.AirPlayXPCHelper" + - "com.apple.RemoteDesktop.agent" + + packages: + security: + - gpg + - gnupg + - pinentry-mac + +#====================================== +# Service configurations +#====================================== +services: + enable: + all: + - sshd + - networkmanager + server: + - firewalld + - chronyd + desktop: + - bluetooth + - cups + disable: + server: + - bluetooth + - cups + - gdm + minimal: + - cups + - bluetooth + +#====================================== +# Development environment configurations +#====================================== +development: + git_config: + - git config --global init.defaultBranch main + - git config --global pull.rebase false + - git config --global core.editor vim + + rust: + components: + - rustc + - cargo + - clippy + - rustfmt + + nodejs: + global_packages: + - typescript + - eslint + - prettier + + python: + global_packages: + - black + - flake8 + - mypy + - requests + - virtualenvwrapper + +#====================================== +# System update checks and maintenance +#====================================== +system_updates: + linux: + kernel_check: + - "uname -r" # Current kernel + - "ls /boot/vmlinuz-* | tail -1 | sed 's/.*vmlinuz-//'" # Latest available + + distro_updates: + arch: + check: "checkupdates" + update: "pacman -Syu" + kernel_update: "pacman -S linux linux-headers" + debian: + check: "apt list --upgradable" + update: "apt update && apt upgrade -y" + kernel_update: "apt install linux-image-generic linux-headers-generic" + rhel: + check: "dnf check-update" + update: "dnf update -y" + kernel_update: "dnf update kernel kernel-headers" + gentoo: + check: "emerge -pv --update --deep --newuse @world" + update: "emerge --update --deep --newuse @world" + kernel_update: "emerge gentoo-sources && genkernel all" + + macos: + system_updates: + check: "softwareupdate -l" + update: "softwareupdate -ia" + major_check: "softwareupdate --list-full-installers" + + windows: + system_updates: + check: "Get-WindowsUpdate -MicrosoftUpdate" + update: "Install-WindowsUpdate -MicrosoftUpdate -AcceptAll -AutoReboot" + feature_updates: "Get-WindowsUpdate -UpdateType Software" + +#====================================== +# Custom installation commands +#====================================== +custom_installs: + yq: + condition: "! command -v yq" + linux: | + mkdir -p "$HOME/.local/bin" + YQ_VERSION=$(curl -s https://api.github.com/repos/mikefarah/yq/releases/latest | grep 'tag_name' | cut -d'"' -f4) + YQ_BINARY="yq_linux_amd64" + curl -L "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" -o "$HOME/.local/bin/yq" + chmod +x "$HOME/.local/bin/yq" + macos: "brew install yq" + windows: "choco install yq" + + homebrew: + condition: "test $(uname) = Darwin && ! command -v brew" + macos: '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + + system_updates: + condition: "true" # Always available + description: "Check and install system updates" + linux: | + case "$CFG_DISTRO" in + arch) checkupdates && sudo pacman -Syu ;; + debian|ubuntu) apt list --upgradable && sudo apt update && sudo apt upgrade -y ;; + rhel|fedora|centos) dnf check-update && sudo dnf update -y ;; + gentoo) emerge -pv --update --deep --newuse @world && sudo emerge --update --deep --newuse @world ;; + *) echo "Unsupported distribution for automatic updates" ;; + esac + macos: "softwareupdate -l && sudo softwareupdate -ia" + windows: | + if (Get-Module -ListAvailable -Name PSWindowsUpdate) { + Get-WindowsUpdate -MicrosoftUpdate + Install-WindowsUpdate -MicrosoftUpdate -AcceptAll -AutoReboot + } else { + Write-Host "PSWindowsUpdate module not installed. Install with: Install-Module PSWindowsUpdate" + } + + zsh_plugins: + condition: "command -v zsh" + description: "Install common Zsh plugins" + linux: | + ZPLUG_DIR="$HOME/.config/zsh/plugins"; mkdir -p "$ZPLUG_DIR"; command -v git >/dev/null 2>&1 || exit 0; c(){ n="$1"; u="$2"; [ -d "$ZPLUG_DIR/$n" ] && return 0; env -i PATH="$PATH" HOME="$HOME" GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=/bin/true git -c credential.helper= -c core.askPass= clone --depth 1 --single-branch "$u" "$ZPLUG_DIR/$n" 2>/dev/null || true; }; c zsh-you-should-use https://github.com/MichaelAquilina/zsh-you-should-use.git; c zsh-syntax-highlighting https://github.com/zsh-users/zsh-syntax-highlighting.git; c zsh-autosuggestions https://github.com/zsh-users/zsh-autosuggestions.git + macos: | + ZPLUG_DIR="$HOME/.config/zsh/plugins"; mkdir -p "$ZPLUG_DIR"; command -v git >/dev/null 2>&1 || exit 0; c(){ n="$1"; u="$2"; [ -d "$ZPLUG_DIR/$n" ] && return 0; env -i PATH="$PATH" HOME="$HOME" GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=/bin/true git -c credential.helper= -c core.askPass= clone --depth 1 --single-branch "$u" "$ZPLUG_DIR/$n" 2>/dev/null || true; }; c zsh-you-should-use https://github.com/MichaelAquilina/zsh-you-should-use.git; c zsh-syntax-highlighting https://github.com/zsh-users/zsh-syntax-highlighting.git; c zsh-autosuggestions https://github.com/zsh-users/zsh-autosuggestions.git + + vscode_extensions: + condition: "command -v code" + description: "Install template VSCode extensions" + linux: | + for e in ms-python.python ms-vscode.cpptools golang.Go rust-lang.rust-analyzer esbenp.prettier-vscode eamodio.gitlens ms-azuretools.vscode-docker hashicorp.terraform redhat.ansible; do code --install-extension "$e" --force >/dev/null 2>&1 || true; done + macos: | + for e in ms-python.python ms-vscode.cpptools golang.Go rust-lang.rust-analyzer esbenp.prettier-vscode eamodio.gitlens ms-azuretools.vscode-docker hashicorp.terraform redhat.ansible; do code --install-extension "$e" --force >/dev/null 2>&1 || true; done + + nix_home_manager: + condition: "command -v nix-env" + description: "Bootstrap Home Manager if missing" + linux: | + if ! command -v home-manager >/dev/null 2>&1; then nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager || true; nix-channel --update || true; nix-shell '<home-manager>' -A install || true; fi + macos: | + if ! command -v home-manager >/dev/null 2>&1; then nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager || true; nix-channel --update || true; nix-shell '<home-manager>' -A install || true; fi + + +#====================================== +# Profile-specific package lists +#====================================== +profiles: + essentials: + description: "Essential packages only (git, curl, wget, vim, zsh)" + packages: + - common + - essentials + + minimal: + description: "Minimal setup for basic development" + packages: + - common + - essentials + - minimal + + dev: + description: "Full development environment" + packages: + - common + - essentials + - minimal + - dev + enable_development: true + + server: + description: "Server configuration" + packages: + - common + - essentials + - minimal + - server + enable_services: server + + full: + description: "Complete installation with all packages" + packages: + - common + - essentials + - minimal + - dev + - server + - desktop + - wm + - media + - fonts + enable_development: true + enable_services: desktop + +#====================================== +# Package management helpers +#====================================== +package_managers: + arch: + update: "pacman -Syu" + install: "pacman -S --noconfirm" + search: "pacman -Ss" + + debian: + update: "apt update && apt upgrade -y" + install: "apt install -y" + search: "apt search" + + rhel: + update: "dnf update -y" + install: "dnf install -y" + search: "dnf search" + + fedora: + update: "dnf update -y" + install: "dnf install -y" + search: "dnf search" + + opensuse: + update: "zypper update -y" + install: "zypper install -y" + search: "zypper search" + + gentoo: + update: "emerge --sync && emerge -uDN @world" + install: "emerge" + search: "emerge --search" + + alpine: + update: "apk update && apk upgrade" + install: "apk add" + search: "apk search" + + void: + update: "xbps-install -Su" + install: "xbps-install -y" + search: "xbps-query -Rs" + + macos: + update: "brew update && brew upgrade" + install: "brew install" + search: "brew search" + + windows: + update: "choco upgrade all -y" + install: "choco install -y" + search: "choco search" + nix: + update: "nix-channel --update && nix-env -u" + install: "nix-env -iA" + search: "nix-env -qaP" |
