aboutsummaryrefslogtreecommitdiff
path: root/common
diff options
context:
space:
mode:
Diffstat (limited to 'common')
-rw-r--r--common/.bash_profile11
-rw-r--r--common/.bashrc371
-rw-r--r--common/.editorconfig80
-rw-r--r--common/.facebin0 -> 11569 bytes
-rw-r--r--common/.gitconfig34
-rw-r--r--common/.gitignore44
-rw-r--r--common/.gitmodules12
-rw-r--r--common/.gitsubtrees7
-rw-r--r--common/.prettierrc.yml5
-rw-r--r--common/.profile102
-rw-r--r--common/.zprofile3
-rw-r--r--common/.zshrc10
-rw-r--r--common/assets/desktop.jpgbin0 -> 523045 bytes
-rw-r--r--common/assets/old_desktop.jpgbin0 -> 108709 bytes
-rw-r--r--common/config/alacritty/alacritty.yml106
-rwxr-xr-xcommon/config/nvim/.gitignore4
-rwxr-xr-xcommon/config/nvim/.luacheckrc5
-rwxr-xr-xcommon/config/nvim/after/ftplugin/c.lua9
-rwxr-xr-xcommon/config/nvim/after/ftplugin/lua.lua.bak35
-rwxr-xr-xcommon/config/nvim/after/ftplugin/markdown.lua37
-rwxr-xr-xcommon/config/nvim/after/ftplugin/vim.lua18
-rwxr-xr-xcommon/config/nvim/autoload/statusline.vim267
-rwxr-xr-xcommon/config/nvim/autoload/utils.vim238
-rwxr-xr-xcommon/config/nvim/colors/colorscheme.vim247
-rwxr-xr-xcommon/config/nvim/init.lua152
-rw-r--r--common/config/nvim/lsp/bashls.lua4
-rw-r--r--common/config/nvim/lsp/clangd.lua5
-rw-r--r--common/config/nvim/lsp/cssls.lua4
-rw-r--r--common/config/nvim/lsp/gopls.lua41
-rw-r--r--common/config/nvim/lsp/html.lua4
-rw-r--r--common/config/nvim/lsp/jsonls.lua4
-rw-r--r--common/config/nvim/lsp/lua_ls.lua20
-rw-r--r--common/config/nvim/lsp/pyright.lua12
-rw-r--r--common/config/nvim/lsp/rust_analyzer.lua5
-rw-r--r--common/config/nvim/lsp/ts_ls.lua8
-rw-r--r--common/config/nvim/lsp/yamlls.lua4
-rwxr-xr-xcommon/config/nvim/lua/plugins/auto-session.lua39
-rwxr-xr-xcommon/config/nvim/lua/plugins/autopairs.lua99
-rwxr-xr-xcommon/config/nvim/lua/plugins/cmp-gh-source.lua70
-rwxr-xr-xcommon/config/nvim/lua/plugins/cmp.lua67
-rwxr-xr-xcommon/config/nvim/lua/plugins/colorizer.lua8
-rwxr-xr-xcommon/config/nvim/lua/plugins/colorscheme.lua24
-rwxr-xr-xcommon/config/nvim/lua/plugins/comment.lua125
-rwxr-xr-xcommon/config/nvim/lua/plugins/dap.lua265
-rwxr-xr-xcommon/config/nvim/lua/plugins/dashboard.lua126
-rwxr-xr-xcommon/config/nvim/lua/plugins/fidget.lua34
-rwxr-xr-xcommon/config/nvim/lua/plugins/friendly-snippets.lua3
-rwxr-xr-xcommon/config/nvim/lua/plugins/fugitive.lua8
-rwxr-xr-xcommon/config/nvim/lua/plugins/fzf.lua43
-rwxr-xr-xcommon/config/nvim/lua/plugins/git.lua8
-rwxr-xr-xcommon/config/nvim/lua/plugins/gitsigns.lua85
-rwxr-xr-xcommon/config/nvim/lua/plugins/goto-preview.lua31
-rwxr-xr-xcommon/config/nvim/lua/plugins/hardtime.lua29
-rwxr-xr-xcommon/config/nvim/lua/plugins/harpoon.lua50
-rwxr-xr-xcommon/config/nvim/lua/plugins/heirline.lua1497
-rwxr-xr-xcommon/config/nvim/lua/plugins/indent-blankline.lua73
-rwxr-xr-xcommon/config/nvim/lua/plugins/interestingwords.lua499
-rwxr-xr-xcommon/config/nvim/lua/plugins/leetcode.lua68
-rwxr-xr-xcommon/config/nvim/lua/plugins/loclist.lua18
-rwxr-xr-xcommon/config/nvim/lua/plugins/lsp.lua674
-rwxr-xr-xcommon/config/nvim/lua/plugins/lualine.lua22
-rwxr-xr-xcommon/config/nvim/lua/plugins/luasnip.lua13
-rwxr-xr-xcommon/config/nvim/lua/plugins/messages.lua85
-rwxr-xr-xcommon/config/nvim/lua/plugins/modify-blend.lua43
-rwxr-xr-xcommon/config/nvim/lua/plugins/navic.lua51
-rwxr-xr-xcommon/config/nvim/lua/plugins/neodev.lua45
-rwxr-xr-xcommon/config/nvim/lua/plugins/neoscroll.lua22
-rwxr-xr-xcommon/config/nvim/lua/plugins/neotest.lua38
-rwxr-xr-xcommon/config/nvim/lua/plugins/notify.lua36
-rwxr-xr-xcommon/config/nvim/lua/plugins/nvim-tree.lua479
-rwxr-xr-xcommon/config/nvim/lua/plugins/overseer.lua14
-rwxr-xr-xcommon/config/nvim/lua/plugins/plenary.lua3
-rwxr-xr-xcommon/config/nvim/lua/plugins/prettier.lua8
-rwxr-xr-xcommon/config/nvim/lua/plugins/quickfix.lua15
-rwxr-xr-xcommon/config/nvim/lua/plugins/snippets.lua33
-rwxr-xr-xcommon/config/nvim/lua/plugins/sniprun.lua57
-rwxr-xr-xcommon/config/nvim/lua/plugins/statuscol.lua37
-rwxr-xr-xcommon/config/nvim/lua/plugins/surround.lua35
-rwxr-xr-xcommon/config/nvim/lua/plugins/telescope.lua740
-rwxr-xr-xcommon/config/nvim/lua/plugins/toggleterm.lua294
-rwxr-xr-xcommon/config/nvim/lua/plugins/treesitter.lua54
-rwxr-xr-xcommon/config/nvim/lua/plugins/trouble.lua73
-rwxr-xr-xcommon/config/nvim/lua/plugins/vimtex.lua45
-rwxr-xr-xcommon/config/nvim/lua/plugins/web-devicons.lua125
-rwxr-xr-xcommon/config/nvim/lua/plugins/which-key.lua53
-rwxr-xr-xcommon/config/nvim/lua/plugins/zen-mode.lua7
-rwxr-xr-xcommon/config/nvim/lua/setup/compat.lua104
-rwxr-xr-xcommon/config/nvim/lua/setup/manager.lua811
-rwxr-xr-xcommon/config/nvim/lua/setup/plugins.lua609
-rwxr-xr-xcommon/config/nvim/lua/user/keys.lua928
-rwxr-xr-xcommon/config/nvim/lua/user/mods.lua1427
-rwxr-xr-xcommon/config/nvim/lua/user/opts.lua438
-rwxr-xr-xcommon/config/nvim/lua/user/view.lua180
-rwxr-xr-xcommon/config/nvim/neovim.ps1917
-rwxr-xr-xcommon/config/nvim/neovim.sh516
-rw-r--r--common/config/nvim/snippets/boilerplate.lua75
-rw-r--r--common/config/nvim/snippets/lua.lua264
-rw-r--r--common/config/nvim/snippets/markdown.lua58
-rw-r--r--common/config/wezterm/wezterm.lua206
-rw-r--r--common/config/zsh/.zshenv346
-rw-r--r--common/config/zsh/.zshrc73
-rw-r--r--common/config/zsh/user/aliases.zsh208
-rw-r--r--common/config/zsh/user/bindings.zsh175
-rw-r--r--common/config/zsh/user/completion.zsh172
-rw-r--r--common/config/zsh/user/functions.zsh1607
-rw-r--r--common/config/zsh/user/options.zsh66
-rw-r--r--common/config/zsh/user/prompt.zsh679
-rw-r--r--common/config/zsh/user/prompt_minimal.zsh295
-rw-r--r--common/config/zsh/user/prompt_new.zsh863
-rw-r--r--common/config/zsh/user/prompt_simple.zsh227
-rwxr-xr-xcommon/install.sh3715
-rw-r--r--common/packages.yml1066
-rw-r--r--common/scripts/README.md17
-rw-r--r--common/scripts/dev/.gitkeep1
-rw-r--r--common/scripts/media/.gitkeep1
-rw-r--r--common/scripts/net/.gitkeep1
-rw-r--r--common/scripts/sec/.gitkeep1
-rwxr-xr-xcommon/scripts/sys/battery_alert.sh43
-rwxr-xr-xcommon/scripts/sys/gsettings.sh40
-rwxr-xr-xcommon/scripts/sys/keep_guest_awake.sh84
-rwxr-xr-xcommon/scripts/sys/low-bat-notifier79
-rwxr-xr-xcommon/scripts/sys/session_manager.sh72
-rwxr-xr-xcommon/scripts/sys/shutdown_watchdog11
-rw-r--r--common/scripts/test/.gitkeep1
-rwxr-xr-xcommon/scripts/utils/backlight_default.sh32
-rwxr-xr-xcommon/scripts/utils/colors.sh78
-rwxr-xr-xcommon/scripts/utils/cryptocheck31
-rwxr-xr-xcommon/scripts/utils/cryptonotify19
-rwxr-xr-xcommon/scripts/utils/disable_meta_key_guest.sh94
-rwxr-xr-xcommon/scripts/utils/heads-up-display88
-rwxr-xr-xcommon/scripts/utils/hex2rgb.sh3
-rwxr-xr-xcommon/scripts/utils/kill-notify4
-rwxr-xr-xcommon/scripts/utils/kill-process6
-rwxr-xr-xcommon/scripts/utils/mnt27
-rwxr-xr-xcommon/scripts/utils/move_terminal65
-rwxr-xr-xcommon/scripts/utils/neovim.sh422
-rwxr-xr-xcommon/scripts/utils/pack88
-rwxr-xr-xcommon/scripts/utils/rofi-network-manager.sh252
-rwxr-xr-xcommon/scripts/utils/root.sh152
-rwxr-xr-xcommon/scripts/utils/run_with_display.sh19
-rwxr-xr-xcommon/scripts/utils/scratchpad147
-rwxr-xr-xcommon/scripts/utils/screenshot102
-rwxr-xr-xcommon/scripts/utils/sendkeys.awk86
-rwxr-xr-xcommon/scripts/utils/sext47
-rwxr-xr-xcommon/scripts/utils/track-books.sh19
-rwxr-xr-xcommon/scripts/utils/umnt22
-rwxr-xr-xcommon/scripts/utils/waypipe_app16
-rwxr-xr-xcommon/scripts/utils/wayvnc_session10
-rwxr-xr-xcommon/scripts/utils/window_manager_name.sh29
-rwxr-xr-xcommon/scripts/utils/xtouch38
-rwxr-xr-xcommon/scripts/virt/checksum.sh36
-rwxr-xr-xcommon/scripts/virt/dos.sh998
-rwxr-xr-xcommon/scripts/virt/server.sh128
-rwxr-xr-xcommon/scripts/virt/ubuntu222
-rwxr-xr-xcommon/scripts/virt/windows.sh1156
155 files changed, 29090 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
new file mode 100644
index 0000000..3aafd06
--- /dev/null
+++ b/common/.face
Binary files differ
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..b847d6c
--- /dev/null
+++ b/common/.profile
@@ -0,0 +1,102 @@
+#!/bin/bash
+
+#~/.profile
+
+# ======================================
+# 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"
+ exec dbus-launch --sh-syntax --exit-with-session "$s" >/dev/null 2>&1
+ ;;
+ 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
new file mode 100644
index 0000000..e85b2ff
--- /dev/null
+++ b/common/assets/desktop.jpg
Binary files differ
diff --git a/common/assets/old_desktop.jpg b/common/assets/old_desktop.jpg
new file mode 100644
index 0000000..3ecb22e
--- /dev/null
+++ b/common/assets/old_desktop.jpg
Binary files differ
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"
diff --git a/common/scripts/README.md b/common/scripts/README.md
new file mode 100644
index 0000000..d3735bc
--- /dev/null
+++ b/common/scripts/README.md
@@ -0,0 +1,17 @@
+# scripts
+
+```
+.scripts/
+├── dev/ # Development tools, IDEs, build scripts, compilers
+├── media/ # Gaming, social media, video, streaming automation
+├── net/ # Networking, SSH, DNS, VPN, monitoring, firewalls
+├── sec/ # Security, auditing, forensics, pentesting, hardening
+├── sys/ # System monitoring, hardware info, diagnostics, admin
+├── utils/ # General utilities, helpers, converters, formatters
+├── virt/ # Virtualization, containers, VMs, Docker, KVM
+├── test/ # Testing, debugging, script validation
+├── .editorconfig # Consistent editing/formatting styles
+├── .gitignore # Files to ignore/not share
+├── LICENSE # Usage and distribution terms
+└── README.md # This documentation
+```
diff --git a/common/scripts/dev/.gitkeep b/common/scripts/dev/.gitkeep
new file mode 100644
index 0000000..cd0b162
--- /dev/null
+++ b/common/scripts/dev/.gitkeep
@@ -0,0 +1 @@
+# This directory is tracked by Git
diff --git a/common/scripts/media/.gitkeep b/common/scripts/media/.gitkeep
new file mode 100644
index 0000000..cd0b162
--- /dev/null
+++ b/common/scripts/media/.gitkeep
@@ -0,0 +1 @@
+# This directory is tracked by Git
diff --git a/common/scripts/net/.gitkeep b/common/scripts/net/.gitkeep
new file mode 100644
index 0000000..cd0b162
--- /dev/null
+++ b/common/scripts/net/.gitkeep
@@ -0,0 +1 @@
+# This directory is tracked by Git
diff --git a/common/scripts/sec/.gitkeep b/common/scripts/sec/.gitkeep
new file mode 100644
index 0000000..cd0b162
--- /dev/null
+++ b/common/scripts/sec/.gitkeep
@@ -0,0 +1 @@
+# This directory is tracked by Git
diff --git a/common/scripts/sys/battery_alert.sh b/common/scripts/sys/battery_alert.sh
new file mode 100755
index 0000000..47fa556
--- /dev/null
+++ b/common/scripts/sys/battery_alert.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+# Send a notification if the laptop battery is either low or is fully charged.
+
+# Battery percentage at which to notify
+WARNING_LEVEL=78
+CRITICAL_LEVEL=5
+BATTERY_DISCHARGING=$(acpi -b | grep "Battery 0" | grep -c "Discharging")
+BATTERY_LEVEL=$(acpi -b | grep "Battery 0" | grep -P -o '[0-9]+(?=%)')
+
+# Use files to store whether we've shown a notification or not (to prevent multiple notifications)
+FULL_FILE=/tmp/batteryfull
+EMPTY_FILE=/tmp/batteryempty
+CRITICAL_FILE=/tmp/batterycritical
+
+# Reset notifications if the computer is charging/discharging
+if [ "$BATTERY_DISCHARGING" -eq 1 ]; then
+ # Battery is discharging
+ if [ -f "$FULL_FILE" ]; then
+ command rm "$FULL_FILE"
+ fi
+ if [ "$BATTERY_LEVEL" -le "$WARNING_LEVEL" ] && [ ! -f "$EMPTY_FILE" ]; then
+ notify-send "Low Battery" "${BATTERY_LEVEL}% of battery remaining." -u critical -i "battery-low-symbolic" -r 9991
+ touch "$EMPTY_FILE"
+ fi
+ if [ "$BATTERY_LEVEL" -le "$CRITICAL_LEVEL" ] && [ ! -f "$CRITICAL_FILE" ]; then
+ notify-send "Battery Critical" "The computer will shut down soon." -u critical -i "battery-caution-symbolic" -r 9991
+ touch "$CRITICAL_FILE"
+ fi
+elif [ "$BATTERY_DISCHARGING" -eq 0 ]; then
+ # Battery is charging
+ if [ "$BATTERY_LEVEL" -gt 99 ] && [ ! -f "$FULL_FILE" ]; then
+ notify-send "Battery Charged" "Battery is fully charged." -i "battery-full-symbolic" -r 9991
+ touch "$FULL_FILE"
+ fi
+ # Reset empty/critical notifications when charging begins
+ if [ -f "$EMPTY_FILE" ]; then
+ command rm "$EMPTY_FILE"
+ fi
+ if [ -f "$CRITICAL_FILE" ]; then
+ command rm "$CRITICAL_FILE"
+ fi
+fi
diff --git a/common/scripts/sys/gsettings.sh b/common/scripts/sys/gsettings.sh
new file mode 100755
index 0000000..9054344
--- /dev/null
+++ b/common/scripts/sys/gsettings.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Disable screen lock
+gsettings set org.gnome.desktop.screensaver lock-enabled false
+gsettings set org.gnome.desktop.session idle-delay 0
+
+# Mutter Overlay Key
+gsettings set org.gnome.mutter overlay-key ''
+
+# Disable update notification
+#gsettings set org.gnome.software download-updates false
+#gsettings set com.ubuntu.update-notifier no-show-notifications true
+#sudo rm /etc/xdg/autostart/upg-notifier-autostart.desktop
+
+#sudo mv /etc/xdg/autostart/update-notifier.desktop /etc/xdg/autostart/update-notifier.desktop.old
+#sudo mv /etc/xdg/autostart/gnome-software-service.desktop /etc/xdg/autostart/gnome-software-service.desktop.old
+
+# Custom Keybinding Names
+gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings "['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/']"
+
+# Custom Keybinding 0
+gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/ binding "<Alt>T"
+gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/ command "scratchpad"
+gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/ name "scratchpad"
+
+# Disable keyboard plugin
+gsettings set org.gnome.settings-daemon.plugins.keyboard active false
+
+# Prefer dark mode
+gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
+
+# Default terminal
+gsettings set org.cinnamon.desktop.default-applications.terminal exec wezterm
+
+# Disable screensaver
+gsettings set org.gnome.desktop.session idle-delay 0
+gsettings set org.gnome.desktop.screensaver lock-enabled false
+
+# Hack
+#gsettings set org.gnome.desktop.app-folders folder-children "['Red', 'SysAdmin', 'Blue']"
diff --git a/common/scripts/sys/keep_guest_awake.sh b/common/scripts/sys/keep_guest_awake.sh
new file mode 100755
index 0000000..13d839b
--- /dev/null
+++ b/common/scripts/sys/keep_guest_awake.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+
+# Host-side script to keep QEMU guest display awake without triggering anything
+# and handle Windows key logic (disable it in the guest, map Caps Lock to Windows key).
+
+MONITOR_SOCKET_DIR="$HOME/machines/vm"
+QEMU_CLASS="qemu-system-x86_64"
+SEND_CMD_MONITOR="sendkey f15"
+SEND_CMD_XDO="key --clearmodifiers F15"
+DISABLE_WINDOWS_KEY_CMD="sendkey meta_l NoSymbol" # Using ctrl_l as a placeholder
+CAPS_LOCK_CMD="sendkey meta_l" # Remapped Caps Lock to Super_L (Windows key)
+
+## Function to disable the Windows key and remap Caps Lock to Super_L
+#disable_windows_key_and_remap_caps_lock() {
+# shopt -s nullglob
+# sockets=("$MONITOR_SOCKET_DIR"/*.socket)
+# shopt -u nullglob
+#
+# for socket in "${sockets[@]}"; do
+# # Get VM name from socket file
+# VM_NAME=$(basename "$socket" .socket)
+# VM_NAME=${VM_NAME%-serial}
+#
+# # Send QMP command to disable Windows key (Super_L key press/remap)
+# qmp_command='{"execute": "device_key_press", "arguments": {"dev": "virtio-keyboard-pci", "key": "capslock"}}'
+# echo "$qmp_command" | socat - "UNIX-CONNECT:$socket" >/dev/null
+#
+# # Send QMP command to remap Caps Lock to Super_L (Windows key)
+# qmp_command='{"execute": "guest-execute", "arguments": {"path": "/usr/bin/xmodmap", "arg": ["-e", "keycode 66 = Super_L"]}}'
+# echo "$qmp_command" | socat - "UNIX-CONNECT:$socket" >/dev/null
+#
+# # Optional: Disable Super_L key (Windows key)
+# qmp_command='{"execute": "guest-execute", "arguments": {"path": "/usr/bin/xmodmap", "arg": ["-e", "keycode 133 = NoSymbol"]}}'
+# echo "$qmp_command" | socat - "UNIX-CONNECT:$socket" >/dev/null
+# done
+#}
+
+# Function to keep the guest display awake by sending F15 key press
+keep_guest_display_awake() {
+ shopt -s nullglob
+ sockets=("$MONITOR_SOCKET_DIR"/*.socket)
+ shopt -u nullglob
+
+ if [ ${#sockets[@]} -eq 0 ]; then
+ sleep 30
+ return
+ fi
+
+ focused_qemu=false
+
+ if command -v xdotool >/dev/null 2>&1; then
+ active_win_id=$(xdotool getwindowfocus 2>/dev/null)
+ if [ "$active_win_id" != "" ]; then
+ active_class=$(xdotool getwindowclassname "$active_win_id" 2>/dev/null)
+ if [[ "$active_class" == "$QEMU_CLASS" ]]; then
+ focused_qemu=true
+ fi
+ fi
+ fi
+
+ if "$focused_qemu"; then
+ # QEMU is focused → send F15 via xdotool
+ #xdotool "$SEND_CMD_XDO"
+ echo ""
+
+ else
+ # QEMU is not focused → send F15 via monitor
+ for socket in "${sockets[@]}"; do
+ echo "$SEND_CMD_MONITOR" | socat - "UNIX-CONNECT:$socket"
+ done
+ fi
+}
+
+# Main loop
+while true; do
+ # Handle Windows key remapping and Caps Lock disablement
+ #disable_windows_key_and_remap_caps_lock
+
+ # Keep the guest display awake
+ keep_guest_display_awake
+
+ # Sleep before next cycle
+ sleep 30
+done
diff --git a/common/scripts/sys/low-bat-notifier b/common/scripts/sys/low-bat-notifier
new file mode 100755
index 0000000..a67b25d
--- /dev/null
+++ b/common/scripts/sys/low-bat-notifier
@@ -0,0 +1,79 @@
+#!/bin/bash
+
+### VARIABLES
+
+POLL_INTERVAL=120 # seconds at which to check battery level
+LAST_NOTIFIED=-1 # track last notified battery percentage to avoid repeated notifications
+
+# If BAT0 doesn't work for you, check available devices with:
+# $ ls -1 /sys/class/power_supply/
+BAT_PATH=/sys/class/power_supply/BAT0
+BAT_STAT=$BAT_PATH/status
+
+if [[ -f $BAT_PATH/charge_full ]]; then
+ BAT_FULL=$BAT_PATH/charge_full
+ BAT_NOW=$BAT_PATH/charge_now
+elif [[ -f $BAT_PATH/energy_full ]]; then
+ BAT_FULL=$BAT_PATH/energy_full
+ BAT_NOW=$BAT_PATH/energy_now
+else
+ exit
+fi
+
+kill_running() {
+ local mypid=$$
+ local pids
+ mapfile -t pids < <(pgrep -f "${0##*/}")
+
+ for pid in "${pids[@]}"; do
+ if [[ $pid -ne $mypid ]]; then
+ kill "$pid"
+ sleep 1
+ fi
+ done
+}
+
+get_battery_icon() {
+ local percent=$1
+ if ((percent > 80)); then
+ echo "🔋" # Full battery
+ elif ((percent > 60)); then
+ echo "🔋" # High battery
+ elif ((percent > 40)); then
+ echo "🔋" # Medium battery
+ elif ((percent > 20)); then
+ echo "🪫" # Low battery
+ else
+ echo "⚠️" # Critical battery
+ fi
+}
+
+# Run only if battery is detected
+if ls -1qA /sys/class/power_supply/ | grep -q BAT; then
+
+ kill_running
+
+ while true; do
+ bf=$(cat "$BAT_FULL")
+ bn=$(cat "$BAT_NOW")
+ bs=$(cat "$BAT_STAT")
+
+ bat_percent=$((100 * $bn / $bf))
+ bat_icon=$(get_battery_icon "$bat_percent")
+
+ # Notify only at exact levels: 20%, 15%, and 10%
+ if [[ ($bat_percent -eq 20 || $bat_percent -eq 15 || $bat_percent -eq 10) && "$bs" = "Discharging" && $bat_percent -ne $LAST_NOTIFIED ]]; then
+ notify-send --urgency=critical "$bat_icon $bat_percent% : $([[ $bat_percent -eq 10 ]] && echo 'Critical Battery! Shutting down in 1 minute...' || echo 'Low Battery!')"
+ LAST_NOTIFIED=$bat_percent
+
+ if [[ $bat_percent -eq 10 ]]; then
+ sleep 60
+ shutdown now
+ fi
+ elif [[ "$bs" = "Charging" ]]; then
+ LAST_NOTIFIED=-1
+ fi
+
+ sleep "$POLL_INTERVAL"
+ done
+fi
diff --git a/common/scripts/sys/session_manager.sh b/common/scripts/sys/session_manager.sh
new file mode 100755
index 0000000..b6a6b03
--- /dev/null
+++ b/common/scripts/sys/session_manager.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+
+cd ~
+
+# Default session to be executed
+unset DISPLAY XAUTHORITY DBUS_SESSION_BUS_ADDRESS
+
+session=""
+
+# Function to display and start the selected session
+display() {
+ # Default list of sessions in priority order
+ default_sessions=("Hyprland" "bspwm" "sway")
+
+ # Check conditions and set session command
+ if [ "$DISPLAY" = "" ] && [ "$XDG_VTNR" -eq 1 ]; then
+ if [ -f ~/.session ]; then
+ session=$(cat ~/.session)
+ rm ~/.session # Remove the session file after reading
+ fi
+
+ if [ "$session" != "" ]; then
+ case "$session" in
+ bspwm )
+ export XDG_SESSION_TYPE="x11"
+ session="startx /usr/bin/bspwm"
+ ;;
+ Hyprland | sway)
+ session="dbus-launch --sh-syntax --exit-with-session $session"
+ ;;
+ *)
+ echo "Session $session is not supported."
+ session=""
+ ;;
+ esac
+ else
+ # Iterate through default sessions to find a suitable one
+ for wm in "${default_sessions[@]}"; do
+ if command -v "$wm" >/dev/null 2>&1; then
+ case "$wm" in
+ bspwm )
+ export XDG_SESSION_TYPE="x11"
+ session="startx /usr/bin/$wm"
+ break
+ ;;
+ Hyprland | sway)
+ session="dbus-launch --sh-syntax --exit-with-session $wm >/dev/null 2>&1 && exit"
+ #show_animation.sh
+ clear
+ break
+ ;;
+ esac
+ fi
+ done
+ fi
+
+ # Execute the session command if session is set
+ if [ "$session" != "" ]; then
+ #echo "Starting session: $session"
+ eval "$session"
+ else
+ echo "No suitable window manager found or conditions not met."
+ fi
+ fi
+}
+
+# Main function
+main() {
+ display
+}
+
+main "$@"
diff --git a/common/scripts/sys/shutdown_watchdog b/common/scripts/sys/shutdown_watchdog
new file mode 100755
index 0000000..79bc7bf
--- /dev/null
+++ b/common/scripts/sys/shutdown_watchdog
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+LOGFILE="/tmp/shutdown_watchdog.log"
+
+while true; do
+ echo "--- $(date) ---" >> "$LOGFILE"
+ uptime >> "$LOGFILE"
+ sensors >> "$LOGFILE" 2>/dev/null
+ acpi -V >> "$LOGFILE" 2>/dev/null
+ sleep 10
+done
diff --git a/common/scripts/test/.gitkeep b/common/scripts/test/.gitkeep
new file mode 100644
index 0000000..cd0b162
--- /dev/null
+++ b/common/scripts/test/.gitkeep
@@ -0,0 +1 @@
+# This directory is tracked by Git
diff --git a/common/scripts/utils/backlight_default.sh b/common/scripts/utils/backlight_default.sh
new file mode 100755
index 0000000..b79e680
--- /dev/null
+++ b/common/scripts/utils/backlight_default.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+set -e
+
+backlight_sys_dir="/sys/class/backlight/intel_backlight"
+
+read -r max_brightness < "${backlight_sys_dir}/max_brightness"
+read -r curr_brightness < "${backlight_sys_dir}/brightness"
+
+if ! groups | grep -q backlight; then
+ echo "User is not in the backlight group"
+ exit 1
+fi
+
+if [ "$#" -eq 0 ] ; then
+ # set to half that of 'max_brightness'
+ echo $((max_brightness / 2)) > "$backlight_sys_dir"/brightness
+ exit 0
+fi
+
+case "$1" in
+ up) increment="+ 10" ;;
+ down) increment="- 10" ;;
+ *) exit 1 ;;
+esac
+
+new_brightness=$(($curr_brightness $increment))
+
+if $((new_brightness < 1)) || $((new_brightness > $max_brightness)); then
+ exit 1
+else
+ echo "$new_brightness" > "$backlight_sys_dir"/brightness
+fi
diff --git a/common/scripts/utils/colors.sh b/common/scripts/utils/colors.sh
new file mode 100755
index 0000000..fc1c10c
--- /dev/null
+++ b/common/scripts/utils/colors.sh
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+colors=$@
+for (( n=0; n < $colors; n++ )) do
+ printf " [%d] $(tput setaf $n)%s$(tput sgr0)" $n "Hello World!
+"
+done
+PADDING='Padding'
+
+main() {
+ local xterm_start=0 \
+ xterm_width=8 \
+ xterm_height=2
+
+ local cube_start=$((xterm_start + xterm_width * xterm_height)) \
+ cube_width=6 \
+ cube_height=$((6 * 6))
+
+ local greys_start=$((cube_start + cube_width * cube_height)) \
+ greys_width=8 \
+ greys_height=3
+
+ color_block $xterm_start $xterm_width $xterm_height
+ color_block $cube_start $cube_width $cube_height use_padding
+ color_block $greys_start $greys_width $greys_height
+ echo
+}
+
+color_block() {
+ local start=$1 width=$2 height=$3 use_padding=$4
+ local max s color_nums colors
+
+ max=$((start + width * height - 1))
+
+ echo
+ for s in $(seq $start $width $max); do
+ color_nums=$(seq $s $((s + width - 1)))
+ colors="${use_padding:+$PADDING }${color_nums}${use_padding:+ $PADDING}"
+
+ printf '%s%s %s%s\n' \
+ "$(fg_bars $colors)" $ansi_reset \
+ "$(bg_bars $colors)" $ansi_reset
+ done
+}
+
+fg_bars() {
+ for color in $@; do
+ color_bar ansi_fg $color ''
+ done
+}
+
+bg_bars() {
+ for color in $@; do
+ color_bar ansi_bg $color ' '
+ done
+}
+
+color_bar() {
+ local ansi=$1 color=$2 trail=$3
+
+ if [ "$color" == $PADDING ]; then
+ printf '%s %s' $ansi_reset "$trail"
+ else
+ local color_seq=$($ansi $color)
+ printf '%s %03d%s' $color_seq $color "$trail"
+ fi
+}
+
+ansi_reset=$'\033[0m'
+
+ansi_fg() {
+ printf '\033[38;5;%dm' $1
+}
+
+ansi_bg() {
+ printf '\033[48;5;%dm' $1
+}
+
+main
diff --git a/common/scripts/utils/cryptocheck b/common/scripts/utils/cryptocheck
new file mode 100755
index 0000000..02ba42d
--- /dev/null
+++ b/common/scripts/utils/cryptocheck
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+if [ ! -d ~/.cache/crypto ]; then
+ mkdir ~/.cache/crypto
+fi
+ticker=(BTC ETH ADA DOT SOL XMR)
+
+for currency in "${ticker[@]}"; do
+ echo "$currency"
+done | while read coin
+ do
+ price=$(curl rate.sx/1$coin)
+ if [ $coin = "BTC" ]; then
+ icon=󰠓
+ elif [ $coin = "ETH" ]; then
+ icon=󰡪
+ elif [ $coin = "ADA" ]; then
+ icon=󰝨
+ elif [ $coin = "DOT" ]; then
+ icon=󰐇
+ elif [ $coin = "SOL" ]; then
+ icon=󰘙
+ elif [ $coin = "XMR" ]; then
+ icon=󰝴
+ fi
+
+ echo "$icon $coin: $price" > ~/.cache/crypto/$coin
+
+ done
+
+date > ~/.cache/crypto/time
+
diff --git a/common/scripts/utils/cryptonotify b/common/scripts/utils/cryptonotify
new file mode 100755
index 0000000..47883c3
--- /dev/null
+++ b/common/scripts/utils/cryptonotify
@@ -0,0 +1,19 @@
+#!/bin/sh
+source cryptocheck
+Output=
+for file in ~/.cache/crypto/*
+do
+ if [ ! -z "$Output" ]; then
+ if [ ! $(basename $file) = "time" ]; then
+ Output="$Output\n$(cat $file)"
+ fi
+ else
+ if [ ! $(basename $file) = "time" ]; then
+ Output="$Output$(cat $file)"
+ fi
+ fi
+
+done
+
+Output="$Output\n$(cat ~/.cache/crypto/time)"
+notify-send "Crypto Prices" "$Output"
diff --git a/common/scripts/utils/disable_meta_key_guest.sh b/common/scripts/utils/disable_meta_key_guest.sh
new file mode 100755
index 0000000..acbbdf5
--- /dev/null
+++ b/common/scripts/utils/disable_meta_key_guest.sh
@@ -0,0 +1,94 @@
+ain "$@"
+#!/usr/bin/env bash
+
+# Function to find the QEMU monitor socket dynamically and extract the VM name
+get_socket_path() {
+ local socket_dir="$HOME/machines/vm" # Directory where your sockets are stored
+ local socket_file=$(find "$socket_dir" -type s -name "*-monitor.socket" | head -n 1)
+
+ echo "[DEBUG] Found socket file: $socket_file" # Debugging line to check the socket file
+
+ if [[ -z "$socket_file" ]]; then
+ echo "Error: No QEMU monitor socket found in $socket_dir." >&2
+ exit 1
+ fi
+
+ # Extract the VM name from the socket file name
+ local vm_name=$(basename "$socket_file" -monitor.socket)
+ echo "[DEBUG] VM name detected: $vm_name" # Debugging line to check VM name
+
+ echo "$socket_file" # Return the full socket path
+ echo "$vm_name" # Return the VM name
+}
+
+# Function to check if sendkeys.awk is in the user's PATH or fall back to specific directories
+find_sendkeys_awk() {
+ # Check if sendkeys.awk is in the user's PATH
+ if command -v sendkeys.awk &>/dev/null; then
+ echo "$(command -v sendkeys.awk)"
+ return 0
+ fi
+
+ # Otherwise, check in specific fallback directories
+ local possible_paths=(
+ "$HOME/.scripts/env/linux/utils/sendkeys.awk"
+ "$HOME/.scripts/sendkeys.awk"
+ )
+
+ for path in "${possible_paths[@]}"; do
+ if [[ -f "$path" ]]; then
+ echo "$path"
+ return 0
+ fi
+ done
+
+ echo "sendkeys.awk not found in the user's PATH or known directories." >&2
+ exit 1
+}
+
+send_guest_command() {
+ local cmd="$1"
+ local socket="$2"
+ echo "$cmd" | awk -f "$SENDKEYS_AWK" | socat - UNIX-CONNECT:"$socket"
+}
+
+main() {
+ # Get the QEMU socket and VM name dynamically
+ SOCKET=$(get_socket_path)
+ VM_NAME=$(basename "$SOCKET" -monitor.socket)
+
+ # Debugging output to verify the socket and VM name
+ echo "[DEBUG] Using socket: $SOCKET"
+ echo "[DEBUG] VM name: $VM_NAME"
+
+ # If no socket file is found, exit
+ if [[ ! -S "$SOCKET" ]]; then
+ echo "Error: QEMU monitor socket for $VM_NAME does not exist or is not available."
+ exit 1
+ fi
+
+ # Try to find the sendkeys.awk file
+ SENDKEYS_AWK=$(find_sendkeys_awk)
+
+ echo "[*] Attempting Caps Lock to Super remapping on all known platforms..."
+
+ # === X11 ===
+ send_guest_command "setxkbmap -option caps:super" "$SOCKET"
+ send_guest_command "xmodmap -e 'remove Mod4 = Super_L Super_R'" "$SOCKET"
+ send_guest_command "xmodmap -e 'keycode 133 = NoSymbol'" "$SOCKET"
+ send_guest_command "xmodmap -e 'keycode 134 = NoSymbol'" "$SOCKET"
+
+ # === Wayland (note: this just gives a reminder) ===
+ send_guest_command "gsettings set org.gnome.desktop.input-sources xkb-options \"['caps:super']\"" "$SOCKET"
+ send_guest_command "echo 'Wayland? Try remapping via wlroots/wlr-keyboard'" "$SOCKET"
+
+ # === Windows (registry scancode map) ===
+ send_guest_command "powershell -Command \"Set-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Keyboard Layout' -Name 'Scancode Map' -Value ([byte[]](0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x3A,0x00,0x5B,0x00,0x00,0x00))\"" "$SOCKET"
+
+ # === macOS (hidutil remap) ===
+ send_guest_command "hidutil property --set '{\"UserKeyMapping\":[{\"HIDKeyboardModifierMappingSrc\":0x700000039,\"HIDKeyboardModifierMappingDst\":0x7000000E3}]}'" "$SOCKET"
+
+ echo "[*] Done sending remapping commands to guest."
+}
+
+main "$@"
diff --git a/common/scripts/utils/heads-up-display b/common/scripts/utils/heads-up-display
new file mode 100755
index 0000000..54f5de1
--- /dev/null
+++ b/common/scripts/utils/heads-up-display
@@ -0,0 +1,88 @@
+#!/bin/sh
+
+# Created By: srdusr
+# Created On: Wed 05 Feb 2023 01:24:37 AM CAT
+# Project: bspwm scratchpad (Heads-Up-Display) with tmux session
+
+### Alternative method
+
+#if id="$(xdo id -N heads-up-display)"; then
+# bspc node "$id" -g hidden -f
+#else
+# #kitty --class "heads-up-display" -e tmux new-session -A -s HUD -e bash >/dev/null 2>&1 &
+# wezterm start --class "heads-up-display" -e tmux new-session -A -s HUD -e bash >/dev/null 2>&1 &
+#fi
+
+#- - - - - - - - - -
+
+### Alternative method
+
+#id=$(xdotool search --class heads-up-display)
+#if [ "$id" = "" ]; then
+# #kitty --class "Heads-Up-Display" -e tmux new-session -A -s HUD -e bash > /dev/null 2>&1 &
+# alacritty --class "heads-up-display" -e tmux new-session -A -s HUD -e bash >/dev/null 2>&1 &
+#else
+# if [ ! -f /tmp/hide_hud ]; then
+# touch /tmp/hide_hud && xdo hide "$id"
+# elif [ -f /tmp/hide_hud ]; then
+# rm /tmp/hide_hud && xdo show "$id"
+# fi
+#fi
+
+
+
+# Set the environment variables to x11 to allow working in Wayland
+export GDK_BACKEND=x11
+export QT_QPA_PLATFORM=xcb
+#export WAYLAND_DISPLAY=""
+export WINIT_UNIX_BACKEND=x11
+
+# Supported terminals and dropdown class
+supported_terminals=("wezterm" "kitty" "alacritty")
+
+# Check if any supported terminal with scratchpad class is running
+for term in "${supported_terminals[@]}"; do
+ if pgrep -f "$term.*--class heads-up-display" >/dev/null; then
+ my_term="$term"
+ break
+ fi
+done
+
+# If no supported terminal is running, start the first available one
+if [ "$my_term" = "" ]; then
+ for term in "${supported_terminals[@]}"; do
+ if command -v "$term" >/dev/null 2>&1; then
+ my_term="$term"
+ break
+ fi
+ done
+ if [ "$my_term" = "" ]; then
+ echo "No supported terminal found." && exit 1
+ fi
+
+ # Start terminal with scratchpad class
+ case "$my_term" in
+ "wezterm") wezterm start --class heads-up-display -e tmux new-session -A -s HUD -e bash htop & ;;
+ "kitty") kitty --class heads-up-display tmux new-session -A -s HUD -e bash htop & ;;
+ "alacritty") alacritty --class heads-up-display -e tmux new-session -A -s HUD -e bash htop & ;;
+
+ esac
+fi
+
+# Get the window ID of the scratchpad terminal
+id="$(xdo id -N heads-up-display)"
+
+# Toggle scratchpad terminal visibility
+if [ "$id" != "" ]; then
+ if xwininfo -id "$id" | grep "Map State: IsViewable" >/dev/null; then
+ # Scratchpad is visible, hide it
+ dimensions="$(xwininfo -id "$id" | awk '/Width:|Height:/ { printf("%s=%s;", tolower($1), $2) }')"
+ xdo hide "$id" 2>/dev/null
+ else
+ # Scratchpad is hidden, show it and restore dimensions
+ xdo show "$id"
+ xdotool windowsize "$id" "$(echo "$dimensions" | tr ';' ' ')" 2>/dev/null
+ xdotool windowactivate "$id"
+ xdotool windowfocus "$id"
+ fi
+fi
diff --git a/common/scripts/utils/hex2rgb.sh b/common/scripts/utils/hex2rgb.sh
new file mode 100755
index 0000000..a8ccc38
--- /dev/null
+++ b/common/scripts/utils/hex2rgb.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+hex="${1}"
+printf "R: %d G: %d B: %d\n" 0x"${hex:0:2}" 0x"${hex:2:2}" 0x"${hex:4:2}"
diff --git a/common/scripts/utils/kill-notify b/common/scripts/utils/kill-notify
new file mode 100755
index 0000000..f7f749e
--- /dev/null
+++ b/common/scripts/utils/kill-notify
@@ -0,0 +1,4 @@
+#!/bin/sh
+# Kills an application and sends a notification that it's been killed
+
+killall "$1" && notify-send "Killed $1"
diff --git a/common/scripts/utils/kill-process b/common/scripts/utils/kill-process
new file mode 100755
index 0000000..5247e5f
--- /dev/null
+++ b/common/scripts/utils/kill-process
@@ -0,0 +1,6 @@
+#!/bin/sh
+# pipes list of processes into rofi/dmenu and kills the selection
+
+proc=$(ps -u $USER -o pid,%mem,%cpu,comm | sort -b -k2 -r | sed -n '1!p' | dmenu -i -p "Kill" | awk '{print $1,$4}')
+
+[ -z "$proc" ] || (kill -15 $(echo $proc | awk '{print $1}') 2>/dev/null && notify-send "$(echo $proc | awk '{print $2}') killed")
diff --git a/common/scripts/utils/mnt b/common/scripts/utils/mnt
new file mode 100755
index 0000000..dc19ca9
--- /dev/null
+++ b/common/scripts/utils/mnt
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+# mount disk
+set -e
+. ~/etc/colors/current
+
+d() case $LABEL in
+Windows) echo "/win" ;;
+*) echo "${1:-$HOME}/dev/$NAME" ;;
+esac
+
+lsblk -Po NAME,SIZE,MOUNTPOINT,FSTYPE,LABEL | while read -r a; do
+ eval "$a"
+
+ [ "$FSTYPE" ] && [ ! "$MOUNTPOINT" ] &&
+ printf "%-4s:%s:%s:-> %s\n" \
+ "$NAME" \
+ "$SIZE" \
+ "${LABEL:-unnamed}" \
+ "$(d \~)"
+
+done | column -ts':' -o' ' | menu -p mount | {
+ read -r NAME _
+ eval "$(lsblk -Polabel "/dev/$NAME")"
+ mkdir -p "$(d)"
+ sudo mount -o "umask=000" "/dev/$NAME" "$(d)"
+ notify-send summary "<span color='#$green'>$NAME: $LABEL</span>\n$(d \~)"
+}
diff --git a/common/scripts/utils/move_terminal b/common/scripts/utils/move_terminal
new file mode 100755
index 0000000..2b447d0
--- /dev/null
+++ b/common/scripts/utils/move_terminal
@@ -0,0 +1,65 @@
+#!/bin/bash
+
+SCRATCHPAD_CLASSES=("scratchpad" "pack" "heads-up-display")
+PRIMARY_MONITOR="eDP-1"
+SECOND_MONITOR="HDMI-A-2"
+
+PRIMARY_WORKSPACES=(2 4 5 6)
+SECOND_WORKSPACES=(1 3)
+
+# Check if second monitor exists
+if ! hyprctl monitors -j | jq -e ".[] | select(.name == \"$SECOND_MONITOR\")" >/dev/null; then
+ # No second monitor, do nothing
+ exit 0
+fi
+
+# Check if list of workspaces has any windows
+has_windows_in_workspaces() {
+ local workspaces=("$@")
+ for ws in "${workspaces[@]}"; do
+ local count
+ count=$(hyprctl clients -j | jq "[.[] | select(.workspace.id == $ws)] | length")
+ if ((count > 0)); then
+ return 0
+ fi
+ done
+ return 1
+}
+
+# Check window presence
+primary_has_windows=false
+second_has_windows=false
+
+if has_windows_in_workspaces "${PRIMARY_WORKSPACES[@]}"; then
+ primary_has_windows=true
+fi
+if has_windows_in_workspaces "${SECOND_WORKSPACES[@]}"; then
+ second_has_windows=true
+fi
+
+# Decide target monitor
+if [[ "$primary_has_windows" == true && "$second_has_windows" == false ]]; then
+ TARGET_MONITOR="$SECOND_MONITOR"
+elif [[ "$primary_has_windows" == false && "$second_has_windows" == true ]]; then
+ TARGET_MONITOR="$PRIMARY_MONITOR"
+else
+ TARGET_MONITOR="$SECOND_MONITOR" # both busy or both empty → second monitor
+fi
+
+# Get workspace id of the target monitor
+TARGET_WORKSPACE=$(hyprctl monitors -j | jq ".[] | select(.name == \"$TARGET_MONITOR\") | .activeWorkspace.id")
+
+# Wait for scratchpad window to appear and move it
+for _ in {1..20}; do
+ for class in "${SCRATCHPAD_CLASSES[@]}"; do
+ client=$(hyprctl clients -j | jq -r ".[] | select(.class == \"$class\") | .address")
+ if [[ -n "$client" ]]; then
+ hyprctl dispatch movetoworkspacesilent "$TARGET_WORKSPACE,address:$client"
+ hyprctl dispatch focuswindow address:"$client"
+ exit 0
+ fi
+ done
+ sleep 0.1
+done
+
+exit 1
diff --git a/common/scripts/utils/neovim.sh b/common/scripts/utils/neovim.sh
new file mode 100755
index 0000000..c9adcca
--- /dev/null
+++ b/common/scripts/utils/neovim.sh
@@ -0,0 +1,422 @@
+#!/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
+
+# 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
+}
+
+# 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"
+
+ # Add 'v' prefix if not present
+ if [[ $version != v* ]]; then
+ version="v$version"
+ fi
+
+ local url="https://github.com/neovim/neovim/releases/download/$version/nvim-linux-x86_64.appimage"
+ install_neovim "$url"
+ local version_output=$(nvim --version)
+ version_id="Version $version $(echo "$version_output" | grep -oP 'NVIM \d+\.\d+\.\d+')"
+}
+
+# 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
+}
+
+# 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
+ # Normalize version
+ if [[ $version != v* ]]; then
+ version="v$version"
+ fi
+ # Check if the specific version exists on GitHub releases
+ if version_exists "$version"; then
+ # Install specific version
+ specific_version "$version" # Pass the normalized version to the function
+ valid_choice=true
+ else
+ printf "${RED}The specified version $version does not exist.${NC}\n"
+ fi
+ ;;
+
+ *)
+ 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/scripts/utils/pack b/common/scripts/utils/pack
new file mode 100755
index 0000000..4b8760c
--- /dev/null
+++ b/common/scripts/utils/pack
@@ -0,0 +1,88 @@
+#!/bin/sh
+
+# Created By: srdusr
+# Created On: Wed 05 Feb 2023 01:24:37 AM CAT
+# Project: bspwm scratchpad (pack) with tmux session
+
+### Alternative method
+
+#if id="$(xdo id -N pack)"; then
+# bspc node "$id" -g hidden -f
+#else
+# #kitty --class "pack" -e tmux new-session -A -s pack -e bash >/dev/null 2>&1 &
+# wezterm start --class "pack" -e tmux new-session -A -s pack -e bash >/dev/null 2>&1 &
+#fi
+
+#- - - - - - - - - -
+
+### Alternative method
+
+#id=$(xdotool search --class pack)
+#if [ "$id" = "" ]; then
+# #kitty --class "pack" -e tmux new-session -A -s pack -e bash > /dev/null 2>&1 &
+# alacritty --class "pack" -e tmux new-session -A -s pack -e bash >/dev/null 2>&1 &
+#else
+# if [ ! -f /tmp/hide_hud ]; then
+# touch /tmp/hide_hud && xdo hide "$id"
+# elif [ -f /tmp/hide_hud ]; then
+# rm /tmp/hide_hud && xdo show "$id"
+# fi
+#fi
+
+
+
+# Set the environment variables to x11 to allow working in Wayland
+export GDK_BACKEND=x11
+export QT_QPA_PLATFORM=xcb
+#export WAYLAND_DISPLAY=""
+export WINIT_UNIX_BACKEND=x11
+
+# Supported terminals and dropdown class
+supported_terminals=("wezterm" "kitty" "alacritty")
+
+# Check if any supported terminal with scratchpad class is running
+for term in "${supported_terminals[@]}"; do
+ if pgrep -f "$term.*--class pack" >/dev/null; then
+ my_term="$term"
+ break
+ fi
+done
+
+# If no supported terminal is running, start the first available one
+if [ "$my_term" = "" ]; then
+ for term in "${supported_terminals[@]}"; do
+ if command -v "$term" >/dev/null 2>&1; then
+ my_term="$term"
+ break
+ fi
+ done
+ if [ "$my_term" = "" ]; then
+ echo "No supported terminal found." && exit 1
+ fi
+
+ # Start terminal with scratchpad class
+ case "$my_term" in
+ "wezterm") wezterm start --class pack -e tmux new-session -A -s pack -e bash & ;;
+ "kitty") kitty --class pack tmux new-session -A -s pack -e bash & ;;
+ "alacritty") alacritty --class pack -e tmux new-session -A -s pack -e bash & ;;
+
+ esac
+fi
+
+# Get the window ID of the scratchpad terminal
+id="$(xdo id -N pack)"
+
+# Toggle scratchpad terminal visibility
+if [ "$id" != "" ]; then
+ if xwininfo -id "$id" | grep "Map State: IsViewable" >/dev/null; then
+ # Scratchpad is visible, hide it
+ dimensions="$(xwininfo -id "$id" | awk '/Width:|Height:/ { printf("%s=%s;", tolower($1), $2) }')"
+ xdo hide "$id" 2>/dev/null
+ else
+ # Scratchpad is hidden, show it and restore dimensions
+ xdo show "$id"
+ xdotool windowsize "$id" "$(echo "$dimensions" | tr ';' ' ')" 2>/dev/null
+ xdotool windowactivate "$id"
+ xdotool windowfocus "$id"
+ fi
+fi
diff --git a/common/scripts/utils/rofi-network-manager.sh b/common/scripts/utils/rofi-network-manager.sh
new file mode 100755
index 0000000..61106d2
--- /dev/null
+++ b/common/scripts/utils/rofi-network-manager.sh
@@ -0,0 +1,252 @@
+#!/bin/bash
+
+# Credit: https://github.com/P3rf/rofi-network-manager
+
+# Default Values
+LOCATION=0
+QRCODE_LOCATION=$LOCATION
+Y_AXIS=0
+X_AXIS=0
+NOTIFICATIONS="off"
+QRCODE_DIR="/tmp/"
+WIDTH_FIX_MAIN=1
+WIDTH_FIX_STATUS=10
+DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PASSWORD_ENTER="if connection is stored,hit enter/esc."
+WIRELESS_INTERFACES=("$(nmcli device | awk '$2=="wifi" {print $1}')")
+WIRELESS_INTERFACES_PRODUCT=()
+WLAN_INT=0
+WIRED_INTERFACES=("$(nmcli device | awk '$2=="ethernet" {print $1}')")
+WIRED_INTERFACES_PRODUCT=()
+ASCII_OUT=false
+CHANGE_BARS=false
+SIGNAL_STRENGTH_0="0"
+SIGNAL_STRENGTH_1="1"
+SIGNAL_STRENGTH_2="12"
+SIGNAL_STRENGTH_3="123"
+SIGNAL_STRENGTH_4="1234"
+VPN_PATTERN='(wireguard|vpn)'
+function initialization() {
+ # Try to source configuration files from .config/rofi/
+ if [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/rofi/rofi-network-manager.conf" ]]; then
+ source "${XDG_CONFIG_HOME:-$HOME/.config}/rofi/rofi-network-manager.conf"
+ RASI_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/rofi/rofi-network-manager.rasi"
+ elif [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/rofi/rofi-network-manager.rasi" ]]; then
+ source "${XDG_CONFIG_HOME:-$HOME/.config}/rofi/rofi-network-manager.rasi"
+ else
+ # Fallback to using files in the script's directory
+ source "$DIR/rofi-network-manager.conf" || source "$DIR/rofi-network-manager.conf"
+ [[ -f "$DIR/rofi-network-manager.rasi" ]] && RASI_DIR="$DIR/rofi-network-manager.rasi"
+ fi
+ for i in "${WIRELESS_INTERFACES[@]}"; do WIRELESS_INTERFACES_PRODUCT+=("$(nmcli -f general.product device show "$i" | awk '{print $2}')"); done
+ for i in "${WIRED_INTERFACES[@]}"; do WIRED_INTERFACES_PRODUCT+=("$(nmcli -f general.product device show "$i" | awk '{print $2}')"); done
+ wireless_interface_state && ethernet_interface_state
+}
+function notification() {
+ [[ "$NOTIFICATIONS" == "true" && -x "$(command -v notify-send)" ]] && notify-send -r "5" -u "normal" "$1" "$2"
+}
+function wireless_interface_state() {
+
+ [[ ${#WIRELESS_INTERFACES[@]} -eq "0" ]] || {
+ ACTIVE_SSID=$(nmcli device status | grep "^${WIRELESS_INTERFACES[WLAN_INT]}." | awk '{print $4}')
+ WIFI_CON_STATE=$(nmcli device status | grep "^${WIRELESS_INTERFACES[WLAN_INT]}." | awk '{print $3}')
+ { [[ "$WIFI_CON_STATE" == "unavailable" ]] && WIFI_LIST="***Wi-Fi Disabled***" && WIFI_SWITCH="~Wi-Fi On" && OPTIONS="${WIFI_LIST}\n${WIFI_SWITCH}\n~Scan\n"; } || { [[ "$WIFI_CON_STATE" =~ "connected" ]] && {
+ PROMPT=${WIRELESS_INTERFACES_PRODUCT[WLAN_INT]}[${WIRELESS_INTERFACES[WLAN_INT]}]
+ WIFI_LIST=$(nmcli --fields SSID,SECURITY,BARS device wifi list ifname "${WIRELESS_INTERFACES[WLAN_INT]}")
+ wifi_list
+ [[ "$ACTIVE_SSID" == "--" ]] && WIFI_SWITCH="~Scan\n~Manual/Hidden\n~Wi-Fi Off" || WIFI_SWITCH="~Scan\n~Disconnect\n~Manual/Hidden\n~Wi-Fi Off"
+ OPTIONS="${WIFI_LIST}\n${WIFI_SWITCH}\n"
+ }; }
+ }
+}
+function ethernet_interface_state() {
+ [[ ${#WIRED_INTERFACES[@]} -eq "0" ]] || {
+ WIRED_CON_STATE=$(nmcli device status | grep "ethernet" | head -1 | awk '{print $3}')
+ { [[ "$WIRED_CON_STATE" == "disconnected" ]] && WIRED_SWITCH="~Eth On"; } || { [[ "$WIRED_CON_STATE" == "connected" ]] && WIRED_SWITCH="~Eth Off"; } || { [[ "$WIRED_CON_STATE" == "unavailable" ]] && WIRED_SWITCH="***Wired Unavailable***"; } || { [[ "$WIRED_CON_STATE" == "connecting" ]] && WIRED_SWITCH="***Wired Initializing***"; }
+ OPTIONS="${OPTIONS}${WIRED_SWITCH}\n"
+ }
+}
+function rofi_menu() {
+ { [[ ${#WIRELESS_INTERFACES[@]} -gt "1" ]] && OPTIONS="${OPTIONS}~Change Wifi Interface\n~More Options"; } || { OPTIONS="${OPTIONS}~More Options"; }
+ { [[ "$WIRED_CON_STATE" == "connected" ]] && PROMPT="${WIRED_INTERFACES_PRODUCT}[$WIRED_INTERFACES]"; } || PROMPT="${WIRELESS_INTERFACES_PRODUCT[WLAN_INT]}[${WIRELESS_INTERFACES[WLAN_INT]}]"
+ SELECTION=$(echo -e "$OPTIONS" | rofi_cmd "$OPTIONS" "$WIDTH_FIX_MAIN" "-a 0")
+ SSID=$(echo "$SELECTION" | sed "s/\s\{2,\}/\|/g" | awk -F "|" '{print $1}')
+ selection_action
+}
+function rofi_cmd() {
+ { [[ -n "${1}" ]] && WIDTH=$(echo -e "$1" | awk '{print length}' | sort -n | tail -1) && ((WIDTH += $2)) && ((WIDTH = WIDTH / 2)); } || { ((WIDTH = $2 / 2)); }
+ rofi -dmenu -i -location "$LOCATION" -yoffset "$Y_AXIS" -xoffset "$X_AXIS" "$3" -theme "$RASI_DIR" -theme-str 'window{width: '"$WIDTH"'em;}textbox-prompt-colon{str:"'"$PROMPT"':";}'"$4"''
+}
+function change_wireless_interface() {
+ { [[ ${#WIRELESS_INTERFACES[@]} -eq "2" ]] && { [[ $WLAN_INT -eq "0" ]] && WLAN_INT=1 || WLAN_INT=0; }; } || {
+ LIST_WLAN_INT=""
+ for i in "${!WIRELESS_INTERFACES[@]}"; do LIST_WLAN_INT=("${LIST_WLAN_INT[@]}${WIRELESS_INTERFACES_PRODUCT[$i]}[${WIRELESS_INTERFACES[$i]}]\n"); done
+ LIST_WLAN_INT[-1]=${LIST_WLAN_INT[-1]::-2}
+ CHANGE_WLAN_INT=$(echo -e "${LIST_WLAN_INT[@]}" | rofi_cmd "${LIST_WLAN_INT[@]}" "$WIDTH_FIX_STATUS")
+ for i in "${!WIRELESS_INTERFACES[@]}"; do [[ $CHANGE_WLAN_INT == "${WIRELESS_INTERFACES_PRODUCT[$i]}[${WIRELESS_INTERFACES[$i]}]" ]] && WLAN_INT=$i && break; done
+ }
+ wireless_interface_state && ethernet_interface_state
+ rofi_menu
+}
+function scan() {
+ [[ "$WIFI_CON_STATE" =~ "unavailable" ]] && change_wifi_state "Wi-Fi" "Enabling Wi-Fi connection" "on" && sleep 2
+ notification "-t 0 Wifi" "Please Wait Scanning"
+ WIFI_LIST=$(nmcli --fields SSID,SECURITY,BARS device wifi list ifname "${WIRELESS_INTERFACES[WLAN_INT]}" --rescan yes)
+ wifi_list
+ wireless_interface_state && ethernet_interface_state
+ notification "-t 1 Wifi" "Please Wait Scanning"
+ rofi_menu
+}
+function wifi_list() {
+ WIFI_LIST=$(echo -e "$WIFI_LIST" | awk -F' +' '{ if (!seen[$1]++) print}' | awk '$1!="--" {print}' | awk '$1 !~ "^'"$ACTIVE_SSID"'"')
+ [[ $ASCII_OUT == "true" ]] && WIFI_LIST=$(echo -e "$WIFI_LIST" | sed 's/\(..*\)\*\{4,4\}/\1▂▄▆█/g' | sed 's/\(..*\)\*\{3,3\}/\1▂▄▆_/g' | sed 's/\(..*\)\*\{2,2\}/\1▂▄__/g' | sed 's/\(..*\)\*\{1,1\}/\1▂___/g')
+ [[ $CHANGE_BARS == "true" ]] && WIFI_LIST=$(echo -e "$WIFI_LIST" | sed 's/\(.*\)▂▄▆█/\1'"$SIGNAL_STRENGTH_4"'/' | sed 's/\(.*\)▂▄▆_/\1'"$SIGNAL_STRENGTH_3"'/' | sed 's/\(.*\)▂▄__/\1'"$SIGNAL_STRENGTH_2"'/' | sed 's/\(.*\)▂___/\1'"$SIGNAL_STRENGTH_1"'/' | sed 's/\(.*\)____/\1'"$SIGNAL_STRENGTH_0"'/')
+}
+function change_wifi_state() {
+ notification "$1" "$2"
+ nmcli radio wifi "$3"
+}
+function change_wired_state() {
+ notification "$1" "$2"
+ nmcli device "$3" "$4"
+}
+function net_restart() {
+ notification "$1" "$2"
+ nmcli networking off && sleep 3 && nmcli networking on
+}
+function disconnect() {
+ ACTIVE_SSID=$(nmcli -t -f GENERAL.CONNECTION dev show "${WIRELESS_INTERFACES[WLAN_INT]}" | cut -d ':' -f2)
+ notification "$1" "You're now disconnected from Wi-Fi network '$ACTIVE_SSID'"
+ nmcli con down id "$ACTIVE_SSID"
+}
+function check_wifi_connected() {
+ [[ "$(nmcli device status | grep "^${WIRELESS_INTERFACES[WLAN_INT]}." | awk '{print $3}')" == "connected" ]] && disconnect "Connection_Terminated"
+}
+function connect() {
+ check_wifi_connected
+ notification "-t 0 Wi-Fi" "Connecting to $1"
+ { [[ $(nmcli dev wifi con "$1" password "$2" ifname "${WIRELESS_INTERFACES[WLAN_INT]}" | grep -c "successfully activated") -eq "1" ]] && notification "Connection_Established" "You're now connected to Wi-Fi network '$1'"; } || notification "Connection_Error" "Connection can not be established"
+}
+function enter_passwword() {
+ PROMPT="Enter_Password" && PASS=$(echo "$PASSWORD_ENTER" | rofi_cmd "$PASSWORD_ENTER" 4 "-password")
+}
+function enter_ssid() {
+ PROMPT="Enter_SSID" && SSID=$(rofi_cmd "" 40)
+}
+function stored_connection() {
+ check_wifi_connected
+ notification "-t 0 Wi-Fi" "Connecting to $1"
+ { [[ $(nmcli dev wifi con "$1" ifname "${WIRELESS_INTERFACES[WLAN_INT]}" | grep -c "successfully activated") -eq "1" ]] && notification "Connection_Established" "You're now connected to Wi-Fi network '$1'"; } || notification "Connection_Error" "Connection can not be established"
+}
+function ssid_manual() {
+ enter_ssid
+ [[ -n $SSID ]] && {
+ enter_passwword
+ { [[ -n "$PASS" ]] && [[ "$PASS" != "$PASSWORD_ENTER" ]] && connect "$SSID" "$PASS"; } || stored_connection "$SSID"
+ }
+}
+function ssid_hidden() {
+ enter_ssid
+ [[ -n $SSID ]] && {
+ enter_passwword && check_wifi_connected
+ [[ -n "$PASS" ]] && [[ "$PASS" != "$PASSWORD_ENTER" ]] && {
+ nmcli con add type wifi con-name "$SSID" ssid "$SSID" ifname "${WIRELESS_INTERFACES[WLAN_INT]}"
+ nmcli con modify "$SSID" wifi-sec.key-mgmt wpa-psk
+ nmcli con modify "$SSID" wifi-sec.psk "$PASS"
+ } || [[ $(nmcli -g NAME con show | grep -c "$SSID") -eq "0" ]] && nmcli con add type wifi con-name "$SSID" ssid "$SSID" ifname "${WIRELESS_INTERFACES[WLAN_INT]}"
+ notification "-t 0 Wifi" "Connecting to $SSID"
+ { [[ $(nmcli con up id "$SSID" | grep -c "successfully activated") -eq "1" ]] && notification "Connection_Established" "You're now connected to Wi-Fi network '$SSID'"; } || notification "Connection_Error" "Connection can not be established"
+ }
+}
+function interface_status() {
+ local -n INTERFACES="$1" && local -n INTERFACES_PRODUCT="$2"
+ for i in "${!INTERFACES[@]}"; do
+ CON_STATE=$(nmcli device status | grep "^${INTERFACES[$i]}." | awk '{print $3}')
+ INT_NAME=${INTERFACES_PRODUCT[$i]}[${INTERFACES[$i]}]
+ [[ "$CON_STATE" == "connected" ]] && STATUS="$INT_NAME:\n\t$(nmcli -t -f GENERAL.CONNECTION dev show "${INTERFACES[$i]}" | awk -F '[:]' '{print $2}') ~ $(nmcli -t -f IP4.ADDRESS dev show "${INTERFACES[$i]}" | awk -F '[:/]' '{print $2}')" || STATUS="$INT_NAME: ${CON_STATE^}"
+ echo -e "$STATUS"
+ done
+}
+function status() {
+ OPTIONS=""
+ [[ ${#WIRED_INTERFACES[@]} -ne "0" ]] && ETH_STATUS="$(interface_status WIRED_INTERFACES WIRED_INTERFACES_PRODUCT)" && OPTIONS="${OPTIONS}${ETH_STATUS}"
+ [[ ${#WIRELESS_INTERFACES[@]} -ne "0" ]] && WLAN_STATUS="$(interface_status WIRELESS_INTERFACES WIRELESS_INTERFACES_PRODUCT)" && { [[ -n ${OPTIONS} ]] && OPTIONS="${OPTIONS}\n${WLAN_STATUS}" || OPTIONS="${OPTIONS}${WLAN_STATUS}"; }
+ ACTIVE_VPN=$(nmcli -g NAME,TYPE con show --active | awk '/:'"$VPN_PATTERN"'/ {sub(/:'"$VPN_PATTERN"'.*/, ""); print}')
+ [[ -n $ACTIVE_VPN ]] && OPTIONS="${OPTIONS}\n${ACTIVE_VPN}[VPN]: $(nmcli -g ip4.address con show "$ACTIVE_VPN" | awk -F '[:/]' '{print $1}')"
+ echo -e "$OPTIONS" | rofi_cmd "$OPTIONS" "$WIDTH_FIX_STATUS" "" "mainbox{children:[listview];}"
+}
+function share_pass() {
+ SSID=$(nmcli dev wifi show-password | grep -oP '(?<=SSID: ).*' | head -1)
+ PASSWORD=$(nmcli dev wifi show-password | grep -oP '(?<=Password: ).*' | head -1)
+ OPTIONS="SSID: ${SSID}\nPassword: ${PASSWORD}"
+ [[ -x "$(command -v qrencode)" ]] && OPTIONS="${OPTIONS}\n~QrCode"
+ SELECTION=$(echo -e "$OPTIONS" | rofi_cmd "$OPTIONS" "$WIDTH_FIX_STATUS" "-a -1" "mainbox{children:[listview];}")
+ selection_action
+}
+function gen_qrcode() {
+ DIRECTIONS=("Center" "Northwest" "North" "Northeast" "East" "Southeast" "South" "Southwest" "West")
+ TMP_SSID="${SSID// /_}"
+ [[ -e $QRCODE_DIR$TMP_SSID.png ]] || qrencode -t png -o "$QRCODE_DIR$TMP_SSID".png -l H -s 25 -m 2 --dpi=192 "WIFI:S:""$SSID"";T:""$(nmcli dev wifi show-password | grep -oP '(?<=Security: ).*' | head -1)"";P:""$PASSWORD"";;"
+ rofi_cmd "" "0" "" "entry{enabled:false;}window{location:""${DIRECTIONS[QRCODE_LOCATION]}"";border-radius:6mm;padding:1mm;width:100mm;height:100mm;
+ background-image:url(\"$QRCODE_DIR$TMP_SSID.png\",both);}"
+}
+function manual_hidden() {
+ OPTIONS="~Manual\n~Hidden" && SELECTION=$(echo -e "$OPTIONS" | rofi_cmd "$OPTIONS" "$WIDTH_FIX_STATUS" "" "mainbox{children:[listview];}")
+ selection_action
+}
+function vpn() {
+ ACTIVE_VPN=$(nmcli -g NAME,TYPE con show --active | awk '/:'"$VPN_PATTERN"'/ {sub(/:'"$VPN_PATTERN"'.*/, ""); print}')
+ [[ $ACTIVE_VPN ]] && OPTIONS="~Deactive $ACTIVE_VPN" || OPTIONS="$(nmcli -g NAME,TYPE connection | awk '/:'"$VPN_PATTERN"'/ {sub(/:'"$VPN_PATTERN"'.*/, ""); print}')"
+ VPN_ACTION=$(echo -e "$OPTIONS" | rofi_cmd "$OPTIONS" "$WIDTH_FIX_STATUS" "" "mainbox {children:[listview];}")
+ [[ -n "$VPN_ACTION" ]] && { { [[ "$VPN_ACTION" =~ "~Deactive" ]] && nmcli connection down "$ACTIVE_VPN" && notification "VPN_Deactivated" "$ACTIVE_VPN"; } || {
+ notification "-t 0 Activating_VPN" "$VPN_ACTION"
+ VPN_OUTPUT=$(nmcli connection up "$VPN_ACTION" 2>/dev/null)
+ { [[ $(echo "$VPN_OUTPUT" | grep -c "Connection successfully activated") -eq "1" ]] && notification "VPN_Successfully_Activated" "$VPN_ACTION"; } || notification "Error_Activating_VPN" "Check your configuration for $VPN_ACTION"
+ }; }
+}
+function more_options() {
+ OPTIONS=""
+ [[ "$WIFI_CON_STATE" == "connected" ]] && OPTIONS="~Share Wifi Password\n"
+ OPTIONS="${OPTIONS}~Status\n~Restart Network"
+ [[ $(nmcli -g NAME,TYPE connection | awk '/:'"$VPN_PATTERN"'/ {sub(/:'"$VPN_PATTERN"'.*/, ""); print}') ]] && OPTIONS="${OPTIONS}\n~VPN"
+ [[ -x "$(command -v nm-connection-editor)" ]] && OPTIONS="${OPTIONS}\n~Open Connection Editor"
+ SELECTION=$(echo -e "$OPTIONS" | rofi_cmd "$OPTIONS" "$WIDTH_FIX_STATUS" "" "mainbox {children:[listview];}")
+ selection_action
+}
+function selection_action() {
+ case "$SELECTION" in
+ "~Disconnect") disconnect "Connection_Terminated" ;;
+ "~Scan") scan ;;
+ "~Status") status ;;
+ "~Share Wifi Password") share_pass ;;
+ "~Manual/Hidden") manual_hidden ;;
+ "~Manual") ssid_manual ;;
+ "~Hidden") ssid_hidden ;;
+ "~Wi-Fi On") change_wifi_state "Wi-Fi" "Enabling Wi-Fi connection" "on" ;;
+ "~Wi-Fi Off") change_wifi_state "Wi-Fi" "Disabling Wi-Fi connection" "off" ;;
+ "~Eth Off") change_wired_state "Ethernet" "Disabling Wired connection" "disconnect" "$WIRED_INTERFACES" ;;
+ "~Eth On") change_wired_state "Ethernet" "Enabling Wired connection" "connect" "$WIRED_INTERFACES" ;;
+ "***Wi-Fi Disabled***") ;;
+ "***Wired Unavailable***") ;;
+ "***Wired Initializing***") ;;
+ "~Change Wifi Interface") change_wireless_interface ;;
+ "~Restart Network") net_restart "Network" "Restarting Network" ;;
+ "~QrCode") gen_qrcode ;;
+ "~More Options") more_options ;;
+ "~Open Connection Editor") nm-connection-editor ;;
+ "~VPN") vpn ;;
+ *)
+ [[ -n "$SELECTION" ]] && [[ "$WIFI_LIST" =~ .*"$SELECTION".* ]] && {
+ [[ "$SSID" == "*" ]] && SSID=$(echo "$SELECTION" | sed "s/\s\{2,\}/\|/g " | awk -F "|" '{print $3}')
+ { [[ "$ACTIVE_SSID" == "$SSID" ]] && nmcli con up "$SSID" ifname "${WIRELESS_INTERFACES[WLAN_INT]}"; } || {
+ [[ "$SELECTION" =~ "WPA2" ]] || [[ "$SELECTION" =~ "WEP" ]] && enter_passwword
+ { [[ -n "$PASS" ]] && [[ "$PASS" != "$PASSWORD_ENTER" ]] && connect "$SSID" "$PASS"; } || stored_connection "$SSID"
+ }
+ }
+ ;;
+ esac
+}
+function main() {
+ initialization && rofi_menu
+}
+main
diff --git a/common/scripts/utils/root.sh b/common/scripts/utils/root.sh
new file mode 100755
index 0000000..a7807bc
--- /dev/null
+++ b/common/scripts/utils/root.sh
@@ -0,0 +1,152 @@
+#!/bin/bash
+
+# Created By: srdusr
+# Created On: Mon 19 Feb 2025 14:18:00 PM CAT
+# Project: Backup and restore system files to/from home extras (system dotfiles) directory
+
+# Dependencies: None
+# NOTE: The backups will be stored in the ~/extras directory, preserving the original file structure. Run as sudo or be prompted for password
+# Example usage:
+# To backup a specific file: root.sh --backup /some_directory/some_file.conf
+# To restore a specific file: root.sh --restore ~/extras/some_directory/some_file.conf
+# To restore all files: root.sh --restore
+#
+
+# Use $SUDO_USER to get the original user when run with sudo, or fall back to the current user if not
+BASE_DIR="/home/${SUDO_USER:-$(whoami)}/extras"
+
+if [ "$EUID" -eq 0 ] && [ "$SUDO_USER" = "" ]; then
+ echo "You are running this script directly as root, not through sudo!"
+ exit 1
+fi
+
+if [ "$EUID" -ne 0 ]; then
+ echo "Elevating to sudo..."
+ exec sudo "$0" "$@" # Re-run the script with sudo
+fi
+
+# Create directories if they do not exist
+create_directory() {
+ local dir=$1
+ if [ ! -d "$dir" ]; then
+ echo "Creating directory: $dir"
+ mkdir -p "$dir"
+ else
+ echo "Directory already exists: $dir"
+ fi
+}
+
+# Backup files
+backup_to_extras() {
+ local src=$1
+
+ # Ensure the file or directory exists
+ if [ -e "$src" ]; then
+ # Strip the leading / from src to avoid double slashes
+ local stripped_src="${src#/}"
+
+ # Determine the destination path
+ dest_dir="$BASE_DIR/$(dirname "$stripped_src")" # Get the directory part of the source
+ dest_file="$BASE_DIR/$stripped_src" # Get the full destination file path
+
+ # Debug: Print paths
+ echo "Source file: $src"
+ echo "Destination directory: $dest_dir"
+ echo "Destination file: $dest_file"
+
+ # Create the necessary directories in extras if they don't exist
+ create_directory "$dest_dir"
+
+ # Backup the file to extras
+ echo "Backing up $src to $dest_file"
+ cp -p "$src" "$dest_file"
+
+ # Set permission to user
+ chown "$SUDO_USER:$SUDO_USER" "$dest_file"
+
+ echo "Backup of $src completed."
+ else
+ echo "Error: The file or directory '$src' does not exist."
+ fi
+}
+
+# Restore files
+restore_from_extras() {
+ local src=$1
+
+ # Ensure the file or directory exists in extras
+ if [ -e "$src" ]; then
+ # Strip the leading / from src to avoid double slashes
+ local stripped_src="${src#/}"
+
+ # Determine the destination path
+ dest_dir="/$(dirname "$stripped_src")" # Get the directory part of the source
+ dest_file="/$stripped_src" # Get the full destination file path
+
+ # Debug: Print paths
+ echo "Source file: $src"
+ echo "Destination directory: $dest_dir"
+ echo "Destination file: $dest_file"
+
+ # Create the necessary directories in the system if they don't exist
+ create_directory "$dest_dir"
+
+ # Backup the file if it exists before restoring
+ if [ -e "$dest_file" ]; then
+ echo "File $dest_file exists, creating a backup..."
+ mv "$dest_file" "$dest_file.bak"
+ echo "Backup created at $dest_file.bak"
+ fi
+
+ # Restore the file from extras
+ echo "Restoring $src to $dest_file"
+ cp -p "$BASE_DIR/$stripped_src" "$dest_file"
+
+ # Set permissions for the restored file
+ chmod 644 "$dest_file"
+
+ echo "Restore of $src completed."
+ else
+ echo "Error: The file or directory '$src' does not exist in extras."
+ fi
+}
+
+# Restore all files from extras
+restore_all_from_extras() {
+ echo "Restoring all files and directories from extras..."
+
+ # Loop over all files and directories in BASE_DIR and restore them
+ find "$BASE_DIR" -type f | while read -r file; do
+ restore_from_extras "$file"
+ done
+
+ echo "Restore completed."
+}
+
+# Backup system files based on user input
+if [ "$1" == "--backup" ]; then
+ if [ "$2" = "" ]; then
+ echo "Error: Please specify the file or directory to backup."
+ exit 1
+ fi
+
+ # Perform the backup
+ echo "Backing up system files to extras..."
+ backup_to_extras "$2"
+ echo "Backup completed."
+
+ # Restore system files based on user input
+elif [ "$1" == "--restore" ]; then
+ if [ "$2" = "" ]; then
+ # If no specific file is provided, restore everything
+ restore_all_from_extras
+ else
+ # Restore a specific file or directory
+ echo "Restoring system files from extras..."
+ restore_from_extras "$2"
+ echo "Restore completed."
+ fi
+
+else
+ echo "Invalid option. Use '--backup' to backup or '--restore' to restore."
+fi
diff --git a/common/scripts/utils/run_with_display.sh b/common/scripts/utils/run_with_display.sh
new file mode 100755
index 0000000..5f1f3a0
--- /dev/null
+++ b/common/scripts/utils/run_with_display.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+run_with_display() {
+ output=$("$@" 2>&1)
+ exit_status=$?
+
+ if [[ $exit_status -ne 0 && ("$output" =~ "cannot open display" || "$output" =~ "DISPLAY environment variable is missing") ]]; then
+ DISPLAY=:0 "$@"
+ else
+ echo "$output"
+ return $exit_status
+ fi
+}
+
+# Call this script with any command you want to run
+command=$1
+shift
+run_with_display "$command" "$@"
+
diff --git a/common/scripts/utils/scratchpad b/common/scripts/utils/scratchpad
new file mode 100755
index 0000000..d47dafe
--- /dev/null
+++ b/common/scripts/utils/scratchpad
@@ -0,0 +1,147 @@
+#!/bin/bash
+
+# Created By: srdusr
+# Created On: Tue 07 Mar 2023 15:06:47 PM CAT
+# Project: Agnostic scratchpad/dropdown terminal that works on most window managers
+
+# Dependencies: wmctrl, xprop, xdo, xdotool
+# NOTE: Ensure script is included in system's path and can therefore be invoked with the command 'scratchpad'.
+# Furthermore make sure the terminal is using x11 as a backend in wayland to allow this to work.
+# Example: wezterm.lua: enable_wayland = false,
+# kitty.conf: linux_display_server x11
+
+# Set the environment variables to x11 to allow working in Wayland
+#export GDK_BACKEND=x11
+#export QT_QPA_PLATFORM=xcb
+#export WAYLAND_DISPLAY=""
+#export WINIT_UNIX_BACKEND=x11
+#export WAYLAND_DISPLAY=wayland-0
+
+# Supported terminals and dropdown class
+supported_terminals=("wezterm" "kitty" "alacritty")
+
+# Check if any supported terminal with scratchpad class is running
+for term in "${supported_terminals[@]}"; do
+ if pgrep -f "$term.*--class scratchpad" >/dev/null; then
+ my_term="$term"
+ break
+ fi
+done
+
+# If no supported terminal is running, start the first available one
+if [ "$my_term" = "" ]; then
+ for term in "${supported_terminals[@]}"; do
+ if command -v "$term" >/dev/null 2>&1; then
+ my_term="$term"
+ break
+ fi
+ done
+ if [ "$my_term" = "" ]; then
+ echo "No supported terminal found." && exit 1
+ fi
+
+ # Start terminal with scratchpad class
+ case "$my_term" in
+ "wezterm") wezterm start --class scratchpad -e tmux new-session -A -s tmux -e bash & ;;
+ "kitty") kitty --class scratchpad tmux new-session -A -s tmux -e bash & ;;
+ "alacritty") alacritty --class scratchpad -e tmux new-session -A -s tmux -e bash & ;;
+ esac
+fi
+
+# Get the window ID of the scratchpad terminal
+id="$(xdo id -N scratchpad)"
+# Get the ID of the currently focused window
+focused_id="$(xdotool getwindowfocus)"
+
+# Get class of the currently focused window
+focused_class="$(xprop -id "$focused_id" WM_CLASS 2>/dev/null | awk -F '"' '{print $4}')"
+
+# Toggle visible/hide and smart focus
+if [ "$id" != "" ]; then
+ if xwininfo -id "$id" | grep "Map State: IsViewable" >/dev/null; then
+ if [ "$focused_id" = "$id" ] || [ "$focused_class" = "scratchpad" ]; then
+ # Scratchpad is focused, hide it
+ dimensions="$(xwininfo -id "$id" | awk '/Width:|Height:/ { printf("%s=%s;", tolower($1), $2) }')"
+ xdo hide "$id" 2>/dev/null
+ else
+ # Scratchpad is visible but not focused
+ xdotool windowactivate "$id"
+ xdotool windowfocus "$id"
+ hyprctl dispatch focuswindow scratchpad
+ fi
+ else
+ # Scratchpad is hidden, show and focus it
+ xdo show "$id"
+ xdotool windowsize "$id" "$(echo "$dimensions" | tr ';' ' ')" 2>/dev/null
+ xdotool windowactivate "$id"
+ xdotool windowfocus "$id"
+ hyprctl dispatch focuswindow scratchpad
+ fi
+fi
+
+## Get the window ID of the PiP window by title
+#pip_id="$(xdo id -n "Picture-in-Picture")"
+#
+## Toggle scratchpad terminal visibility
+#if [ "$id" != "" ]; then
+# if xwininfo -id "$id" | grep "Map State: IsViewable" >/dev/null; then
+# # Scratchpad is visible, hide it
+# dimensions="$(xwininfo -id "$id" | awk '/Width:|Height:/ { printf("%s=%s;", tolower($1), $2) }')"
+# xdo hide "$id" 2>/dev/null
+# else
+# # Scratchpad is hidden, show it and restore dimensions
+# xdo show "$id"
+# xdotool windowsize "$id" "$(echo "$dimensions" | tr ';' ' ')" 2>/dev/null
+# xdotool windowactivate "$id"
+# xdotool windowfocus "$id"
+#
+# # Adjust layer based on PiP window presence
+# if [ "$pip_id" != "" ]; then
+# hyprctl dispatch layer "$id" 1
+# else
+# hyprctl dispatch layer "$id" 0
+# fi
+#
+# hyprctl dispatch focuswindow scratchpad
+# fi
+#fi
+
+## Get the window ID of the scratchpad terminal
+#id=$(hyprctl clients -j | jq -r '.[] | select(.class=="scratchpad").address')
+#
+## Toggle scratchpad terminal visibility
+#if [ -n "$id" ]; then
+# focused=$(hyprctl activewindow -j | jq -r '.address')
+# if [ "$focused" = "$id" ]; then
+# # Scratchpad is focused, minimize it
+# hyprctl dispatch move "address:$id workspace silent 0"
+# else
+# # Show scratchpad, set above normal windows but below PiP
+# hyprctl dispatch movetoworkspace current address:$id
+# hyprctl dispatch focuswindow address:$id
+# hyprctl dispatch layer "address:$id above"
+# hyprctl dispatch focuswindow address:$id
+# fi
+#fi
+
+## Get the window ID of the PiP window by title
+#pip_id=$(hyprctl clients -j | jq -r '.[] | select(.title=="Picture-in-Picture").address')
+#
+## Toggle scratchpad terminal visibility
+#if [ -n "$id" ]; then
+# focused=$(hyprctl activewindow -j | jq -r '.address')
+# if [ "$focused" = "$id" ]; then
+# # Scratchpad is focused, minimize it
+# hyprctl dispatch move "address:$id workspace silent 0"
+# else
+# # Show scratchpad, set above normal windows but below PiP
+# hyprctl dispatch movetoworkspace current address:$id
+# hyprctl dispatch focuswindow address:$id
+# if [ -n "$pip_id" ]; then
+# hyprctl dispatch layer "address:$id below"
+# else
+# hyprctl dispatch layer "address:$id above"
+# fi
+# hyprctl dispatch focuswindow address:$id
+# fi
+#fi
diff --git a/common/scripts/utils/screenshot b/common/scripts/utils/screenshot
new file mode 100755
index 0000000..b209574
--- /dev/null
+++ b/common/scripts/utils/screenshot
@@ -0,0 +1,102 @@
+#!/bin/sh
+
+# === Config: directory for QEMU sockets ===
+QEMU_SOCK_DIR="/home/$(whoami)/virt/machines"
+
+# === Send 'print' key to QEMU monitor ===
+send_print_to_qemu() {
+ monitor_socket=$(find "$QEMU_SOCK_DIR" -maxdepth 1 -type s -name '*-monitor.socket' | head -n1)
+ if [ "$monitor_socket" = "" ]; then
+ echo "No QEMU monitor socket found."
+ return 1
+ fi
+ echo "Using QEMU monitor socket: $monitor_socket"
+ if ! printf "sendkey print\n" | socat - UNIX-CONNECT:"$monitor_socket"; then
+ echo "Failed to send key via socat."
+ return 1
+ fi
+ echo "Sent 'sendkey print' to QEMU successfully."
+ return 0
+}
+
+# === Detect if current window is QEMU (X11 only) ===
+if [ "$DISPLAY" != "" ] && command -v xprop >/dev/null 2>&1; then
+ win_id=$(xprop -root _NET_ACTIVE_WINDOW | awk -F' ' '{print $5}')
+ class=$(xprop -id "$win_id" WM_CLASS 2>/dev/null | awk -F'"' '{print $4}')
+ if [ "$class" = "qemu-system-x86_64" ]; then
+ send_print_to_qemu && exit 0
+ # if sending fails, optionally fallback or exit anyway
+ exit 1
+ fi
+fi
+#if command -v hyprctl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
+# focused_class="$(hyprctl activewindow -j | jq -r '.class' 2>/dev/null)"
+# if [ "$focused_class" = "qemu-system-x86_64" ]; then
+# send_print_to_qemu && exit 0
+# fi
+#fi
+
+DIR="$HOME/pictures/screenshots"
+OUTPUT_DIR="$HOME/documents/ocr_output"
+
+# Create the directories if they don't exist
+[ ! -d "$DIR" ] && mkdir -pv "$DIR"
+[ ! -d "$OUTPUT_DIR" ] && mkdir -pv "$OUTPUT_DIR"
+
+file="$DIR/screenshot_$(date '+%Y-%m-%d_%H-%M-%S').png"
+text_file="$OUTPUT_DIR/ocr_output_$(date '+%Y-%m-%d_%H-%M-%S')"
+
+copy_to_clipboard() {
+ if [ "$WAYLAND_DISPLAY" != "" ]; then
+ wl-copy <"$1"
+ elif [ "$DISPLAY" != "" ]; then
+ xclip -selection clipboard -i <"$1"
+ else
+ echo "No display server detected. Cannot copy to clipboard."
+ return 1
+ fi
+}
+
+case "$1" in
+screen) grim "$file" ;;
+output) slurp -o -r | grim -g - "$file" ;;
+area) slurp | grim -g - "$file" ;;
+output-area) slurp -o | grim -g - "$file" ;;
+ocr) slurp | grim -g - "$file" && tesseract "$file" "$text_file" ;;
+ocr-clipboard)
+ slurp | grim -g - "$file" && tesseract "$file" "$text_file"
+ if [ -f "$text_file.txt" ]; then
+ copy_to_clipboard "$text_file.txt"
+ CLIP_STATUS=$?
+ if [ "$CLIP_STATUS" -eq 0 ]; then
+ command rm -f "$text_file.txt"
+ notify-send -t 10000 --app-name "Screenshot" "OCR to Clipboard" "Text copied to clipboard."
+ echo "OCR output copied to clipboard and file deleted."
+ else
+ notify-send -t 10000 --app-name "Screenshot" "Clipboard Copy Failed" "Failed to copy text to clipboard."
+ echo "Failed to copy text to clipboard."
+ fi
+ else
+ notify-send -t 10000 --app-name "Screenshot" "OCR Error" "OCR process failed."
+ exit 1
+ fi
+ ;;
+*)
+ echo "Invalid argument"
+ notify-send -t 10000 --app-name "Screenshot" "Screenshot" "Something went wrong."
+ exit 1
+ ;;
+esac
+
+if [ "$1" = "ocr" ]; then
+ if [ -f "$text_file" ]; then
+ notify-send -t 10000 --app-name "Screenshot" "OCR Complete" "Text saved to $text_file"
+ echo "OCR output saved to: $text_file"
+ else
+ notify-send -t 10000 --app-name "Screenshot" "OCR Error" "OCR process failed."
+ exit 1
+ fi
+else
+ notify-send -t 10000 --app-name "Screenshot" "Screenshot" "Saved as $file"
+ echo "$file"
+fi
diff --git a/common/scripts/utils/sendkeys.awk b/common/scripts/utils/sendkeys.awk
new file mode 100755
index 0000000..16a3fa9
--- /dev/null
+++ b/common/scripts/utils/sendkeys.awk
@@ -0,0 +1,86 @@
+#!/usr/bin/env awk -f
+#
+# AWK script to send multiple `sendkey` commands to a QEMU virtual machine.
+# It writes at a rate of roughly 40 keys per second, due to lower delays
+# resulting in garbage output.
+#
+# It makes use of a TCP client created by an external utility, such as OpenBSD
+# Netcat, to interact with QEMU's monitor and send a stream of `sendkey`
+# commands. This is a practical way to transfer a small file or to script
+# interactions with a terminal user interface.
+
+BEGIN {
+ # Set default delay if not provided via command-line args
+ if (!delay) {
+ delay = 0.025
+ }
+
+ # Define key mappings for common characters and symbols
+ key["#"] = "backspace"
+ key[" "] = "tab"
+ key[" "] = "spc"
+ key["!"] = "shift-1"
+ key["\""] = "shift-apostrophe"
+ key["#"] = "shift-3"
+ key["$"] = "shift-4"
+ key["%"] = "shift-5"
+ key["&"] = "shift-7"
+ key["'"] = "apostrophe"
+ key["("] = "shift-9"
+ key[")"] = "shift-0"
+ key["*"] = "shift-8"
+ key["+"] = "shift-equal"
+ key[","] = "comma"
+ key["-"] = "minus"
+ key["."] = "dot"
+ key["/"] = "slash"
+ key[":"] = "shift-semicolon"
+ key[";"] = "semicolon"
+ key["<"] = "shift-comma"
+ key["="] = "equal"
+ key[">"] = "shift-dot"
+ key["?"] = "shift-slash"
+ key["@"] = "shift-2"
+
+ # Map numbers
+ for (i = 48; i < 48 + 10; ++i) {
+ number = sprintf("%c", i)
+ key[number] = number
+ }
+
+ # Map letters A-Z, including shift
+ for (i = 65; i < 65 + 26; ++i) {
+ key[sprintf("%c", i)] = sprintf("shift-%c", i + 32)
+ }
+
+ # Other symbols
+ key["["] = "bracket_left"
+ key["\\"] = "backslash"
+ key["]"] = "bracket_right"
+ key["^"] = "shift-6"
+ key["_"] = "shift-minus"
+ key["`"] = "grave_accent"
+ key["{"] = "shift-bracket_left"
+ key["|"] = "shift-backslash"
+ key["}"] = "shift-bracket_right"
+ key["~"] = "shift-grave_accent"
+ key[""] = "delete"
+
+ # Handle Super and Caps Lock key mappings (for remapping Caps to Super)
+ key["capslock"] = "super"
+ key["super"] = "super"
+
+ # Handle other keys if needed
+}
+
+{
+ split($0, chars, "")
+ for (i = 1; i <= length($0); i++) {
+ # Print sendkey command for the character, mapping it through the key[] array
+ if (key[chars[i]] != "") {
+ printf("sendkey %s\n", key[chars[i]])
+ }
+ system("sleep " delay) # Sleep for the defined delay
+ }
+ printf "sendkey ret\n" # Send "return" (enter) key at the end
+}
diff --git a/common/scripts/utils/sext b/common/scripts/utils/sext
new file mode 100755
index 0000000..b120929
--- /dev/null
+++ b/common/scripts/utils/sext
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+#Sext (Shutdown Exit)
+# List of package managers to check for
+package_managers=("emerge" "apt" "dnf" "pacman" "zypper")
+
+# Set how often to check (in seconds)
+check_interval=30
+
+# Check if any package manager is running
+is_package_manager_running() {
+ for pm in "${package_managers[@]}"; do
+ if pgrep -x "$pm" >/dev/null; then
+ return 0 # Return 0 (true) if any package manager is running
+ fi
+ done
+ return 1 # Return 1 (false) if no package manager is running
+}
+
+# Safely shutdown the system
+safe_shutdown() {
+ # Try using shutdown without sudo first
+ if shutdown -h now; then
+ echo "Shutdown initiated successfully using 'shutdown'."
+ # If shutdown fails, try using poweroff
+ elif poweroff; then
+ echo "Shutdown initiated successfully using 'poweroff'."
+ # If poweroff fails, try using sudo poweroff
+ elif sudo poweroff; then
+ echo "Shutdown initiated successfully with 'sudo poweroff'."
+ # If both shutdown and poweroff fail, try with sudo shutdown
+ elif sudo shutdown -h now; then
+ echo "Shutdown initiated successfully with 'sudo shutdown'."
+ else
+ echo "Shutdown command failed. Please check your system configuration."
+ fi
+}
+
+# Loop until no package manager process is running
+while is_package_manager_running; do
+ echo "Package manager is still running. Checking again in $check_interval seconds..."
+ sleep "$check_interval"
+done
+
+# Once the process completes, initiate a safe shutdown
+echo "Package manager has finished. Attempting to shutdown..."
+safe_shutdown
diff --git a/common/scripts/utils/track-books.sh b/common/scripts/utils/track-books.sh
new file mode 100755
index 0000000..f13add8
--- /dev/null
+++ b/common/scripts/utils/track-books.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Created By: srdusr
+# Created On: Wed 25 Oct 2023 13:45:52 CAT
+# Project: Simple script to track most recent books opened, mainly for neovim usage.
+
+# Dependencies: inotify-tools
+
+books_directory="$HOME/documents/books"
+recent_books_file="$HOME/.config/nvim/tmp/recent_books.txt"
+
+inotifywait -m -e CREATE -e OPEN -r "$books_directory" |
+ while read -r path action file; do
+ if [[ $file == *.pdf || $file == *.epub ]]; then
+ echo "$path/$file" >>"$recent_books_file"
+ # Remove duplicates and overwrite the recent_books_file
+ sort -u -o "$recent_books_file" "$recent_books_file"
+ fi
+ done
diff --git a/common/scripts/utils/umnt b/common/scripts/utils/umnt
new file mode 100755
index 0000000..6d9b788
--- /dev/null
+++ b/common/scripts/utils/umnt
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# unmount disk
+set -e
+. ~/etc/colors/current
+
+lsblk -Po NAME,SIZE,MOUNTPOINT,FSTYPE,LABEL | while read -r a; do
+ eval "$a"
+
+ [ "$MOUNTPOINT" ] && ! grep -iq "\s$MOUNTPOINT\s" /etc/fstab &&
+ printf "%-4s:%s:%s:<- %s\n" \
+ "$NAME" \
+ "$SIZE" \
+ "${LABEL:-unnamed}" \
+ "${MOUNTPOINT//$HOME/\~}"
+
+ done | column -ts':' -o' ' | menu -p unmount | {
+ read -r NAME _
+ eval "$(lsblk -Po LABEL,MOUNTPOINT "/dev/$NAME")"
+ notify-send summary "<span color='#$red'>$NAME: $LABEL</span>\n${MOUNTPOINT/$HOME/\~}"
+ sudo umount "$MOUNTPOINT"
+ sudo rmdir "$HOME/dev/$NAME" || :
+}
diff --git a/common/scripts/utils/waypipe_app b/common/scripts/utils/waypipe_app
new file mode 100755
index 0000000..a8520bc
--- /dev/null
+++ b/common/scripts/utils/waypipe_app
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# Usage: ./waypipe_app user@remote <app>
+# Example: ./waypipe_app user@remote gedit
+
+if [ $# -lt 2 ]; then
+ echo "Usage: $0 user@remote <app> [args...]"
+ exit 1
+fi
+
+REMOTE=$1
+shift
+APP="$@"
+
+# Run the app remotely, forward display over SSH with waypipe
+waypipe ssh "$REMOTE" $APP
diff --git a/common/scripts/utils/wayvnc_session b/common/scripts/utils/wayvnc_session
new file mode 100755
index 0000000..1f2f30b
--- /dev/null
+++ b/common/scripts/utils/wayvnc_session
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# Usage: ./wayvnc_session [address] [port]
+# Default: 0.0.0.0 5900
+
+ADDR=${1:-0.0.0.0}
+PORT=${2:-5900}
+
+echo "Starting Wayland VNC server on $ADDR:$PORT"
+wayvnc $ADDR $PORT
diff --git a/common/scripts/utils/window_manager_name.sh b/common/scripts/utils/window_manager_name.sh
new file mode 100755
index 0000000..cfff9ee
--- /dev/null
+++ b/common/scripts/utils/window_manager_name.sh
@@ -0,0 +1,29 @@
+#! /bin/bash
+
+
+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}"
+}
+
+
+windowManagerName
diff --git a/common/scripts/utils/xtouch b/common/scripts/utils/xtouch
new file mode 100755
index 0000000..a5eb23b
--- /dev/null
+++ b/common/scripts/utils/xtouch
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+check_valid() {
+ if [ -f "$1" ]; then
+ echo "The file "$1" already exists!"
+ exit 1
+ fi
+}
+
+create_script() {
+ touch "$1"
+ chmod +x "$1"
+ echo "$0: $2 script file $1 created with exec permissions"
+ echo -e '#!/usr/bin/env '"$2" > "$1"
+}
+
+usage() {
+ echo -e "Quickly create executable script\n"
+ echo "Usage:"
+ echo " xtouch [ -w FILE LANG ]"
+ echo -e " xtouch ( -h | --help )\n"
+ echo "Arguments:"
+ echo "FILE Name to give the scripts"
+ echo -e "LANG Language in which the script will be written\n"
+ echo "Options:"
+ echo " -w FILE LANG Creates a <LANG> executable script file named <FILE>."
+ echo " -h --help Show this screen."
+}
+
+case "$1" in
+ '-w')
+ check_valid "$2"
+ create_script "$2" "$3"
+ ;;
+ *)
+ usage
+ ;;
+esac
diff --git a/common/scripts/virt/checksum.sh b/common/scripts/virt/checksum.sh
new file mode 100755
index 0000000..adb6a24
--- /dev/null
+++ b/common/scripts/virt/checksum.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Check if the correct number of arguments are provided
+if [[ $# -ne 2 ]]; then
+ echo "Usage: checksum <file> <website>"
+ exit 1
+fi
+
+FILE=$1
+CHECKSUM_PAGE=$2
+
+# Fetch the HTML content of the page
+HTML_CONTENT=$(curl -s "$CHECKSUM_PAGE")
+
+# Try searching for checksums with a more targeted approach, like matching a checksum pattern or look for files with checksum labels
+CHECKSUM=$(echo "$HTML_CONTENT" | grep -oP '([a-f0-9]{64})' | head -n 1) # Try specifically looking for SHA256 checksum patterns
+
+# Check if any checksum was found
+if [[ -z "$CHECKSUM" ]]; then
+ echo "Checksum not found on the page."
+ exit 1
+fi
+
+# Calculate the checksum of the file locally
+LOCAL_CHECKSUM=$(sha256sum "$FILE" | awk '{print $1}')
+
+echo "Local checksum: $LOCAL_CHECKSUM"
+echo "Remote checksum: $CHECKSUM"
+
+# Compare the local checksum with the one from the website
+if [[ "$LOCAL_CHECKSUM" == "$CHECKSUM" ]]; then
+ echo "The checksums match! The file is verified."
+
+else
+ echo "The checksums do not match. The file may be corrupted or tampered with."
+fi
diff --git a/common/scripts/virt/dos.sh b/common/scripts/virt/dos.sh
new file mode 100755
index 0000000..0b50c4b
--- /dev/null
+++ b/common/scripts/virt/dos.sh
@@ -0,0 +1,998 @@
+#!/usr/bin/env bash
+
+# Set variables
+HOST_DIR="virt"
+VM_NAME="dos"
+VM_SIZE="80G" # Disk size in GB
+VM_RAM="8G" # RAM size
+VM_CPU="6" # Number of virtual CPUs
+CORES=$((VM_CPU / 2))
+THREADS_PER_CORE=2
+SOCKETS=1
+
+VM_DIR="$HOST_DIR/machines"
+IMAGE_DIR="$HOST_DIR/images"
+WIN_ISO_DIR="${IMAGE_DIR}/${VM_NAME}" # Directory for Windows ISO
+VM_DIR="$WIN_ISO_DIR"
+SOCKET_DIR="$VM_DIR"
+SHARED_DIR="${HOST_DIR}/shared"
+FIRMWARE_DIR="${HOST_DIR}/firmware"
+TPM_DIR="$WIN_ISO_DIR"
+TPM_SOCKET="${WIN_ISO_DIR}/${VM_NAME}.swtpm-sock"
+GUEST_PORT=22
+QCOW2_FILE="${VM_DIR}/${VM_NAME}.qcow2"
+RAW_FILE="${VM_DIR}/${VM_NAME}.raw"
+
+# Anti-detection: Generate realistic hardware identifiers
+REAL_MAC="00:1A:2B:3C:4D:5E" # Example Dell MAC - replace with your choice
+REAL_SERIAL="$(openssl rand -hex 8 | tr '[:lower:]' '[:upper:]')"
+REAL_UUID="$(uuidgen)"
+REAL_VENDOR="Dell Inc."
+REAL_PRODUCT="OptiPlex 7090"
+REAL_VERSION="01"
+REAL_FAMILY="OptiPlex"
+
+# Try to find an available host port starting from 22220
+HOST_PORT_START=22220
+HOST_PORT_END=22300
+
+for ((port = HOST_PORT_START; port <= HOST_PORT_END; port++)); do
+ if ! ss -tuln | grep -q ":$port\b"; then
+ HOST_PORT=$port
+ echo "Using available port: $HOST_PORT"
+ break
+ fi
+done
+
+if [[ $port -gt $HOST_PORT_END ]]; then
+ echo "Error: No available ports found between $HOST_PORT_START and $HOST_PORT_END" >&2
+ exit 1
+fi
+
+# Set SMP configuration
+SMP_CONFIG="cores=$CORES,threads=$THREADS_PER_CORE,sockets=$SOCKETS"
+
+# Create necessary directories
+mkdir -p "${HOME}/${HOST_DIR}"
+mkdir -p "$IMAGE_DIR" "$SHARED_DIR" "$FIRMWARE_DIR"
+mkdir -p "$WIN_ISO_DIR" "$VM_DIR"
+mkdir -p "${WIN_ISO_DIR}/unattended"
+
+# Define ISO paths and URLs
+ISO_VIRTIO="${WIN_ISO_DIR}/virtio-win.iso"
+ISO_UNATTENDED="${WIN_ISO_DIR}/unattended.iso"
+
+# Find Windows ISO with flexible pattern matching
+find_windows_iso() {
+ # Check if directory exists
+ if [[ ! -d "$WIN_ISO_DIR" ]]; then
+ mkdir -p "$WIN_ISO_DIR"
+ fi
+
+ # Try to find any Windows ISO using case-insensitive patterns
+ local found_iso
+ found_iso=$(find "$WIN_ISO_DIR" -maxdepth 1 -type f \( \
+ -iname "*win11*.iso" -o \
+ -iname "*win*11*.iso" -o \
+ -iname "Win*.iso" -o \
+ -iname "Win11*.iso" -o \
+ -iname "Win*11*.iso" -o \
+ -iname "*windows*11*.iso" -o \
+ -iname "*windows11*.iso" \
+ \) -exec stat --format="%Y %n" {} \; | sort -n | tail -n 1 | cut -d' ' -f2-)
+
+ if [[ -n "$found_iso" && -f "$found_iso" ]]; then
+ echo "$found_iso"
+ return 0
+ fi
+
+ return 1
+}
+
+# Define download URLs
+VIRTIO_ISO_URL="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso"
+
+# Print colored messages
+print_info() { echo -e "\033[1;34m[INFO]\033[0m $1" >&2; }
+print_success() { echo -e "\033[1;32m[SUCCESS]\033[0m $1" >&2; }
+print_warning() { echo -e "\033[1;33m[WARNING]\033[0m $1" >&2; }
+print_error() { echo -e "\033[1;31m[ERROR]\033[0m $1" >&2; }
+
+# Helper: verify file integrity
+verify_file() {
+ local file="$1"
+ local expected_sha256="$2"
+
+ if [[ ! -f "$file" ]]; then
+ return 1
+ fi
+
+ if [[ -n "$expected_sha256" ]]; then
+ local actual_sha256
+ actual_sha256=$(sha256sum "$file" | cut -d' ' -f1)
+ if [[ "$actual_sha256" != "$expected_sha256" ]]; then
+ print_error "File integrity check failed for $file"
+ return 1
+ fi
+ fi
+
+ return 0
+}
+
+# Helper: download file with verification
+download_file() {
+ local url="$1"
+ local dest="$2"
+ local expected_sha256="$3"
+ local allow_failure="$4"
+
+ # Check if file exists and is valid
+ if [[ -f "$dest" ]]; then
+ if verify_file "$dest" "$expected_sha256"; then
+ print_info "File $dest already exists and verified."
+ return 0
+ else
+ print_warning "File $dest exists but failed verification. Redownloading..."
+ rm -f "$dest"
+ fi
+ fi
+
+ print_info "Downloading $url..."
+ if ! curl -fL --progress-bar -o "$dest" "$url"; then
+ print_error "Failed to download $url."
+ if [[ "$allow_failure" != "true" ]]; then
+ return 1
+ fi
+ else
+ # Verify downloaded file
+ if ! verify_file "$dest" "$expected_sha256"; then
+ print_error "Downloaded file failed verification"
+ rm -f "$dest"
+ return 1
+ fi
+ print_success "Successfully downloaded and verified $dest"
+ fi
+
+ return 0
+}
+
+# Handle curl errors
+handle_curl_error() {
+ local exit_code=$1
+ case $exit_code in
+ 6) print_error "Couldn't resolve host" ;;
+ 7) print_error "Failed to connect to host" ;;
+ 22) print_error "HTTP page not retrieved (404, etc.)" ;;
+ 28) print_error "Operation timeout" ;;
+ *) print_error "Curl failed with exit code $exit_code" ;;
+ esac
+}
+
+# Download Windows 11 ISO using Microsoft's API
+download_windows_iso() {
+ local windows_version="11" # Default to Windows 11
+ local language="English (United States)" # Default language
+
+ # Parse arguments if provided
+ if [[ -n "$1" ]]; then
+ windows_version="$1"
+ fi
+
+ print_info "Attempting to download Windows $windows_version ISO from Microsoft..."
+
+ # Set required variables
+ local user_agent="Mozilla/5.0 (X11; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0"
+ local session_id="$(uuidgen)"
+ local profile="606624d44113"
+ local url="https://www.microsoft.com/en-us/software-download/windows$windows_version"
+
+ # Add ISO to URL for Windows 10
+ case "$windows_version" in
+ 10) url="${url}ISO" ;;
+ esac
+
+ # Step 1: Get download page HTML
+ print_info "Fetching download page: $url"
+ local iso_download_page_html
+ iso_download_page_html="$(curl --disable --silent --user-agent "$user_agent" --header "Accept:" --max-filesize 1M --fail --proto =https --tlsv1.2 --http1.1 -- "$url")" || {
+ handle_curl_error $?
+ print_error "Failed to fetch the download page. Please download Windows $windows_version ISO manually from $url"
+ return 1
+ }
+
+ # Step 2: Extract Product Edition ID
+ print_info "Getting Product Edition ID..."
+ local product_edition_id
+ product_edition_id="$(echo "$iso_download_page_html" | grep -Eo '<option value="[0-9]+">Windows' | cut -d '"' -f 2 | head -n 1 | tr -cd '0-9' | head -c 16)"
+
+ if [[ -z "$product_edition_id" ]]; then
+ print_error "Failed to extract product edition ID."
+ print_error "Please download Windows $windows_version ISO manually from $url"
+ return 1
+ fi
+
+ print_success "Product Edition ID: $product_edition_id"
+
+ # Step 3: Register session ID
+ print_info "Registering session ID: $session_id"
+ curl --disable --silent --output /dev/null --user-agent "$user_agent" \
+ --header "Accept:" --max-filesize 100K --fail --proto =https --tlsv1.2 \
+ --http1.1 -- "https://vlscppe.microsoft.com/tags?org_id=y6jn8c31&session_id=$session_id" || {
+ print_error "Failed to register session ID."
+ return 1
+ }
+
+ # Step 4: Get language SKU ID
+ print_info "Getting language SKU ID..."
+ local language_skuid_table_json
+ language_skuid_table_json="$(curl --disable -s --fail --max-filesize 100K --proto =https --tlsv1.2 --http1.1 \
+ "https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?profile=${profile}&ProductEditionId=${product_edition_id}&SKU=undefined&friendlyFileName=undefined&Locale=en-US&sessionID=${session_id}")" || {
+ handle_curl_error $?
+ print_error "Failed to get language SKU information."
+ return 1
+ }
+
+ # Extract SKU ID for selected language
+ local sku_id
+
+ # Try with jq if available (more reliable)
+ if command -v jq >/dev/null 2>&1; then
+ sku_id="$(echo "$language_skuid_table_json" | jq -r '.Skus[] | select(.LocalizedLanguage=="'"$language"'" or .Language=="'"$language"'").Id')"
+ else
+ # Fallback to grep/cut if jq not available
+ sku_id="$(echo "$language_skuid_table_json" | grep -o '"Id":"[^"]*","Language":"'"$language"'"' | cut -d'"' -f4)"
+
+ if [[ -z "$sku_id" ]]; then
+ # Try alternative extraction method
+ sku_id="$(echo "$language_skuid_table_json" | grep -o '"LocalizedLanguage":"'"$language"'","Id":"[^"]*"' | cut -d'"' -f6)"
+ fi
+ fi
+
+ if [[ -z "$sku_id" ]]; then
+ print_error "Failed to extract SKU ID for $language."
+ return 1
+ fi
+
+ print_success "SKU ID: $sku_id"
+
+ # Step 5: Get ISO download link
+ print_info "Getting ISO download link..."
+ local iso_download_link_json
+ iso_download_link_json="$(curl --disable -s --fail --referer "$url" \
+ "https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?profile=${profile}&productEditionId=undefined&SKU=${sku_id}&friendlyFileName=undefined&Locale=en-US&sessionID=${session_id}")"
+
+ local failed=0
+
+ if [[ -z "$iso_download_link_json" ]]; then
+ print_error "Microsoft servers gave an empty response to the download request."
+ failed=1
+ fi
+
+ if echo "$iso_download_link_json" | grep -q "Sentinel marked this request as rejected."; then
+ print_error "Microsoft blocked the automated download request based on your IP address."
+ failed=1
+ fi
+
+ if [[ "$failed" -eq 1 ]]; then
+ print_warning "Please manually download the Windows $windows_version ISO using a web browser from: $url"
+ print_warning "Save the downloaded ISO to: $WIN_ISO_DIR"
+ return 1
+ fi
+
+ # Extract 64-bit ISO download URL
+ local iso_download_link
+
+ # Try with jq if available
+ if command -v jq >/dev/null 2>&1; then
+ iso_download_link="$(echo "$iso_download_link_json" | jq -r '.ProductDownloadOptions[].Uri' | grep x64 | head -n 1)"
+ else
+ # Fallback to grep/cut if jq not available
+ iso_download_link="$(echo "$iso_download_link_json" | grep -o '"Uri":"[^"]*x64[^"]*"' | cut -d'"' -f4 | head -n 1)"
+ fi
+
+ if [[ -z "$iso_download_link" ]]; then
+ print_error "Failed to extract the download link from Microsoft's response."
+ print_warning "Manually download the Windows $windows_version ISO using a web browser from: $url"
+ return 1
+ fi
+
+ print_success "Got download link: ${iso_download_link%%\?*}"
+
+ # Extract filename from URL
+ local file_name="$(echo "$iso_download_link" | cut -d'?' -f1 | rev | cut -d'/' -f1 | rev)"
+
+ # If filename couldn't be extracted, use default
+ if [[ -z "$file_name" || "$file_name" == "$iso_download_link" ]]; then
+ file_name="windows-$windows_version.iso"
+ fi
+
+ # Step 6: Download the ISO
+ print_info "Downloading Windows $windows_version ISO to $WIN_ISO_DIR/$file_name. This may take a while..."
+
+ # Direct curl download
+ curl --disable --progress-bar --fail --location --proto '=https' --tlsv1.2 --http1.1 \
+ --retry 3 --retry-delay 3 --connect-timeout 30 \
+ --output "$WIN_ISO_DIR/$file_name" "$iso_download_link" || {
+ handle_curl_error $?
+ return 1
+ }
+
+ if [[ $? -ne 0 ]]; then
+ print_error "Failed to download the Windows $windows_version ISO."
+ return 1
+ fi
+
+ print_success "Successfully downloaded Windows $windows_version ISO to $WIN_ISO_DIR/$file_name"
+
+ # Return the downloaded filename so calling code can use it
+ echo "$file_name"
+ return 0
+}
+
+# Create unattended installation ISO with proper Windows 11 bypass
+create_unattended_iso() {
+ print_info "Creating unattended installation ISO..."
+
+ # Create enhanced autounattend.xml with Windows 11 TPM/Secure Boot bypass
+ if [ ! -f "$WIN_ISO_DIR/unattended/autounattend.xml" ]; then
+ print_info "Creating enhanced autounattend.xml with Windows 11 bypass..."
+ mkdir -p "$WIN_ISO_DIR/unattended"
+ cat >"$WIN_ISO_DIR/unattended/autounattend.xml" <<'EOF'
+<?xml version="1.0" encoding="utf-8"?>
+<unattend xmlns="urn:schemas-microsoft-com:unattend">
+ <settings pass="windowsPE">
+ <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <SetupUILanguage>
+ <UILanguage>en-US</UILanguage>
+ </SetupUILanguage>
+ <InputLocale>en-US</InputLocale>
+ <SystemLocale>en-US</SystemLocale>
+ <UILanguage>en-US</UILanguage>
+ <UILanguageFallback>en-US</UILanguageFallback>
+ <UserLocale>en-US</UserLocale>
+ </component>
+ <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <DiskConfiguration>
+ <Disk wcm:action="add">
+ <CreatePartitions>
+ <CreatePartition wcm:action="add">
+ <Order>1</Order>
+ <Type>Primary</Type>
+ <Size>100</Size>
+ </CreatePartition>
+ <CreatePartition wcm:action="add">
+ <Order>2</Order>
+ <Type>EFI</Type>
+ <Size>100</Size>
+ </CreatePartition>
+ <CreatePartition wcm:action="add">
+ <Order>3</Order>
+ <Type>MSR</Type>
+ <Size>16</Size>
+ </CreatePartition>
+ <CreatePartition wcm:action="add">
+ <Order>4</Order>
+ <Type>Primary</Type>
+ <Extend>true</Extend>
+ </CreatePartition>
+ </CreatePartitions>
+ <ModifyPartitions>
+ <ModifyPartition wcm:action="add">
+ <Order>1</Order>
+ <PartitionID>1</PartitionID>
+ <Label>WINRE</Label>
+ <Format>NTFS</Format>
+ <TypeID>DE94BBA4-06D1-4D40-A16A-BFD50179D6AC</TypeID>
+ </ModifyPartition>
+ <ModifyPartition wcm:action="add">
+ <Order>2</Order>
+ <PartitionID>2</PartitionID>
+ <Label>System</Label>
+ <Format>FAT32</Format>
+ </ModifyPartition>
+ <ModifyPartition wcm:action="add">
+ <Order>3</Order>
+ <PartitionID>3</PartitionID>
+ </ModifyPartition>
+ <ModifyPartition wcm:action="add">
+ <Order>4</Order>
+ <PartitionID>4</PartitionID>
+ <Label>Windows</Label>
+ <Format>NTFS</Format>
+ </ModifyPartition>
+ </ModifyPartitions>
+ <DiskID>0</DiskID>
+ <WillWipeDisk>true</WillWipeDisk>
+ </Disk>
+ </DiskConfiguration>
+ <ImageInstall>
+ <OSImage>
+ <InstallTo>
+ <DiskID>0</DiskID>
+ <PartitionID>4</PartitionID>
+ </InstallTo>
+ <InstallToAvailablePartition>false</InstallToAvailablePartition>
+ <WillShowUI>OnError</WillShowUI>
+ <InstallFrom>
+ <MetaData wcm:action="add">
+ <Key>/IMAGE/INDEX</Key>
+ <Value>6</Value>
+ </MetaData>
+ </InstallFrom>
+ </OSImage>
+ </ImageInstall>
+ <UserData>
+ <AcceptEula>true</AcceptEula>
+ <FullName>Windows User</FullName>
+ <Organization>Windows</Organization>
+ <ProductKey>
+ <WillShowUI>Never</WillShowUI>
+ </ProductKey>
+ </UserData>
+ <!-- Windows 11 Requirements Bypass -->
+ <RunSynchronous>
+ <RunSynchronousCommand wcm:action="add">
+ <Order>1</Order>
+ <Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path>
+ </RunSynchronousCommand>
+ <RunSynchronousCommand wcm:action="add">
+ <Order>2</Order>
+ <Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
+ </RunSynchronousCommand>
+ <RunSynchronousCommand wcm:action="add">
+ <Order>3</Order>
+ <Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
+ </RunSynchronousCommand>
+ <RunSynchronousCommand wcm:action="add">
+ <Order>4</Order>
+ <Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassStorageCheck /t REG_DWORD /d 1 /f</Path>
+ </RunSynchronousCommand>
+ <RunSynchronousCommand wcm:action="add">
+ <Order>5</Order>
+ <Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassCPUCheck /t REG_DWORD /d 1 /f</Path>
+ </RunSynchronousCommand>
+ </RunSynchronous>
+ <EnableFirewall>false</EnableFirewall>
+ <EnableNetwork>true</EnableNetwork>
+ </component>
+ </settings>
+ <settings pass="specialize">
+ <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <InputLocale>en-US</InputLocale>
+ <SystemLocale>en-US</SystemLocale>
+ <UILanguage>en-US</UILanguage>
+ <UILanguageFallback>en-US</UILanguageFallback>
+ <UserLocale>en-US</UserLocale>
+ </component>
+ <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <ComputerName>DESKTOP-PC</ComputerName>
+ <TimeZone>UTC</TimeZone>
+ </component>
+ <component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <SkipAutoActivation>true</SkipAutoActivation>
+ </component>
+ </settings>
+ <settings pass="oobeSystem">
+ <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <AutoLogon>
+ <Password>
+ <Value>password</Value>
+ <PlainText>true</PlainText>
+ </Password>
+ <LogonCount>1</LogonCount>
+ <Username>Administrator</Username>
+ <Enabled>true</Enabled>
+ </AutoLogon>
+ <OOBE>
+ <HideEULAPage>true</HideEULAPage>
+ <HideLocalAccountScreen>true</HideLocalAccountScreen>
+ <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
+ <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
+ <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
+ <NetworkLocation>Home</NetworkLocation>
+ <ProtectYourPC>1</ProtectYourPC>
+ <SkipMachineOOBE>true</SkipMachineOOBE>
+ <SkipUserOOBE>true</SkipUserOOBE>
+ </OOBE>
+ <UserAccounts>
+ <AdministratorPassword>
+ <Value>password</Value>
+ <PlainText>true</PlainText>
+ </AdministratorPassword>
+ <LocalAccounts>
+ <LocalAccount wcm:action="add">
+ <Password>
+ <Value>password</Value>
+ <PlainText>true</PlainText>
+ </Password>
+ <Name>User</Name>
+ <Group>Administrators</Group>
+ <DisplayName>User</DisplayName>
+ </LocalAccount>
+ </LocalAccounts>
+ </UserAccounts>
+ <!-- Additional Windows 11 bypass commands -->
+ <FirstLogonCommands>
+ <SynchronousCommand wcm:action="add">
+ <Order>1</Order>
+ <CommandLine>reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU /v NoAutoUpdate /t REG_DWORD /d 1 /f</CommandLine>
+ </SynchronousCommand>
+ <SynchronousCommand wcm:action="add">
+ <Order>2</Order>
+ <CommandLine>reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 0 /f</CommandLine>
+ </SynchronousCommand>
+ <SynchronousCommand wcm:action="add">
+ <Order>3</Order>
+ <CommandLine>powershell -Command "Set-ExecutionPolicy Unrestricted -Force"</CommandLine>
+ </SynchronousCommand>
+ </FirstLogonCommands>
+ </component>
+ </settings>
+</unattend>
+EOF
+ fi
+
+ # Create the unattended ISO
+ if command -v genisoimage >/dev/null 2>&1; then
+ print_info "Creating unattended ISO using genisoimage..."
+ genisoimage -J -r -o "$WIN_ISO_DIR/unattended.iso" "$WIN_ISO_DIR/unattended" 2>/dev/null
+ return $?
+ elif command -v mkisofs >/dev/null 2>&1; then
+ print_info "Creating unattended ISO using mkisofs..."
+ mkisofs -J -r -o "$WIN_ISO_DIR/unattended.iso" "$WIN_ISO_DIR/unattended" 2>/dev/null
+ return $?
+ elif command -v xorriso >/dev/null 2>&1; then
+ print_info "Creating unattended ISO using xorriso..."
+ xorriso -as genisoimage -J -r -o "$WIN_ISO_DIR/unattended.iso" "$WIN_ISO_DIR/unattended" 2>/dev/null
+ return $?
+ else
+ print_warning "No ISO creation tool found (genisoimage, mkisofs, or xorriso)"
+ print_warning "Installing genisoimage..."
+ if command -v apt-get >/dev/null 2>&1; then
+ sudo apt-get update && sudo apt-get install -y genisoimage
+ elif command -v yum >/dev/null 2>&1; then
+ sudo yum install -y genisoimage
+ elif command -v pacman >/dev/null 2>&1; then
+ sudo pacman -S --noconfirm cdrtools
+ fi
+
+ # Try again after installation
+ if command -v genisoimage >/dev/null 2>&1; then
+ print_info "Creating unattended ISO using newly installed genisoimage..."
+ genisoimage -J -r -o "$WIN_ISO_DIR/unattended.iso" "$WIN_ISO_DIR/unattended" 2>/dev/null
+ return $?
+ else
+ print_error "Failed to install ISO creation tools"
+ return 1
+ fi
+ fi
+}
+
+# Download or locate essential files
+prepare_files() {
+ # Find Windows ISO
+ WINDOWS_ISO_PATH=$(find_windows_iso | tail -n 1 | xargs)
+ local iso_found=false
+
+ # Check if we found a valid ISO
+ if [[ -n "$WINDOWS_ISO_PATH" && -f "$WINDOWS_ISO_PATH" ]]; then
+ print_success "Using Windows ISO: $WINDOWS_ISO_PATH"
+ iso_found=true
+ else
+ print_warning "Windows ISO not found, attempting to download..."
+ if download_windows_iso; then
+ # Check again after download attempt
+ WINDOWS_ISO_PATH=$(find_windows_iso | tail -n 1 | xargs)
+ if [[ -n "$WINDOWS_ISO_PATH" && -f "$WINDOWS_ISO_PATH" ]]; then
+ print_success "Using downloaded Windows ISO: $WINDOWS_ISO_PATH"
+ iso_found=true
+ fi
+ fi
+ fi
+
+ # If we still don't have an ISO, exit
+ if [[ "$iso_found" != "true" ]]; then
+ print_error "Could not find or download Windows ISO"
+ print_info "Please place your Windows ISO file in: $WIN_ISO_DIR"
+ exit 1
+ fi
+
+ # Download VirtIO drivers if missing (optional for stealth mode)
+ if [[ ! -f "$ISO_VIRTIO" ]]; then
+ print_info "VirtIO drivers ISO not found, downloading (for fallback)..."
+ download_file "$VIRTIO_ISO_URL" "$ISO_VIRTIO" "" "true"
+ else
+ print_success "VirtIO drivers ISO already exists: $ISO_VIRTIO"
+ fi
+
+ # Create unattended ISO if it doesn't exist
+ if [[ ! -f "$ISO_UNATTENDED" ]]; then
+ create_unattended_iso
+ if [[ $? -eq 0 ]]; then
+ print_success "Created unattended ISO: $ISO_UNATTENDED"
+ else
+ print_error "Failed to create unattended ISO"
+ exit 1
+ fi
+ else
+ print_success "Unattended ISO already exists: $ISO_UNATTENDED"
+ fi
+}
+
+# Locate OVMF firmware files
+locate_ovmf() {
+ OVMF_DIRS=(
+ "/usr/share/OVMF"
+ "/usr/share/qemu"
+ "/usr/lib/qemu"
+ "/usr/share/edk2"
+ "/usr/lib/edk2"
+ "/usr/share/edk2/ovmf"
+ "/usr/share/edk2-ovmf"
+ "/usr/share/qemu/edk2-x86_64-code.fd"
+ )
+
+ OVMF_CODE=""
+ OVMF_VARS=""
+
+ for dir in "${OVMF_DIRS[@]}"; do
+ if [[ -f "$dir" ]]; then
+ # Handle direct file paths
+ if [[ "$dir" == *"code.fd" ]]; then
+ OVMF_CODE="$dir"
+ OVMF_VARS="$(dirname "$dir")/edk2-x86_64-vars.fd"
+ fi
+ elif [[ -d "$dir" ]]; then
+ # Handle directories
+ [[ -z "$OVMF_CODE" ]] && OVMF_CODE=$(find "$dir" -type f -name "OVMF_CODE.fd" -o -name "edk2-x86_64-code.fd" 2>/dev/null | head -n 1)
+ [[ -z "$OVMF_VARS" ]] && OVMF_VARS=$(find "$dir" -type f -name "OVMF_VARS.fd" -o -name "edk2-x86_64-vars.fd" 2>/dev/null | head -n 1)
+ fi
+ [[ -n "$OVMF_CODE" && -n "$OVMF_VARS" ]] && break
+ done
+
+ # Try package-specific locations
+ if [[ -z "$OVMF_CODE" || -z "$OVMF_VARS" ]]; then
+ # Ubuntu/Debian locations
+ [[ -z "$OVMF_CODE" ]] && OVMF_CODE="/usr/share/OVMF/OVMF_CODE_4M.fd"
+ [[ -z "$OVMF_VARS" && -f "/usr/share/OVMF/OVMF_VARS_4M.fd" ]] && OVMF_VARS="/usr/share/OVMF/OVMF_VARS_4M.fd"
+
+ # Arch Linux locations
+ [[ -z "$OVMF_CODE" ]] && OVMF_CODE="/usr/share/edk2-ovmf/x64/OVMF_CODE.fd"
+ [[ -z "$OVMF_VARS" && -f "/usr/share/edk2-ovmf/x64/OVMF_VARS.fd" ]] && OVMF_VARS="/usr/share/edk2-ovmf/x64/OVMF_VARS.fd"
+ fi
+
+ # Ensure a writable copy of OVMF_VARS.fd
+ local original_ovmf_vars="$OVMF_VARS"
+ OVMF_VARS="$FIRMWARE_DIR/OVMF_VARS.fd"
+
+ if [[ ! -f "$OVMF_VARS" && -f "$original_ovmf_vars" ]]; then
+ print_info "Copying OVMF_VARS.fd to $OVMF_VARS"
+ cp "$original_ovmf_vars" "$OVMF_VARS" 2>/dev/null || {
+ print_error "Failed to copy OVMF_VARS.fd!"
+ print_info "Trying to install OVMF firmware..."
+
+ # Try to install OVMF
+ if command -v apt-get >/dev/null 2>&1; then
+ sudo apt-get update && sudo apt-get install -y ovmf
+ elif command -v yum >/dev/null 2>&1; then
+ sudo yum install -y edk2-ovmf
+ elif command -v pacman >/dev/null 2>&1; then
+ sudo pacman -S --noconfirm edk2-ovmf
+ fi
+
+ # Try to locate again after installation
+ locate_ovmf
+ return
+ }
+ fi
+
+ # Check if required files exist
+ if [[ -z "$OVMF_CODE" || ! -f "$OVMF_CODE" ]]; then
+ print_error "OVMF_CODE.fd not found!"
+ print_info "Please install OVMF firmware package:"
+ print_info " Ubuntu/Debian: sudo apt install ovmf"
+ print_info " RHEL/CentOS: sudo yum install edk2-ovmf"
+ print_info " Arch: sudo pacman -S edk2-ovmf"
+ exit 1
+ fi
+ if [[ ! -f "$OVMF_VARS" ]]; then
+ print_error "OVMF_VARS.fd not found or could not be copied!"
+ exit 1
+ fi
+
+ print_success "Found OVMF firmware: $OVMF_CODE"
+ print_success "Using OVMF vars: $OVMF_VARS"
+}
+
+# Create VM disk image
+create_disk() {
+ # Check if the qcow2 image file exists; if not, create it
+ if [[ ! -f "$QCOW2_FILE" ]]; then
+ print_info "Creating $QCOW2_FILE with a size of $VM_SIZE"
+ qemu-img create -f qcow2 "$QCOW2_FILE" "$VM_SIZE" || {
+ print_error "Failed to create qcow2 image!"
+ exit 1
+ }
+ print_success "Created VM disk image: $QCOW2_FILE"
+ else
+ print_success "VM disk image already exists: $QCOW2_FILE"
+ fi
+}
+
+# Helper function to convert QCOW2 to RAW for stealth
+convert_to_raw() {
+ if [[ -f "$QCOW2_FILE" && ! -f "$RAW_FILE" ]]; then
+ print_info "Converting QCOW2 to RAW format for stealth..."
+ qemu-img convert -f qcow2 -O raw "$QCOW2_FILE" "$RAW_FILE" || {
+ print_error "Failed to convert to RAW format"
+ exit 1
+ }
+ print_success "Successfully converted to RAW format: $RAW_FILE"
+ elif [[ ! -f "$RAW_FILE" ]]; then
+ print_info "Creating RAW disk image..."
+ qemu-img create -f raw "$RAW_FILE" "$VM_SIZE" || {
+ print_error "Failed to create RAW disk image"
+ exit 1
+ }
+ print_success "Created RAW disk image: $RAW_FILE"
+ else
+ print_success "RAW disk image already exists: $RAW_FILE"
+ fi
+}
+
+# Check dependencies
+check_dependencies() {
+ local missing_deps=()
+
+ # Check for essential tools
+ command -v qemu-system-x86_64 >/dev/null 2>&1 || missing_deps+=("qemu-system-x86_64")
+ command -v swtpm >/dev/null 2>&1 || missing_deps+=("swtpm")
+ command -v curl >/dev/null 2>&1 || missing_deps+=("curl")
+ command -v uuidgen >/dev/null 2>&1 || missing_deps+=("uuidgen")
+ command -v openssl >/dev/null 2>&1 || missing_deps+=("openssl")
+
+ if [[ ${#missing_deps[@]} -gt 0 ]]; then
+ print_error "Missing required dependencies: ${missing_deps[*]}"
+ print_info "Please install them using your package manager:"
+ print_info " Ubuntu/Debian: sudo apt install qemu-system-x86 swtpm curl uuid-runtime openssl"
+ print_info " RHEL/CentOS: sudo yum install qemu-kvm swtpm curl util-linux openssl"
+ print_info " Arch: sudo pacman -S qemu swtpm curl util-linux openssl"
+ exit 1
+ fi
+}
+
+start_vm() {
+ # Verify that we have a Windows ISO
+ if [[ -z "$WINDOWS_ISO_PATH" || ! -f "$WINDOWS_ISO_PATH" ]]; then
+ print_error "Windows ISO file not found. Cannot start VM."
+ print_info "Please download Windows 11 ISO and save it to: $WIN_ISO_DIR"
+ exit 1
+ fi
+
+ # Anti-detection: Generate realistic hardware identifiers
+ REAL_MAC="00:1A:79:$(openssl rand -hex 3 | sed 's/../&:/g; s/:$//')" # Dell OUI
+ REAL_SERIAL="$(openssl rand -hex 8 | tr '[:lower:]' '[:upper:]')"
+ REAL_UUID="$(uuidgen)"
+ REAL_VENDOR="Dell Inc."
+ REAL_PRODUCT="OptiPlex 7090"
+ REAL_VERSION="01"
+ REAL_FAMILY="OptiPlex"
+
+ # Stop any existing swtpm process for this VM
+ if [[ -f "$TPM_SOCKET" ]]; then
+ print_info "Cleaning up existing TPM socket..."
+ rm -f "$TPM_SOCKET"
+ fi
+
+ # Start swtpm (TPM emulator)
+ print_info "Starting TPM emulator..."
+ /sbin/swtpm socket \
+ --ctrl type=unixio,path="$TPM_SOCKET" \
+ --terminate \
+ --tpmstate dir="$TPM_DIR" \
+ --tpm2 &
+
+ # Give swtpm a moment to create the socket
+ sleep 1
+
+ # Verify TPM socket exists
+ if [[ ! -S "$TPM_SOCKET" ]]; then
+ print_error "TPM socket not created: $TPM_SOCKET"
+ exit 1
+ fi
+
+ print_info "Starting stealth Windows 11 VM..."
+ print_info "Using Windows ISO: $WINDOWS_ISO_PATH"
+ print_info "VM will boot from unattended installation ISO"
+ print_info "Default login: Username=User, Password=password"
+ print_info "Anti-detection measures enabled"
+
+ # Build qemu arguments in an array for safety and readability
+ QEMU_ARGS=(
+ # Basic VM configuration
+ -name "$REAL_PRODUCT",process="$VM_NAME"
+ -machine q35,hpet=off,smm=on,vmport=off,accel=kvm
+
+ # Global optimizations
+ -global kvm-pit.lost_tick_policy=discard
+ -global ICH9-LPC.disable_s3=1
+
+ # CPU configuration (stealth)
+ -cpu host,-hypervisor,+invtsc,+ssse3,l3-cache=on,migratable=no
+ -smp "$SMP_CONFIG"
+ -m "$VM_RAM"
+
+ # SMBIOS spoofing for anti-detection
+ -smbios type=0,vendor="$REAL_VENDOR",version="$REAL_VERSION",date="03/15/2023"
+ -smbios type=1,manufacturer="$REAL_VENDOR",product="$REAL_PRODUCT",version="$REAL_VERSION",serial="$REAL_SERIAL",uuid="$REAL_UUID",family="$REAL_FAMILY"
+ -smbios type=2,manufacturer="$REAL_VENDOR",product="0NK70N",version="A00",serial="$REAL_SERIAL"
+ -smbios type=3,manufacturer="$REAL_VENDOR",version="$REAL_VERSION",serial="$REAL_SERIAL"
+ -smbios type=4,manufacturer="Intel(R) Corporation",version="11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz"
+
+ # Process management
+ -pidfile "$VM_DIR/$VM_NAME.pid"
+ -rtc base=localtime,clock=host,driftfix=slew
+
+ # Display and graphics
+ -vga qxl
+ -display sdl
+
+ # Boot configuration
+ -boot menu=on,splash-time=0,order=d,reboot-timeout=5000
+
+ # Random number generator
+ -object rng-random,id=rng0,filename=/dev/urandom
+ -device i82801b11-bridge,id=pci.1
+
+ # usb
+ -device usb-ehci,id=usb
+ -device usb-kbd,bus=usb.0
+ -device usb-tablet,bus=usb.0
+ -k en-us
+
+ # audio (pipewire)
+ -audiodev pipewire,id=audio0
+ -device AC97,audiodev=audio0
+
+ # Network (realistic NIC with stealth MAC)
+ -device rtl8139,netdev=nic,mac="$REAL_MAC"
+ -netdev user,hostname="$VM_NAME",hostfwd=tcp::"$HOST_PORT"-:"$GUEST_PORT",smb="$SHARED_DIR",id=nic
+
+ # UEFI firmware
+ -global driver=cfi.pflash01,property=secure,value=on
+ -drive if=pflash,format=raw,unit=0,file="$OVMF_CODE",readonly=on
+ -drive if=pflash,format=raw,unit=1,file="$OVMF_VARS"
+
+ # Main storage (AHCI for compatibility)
+ -device ahci,id=ahci
+ -drive if=none,id=disk0,format=raw,file="$RAW_FILE",cache=writeback
+ -device ide-hd,bus=ahci.0,drive=disk0
+
+ # CD-ROM drives (Windows ISO + Unattended)
+ -drive media=cdrom,index=0,file="$WINDOWS_ISO_PATH",if=ide
+ -drive media=cdrom,index=1,file="$ISO_UNATTENDED",if=ide
+
+ # TPM 2.0
+ -chardev socket,id=chrtpm,path="$TPM_SOCKET"
+ -tpmdev emulator,id=tpm0,chardev=chrtpm
+ -device tpm-tis,tpmdev=tpm0
+
+ # Monitoring and management sockets
+ -monitor unix:"$SOCKET_DIR/$VM_NAME-monitor.socket",server,nowait
+ -serial unix:"$SOCKET_DIR/$VM_NAME-serial.socket",server,nowait
+ )
+
+ # Add VirtIO ISO if it exists (as fallback)
+ if [[ -f "$ISO_VIRTIO" ]]; then
+ QEMU_ARGS+=(-drive media=cdrom,index=2,file="$ISO_VIRTIO",if=ide,readonly=on)
+ fi
+
+ print_info "Starting QEMU with stealth configuration..."
+ print_info "Monitor socket: $SOCKET_DIR/$VM_NAME-monitor.socket"
+ print_info "Serial socket: $SOCKET_DIR/$VM_NAME-serial.socket"
+ print_info "SSH port forwarding: localhost:$HOST_PORT -> VM:$GUEST_PORT"
+
+ # Execute qemu
+ exec qemu-system-x86_64 "${QEMU_ARGS[@]}"
+
+}
+
+# Helper function to convert QCOW2 to RAW for stealth
+convert_to_raw() {
+ if [[ -f "$QCOW2_FILE" && ! -f "$RAW_FILE" ]]; then
+ print_info "Converting QCOW2 to RAW format for stealth..."
+ qemu-img convert -f qcow2 -O raw "$QCOW2_FILE" "$RAW_FILE"
+ if [[ $? -eq 0 ]]; then
+ print_success "Successfully converted to RAW format"
+ else
+ print_error "Failed to convert to RAW format"
+ exit 1
+ fi
+ elif [[ ! -f "$RAW_FILE" ]]; then
+ print_info "Creating RAW disk image..."
+ qemu-img create -f raw "$RAW_FILE" "$VM_SIZE"
+ fi
+}
+
+# Anti-detection validation commands
+show_validation_commands() {
+ cat << 'EOF'
+=== VM DETECTION VALIDATION COMMANDS ===
+
+Run these commands inside Windows to verify stealth:
+
+# PowerShell - Check hardware info:
+Get-WmiObject -Class Win32_ComputerSystem | Select-Object Manufacturer, Model, TotalPhysicalMemory
+Get-WmiObject -Class Win32_BIOS | Select-Object Manufacturer, Version, SerialNumber
+Get-WmiObject -Class Win32_BaseBoard | Select-Object Manufacturer, Product, SerialNumber
+Get-WmiObject -Class Win32_Processor | Select-Object Name, Manufacturer, MaxClockSpeed
+
+# Check for hypervisor presence:
+Get-WmiObject -Class Win32_ComputerSystem | Select-Object HypervisorPresent
+bcdedit /enum | findstr hypervisorlaunchtype
+
+# Registry checks for VM artifacts:
+reg query "HKLM\HARDWARE\DESCRIPTION\System" /v SystemBiosVersion
+reg query "HKLM\HARDWARE\DESCRIPTION\System\BIOS" /v SystemManufacturer
+reg query "HKLM\SYSTEM\CurrentControlSet\Services" | findstr -i "vbox\|vmware\|qemu\|virtio"
+
+# Check network adapter:
+Get-WmiObject -Class Win32_NetworkAdapter | Where-Object {$_.NetConnectionStatus -eq 2} | Select-Object Name, MACAddress, Manufacturer
+
+# Check PCI devices for VM signatures:
+Get-WmiObject -Class Win32_PnPEntity | Where-Object {$_.Name -match "VirtIO|QEMU|VMware|VirtualBox|Hyper-V"} | Select-Object Name, DeviceID
+
+# Check running services:
+Get-Service | Where-Object {$_.Name -match "vbox|vmware|qemu|virtio|spice"}
+
+# Check for VM-specific processes:
+Get-Process | Where-Object {$_.ProcessName -match "vbox|vmware|qemu|virtio"}
+
+# Additional checks:
+systeminfo | findstr /C:"System Manufacturer" /C:"System Model" /C:"BIOS Version"
+wmic computersystem get manufacturer,model,name,systemtype
+
+EOF
+}
+
+# Cleanup function
+cleanup() {
+ print_info "Cleaning up..."
+
+ # Kill swtpm if running
+ if [[ -f "$TPM_SOCKET" ]]; then
+ pkill -f "swtpm.*$TPM_SOCKET" 2>/dev/null || true
+ rm -f "$TPM_SOCKET" 2>/dev/null || true
+ fi
+
+ # Remove PID file
+ if [[ -f "$VM_DIR/$VM_NAME.pid" ]]; then
+ rm -f "$VM_DIR/$VM_NAME.pid" 2>/dev/null || true
+ fi
+}
+
+# Trap cleanup on exit
+trap cleanup EXIT
+
+# Main execution function
+main() {
+ print_info "=== Windows 11 Stealth VM Setup ==="
+ print_info "VM Name: $VM_NAME"
+ print_info "VM Size: $VM_SIZE"
+ print_info "VM RAM: $VM_RAM"
+ print_info "VM CPUs: $VM_CPU"
+ print_info "Host Port: $HOST_PORT"
+
+ check_dependencies
+ prepare_files
+ locate_ovmf
+ create_disk
+ convert_to_raw
+ show_validation_commands
+
+ print_success "Setup complete! Starting VM..."
+ start_vm
+}
+
+# Run main function
+main "$@"
diff --git a/common/scripts/virt/server.sh b/common/scripts/virt/server.sh
new file mode 100755
index 0000000..6d937fd
--- /dev/null
+++ b/common/scripts/virt/server.sh
@@ -0,0 +1,128 @@
+#!/usr/bin/env bash
+
+# Set variables
+HOST_DIR="machines"
+VM_NAME="proxmox"
+VM_DIR="$HOME/machines/vm"
+IMAGE_DIR="$HOME/machines/images"
+SOCKET_DIR="$VM_DIR"
+#ISO_FILE="$IMAGE_DIR/proxmox-ve_8.3-1.iso"
+ISO_FILE=$(find "$IMAGE_DIR" -type f -iname "proxmox*.iso" -exec stat --format="%Y %n" {} \; | sort -n | tail -n 1 | cut -d' ' -f2-)
+QCOW2_FILE="$VM_DIR/$VM_NAME.qcow2"
+HOST_PORT=22220
+GUEST_PORT=22
+SHARED_DIR="$HOME/machines/shared"
+FIRMWARE_DIR="$HOME/machines/firmware"
+VM_SIZE="300G" # Disk size in GB
+VM_RAM="12G" # RAM size
+VM_CPU="6" # Number of virtual CPUs
+CORES=$((VM_CPU / 2))
+THREADS_PER_CORE=2
+SOCKETS=1
+
+# Set SMP configuration
+SMP_CONFIG="cores=$CORES,threads=$THREADS_PER_CORE,sockets=$SOCKETS"
+
+# Ensure necessary directories exist
+mkdir -p "$HOME"/"$HOST_DIR"
+mkdir -p "$VM_DIR" "$IMAGE_DIR" "$SHARED_DIR" "$FIRMWARE_DIR"
+
+# Locate OVMF firmware files
+OVMF_DIRS=(
+ "/usr/share/OVMF"
+ "/usr/share/qemu"
+ "/usr/lib/qemu"
+ "/usr/share/edk2"
+ "/usr/lib/edk2"
+)
+
+OVMF_CODE=""
+OVMF_VARS=""
+
+for dir in "${OVMF_DIRS[@]}"; do
+ [[ -z "$OVMF_CODE" ]] && OVMF_CODE=$(find "$dir" -type f -name "OVMF_CODE.fd" -o -name "edk2-x86_64-code.fd" 2>/dev/null | head -n 1)
+ [[ -z "$OVMF_VARS" ]] && OVMF_VARS=$(find "$dir" -type f -name "OVMF_VARS.fd" 2>/dev/null | head -n 1)
+ [[ -n "$OVMF_CODE" && -n "$OVMF_VARS" ]] && break
+done
+
+# Ensure a writable copy of OVMF_VARS.fd
+OVMF_VARS="$FIRMWARE_DIR/OVMF_VARS.fd"
+
+if [[ ! -f "$OVMF_VARS" ]]; then
+ echo "Copying OVMF_VARS.fd to $OVMF_VARS"
+ cp /usr/share/edk2/OvmfX64/OVMF_VARS.fd "$OVMF_VARS" 2>/dev/null || {
+ echo "Error: Failed to copy OVMF_VARS.fd!" >&2
+ exit 1
+ }
+fi
+
+# Check if required files exist
+if [[ -z "$OVMF_CODE" ]]; then
+ echo "Error: OVMF_CODE.fd not found!" >&2
+ exit 1
+fi
+if [[ ! -f "$OVMF_VARS" ]]; then
+ echo "Error: OVMF_VARS.fd not found or could not be copied!" >&2
+ exit 1
+fi
+if [[ ! -f "$ISO_FILE" ]]; then
+ echo "Warning: $ISO_FILE ISO not found at $IMAGE_DIR"
+fi
+
+# Check if the qcow2 image file exists; if not, create it
+if [[ ! -f "$QCOW2_FILE" ]]; then
+ echo "Creating $QCOW2_FILE with a size of $VM_SIZE"
+ qemu-img create -f qcow2 "$QCOW2_FILE" "$VM_SIZE" || {
+ echo "Error: Failed to create qcow2 image!" >&2
+ exit 1
+ }
+else
+ echo ""
+fi
+
+# Run QEMU
+/sbin/qemu-system-x86_64 \
+ -name "$VM_NAME",process="$VM_NAME" \
+ -machine q35,smm=off,vmport=off,accel=kvm \
+ -global kvm-pit.lost_tick_policy=discard \
+ -cpu host \
+ -smp "$SMP_CONFIG" \
+ -m "$VM_RAM" \
+ -device virtio-balloon \
+ -pidfile "$VM_DIR/$VM_NAME.pid" \
+ -rtc base=utc,clock=host \
+ -vga none \
+ -device virtio-vga-gl,xres=1280,yres=800 \
+ -display sdl,gl=on \
+ -device virtio-rng-pci,rng=rng0 \
+ -object rng-random,id=rng0,filename=/dev/urandom \
+ -device qemu-xhci,id=spicepass \
+ -chardev spicevmc,id=usbredirchardev1,name=usbredir \
+ -device usb-redir,chardev=usbredirchardev1,id=usbredirdev1 \
+ -chardev spicevmc,id=usbredirchardev2,name=usbredir \
+ -device usb-redir,chardev=usbredirchardev2,id=usbredirdev2 \
+ -chardev spicevmc,id=usbredirchardev3,name=usbredir \
+ -device usb-redir,chardev=usbredirchardev3,id=usbredirdev3 \
+ -device pci-ohci,id=smartpass \
+ -device usb-ccid \
+ -device usb-ehci,id=input \
+ -device usb-kbd,bus=input.0 \
+ -k en-us \
+ -device usb-tablet,bus=input.0 \
+ -audiodev pipewire,id=audio0 \
+ -device intel-hda \
+ -device hda-micro,audiodev=audio0 \
+ -device virtio-net,netdev=nic \
+ -netdev user,hostname="$VM_NAME",hostfwd=tcp::"$HOST_PORT"-:"$GUEST_PORT",id=nic \
+ -fsdev local,id=fsdev0,path="$SHARED_DIR",security_model=mapped-xattr \
+ -device virtio-9p-pci,fsdev=fsdev0,mount_tag="SharedDir" \
+ -global driver=cfi.pflash01,property=secure,value=on \
+ -drive if=pflash,format=raw,unit=0,file="$OVMF_CODE",readonly=on \
+ -drive if=pflash,format=raw,unit=1,file="$OVMF_VARS" \
+ -drive media=cdrom,index=0,file="$ISO_FILE" \
+ -device virtio-blk-pci,drive=SystemDisk \
+ -drive id=SystemDisk,if=none,format=qcow2,file="$QCOW2_FILE" \
+ -monitor unix:"$SOCKET_DIR/$VM_NAME-monitor.socket",server,nowait \
+ -serial unix:"$SOCKET_DIR/$VM_NAME-serial.socket",server,nowait
+#-serial unix:"$SOCKET_DIR/$VM_NAME-serial.socket",server,nowait 2>/dev/null
+#-netdev user,hostname="$VM_NAME",hostfwd=tcp::"$HOST_PORT"-:"$GUEST_PORT",smb="$SHARED_DIR",id=nic \
diff --git a/common/scripts/virt/ubuntu b/common/scripts/virt/ubuntu
new file mode 100755
index 0000000..c93941d
--- /dev/null
+++ b/common/scripts/virt/ubuntu
@@ -0,0 +1,222 @@
+#!/usr/bin/env bash
+
+# Set variables
+HOST_DIR="$HOME/virt"
+VM_DIR="$HOME/virt/machines"
+IMAGE_DIR="$HOME/virt/images"
+SOCKET_DIR="$VM_DIR"
+
+BASE_NAME=$(basename "$0" .sh | sed 's/[0-9]*$//')
+VM_INDEX=$(basename "$0" .sh | grep -o '[0-9]*$')
+VM_INDEX=${VM_INDEX:-1} # default to 1 if no number
+
+VM_NAME="${BASE_NAME}${VM_INDEX}"
+QCOW2_FILE="$VM_DIR/$VM_NAME.qcow2"
+
+# If the file exists, use it
+if [[ -f "$QCOW2_FILE" ]]; then
+ echo "Using existing VM image: $QCOW2_FILE"
+else
+ # Loop to find first available index if not
+ while [[ -f "$QCOW2_FILE" ]]; do
+ ((VM_INDEX++))
+ VM_NAME="${BASE_NAME}${VM_INDEX}"
+ QCOW2_FILE="$VM_DIR/$VM_NAME.qcow2"
+ done
+ echo "Creating new VM image: $QCOW2_FILE"
+ qemu-img create -f qcow2 "$QCOW2_FILE" "$VM_SIZE" || {
+ echo "Error: Failed to create qcow2 image!" >&2
+ exit 1
+ }
+fi
+
+#ISO_FILE=$(ls -1t "$IMAGE_DIR"/ubuntu-*-desktop-*-amd64.iso 2>/dev/null | head -n1)
+#if [[ -z "$ISO_FILE" ]]; then
+# echo "Error: No Ubuntu ISO found in $IMAGE_DIR" >&2
+# exit 1
+#fi
+#echo "Using ISO: $ISO_FILE"
+
+# Directory to remember ISO choices
+CHOICES_DIR="$VM_DIR/.iso_choices"
+mkdir -p "$CHOICES_DIR"
+
+CHOICE_FILE="$CHOICES_DIR/$BASE_NAME.choice"
+
+if [[ -f "$CHOICE_FILE" ]]; then
+ # Already have a saved ISO for this VM
+ ISO_FILE=$(<"$CHOICE_FILE")
+ echo "Using previously selected ISO: $ISO_FILE"
+else
+ # List all ISO files newest first
+ mapfile -t ISO_LIST < <(ls -1t "$IMAGE_DIR"/*.iso 2>/dev/null)
+ if [[ ${#ISO_LIST[@]} -eq 0 ]]; then
+ echo "Error: No ISO files found in $IMAGE_DIR" >&2
+ exit 1
+ fi
+
+ echo "Available ISOs:"
+ PS3="Select an ISO to boot (default: 1): "
+ select ISO_FILE in "${ISO_LIST[@]}"; do
+ ISO_FILE=${ISO_FILE:-${ISO_LIST[0]}}
+ echo "Using ISO: $ISO_FILE"
+ # Save choice so we don’t ask again
+ echo "$ISO_FILE" > "$CHOICE_FILE"
+ break
+ done
+fi
+
+QCOW2_FILE="$VM_DIR/$VM_NAME.qcow2"
+GUEST_PORT=22
+SHARED_DIR="$HOST_DIR/shared"
+VM_SIZE="60G" # Disk size in GB
+VM_RAM="8G" # RAM size
+VM_CPU="6" # Number of virtual CPUs
+CORES=$((VM_CPU / 2))
+THREADS_PER_CORE=2
+SOCKETS=1
+SHARED_DIR="$HOST_DIR/shared"
+FIRMWARE_DIR="$HOST_DIR/firmware"
+
+SMP_CONFIG="cores=$CORES,threads=$THREADS_PER_CORE,sockets=$SOCKETS"
+
+# Set SPICE_NOGRAB environment variable
+export SPICE_NOGRAB=1
+
+# Try to find an available host port starting from 22220
+HOST_PORT_START=22220
+HOST_PORT_END=22300
+
+for ((port = HOST_PORT_START; port <= HOST_PORT_END; port++)); do
+ if ! ss -tuln | grep -q ":$port\b"; then
+ HOST_PORT=$port
+ echo "Using available port: $HOST_PORT"
+ break
+ fi
+done
+
+if [[ $port -gt $HOST_PORT_END ]]; then
+ echo "Error: No available ports found between $HOST_PORT_START and $HOST_PORT_END" >&2
+ exit 1
+fi
+
+# Ensure necessary directories exist
+mkdir -p "$HOST_DIR"
+mkdir -p "$VM_DIR" "$IMAGE_DIR" "$SHARED_DIR" "$FIRMWARE_DIR"
+
+# Locate OVMF firmware files
+OVMF_DIRS=(
+ "/usr/share/OVMF"
+ "/usr/share/qemu"
+ "/usr/lib/qemu"
+ "/usr/share/edk2"
+ "/usr/lib/edk2"
+)
+
+OVMF_CODE=""
+OVMF_VARS=""
+
+for dir in "${OVMF_DIRS[@]}"; do
+ [[ -z "$OVMF_CODE" ]] && OVMF_CODE=$(find "$dir" -type f -name "OVMF_CODE.fd" -o -name "edk2-x86_64-code.fd" 2>/dev/null | head -n 1)
+ [[ -z "$OVMF_VARS" ]] && OVMF_VARS=$(find "$dir" -type f -name "OVMF_VARS.fd" 2>/dev/null | head -n 1)
+ [[ -n "$OVMF_CODE" && -n "$OVMF_VARS" ]] && break
+done
+
+# Ensure a writable copy of OVMF_VARS.fd
+OVMF_VARS="$IMAGE_DIR/OVMF_VARS.fd"
+
+if [[ ! -f "$OVMF_VARS" ]]; then
+ echo "Copying OVMF_VARS.fd to $OVMF_VARS"
+ cp /usr/share/edk2/OvmfX64/OVMF_VARS.fd "$OVMF_VARS" 2>/dev/null || {
+ echo "Error: Failed to copy OVMF_VARS.fd!" >&2
+ exit 1
+ }
+fi
+
+# Check if required files exist
+if [[ -z "$OVMF_CODE" ]]; then
+ echo "Error: OVMF_CODE.fd not found!" >&2
+ exit 1
+fi
+if [[ ! -f "$OVMF_VARS" ]]; then
+ echo "Error: OVMF_VARS.fd not found or could not be copied!" >&2
+ exit 1
+fi
+if [[ ! -f "$ISO_FILE" ]]; then
+ echo "Warning: $ISO_FILE ISO not found at $IMAGE_DIR"
+fi
+
+# Check if the qcow2 image file exists; if not, create it
+if [[ ! -f "$QCOW2_FILE" ]]; then
+ echo "Creating $QCOW2_FILE with a size of $VM_SIZE"
+ qemu-img create -f qcow2 "$QCOW2_FILE" "$VM_SIZE" || {
+ echo "Error: Failed to create qcow2 image!" >&2
+ exit 1
+ }
+else
+ echo ""
+fi
+
+# Run QEMU
+/sbin/qemu-system-x86_64 \
+ -name "$VM_NAME",process="$VM_NAME" \
+ -machine q35,smm=off,vmport=off,accel=kvm \
+ -enable-kvm \
+ -global kvm-pit.lost_tick_policy=discard \
+ -cpu host \
+ -smp "$SMP_CONFIG" \
+ -m "$VM_RAM" \
+ -device virtio-balloon \
+ -pidfile "$VM_DIR/$VM_NAME.pid" \
+ -rtc base=utc,clock=host \
+ -vga none \
+ -device virtio-vga-gl,xres=1280,yres=800 \
+ -display sdl,gl=on \
+ -device virtio-rng-pci,rng=rng0 \
+ -device virtio-serial \
+ -object rng-random,id=rng0,filename=/dev/urandom \
+ -device qemu-xhci,id=spicepass \
+ -chardev spicevmc,id=usbredirchardev1,name=usbredir \
+ -device usb-redir,chardev=usbredirchardev1,id=usbredirdev1 \
+ -chardev spicevmc,id=usbredirchardev2,name=usbredir \
+ -device usb-redir,chardev=usbredirchardev2,id=usbredirdev2 \
+ -chardev spicevmc,id=usbredirchardev3,name=usbredir \
+ -device usb-redir,chardev=usbredirchardev3,id=usbredirdev3 \
+ -device pci-ohci,id=smartpass \
+ -device usb-ccid \
+ -device usb-ehci,id=input \
+ -device usb-kbd,bus=input.0 \
+ -k en-us \
+ -device usb-tablet,bus=input.0 \
+ -audiodev pipewire,id=audio0 \
+ -device intel-hda \
+ -device hda-micro,audiodev=audio0 \
+ -device virtio-net,netdev=nic \
+ -netdev user,hostname="$VM_NAME",hostfwd=tcp::"$HOST_PORT"-:"$GUEST_PORT",id=nic \
+ -device virtio-9p-pci,fsdev=fsdev0,mount_tag="Public-$(whoami)" \
+ -global driver=cfi.pflash01,property=secure,value=on \
+ -drive if=pflash,format=raw,unit=0,file="$OVMF_CODE",readonly=on \
+ -drive if=pflash,format=raw,unit=1,file="$OVMF_VARS" \
+ -drive media=cdrom,index=0,file="$ISO_FILE" \
+ -device virtio-blk-pci,drive=SystemDisk \
+ -drive id=SystemDisk,if=none,format=qcow2,file="$QCOW2_FILE" \
+ -fsdev local,id=fsdev0,path="$SHARED_DIR",security_model=mapped-xattr \
+ -monitor unix:"$SOCKET_DIR/$VM_NAME-monitor.socket",server,nowait \
+ -serial unix:"$SOCKET_DIR/$VM_NAME-serial.socket",server,nowait
+
+#-display sdl,gl=on \
+#-display gtk,gl=on \
+#-display gtk,grab-on-hover=on,grab-mod=none \
+#-qmp unix:/tmp/qmp-sock,server,nowait \
+#-qmp unix:"$SOCKET_DIR/$VM_NAME-qmp.socket",server,nowait \
+#-chardev socket,path="$VM_DIR/$VM_NAME-qga.sock",server=on,wait=off,id=qga0 \
+
+# Network Isolation:
+#-netdev user,hostname="$VM_NAME",restrict=yes,id=nic \
+
+# No file-sharing:
+#-netdev user,hostname="$VM_NAME",hostfwd=tcp::"$HOST_PORT"-:"$GUEST_PORT",id=nic \
+
+# File-sharing and networking:
+#-netdev user,hostname="$VM_NAME",hostfwd=tcp::"$HOST_PORT"-:"$GUEST_PORT",smb="$SHARED_DIR",id=nic \
+#-device virtio-9p-pci,fsdev=fsdev0,mount_tag="Public-$(whoami)" \
diff --git a/common/scripts/virt/windows.sh b/common/scripts/virt/windows.sh
new file mode 100755
index 0000000..1863f8a
--- /dev/null
+++ b/common/scripts/virt/windows.sh
@@ -0,0 +1,1156 @@
+#!/usr/bin/env bash
+
+# Windows VM Creation Script
+# Description: Creates and manages Windows virtual machines using QEMU/KVM
+# Features:
+# - Supports Windows 10, 11, and Server
+# - Automatic dependency checking
+# - Multiple VM instance support
+# - Cache management
+# - Unattended installation
+# - SMB sharing support
+# - Network control
+
+# Usage: ./windows.sh [VERSION] [OPTIONS]
+# Versions:
+# 10 Windows 10
+# 11 Windows 11 (default)
+# server Windows Server 2022
+# server2019 Windows Server 2019
+# server2016 Windows Server 2016
+
+# Options:
+# --help Show this help message
+# --clean Clean dependency cache
+# --check-deps Only check dependencies and exit
+# --download-iso Download Windows ISO and exit
+# --name NAME Set custom VM name
+# --ram SIZE Set RAM size (default: 8G)
+# --cpu NUM Set number of CPUs (default: 6)
+# --disk SIZE Set disk size (default: 80G)
+# --enable-smb Enable SMB sharing (default: disabled)
+# --disable-net Disable network connectivity (default: enabled)
+
+# Parse command line arguments
+WINDOWS_VERSION="11"
+CLEAN_CACHE=false
+CHECK_DEPS_ONLY=false
+DOWNLOAD_ISO_ONLY=false
+VM_NAME="windows-11"
+VM_RAM="8G"
+VM_CPU="6"
+VM_SIZE="80G"
+ENABLE_SMB=false
+DISABLE_NET=false
+
+# Handle version argument if it's the first argument
+if [[ $# -gt 0 && ! $1 =~ ^-- ]]; then
+ case "$1" in
+ "10")
+ WINDOWS_VERSION="10"
+ VM_NAME="windows-10"
+ ;;
+ "server" | "server2022")
+ WINDOWS_VERSION="server2022"
+ VM_NAME="windows-server-2022"
+ ;;
+ "server2019")
+ WINDOWS_VERSION="server2019"
+ VM_NAME="windows-server-2019"
+ ;;
+ "server2016")
+ WINDOWS_VERSION="server2016"
+ VM_NAME="windows-server-2016"
+ ;;
+ *)
+ echo "Unknown Windows version: $1"
+ echo "Use --help for usage information"
+ exit 1
+ ;;
+ esac
+ shift
+fi
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --help)
+ echo "Windows VM Creation Script"
+ echo "Usage: $0 [VERSION] [OPTIONS]"
+ echo
+ echo "Versions:"
+ echo " 10 Windows 10"
+ echo " 11 Windows 11 (default)"
+ echo " server Windows Server 2022"
+ echo " server2019 Windows Server 2019"
+ echo " server2016 Windows Server 2016"
+ echo
+ echo "Options:"
+ echo " --help Show this help message"
+ echo " --clean Clean dependency cache"
+ echo " --check-deps Only check dependencies and exit"
+ echo " --download-iso Download Windows ISO and exit"
+ echo " --name NAME Set custom VM name"
+ echo " --ram SIZE Set RAM size (default: 8G)"
+ echo " --cpu NUM Set number of CPUs (default: 6)"
+ echo " --disk SIZE Set disk size (default: 80G)"
+ echo " --enable-smb Enable SMB sharing (default: disabled)"
+ echo " --disable-net Disable network connectivity (default: enabled)"
+ exit 0
+ ;;
+ --clean)
+ CLEAN_CACHE=true
+ shift
+ ;;
+ --check-deps)
+ CHECK_DEPS_ONLY=true
+ shift
+ ;;
+ --download-iso)
+ DOWNLOAD_ISO_ONLY=true
+ shift
+ ;;
+ --name)
+ VM_NAME="$2"
+ shift 2
+ ;;
+ --ram)
+ VM_RAM="$2"
+ shift 2
+ ;;
+ --cpu)
+ VM_CPU="$2"
+ shift 2
+ ;;
+ --disk)
+ VM_SIZE="$2"
+ shift 2
+ ;;
+ --enable-smb)
+ ENABLE_SMB=true
+ shift
+ ;;
+ --disable-net)
+ DISABLE_NET=true
+ shift
+ ;;
+ *)
+ echo "Unknown option: $1"
+ echo "Use --help for usage information"
+ exit 1
+ ;;
+ esac
+done
+
+# Set variables
+HOST_DIR="$HOME/virt-new-new"
+VM_DIR="$HOST_DIR/machines"
+IMAGE_DIR="$HOST_DIR/images"
+WIN_ISO_DIR="${IMAGE_DIR}/${VM_NAME}" # Directory for Windows ISO
+SOCKET_DIR="$VM_DIR"
+SHARED_DIR="${HOST_DIR}/shared"
+FIRMWARE_DIR="${HOST_DIR}/firmware"
+TPM_DIR="$WIN_ISO_DIR"
+TPM_SOCKET="${WIN_ISO_DIR}/${VM_NAME}.swtpm-sock"
+GUEST_PORT=22
+QCOW2_FILE="${VM_DIR}/${VM_NAME}.qcow2"
+
+# Try to find an available host port starting from 22220
+HOST_PORT_START=22220
+HOST_PORT_END=22300
+
+for ((port = HOST_PORT_START; port <= HOST_PORT_END; port++)); do
+ if ! ss -tuln | grep -q ":$port\b"; then
+ HOST_PORT=$port
+ echo "Using available port: $HOST_PORT"
+ break
+ fi
+done
+
+if [[ $port -gt $HOST_PORT_END ]]; then
+ echo "Error: No available ports found between $HOST_PORT_START and $HOST_PORT_END" >&2
+ exit 1
+fi
+
+# Set SMP configuration
+CORES=$((VM_CPU / 2))
+THREADS_PER_CORE=2
+SOCKETS=1
+SMP_CONFIG="cores=$CORES,threads=$THREADS_PER_CORE,sockets=$SOCKETS"
+
+# Create necessary directories
+mkdir -p "${HOME}/${HOST_DIR}"
+mkdir -p "$IMAGE_DIR" "$SHARED_DIR" "$FIRMWARE_DIR"
+mkdir -p "$WIN_ISO_DIR" "$VM_DIR"
+mkdir -p "${WIN_ISO_DIR}/unattended"
+
+# Define ISO paths and URLs
+ISO_VIRTIO="${WIN_ISO_DIR}/virtio-win.iso"
+ISO_UNATTENDED="${WIN_ISO_DIR}/unattended.iso"
+
+# Find Windows ISO with flexible pattern matching
+find_windows_iso() {
+ # Check if directory exists
+ if [[ ! -d "$WIN_ISO_DIR" ]]; then
+ mkdir -p "$WIN_ISO_DIR"
+ fi
+
+ # Try to find any Windows ISO using case-insensitive patterns
+ local found_iso
+ found_iso=$(find "$WIN_ISO_DIR" -maxdepth 1 -type f \( \
+ -iname "*win11*.iso" -o \
+ -iname "*win*11*.iso" -o \
+ -iname "Win*.iso" -o \
+ -iname "Win11*.iso" -o \
+ -iname "Win*11*.iso" -o \
+ -iname "*windows*11*.iso" -o \
+ -iname "*windows11*.iso" \
+ \) -exec stat --format="%Y %n" {} \; | sort -n | tail -n 1 | cut -d' ' -f2-)
+
+ if [[ -n "$found_iso" && -f "$found_iso" ]]; then
+ echo "$found_iso"
+ return 0
+ fi
+
+ return 1
+}
+
+# Define download URLs
+VIRTIO_ISO_URL="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso"
+SPICE_WEBDAVD_URL="https://www.spice-space.org/download/windows/spice-webdavd/spice-webdavd-x64-latest.msi"
+SPICE_VDAGENT_URL="https://www.spice-space.org/download/windows/spice-vdagent/spice-vdagent-x64-latest.msi"
+SPICE_VDAGENT_FALLBACK_URL="https://www.spice-space.org/download/windows/spice-vdagent/spice-vdagent-x64-0.10.0.msi"
+USBDK_URL="https://www.spice-space.org/download/windows/usbdk/UsbDk_1.0.22_x64.msi"
+
+# Fido download URL (Windows ISO downloader)
+#FIDO_URL="https://github.com/pbatard/Fido/raw/master/Fido.ps1"
+#FIDO_PATH="$WIN_DIR/Fido.ps1"
+
+# Print colored messages
+print_info() { echo -e "\033[1;34m[INFO]\033[0m $1" >&2; }
+print_success() { echo -e "\033[1;32m[SUCCESS]\033[0m $1" >&2; }
+print_warning() { echo -e "\033[1;33m[WARNING]\033[0m $1" >&2; }
+print_error() { echo -e "\033[1;31m[ERROR]\033[0m $1" >&2; }
+
+# Helper: verify file integrity
+verify_file() {
+ local file="$1"
+ local expected_sha256="$2"
+
+ if [[ ! -f "$file" ]]; then
+ return 1
+ fi
+
+ if [[ -n "$expected_sha256" ]]; then
+ local actual_sha256
+ actual_sha256=$(sha256sum "$file" | cut -d' ' -f1)
+ if [[ "$actual_sha256" != "$expected_sha256" ]]; then
+ print_error "File integrity check failed for $file"
+ return 1
+ fi
+ fi
+
+ return 0
+}
+
+# Helper: download file with verification
+download_file() {
+ local url="$1"
+ local dest="$2"
+ local expected_sha256="$3"
+ local allow_failure="$4"
+
+ # Check if file exists and is valid
+ if [[ -f "$dest" ]]; then
+ if verify_file "$dest" "$expected_sha256"; then
+ print_info "File $dest already exists and verified."
+ return 0
+ else
+ print_warning "File $dest exists but failed verification. Redownloading..."
+ rm -f "$dest"
+ fi
+ fi
+
+ print_info "Downloading $url..."
+ if ! curl -fL --progress-bar -o "$dest" "$url"; then
+ print_error "Failed to download $url."
+ if [[ "$allow_failure" != "true" ]]; then
+ return 1
+ fi
+ else
+ # Verify downloaded file
+ if ! verify_file "$dest" "$expected_sha256"; then
+ print_error "Downloaded file failed verification"
+ rm -f "$dest"
+ return 1
+ fi
+ print_success "Successfully downloaded and verified $dest"
+ fi
+
+ return 0
+}
+
+# Download Windows 11 ISO using Microsoft's API
+download_windows_iso() {
+ local windows_version="11" # Default to Windows 11
+ local language="English (United States)" # Default language
+
+ # Parse arguments if provided
+ if [[ -n "$1" ]]; then
+ windows_version="$1"
+ fi
+
+ print_info "Attempting to download Windows $windows_version ISO from Microsoft..."
+
+ # Set required variables
+ local user_agent="Mozilla/5.0 (X11; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0"
+ local session_id="$(uuidgen)"
+ local profile="606624d44113"
+ local url="https://www.microsoft.com/en-us/software-download/windows$windows_version"
+
+ # Add ISO to URL for Windows 10
+ case "$windows_version" in
+ 10) url="${url}ISO" ;;
+ esac
+
+ # Step 1: Get download page HTML
+ print_info "Fetching download page: $url"
+ local iso_download_page_html
+ iso_download_page_html="$(curl --disable --silent --user-agent "$user_agent" --header "Accept:" --max-filesize 1M --fail --proto =https --tlsv1.2 --http1.1 -- "$url")" || {
+ handle_curl_error $?
+ print_error "Failed to fetch the download page. Please download Windows $windows_version ISO manually from $url"
+ return 1
+ }
+
+ # Step 2: Extract Product Edition ID
+ print_info "Getting Product Edition ID..."
+ local product_edition_id
+ product_edition_id="$(echo "$iso_download_page_html" | grep -Eo '<option value="[0-9]+">Windows' | cut -d '"' -f 2 | head -n 1 | tr -cd '0-9' | head -c 16)"
+
+ if [[ -z "$product_edition_id" ]]; then
+ print_error "Failed to extract product edition ID."
+ print_error "Please download Windows $windows_version ISO manually from $url"
+ return 1
+ fi
+
+ print_success "Product Edition ID: $product_edition_id"
+
+ # Step 3: Register session ID
+ print_info "Registering session ID: $session_id"
+ curl --disable --silent --output /dev/null --user-agent "$user_agent" \
+ --header "Accept:" --max-filesize 100K --fail --proto =https --tlsv1.2 \
+ --http1.1 -- "https://vlscppe.microsoft.com/tags?org_id=y6jn8c31&session_id=$session_id" || {
+ print_error "Failed to register session ID."
+ return 1
+ }
+
+ # Step 4: Get language SKU ID
+ print_info "Getting language SKU ID..."
+ local language_skuid_table_json
+ language_skuid_table_json="$(curl --disable -s --fail --max-filesize 100K --proto =https --tlsv1.2 --http1.1 \
+ "https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?profile=${profile}&ProductEditionId=${product_edition_id}&SKU=undefined&friendlyFileName=undefined&Locale=en-US&sessionID=${session_id}")" || {
+ handle_curl_error $?
+ print_error "Failed to get language SKU information."
+ return 1
+ }
+
+ # Extract SKU ID for selected language
+ local sku_id
+
+ # Try with jq if available (more reliable)
+ if command -v jq >/dev/null 2>&1; then
+ sku_id="$(echo "$language_skuid_table_json" | jq -r '.Skus[] | select(.LocalizedLanguage=="'"$language"'" or .Language=="'"$language"'").Id')"
+ else
+ # Fallback to grep/cut if jq not available
+ sku_id="$(echo "$language_skuid_table_json" | grep -o '"Id":"[^"]*","Language":"'"$language"'"' | cut -d'"' -f4)"
+
+ if [[ -z "$sku_id" ]]; then
+ # Try alternative extraction method
+ sku_id="$(echo "$language_skuid_table_json" | grep -o '"LocalizedLanguage":"'"$language"'","Id":"[^"]*"' | cut -d'"' -f6)"
+ fi
+ fi
+
+ if [[ -z "$sku_id" ]]; then
+ print_error "Failed to extract SKU ID for $language."
+ return 1
+ fi
+
+ print_success "SKU ID: $sku_id"
+
+ # Step 5: Get ISO download link
+ print_info "Getting ISO download link..."
+ local iso_download_link_json
+ iso_download_link_json="$(curl --disable -s --fail --referer "$url" \
+ "https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?profile=${profile}&productEditionId=undefined&SKU=${sku_id}&friendlyFileName=undefined&Locale=en-US&sessionID=${session_id}")"
+
+ local failed=0
+
+ if [[ -z "$iso_download_link_json" ]]; then
+ print_error "Microsoft servers gave an empty response to the download request."
+ failed=1
+ fi
+
+ if echo "$iso_download_link_json" | grep -q "Sentinel marked this request as rejected."; then
+ print_error "Microsoft blocked the automated download request based on your IP address."
+ failed=1
+ fi
+
+ if [[ "$failed" -eq 1 ]]; then
+ print_warning "Please manually download the Windows $windows_version ISO using a web browser from: $url"
+ print_warning "Save the downloaded ISO to: $WIN_ISO_DIR"
+ return 1
+ fi
+
+ # Extract 64-bit ISO download URL
+ local iso_download_link
+
+ # Try with jq if available
+ if command -v jq >/dev/null 2>&1; then
+ iso_download_link="$(echo "$iso_download_link_json" | jq -r '.ProductDownloadOptions[].Uri' | grep x64 | head -n 1)"
+ else
+ # Fallback to grep/cut if jq not available
+ iso_download_link="$(echo "$iso_download_link_json" | grep -o '"Uri":"[^"]*x64[^"]*"' | cut -d'"' -f4 | head -n 1)"
+ fi
+
+ if [[ -z "$iso_download_link" ]]; then
+ print_error "Failed to extract the download link from Microsoft's response."
+ print_warning "Manually download the Windows $windows_version ISO using a web browser from: $url"
+ return 1
+ fi
+
+ print_success "Got download link: ${iso_download_link%%\?*}"
+
+ # Extract filename from URL
+ local file_name="$(echo "$iso_download_link" | cut -d'?' -f1 | rev | cut -d'/' -f1 | rev)"
+
+ # If filename couldn't be extracted, use default
+ if [[ -z "$file_name" || "$file_name" == "$iso_download_link" ]]; then
+ file_name="windows-$windows_version.iso"
+ fi
+
+ # Step 6: Download the ISO
+ print_info "Downloading Windows $windows_version ISO to $WIN_ISO_DIR/$file_name. This may take a while..."
+
+ # Check which download function to use
+ if type web_get >/dev/null 2>&1; then
+ web_get "$iso_download_link" "$WIN_ISO_DIR" "$file_name"
+ else
+ # Fallback to direct curl download
+ curl --disable --progress-bar --fail --location --proto '=https' --tlsv1.2 --http1.1 \
+ --retry 3 --retry-delay 3 --connect-timeout 30 \
+ --output "$WIN_ISO_DIR/$file_name" "$iso_download_link" || {
+ handle_curl_error $?
+ return 1
+ }
+ fi
+
+ if [[ $? -ne 0 ]]; then
+ print_error "Failed to download the Windows $windows_version ISO."
+ return 1
+ fi
+
+ print_success "Successfully downloaded Windows $windows_version ISO to $WIN_ISO_DIR/$file_name"
+
+ # Return the downloaded filename so calling code can use it
+ echo "$file_name"
+ return 0
+}
+
+# Create unattended installation ISO
+create_unattended_iso() {
+ print_info "Creating unattended installation ISO..."
+
+ # Create basic autounattend.xml if it doesn't exist
+ if [ ! -f "$WIN_ISO_DIR/unattended/autounattend.xml" ]; then
+ print_info "Creating autounattend.xml..."
+ cat >"$WIN_ISO_DIR/unattended/autounattend.xml" <<'EOF'
+<?xml version="1.0" encoding="utf-8"?>
+<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
+ <settings pass="windowsPE">
+ <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
+ <SetupUILanguage>
+ <UILanguage>en-US</UILanguage>
+ </SetupUILanguage>
+ <InputLocale>0409:00000409</InputLocale>
+ <SystemLocale>en-US</SystemLocale>
+ <UILanguage>en-US</UILanguage>
+ <UserLocale>en-US</UserLocale>
+ </component>
+ <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
+ <ImageInstall>
+ <OSImage>
+ <Compact>false</Compact>
+ <InstallTo>
+ <DiskID>0</DiskID>
+ <PartitionID>3</PartitionID>
+ </InstallTo>
+ </OSImage>
+ </ImageInstall>
+ <UserData>
+ <AcceptEula>true</AcceptEula>
+ </UserData>
+ <UseConfigurationSet>false</UseConfigurationSet>
+ <RunSynchronous>
+ <RunSynchronousCommand wcm:action="add">
+ <Order>1</Order>
+ <Path>cmd.exe /c "&gt;&gt;"X:\diskpart.txt" (echo SELECT DISK=0&amp;echo CLEAN&amp;echo CONVERT GPT&amp;echo CREATE PARTITION EFI SIZE=300&amp;echo FORMAT QUICK FS=FAT32 LABEL="System"&amp;echo CREATE PARTITION MSR SIZE=16)"</Path>
+ </RunSynchronousCommand>
+ <RunSynchronousCommand wcm:action="add">
+ <Order>2</Order>
+ <Path>cmd.exe /c "&gt;&gt;"X:\diskpart.txt" (echo CREATE PARTITION PRIMARY&amp;echo SHRINK MINIMUM=1000&amp;echo FORMAT QUICK FS=NTFS LABEL="Windows"&amp;echo CREATE PARTITION PRIMARY&amp;echo FORMAT QUICK FS=NTFS LABEL="Recovery")"</Path>
+ </RunSynchronousCommand>
+ <RunSynchronousCommand wcm:action="add">
+ <Order>3</Order>
+ <Path>cmd.exe /c "&gt;&gt;"X:\diskpart.txt" (echo SET ID="de94bba4-06d1-4d40-a16a-bfd50179d6ac"&amp;echo GPT ATTRIBUTES=0x8000000000000001)"</Path>
+ </RunSynchronousCommand>
+ <RunSynchronousCommand wcm:action="add">
+ <Order>4</Order>
+ <Path>cmd.exe /c "diskpart.exe /s "X:\diskpart.txt" &gt;&gt;"X:\diskpart.log" || ( type "X:\diskpart.log" &amp; echo diskpart encountered an error. &amp; pause &amp; exit /b 1 )"</Path>
+ </RunSynchronousCommand>
+ </RunSynchronous>
+ </component>
+ </settings>
+ <settings pass="specialize">
+ <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
+ <ComputerName>Win11-VM</ComputerName>
+ <TimeZone>UTC</TimeZone>
+ </component>
+ </settings>
+ <settings pass="oobeSystem">
+ <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
+ <InputLocale>0409:00000409</InputLocale>
+ <SystemLocale>en-US</SystemLocale>
+ <UILanguage>en-US</UILanguage>
+ <UserLocale>en-US</UserLocale>
+ </component>
+ <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
+ <UserAccounts>
+ <LocalAccounts>
+ <LocalAccount wcm:action="add">
+ <Name>Admin</Name>
+ <DisplayName></DisplayName>
+ <Group>Administrators</Group>
+ <Password>
+ <Value>password</Value>
+ <PlainText>true</PlainText>
+ </Password>
+ </LocalAccount>
+ <LocalAccount wcm:action="add">
+ <Name>User</Name>
+ <DisplayName></DisplayName>
+ <Group>Users</Group>
+ <Password>
+ <Value>password</Value>
+ <PlainText>true</PlainText>
+ </Password>
+ </LocalAccount>
+ </LocalAccounts>
+ </UserAccounts>
+ <AutoLogon>
+ <Username>Admin</Username>
+ <Enabled>true</Enabled>
+ <LogonCount>1</LogonCount>
+ <Password>
+ <Value>password</Value>
+ <PlainText>true</PlainText>
+ </Password>
+ </AutoLogon>
+ <OOBE>
+ <ProtectYourPC>3</ProtectYourPC>
+ <HideEULAPage>true</HideEULAPage>
+ <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
+ <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
+ <HideLocalAccountScreen>true</HideLocalAccountScreen>
+ <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
+ <SkipMachineOOBE>true</SkipMachineOOBE>
+ <SkipUserOOBE>true</SkipUserOOBE>
+ </OOBE>
+ </component>
+ </settings>
+</unattend>
+EOF
+ fi
+
+ # Create the unattended ISO
+ local iso_tool=""
+
+ if command -v genisoimage >/dev/null 2>&1; then
+ iso_tool="genisoimage"
+ elif command -v mkisofs >/dev/null 2>&1; then
+ iso_tool="mkisofs"
+ elif command -v xorriso >/dev/null 2>&1; then
+ iso_tool="xorriso"
+ fi
+
+ if [ "$iso_tool" != "" ]; then
+ print_info "Creating unattended ISO using $iso_tool..."
+
+ if [ "$iso_tool" = "xorriso" ]; then
+ xorriso -as genisoimage -J -r -o "$WIN_ISO_DIR/unattended.iso" "$WIN_ISO_DIR/unattended"
+ else
+ "$iso_tool" -J -r -o "$WIN_ISO_DIR/unattended.iso" "$WIN_ISO_DIR/unattended"
+ fi
+
+ return $?
+ else
+ print_warning "No ISO creation tool found (genisoimage, mkisofs, or xorriso)"
+ print_warning "Creating empty ISO file as fallback"
+ touch "$WIN_ISO_DIR/unattended.iso"
+ return 1
+ fi
+}
+
+# Download or locate essential files
+prepare_files() {
+ # Find Windows ISO
+ WINDOWS_ISO_PATH=$(find_windows_iso | tail -n 1 | xargs)
+ local iso_found=false
+
+ # Check if we found a valid ISO
+ if [[ -n "$WINDOWS_ISO_PATH" && -f "$WINDOWS_ISO_PATH" ]]; then
+ print_success "Using Windows ISO: $WINDOWS_ISO_PATH"
+ iso_found=true
+ else
+ print_warning "Windows ISO not found, attempting to download..."
+ if download_windows_iso; then
+ # Check again after download attempt
+ WINDOWS_ISO_PATH=$(find_windows_iso | tail -n 1 | xargs)
+ if [[ -n "$WINDOWS_ISO_PATH" && -f "$WINDOWS_ISO_PATH" ]]; then
+ print_success "Using downloaded Windows ISO: $WINDOWS_ISO_PATH"
+ iso_found=true
+ fi
+ fi
+ fi
+
+ # If we still don't have an ISO, exit
+ if [[ "$iso_found" != "true" ]]; then
+ print_error "Could not find or download Windows ISO"
+ print_info "Please place your Windows ISO file in: $WIN_ISO_DIR"
+ exit 1
+ fi
+
+ # Download VirtIO drivers if missing
+ if [[ ! -f "$ISO_VIRTIO" ]]; then
+ print_info "VirtIO drivers ISO not found, downloading..."
+ download_file "$VIRTIO_ISO_URL" "$ISO_VIRTIO"
+ else
+ print_success "VirtIO drivers ISO already exists: $ISO_VIRTIO"
+ fi
+
+ # Define the directory containing the MSI files
+ UNATTENDED_DIR="$WIN_ISO_DIR/unattended"
+ mkdir -p "$UNATTENDED_DIR"
+
+ # Find the latest Spice WebDAVD MSI
+ SPICE_WEBDAVD_MSI=$(find "$UNATTENDED_DIR" -type f -iname "spice-webdavd-x64*.msi" -exec stat --format="%Y %n" {} \; | sort -n | tail -n 1 | cut -d' ' -f2-)
+
+ # Find the latest Spice VD Agent MSI
+ SPICE_VDAGENT_MSI=$(find "$UNATTENDED_DIR" -type f -iname "spice-vdagent-x64*.msi" -exec stat --format="%Y %n" {} \; | sort -n | tail -n 1 | cut -d' ' -f2-)
+
+ # Find the latest UsbDk MSI
+ USBDK_MSI=$(find "$UNATTENDED_DIR" -type f -iname "UsbDk_*_x64.msi" -exec stat --format="%Y %n" {} \; | sort -n | tail -n 1 | cut -d' ' -f2-)
+
+ # Spice Guest Tools + UsbDk (only if missing)
+ if [[ ! -f "$SPICE_WEBDAVD_MSI" ]]; then
+ print_info "Downloading Spice WebDAVD..."
+ curl -L -o "$UNATTENDED_DIR/spice-webdavd-x64-latest.msi" "$SPICE_WEBDAVD_URL" || {
+ print_error "Failed to download Spice WebDAVD"
+ return 1
+ }
+ else
+ print_success "Spice WebDAVD MSI already exists: $SPICE_WEBDAVD_MSI"
+ fi
+
+ if [[ ! -f "$SPICE_VDAGENT_MSI" ]]; then
+ print_info "Downloading Spice VD Agent..."
+ # Try multiple mirrors for spice-vdagent
+ local spice_mirrors=(
+ "https://www.spice-space.org/download/windows/spice-vdagent/spice-vdagent-x64-latest.msi"
+ "https://www.spice-space.org/download/windows/spice-vdagent/spice-vdagent-x64-0.10.0.msi"
+ "https://www.spice-space.org/download/windows/spice-vdagent/spice-vdagent-x64-0.9.0.msi"
+ "https://www.spice-space.org/download/windows/spice-vdagent/spice-vdagent-x64-0.8.0.msi"
+ "https://www.spice-space.org/download/windows/spice-vdagent/spice-vdagent-x64-0.7.0.msi"
+ )
+
+ local download_success=false
+ for mirror in "${spice_mirrors[@]}"; do
+ print_info "Trying mirror: $mirror"
+ if curl -L -o "$UNATTENDED_DIR/spice-vdagent-x64-latest.msi" "$mirror"; then
+ # Verify the downloaded file
+ if [[ -s "$UNATTENDED_DIR/spice-vdagent-x64-latest.msi" ]]; then
+ download_success=true
+ break
+ else
+ print_warning "Downloaded file is empty, trying next mirror..."
+ rm -f "$UNATTENDED_DIR/spice-vdagent-x64-latest.msi"
+ fi
+ fi
+ done
+
+ if [[ "$download_success" == "false" ]]; then
+ print_error "Failed to download Spice VD Agent from all mirrors"
+ return 1
+ fi
+ else
+ print_success "Spice VD Agent MSI already exists: $SPICE_VDAGENT_MSI"
+ fi
+
+ if [[ ! -f "$USBDK_MSI" ]]; then
+ print_info "Downloading UsbDk..."
+ curl -L -o "$UNATTENDED_DIR/UsbDk_1.0.22_x64.msi" "$USBDK_URL" || {
+ print_error "Failed to download UsbDk"
+ return 1
+ }
+ else
+ print_success "UsbDk MSI already exists: $USBDK_MSI"
+ fi
+
+ # Create unattended ISO if it doesn't exist
+ if [[ ! -f "$ISO_UNATTENDED" ]]; then
+ create_unattended_iso
+ else
+ print_success "Unattended ISO already exists: $ISO_UNATTENDED"
+ fi
+}
+
+# Locate OVMF firmware files
+locate_ovmf() {
+ OVMF_DIRS=(
+ "/usr/share/OVMF"
+ "/usr/share/qemu"
+ "/usr/lib/qemu"
+ "/usr/share/edk2"
+ "/usr/lib/edk2"
+ "/usr/share/edk2/ovmf"
+ "/usr/share/edk2-ovmf"
+ )
+
+ OVMF_CODE=""
+ OVMF_VARS=""
+
+ for dir in "${OVMF_DIRS[@]}"; do
+ [[ -z "$OVMF_CODE" ]] && OVMF_CODE=$(find "$dir" -type f -name "OVMF_CODE.fd" -o -name "edk2-x86_64-code.fd" 2>/dev/null | head -n 1)
+ [[ -z "$OVMF_VARS" ]] && OVMF_VARS=$(find "$dir" -type f -name "OVMF_VARS.fd" 2>/dev/null | head -n 1)
+ [[ -n "$OVMF_CODE" && -n "$OVMF_VARS" ]] && break
+ done
+
+ # Ensure a writable copy of OVMF_VARS.fd
+ local original_ovmf_vars="$OVMF_VARS"
+ OVMF_VARS="$FIRMWARE_DIR/OVMF_VARS.fd"
+
+ if [[ ! -f "$OVMF_VARS" && -f "$original_ovmf_vars" ]]; then
+ print_info "Copying OVMF_VARS.fd to $OVMF_VARS"
+ cp "$original_ovmf_vars" "$OVMF_VARS" 2>/dev/null || {
+ print_error "Failed to copy OVMF_VARS.fd!"
+ exit 1
+ }
+ fi
+
+ # Check if required files exist
+ if [[ -z "$OVMF_CODE" || ! -f "$OVMF_CODE" ]]; then
+ print_error "OVMF_CODE.fd not found!"
+ exit 1
+ fi
+ if [[ ! -f "$OVMF_VARS" ]]; then
+ print_error "OVMF_VARS.fd not found or could not be copied!"
+ exit 1
+ fi
+ #}
+}
+
+# Create VM disk image
+create_disk() {
+ # Check if the qcow2 image file exists; if not, create it
+ if [[ ! -f "$QCOW2_FILE" ]]; then
+ print_info "Creating $QCOW2_FILE with a size of $VM_SIZE"
+ qemu-img create -f qcow2 "$QCOW2_FILE" "$VM_SIZE" || {
+ print_error "Failed to create qcow2 image!"
+ exit 1
+ }
+ else
+ print_success "VM disk image already exists: $QCOW2_FILE"
+ fi
+}
+
+# Generate unique PID file for this instance
+generate_pid_file() {
+ local base_pid_file="$VM_DIR/$VM_NAME.pid"
+ local pid_file="$base_pid_file"
+ local counter=1
+
+ # If the base PID file exists, try to find an available number
+ while [[ -f "$pid_file" ]]; do
+ pid_file="${base_pid_file%.pid}-${counter}.pid"
+ ((counter++))
+ done
+
+ echo "$pid_file"
+}
+
+# Start the VM
+start_vm() {
+ # Verify that we have a Windows ISO
+ if [[ -z "$WINDOWS_ISO_PATH" || ! -f "$WINDOWS_ISO_PATH" ]]; then
+ print_error "Windows ISO file not found. Cannot start VM."
+ print_info "Please download Windows 11 ISO and save it to: $WIN_ISO_DIR"
+ exit 1
+ fi
+
+ # Start swtpm
+ print_info "Starting TPM emulator..."
+ /sbin/swtpm socket \
+ --ctrl type=unixio,path="$TPM_SOCKET" \
+ --terminate \
+ --tpmstate dir="$TPM_DIR" \
+ --tpm2 &
+
+ # Wait for swtpm socket
+ sleep 1
+
+ print_info "Starting Windows 11 VM..."
+ print_info "Using Windows ISO: $WINDOWS_ISO_PATH"
+
+ # Build network options
+ local network_opts=()
+ if [[ "$DISABLE_NET" == "true" ]]; then
+ network_opts=(
+ "-netdev" "none,id=nic"
+ "-device" "virtio-net,netdev=nic"
+ )
+ else
+ if [[ "$ENABLE_SMB" == "true" ]]; then
+ network_opts=(
+ "-netdev" "user,id=nic,hostname=$VM_NAME,hostfwd=tcp::$HOST_PORT-:$GUEST_PORT,smb=$SHARED_DIR"
+ "-device" "virtio-net,netdev=nic"
+ )
+ else
+ network_opts=(
+ "-netdev" "user,id=nic,hostname=$VM_NAME,hostfwd=tcp::$HOST_PORT-:$GUEST_PORT"
+ "-device" "virtio-net,netdev=nic"
+ )
+ fi
+ fi
+
+ # Run QEMU
+ /sbin/qemu-system-x86_64 \
+ -name "$VM_NAME",process="$VM_NAME" \
+ -machine q35,hpet=off,smm=on,vmport=off,accel=kvm \
+ -global kvm-pit.lost_tick_policy=discard \
+ -global ICH9-LPC.disable_s3=1 \
+ -cpu host,+hypervisor,+invtsc,l3-cache=on,migratable=no,hv_passthrough \
+ -smp "$SMP_CONFIG" \
+ -m "$VM_RAM" \
+ -device virtio-balloon \
+ -pidfile "$PID_FILE" \
+ -rtc base=localtime,clock=host,driftfix=slew \
+ -vga none \
+ -device virtio-vga-gl,xres=1280,yres=800 \
+ -display sdl,gl=on \
+ -boot menu=on,splash-time=0,order=d,reboot-timeout=5000 \
+ -device virtio-rng-pci,rng=rng0 \
+ -object rng-random,id=rng0,filename=/dev/urandom \
+ -device qemu-xhci,id=spicepass \
+ -chardev spicevmc,id=usbredirchardev1,name=usbredir \
+ -device usb-redir,chardev=usbredirchardev1,id=usbredirdev1 \
+ -chardev spicevmc,id=usbredirchardev2,name=usbredir \
+ -device usb-redir,chardev=usbredirchardev2,id=usbredirdev2 \
+ -chardev spicevmc,id=usbredirchardev3,name=usbredir \
+ -device usb-redir,chardev=usbredirchardev3,id=usbredirdev3 \
+ -device pci-ohci,id=smartpass \
+ -device usb-ccid \
+ -chardev spicevmc,id=ccid,name=smartcard \
+ -device ccid-card-passthru,chardev=ccid \
+ -device usb-ehci,id=input \
+ -device usb-kbd,bus=input.0 \
+ -k en-us \
+ -device usb-tablet,bus=input.0 \
+ -audiodev pipewire,id=audio0 \
+ -device intel-hda \
+ -device hda-micro,audiodev=audio0 \
+ "${network_opts[@]}" \
+ -global driver=cfi.pflash01,property=secure,value=on \
+ -drive if=pflash,format=raw,unit=0,file="$OVMF_CODE",readonly=on \
+ -drive if=pflash,format=raw,unit=1,file="$OVMF_VARS" \
+ -drive media=cdrom,index=1,file="$ISO_UNATTENDED" \
+ -drive media=cdrom,index=0,file="$WINDOWS_ISO_PATH" \
+ -drive media=cdrom,index=2,file="$ISO_VIRTIO" \
+ -device virtio-blk-pci,drive=SystemDisk \
+ -drive id=SystemDisk,if=none,format=qcow2,file="$QCOW2_FILE" \
+ -chardev socket,id=chrtpm,path="$TPM_SOCKET" \
+ -tpmdev emulator,id=tpm0,chardev=chrtpm \
+ -device tpm-tis,tpmdev=tpm0 \
+ -monitor unix:"$SOCKET_DIR/$VM_NAME-monitor.socket",server,nowait \
+ -serial unix:"$SOCKET_DIR/$VM_NAME-serial.socket",server,nowait
+}
+
+# Check dependencies
+check_dependencies() {
+ # Cache file for dependency checks and VM configuration
+ local cache_name="windows-vm-${VM_NAME}-${VM_RAM}-${VM_CPU}-${VM_SIZE}"
+ local cache_file="${HOME}/.cache/${cache_name}.cache"
+ local cache_dir=$(dirname "$cache_file")
+ local cache_valid=false
+ local missing_deps=()
+ local pkg_manager=""
+ local pkg_install_cmd=""
+ local privilege_cmd=""
+
+ # Clean cache if requested
+ if [[ "$CLEAN_CACHE" == "true" ]]; then
+ print_info "Cleaning dependency cache..."
+ rm -f "$cache_file"
+ fi
+
+ # Create cache directory if it doesn't exist
+ mkdir -p "$cache_dir"
+
+ # Check if cache is valid (less than 24 hours old)
+ if [[ -f "$cache_file" ]]; then
+ local cache_age=$(($(date +%s) - $(stat -c %Y "$cache_file")))
+ if [[ $cache_age -lt 86400 ]]; then # 24 hours in seconds
+ # Read cached configuration
+ local cached_config
+ cached_config=$(head -n 1 "$cache_file" 2>/dev/null)
+ # Compare with current configuration
+ if [[ "$cached_config" == "${VM_NAME}-${VM_RAM}-${VM_CPU}-${VM_SIZE}" ]]; then
+ cache_valid=true
+ else
+ print_info "VM configuration changed, invalidating cache..."
+ rm -f "$cache_file"
+ fi
+ fi
+ fi
+
+ # Check KVM group membership
+ if ! groups | grep -q -E 'kvm|qemu'; then
+ print_warning "User is not a member of kvm or qemu group"
+ print_info "You may need to add your user to the kvm group:"
+ print_info "sudo usermod -aG kvm $USER"
+ fi
+
+ # Detect package manager and privilege command
+ if command -v emerge >/dev/null 2>&1; then
+ pkg_manager="emerge"
+ pkg_install_cmd="emerge --ask --noreplace"
+ privilege_cmd="sudo"
+ # Gentoo package mapping
+ declare -A pkg_map=(
+ ["qemu-system-x86_64"]="app-emulation/qemu"
+ ["qemu-img"]="app-emulation/qemu"
+ ["swtpm"]="app-crypt/swtpm"
+ ["genisoimage"]="app-cdr/cdrtools"
+ ["curl"]="net-misc/curl"
+ ["uuidgen"]="sys-apps/util-linux"
+ ["jq"]="app-misc/jq"
+ ["glxinfo"]="x11-apps/mesa-progs"
+ ["lspci"]="sys-apps/pciutils"
+ ["ps"]="sys-process/procps"
+ ["python3"]="dev-lang/python"
+ ["mkisofs"]="app-cdr/cdrtools"
+ ["lsusb"]="sys-apps/usbutils"
+ ["socat"]="net-misc/socat"
+ ["spicy"]="app-emulation/spice"
+ ["xrandr"]="x11-apps/xrandr"
+ ["zsync"]="net-misc/zsync"
+ ["unzip"]="app-arch/unzip"
+ )
+ elif command -v apt-get >/dev/null 2>&1; then
+ pkg_manager="apt-get"
+ pkg_install_cmd="apt-get install -y"
+ privilege_cmd="sudo"
+ # Debian/Ubuntu package mapping
+ declare -A pkg_map=(
+ ["qemu-system-x86_64"]="qemu-system-x86"
+ ["qemu-img"]="qemu-utils"
+ ["swtpm"]="swtpm-tools"
+ ["genisoimage"]="genisoimage"
+ ["curl"]="curl"
+ ["uuidgen"]="uuid-runtime"
+ ["jq"]="jq"
+ ["glxinfo"]="mesa-utils"
+ ["lspci"]="pciutils"
+ ["ps"]="procps"
+ ["python3"]="python3"
+ ["mkisofs"]="genisoimage"
+ ["lsusb"]="usbutils"
+ ["socat"]="socat"
+ ["spicy"]="spice-client-gtk"
+ ["xrandr"]="x11-xserver-utils"
+ ["zsync"]="zsync"
+ ["unzip"]="unzip"
+ )
+ elif command -v dnf >/dev/null 2>&1; then
+ pkg_manager="dnf"
+ pkg_install_cmd="dnf install -y"
+ privilege_cmd="sudo"
+ # Fedora package mapping
+ declare -A pkg_map=(
+ ["qemu-system-x86_64"]="qemu-system-x86"
+ ["qemu-img"]="qemu-img"
+ ["swtpm"]="swtpm"
+ ["genisoimage"]="genisoimage"
+ ["curl"]="curl"
+ ["uuidgen"]="util-linux"
+ ["jq"]="jq"
+ ["glxinfo"]="mesa-demos"
+ ["lspci"]="pciutils"
+ ["ps"]="procps-ng"
+ ["python3"]="python3"
+ ["mkisofs"]="genisoimage"
+ ["lsusb"]="usbutils"
+ ["socat"]="socat"
+ ["spicy"]="spice-gtk-tools"
+ ["xrandr"]="xorg-x11-server-utils"
+ ["zsync"]="zsync"
+ ["unzip"]="unzip"
+ )
+ elif command -v pacman >/dev/null 2>&1; then
+ pkg_manager="pacman"
+ pkg_install_cmd="pacman -S --noconfirm"
+ privilege_cmd="sudo"
+ # Arch package mapping
+ declare -A pkg_map=(
+ ["qemu-system-x86_64"]="qemu"
+ ["qemu-img"]="qemu"
+ ["swtpm"]="swtpm"
+ ["genisoimage"]="cdrtools"
+ ["curl"]="curl"
+ ["uuidgen"]="util-linux"
+ ["jq"]="jq"
+ ["glxinfo"]="mesa-utils"
+ ["lspci"]="pciutils"
+ ["ps"]="procps-ng"
+ ["python3"]="python"
+ ["mkisofs"]="cdrtools"
+ ["lsusb"]="usbutils"
+ ["socat"]="socat"
+ ["spicy"]="spice-gtk"
+ ["xrandr"]="xorg-xrandr"
+ ["zsync"]="zsync"
+ ["unzip"]="unzip"
+ )
+ fi
+
+ # List of required commands and their package names
+ local deps=(
+ "qemu-system-x86_64"
+ "qemu-img"
+ "swtpm"
+ "genisoimage"
+ "curl"
+ "uuidgen"
+ "jq"
+ "glxinfo"
+ "lspci"
+ "ps"
+ "python3"
+ "mkisofs"
+ "lsusb"
+ "socat"
+ "spicy"
+ "xrandr"
+ "zsync"
+ "unzip"
+ )
+
+ # If cache is valid, read from it
+ if [[ "$cache_valid" == "true" ]]; then
+ print_info "Using cached dependency information..."
+ # Skip the first line (configuration) and read dependencies
+ tail -n +2 "$cache_file" | while IFS= read -r dep; do
+ if ! command -v "$dep" >/dev/null 2>&1; then
+ missing_deps+=("$dep")
+ fi
+ done
+ else
+ # Check each dependency and cache the results
+ print_info "Checking dependencies..."
+
+ # Clear cache file and write current configuration
+ echo "${VM_NAME}-${VM_RAM}-${VM_CPU}-${VM_SIZE}" >"$cache_file"
+
+ for dep in "${deps[@]}"; do
+ # Special case for genisoimage/mkisofs
+ if [[ "$dep" == "genisoimage" ]]; then
+ if ! command -v genisoimage >/dev/null 2>&1 && ! command -v mkisofs >/dev/null 2>&1; then
+ missing_deps+=("$dep")
+ echo "$dep" >>"$cache_file"
+ fi
+ # Special case for xdg-user-dirs
+ elif [[ "$dep" == "xdg-user-dirs" ]]; then
+ if ! pkg-config --exists xdg-user-dirs; then
+ missing_deps+=("$dep")
+ echo "$dep" >>"$cache_file"
+ fi
+ # Normal case for other dependencies
+ elif ! command -v "$dep" >/dev/null 2>&1; then
+ missing_deps+=("$dep")
+ echo "$dep" >>"$cache_file"
+ fi
+ done
+ fi
+
+ if [[ ${#missing_deps[@]} -gt 0 ]]; then
+ print_warning "Missing dependencies:"
+ for dep in "${missing_deps[@]}"; do
+ if [[ -n "${pkg_map[$dep]}" ]]; then
+ print_warning "- $dep (package: ${pkg_map[$dep]})"
+ else
+ print_warning "- $dep"
+ fi
+ done
+
+ if [[ -n "$pkg_manager" ]]; then
+ print_info "Detected package manager: $pkg_manager"
+ read -p "Would you like to install the missing dependencies? (y/N) " -n 1 -r
+ echo
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ # Check for privilege command
+ if ! command -v "$privilege_cmd" >/dev/null 2>&1; then
+ print_error "Privilege command ($privilege_cmd) not found"
+ print_info "Please install the missing packages manually"
+ else
+ # Install missing packages
+ print_info "Installing missing dependencies..."
+ local pkgs_to_install=()
+ for dep in "${missing_deps[@]}"; do
+ if [[ -n "${pkg_map[$dep]}" ]]; then
+ pkgs_to_install+=("${pkg_map[$dep]}")
+ fi
+ done
+ "$privilege_cmd" "$pkg_install_cmd" "${pkgs_to_install[@]}"
+
+ # Clear cache after installation
+ rm -f "$cache_file"
+ fi
+ else
+ print_warning "Continuing without installing dependencies. Some features may not work correctly."
+ fi
+ else
+ print_warning "No supported package manager found"
+ print_info "Please install the missing packages manually"
+ fi
+ else
+ print_success "All required dependencies are installed"
+ fi
+}
+
+# Main execution
+check_dependencies
+
+if [[ "$CHECK_DEPS_ONLY" == "true" ]]; then
+ exit 0
+fi
+
+if [[ "$DOWNLOAD_ISO_ONLY" == "true" ]]; then
+ download_windows_iso "$WINDOWS_VERSION"
+ exit 0
+fi
+
+# Generate unique PID file for this instance
+PID_FILE=$(generate_pid_file)
+print_info "Using PID file: $PID_FILE"
+
+prepare_files
+locate_ovmf
+create_disk
+start_vm