From 6fe8529d1029933114c82f3aebcaf5676cfae078 Mon Sep 17 00:00:00 2001 From: Marek Paulik Date: Thu, 4 Jun 2026 20:52:54 +0200 Subject: [PATCH] Add server CLI tools installer for Ubuntu/Debian servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a self-contained repo with an interactive install.sh that provisions zsh, oh-my-zsh, aliases, bat/yazi configs, docker shortcuts, and git-delta pager setup. Configs are adapted from dotfiles for SSH-only servers — no desktop or neovim dependencies. Installer supports --all, --update (git pull + re-run), and --uninstall with backup restore. Core packages include vim (default editor), yazi via apt, and idempotent apt installs with warnings for eza and git-delta when repos lack them. --- .../handoff/2026-06-04-1854-initial-setup.md | 69 +++ .gitignore | 4 + README.md | 78 +++ configs/aliases/.aliases | 52 ++ configs/aliases/.docker_aliases | 69 +++ configs/aliases/.functions.sh | 136 +++++ configs/aliases/.git_aliases | 179 ++++++ configs/aliases/.git_bash_autocomplete | 11 + configs/bat/config | 1 + configs/custom_tools/backup_dotfiles.sh | 30 + configs/yazi/yazi.toml | 3 + configs/zsh/zhann-elvis.zsh-theme | 29 + configs/zshrc | 19 + install.sh | 578 ++++++++++++++++++ 14 files changed, 1258 insertions(+) create mode 100644 .agents/handoff/2026-06-04-1854-initial-setup.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 configs/aliases/.aliases create mode 100644 configs/aliases/.docker_aliases create mode 100644 configs/aliases/.functions.sh create mode 100644 configs/aliases/.git_aliases create mode 100644 configs/aliases/.git_bash_autocomplete create mode 100644 configs/bat/config create mode 100644 configs/custom_tools/backup_dotfiles.sh create mode 100644 configs/yazi/yazi.toml create mode 100644 configs/zsh/zhann-elvis.zsh-theme create mode 100644 configs/zshrc create mode 100755 install.sh diff --git a/.agents/handoff/2026-06-04-1854-initial-setup.md b/.agents/handoff/2026-06-04-1854-initial-setup.md new file mode 100644 index 0000000..04177f6 --- /dev/null +++ b/.agents/handoff/2026-06-04-1854-initial-setup.md @@ -0,0 +1,69 @@ +# Handoff: Server CLI Tools — initial setup complete + +**Date:** 2026-06-04T18:54+02:00 +**Focus for next session:** Initial commit, push to remote, and any refinements to the installer or configs. + +## Context + +Standalone repo for provisioning Ubuntu/Debian servers with a consistent CLI environment — zsh, aliases, git config, Docker shortcuts, and core tools. Extracted and adapted from the user's personal [dotfiles repo](https://github.com/neurobrko/dotfiles), stripped of desktop/neovim/GUI items to be server-SSH-only. + +## Current state +- Working directory: `/home/elvis/code/server_cli_tools` +- Branch / commit: `master` — repo initialized, all files staged, **no commits yet** +- Last action taken: `git init && git add .` +- Build/test status: `bash -n install.sh` passes; `./install.sh --help` works + +## Artifacts + +### Key files +- `install.sh` — main installer (6 components, interactive menu or `--all`) +- `configs/aliases/.aliases` — general aliases (server-adapted, no neovim/opencode/SyncSuite) +- `configs/aliases/.functions.sh` — helper functions, alias help system, uses `${EDITOR:-nano}` +- `configs/aliases/.git_aliases` — git shortcuts (removed lazygit, hardcoded gp path) +- `configs/aliases/.docker_aliases` — docker management (unchanged from dotfiles) +- `configs/aliases/.git_bash_autocomplete` — bash git completion +- `configs/custom_tools/backup_dotfiles.sh` — backup conflicting dotfiles +- `configs/custom_tools/get_ip.sh` — get+copy interface IP +- `configs/custom_tools/strip_file.sh` — strip comments/blanks from configs +- `configs/bat/config` — bat theme (base16) +- `configs/yazi/yazi.toml` — yazi file manager config +- `configs/zsh/zhann-elvis.zsh-theme` — custom git-aware zsh prompt +- `configs/zshrc` — .zshrc with oh-my-zsh, plugins, fzf integration +- `README.md` — repo documentation +- `.gitignore` — swap files, DS_Store + +### Uncommitted changes +Everything is staged (`git add .`), awaiting initial commit. 15 files total. + +## What's done +- Analyzed dotfiles repo structure and identified server-relevant components +- Created self-contained repo with all configs bundled (no dependency on dotfiles repo) +- Adapted aliases: removed `nv`, `noc`, `fnv`, `nvc`, `oc`, `rtr`, `rfm`, `gp`, `lg`, `xc`, `dot`, `ape` +- Adapted functions: replaced `nv` with `${EDITOR:-nano}`, removed `fuzzy_nvim`, `activate_poetry_env` +- Changed `py` alias from `python3.12` to `python3` +- Created `.zshrc` with portable fzf sourcing (tries multiple paths) +- Built interactive installer with toggle menu, `--all` mode, backup system, idempotent checks +- Installer components: core packages, zsh+oh-my-zsh, aliases+tools, terminal configs (bat/yazi), docker aliases, git config (delta) + +## What's next +1. Create initial commit +2. Set up remote (Gitea at git.mpx.sk or GitHub) and push +3. Test on a fresh server (e.g. Hetzner VPS from the re_kit project) +4. Consider: add `uninstall.sh` or `--uninstall` flag? +5. Consider: add `update.sh` to pull latest and re-run selective installs? + +## Open questions / decisions pending +- **Remote location**: where to host this repo (Gitea vs GitHub) +- **Branch name**: currently `master` (git default), may want to rename to `main` +- **yazi installation**: currently only installs config; consider adding cargo-based yazi install to the script + +## Gotchas +- The `eza` package is not available in all Ubuntu versions (e.g. older than 24.04). The installer warns and suggests manual install. +- Same for `git-delta` — not in all apt repos. Falls back to a warning with a link. +- The `bat` binary is `batcat` on Debian/Ubuntu — the installer creates a symlink at `/usr/local/bin/bat`. +- The `.zshrc` sets `EDITOR=nano` by default since this is server-only (no neovim). +- `fzf` shell integration varies across distro versions — the zshrc tries both the packaged path and `fzf --zsh`. + +## Suggested skills +- `create-commit-message` — for the initial commit +- `copy-to-clipboard` — if user wants to copy commands for server testing diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44fa520 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.swp +*.swo +*~ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8eb97f --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Server CLI Tools + +Interactive installer for setting up a consistent shell environment on Ubuntu/Debian servers. Clone to any server, run the installer, pick what you need. + +## Quick start + +```sh +git clone ~/server_cli_tools +cd ~/server_cli_tools +./install.sh # interactive menu +./install.sh --all # install everything +./install.sh --update # pull latest + re-run menu +./install.sh --uninstall # remove installed configs +``` + +## What's included + +| # | Component | What it does | +|---|-----------|-------------| +| 1 | **Core packages** | `vim`, `bat`, `eza`, `fzf`, `htop`, `tmux`, `zsh`, `ripgrep`, `fd-find`, `zoxide`, `yazi`, `git-delta` | +| 2 | **Zsh + Oh-My-Zsh** | Shell, plugins (`zsh-autosuggestions`, `zsh-syntax-highlighting`, `z`), custom prompt theme | +| 3 | **Aliases + tools** | General, git, and docker aliases with a self-documenting help system (`h`) | +| 4 | **Terminal configs** | `bat` theme (base16), `yazi` file manager config | +| 5 | **Docker aliases** | Start/stop toggle, bulk stop/remove, image management | +| 6 | **Git config** | `git-delta` as pager with side-by-side diffs | + +## Alias highlights + +Type `h` after installation to see all aliases with descriptions. Some highlights: + +``` +ll — eza with icons, groups, timestamps +pf — fzf + bat file preview +cdf — fuzzy cd into any subdirectory +mkcd — mkdir + cd in one command +twd — tar current directory +gdn — numbered git diff (then gdn 3 to diff file #3) +gcpm — git add -u + commit + push in one command +gstm — git stash with mandatory message +ds — toggle docker container start/stop +da — docker ps -a +``` + +## How it works + +- **Idempotent** — safe to run multiple times; skips already-installed packages +- **Non-destructive** — existing configs are backed up to `~/.server_cli_tools_bkp//` +- **Selective** — interactive menu lets you toggle individual components +- **Ubuntu/Debian** — uses `apt`; warns on other distros (configs still work) + +## Repo structure + +``` +server_cli_tools/ + install.sh # main installer + configs/ + aliases/ + .aliases # general aliases (server-adapted) + .functions.sh # helper functions + alias help system + .git_aliases # git shortcuts + stash management + .docker_aliases # docker management aliases + .git_bash_autocomplete # bash git completion + custom_tools/ + backup_dotfiles.sh # backup conflicting dotfiles + bat/config # bat theme + yazi/yazi.toml # yazi file manager config + zsh/zhann-elvis.zsh-theme # custom zsh prompt (git-aware) + zshrc # .zshrc with oh-my-zsh + plugins +``` + +## Customization + +After installation, create local overrides that won't be tracked: + +- `~/.config/aliases/.local_aliases` — machine-specific aliases +- `~/.config/aliases/.custom_aliases` — temporary/personal aliases + +Use `ea l` to edit local aliases, `ea c` for custom, `sa` to re-source. diff --git a/configs/aliases/.aliases b/configs/aliases/.aliases new file mode 100644 index 0000000..a2b2d85 --- /dev/null +++ b/configs/aliases/.aliases @@ -0,0 +1,52 @@ +#!/bin/bash + +# shellcheck source=.functions.sh +. ~/.config/aliases/.functions.sh + +### General aliases +# ll pimped with eza +alias ll="eza -lag --icons=always --group-directories-first --smart-group --time-style='+%d %b %y %H:%M'" +# du all items in folder, human readable +alias dh='du -chs *' +# yazi file manager +alias y='yazi' +# preview/find file using fzf and bat +alias pf='fzf --preview "bat --color=always {}"' +# cd to dir using fuzzy finder (fzf) +alias cdf='cd "$(find . -type d | fzf)"' +# (executed from / or /home might be slow) +next_line +# cd to file's directory +alias cdtf=cd_to_file +# mkdir & cd to it +alias mkcd=mkdir_and_cd +# grep shorthand +alias g='grep' +# python interpreter/REPL +alias py='python3' +# source ~/.config/aliases/.aliases +alias sa='source ~/.config/aliases/.aliases' +# edit aliases files ('ea h' for help) +alias ea=edit_aliases +# pack 'cwd' to tar.gz in parent with 'cwd' name +alias twd='tar -czf "../$(basename "$PWD").tar.gz" .' +# print alias description +alias h='display_alias_descriptions' + +# LOCAL/CUSTOM bash aliases +# You can create custom local aliases in .local_aliases +# or .custom_aliases file for some temporary aliases +if [ -f ~/.config/aliases/.local_aliases ]; then + . ~/.config/aliases/.local_aliases +fi +if [ -f ~/.config/aliases/.custom_aliases ]; then + . ~/.config/aliases/.custom_aliases +fi +# source git aliases +if [ -f ~/.config/aliases/.git_aliases ]; then + . ~/.config/aliases/.git_aliases +fi +# source docker aliases +if [ -f ~/.config/aliases/.docker_aliases ]; then + . ~/.config/aliases/.docker_aliases +fi diff --git a/configs/aliases/.docker_aliases b/configs/aliases/.docker_aliases new file mode 100644 index 0000000..5848162 --- /dev/null +++ b/configs/aliases/.docker_aliases @@ -0,0 +1,69 @@ +#!/bin/bash + +docker_start_stop() { + local cname="$1" + if [ -z "$cname" ]; then + e_info "Usage: docker_toggle " + return 1 + fi + state=$(docker inspect -f '{{.State.Status}}' "$cname" 2>/dev/null) + case $state in + "running") + docker stop "$cname" + e_info "Container '$cname' stopped." + ;; + "exited") + docker start "$cname" + e_info "Container '$cname' started." + ;; + *) + e_err "Container '$cname' not found or not in a valid state." + return 1 + ;; + esac +} + +docker_stop_all() { + for cont in $(docker ps -a --format {{.Names}}); do + docker stop $cont >/dev/null + e_info "Container $cont stopped." + done +} + +docker_remove() { + container=$(docker rm $1) + e_info "Container $container removed." +} + +docker_remove_all() { + for cont in $(docker ps -a --format {{.Names}}); do + docker rm $cont >/dev/null + e_info "Container $cont removed." + done +} + +docker_remove_all_images() { + for img in $(docker image ls --format '{{.Repository}}:{{.Tag}}'); do + docker image rm $img >/dev/null + e_info "Image $img removed." + done +} + +### Docker aliases +# show docker containers +alias da='docker ps -a' +# start or stop docker +alias ds=docker_start_stop +# stop all docker containers +alias dsa=docker_stop_all +# remove docker +alias dr=docker_remove +# remove all docker containers +alias dra=docker_remove_all +# list docker images in 'repo:tag' +alias dia='docker image ls --format {{.Repository}}:{{.Tag}}' +# remove docker +alias dri='docker image rm' +# remove all docker images +alias dria=docker_remove_all_images + diff --git a/configs/aliases/.functions.sh b/configs/aliases/.functions.sh new file mode 100644 index 0000000..4623964 --- /dev/null +++ b/configs/aliases/.functions.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +e_err() { + echo -e "\e[1;31m$1\e[0m" +} + +e_info() { + echo -e "\e[1;34m$1\e[0m" +} + +e_succ() { + echo -e "\e[1;32m$1\e[0m" +} + +e_warn() { + echo -e "\e[1;38;5;208m$1\e[0m" +} + +function fake_alias() { + : +} + +function next_line() { + : +} + +# FILL LINE FUNCTIONS +fill_line_with_string() { + local string="$1" + local length="$2" + local fill="${3:- }" + if [[ ${#fill} -eq 1 ]]; then + beg_char=$fill + end_char=$fill + fi + if [[ ${#fill} -eq 2 ]]; then + beg_char="${fill:0:1}" + end_char="${fill:1:1}" + fi + if [[ ${#fill} -gt 2 ]]; then + beg_char="${fill:0:1}" + end_char="${fill:0:1}" + fi + local padding=$(((length - ${#string} - 2) / 2)) + local beg + beg=$(printf "%${padding}s" | tr ' ' "$beg_char") + local end + end=$(printf "%${padding}s" | tr ' ' "$end_char") + printf "%s%s%s" "$beg" " $string " "$end" + echo "" +} + +fill_line() { + local length="$1" + local fill="$2" + printf "%*s\n" "$length" "" | tr ' ' "$fill" +} + +# ALIAS DESCRIPTIONS +line_length=59 + +get_alias_descriptions() { + local file_path="$1" + if [ -f "$file_path" ]; then + echo "" + echo "@ $file_path" + local description="" + while IFS= read -r line; do + if [[ "$line" =~ ^### ]]; then + fill_line_with_string "${line#'### '}" "$line_length" "-" + elif [[ "$line" =~ ^# ]]; then + description="${line#"# "}" + elif [[ "$line" =~ ^(alias|fake_alias) ]]; then + alias_name=$(printf "%10s" "$(echo "$line" | cut -d'=' -f1 | awk '{print $2}')") + echo "$alias_name :: $description" + elif [[ $line == "next_line" ]]; then + printf "%*s%s\n" "14" "" "$description" + else + description="" + fi + done <"$file_path" + fi +} + +display_alias_descriptions() { + echo "" + fill_line_with_string "ALIASES DESCRIPTIONS" "$line_length" " " + get_alias_descriptions "$ALIASES"/.aliases + get_alias_descriptions "$ALIASES"/.git_aliases + get_alias_descriptions "$ALIASES"/.docker_aliases + get_alias_descriptions "$ALIASES"/.local_aliases + get_alias_descriptions "$ALIASES"/.custom_aliases + fill_line "$line_length" "-" +} + +ea_help() { + e_info "Usage: ea [c|l|h]" + echo "Edit ~/.config/aliases/.aliases file when used without arguments" + echo "Args: c :: edit ~/.config/aliases/.custom_aliases" + echo " l :: edit ~/.config/aliases/.local_aliases" + echo " f edit ~/.config/aliases/.functions.sh" + echo " h :: print help" +} + +edit_aliases() { + if [ $# -eq 0 ]; then + ${EDITOR:-nano} "$ALIASES"/.aliases + elif [ $# -gt 1 ]; then + e_err "Too many arguments!" + ea_help + else + case "$1" in + l) ${EDITOR:-nano} "$ALIASES"/.local_aliases ;; + c) ${EDITOR:-nano} "$ALIASES"/.custom_aliases ;; + f) ${EDITOR:-nano} "$ALIASES"/.functions.sh ;; + h) ea_help ;; + *) e_err "Invalid option!"; ea_help ;; + esac + fi +} + +cd_to_file() { + cwd=$(pwd) + cd "${1:-.}" || return + file=$(fzf --preview 'bat --color=always {}') + if [ -z "$file" ]; then + cd "$cwd" || return + else + cd "$(dirname "$file")" || return + fi +} + +mkdir_and_cd() { + mkdir -p "$1" || e_err "Failed to create directory: $1" + cd "$1" || e_err "Failed to navigate to directory: $1" +} diff --git a/configs/aliases/.git_aliases b/configs/aliases/.git_aliases new file mode 100644 index 0000000..f985565 --- /dev/null +++ b/configs/aliases/.git_aliases @@ -0,0 +1,179 @@ +#!/bin/bash + +# GIT FUNCTIONS + +# git checkout with tab completion +# bash override for zsh gco alias +if [ -n "$BASH_VERSION" ] && [ -f ~/.config/aliases/.git_bash_autocomplete ]; then + . ~/.config/aliases/.git_bash_autocomplete +fi + +# git create stast with message +git_stash_msg() { + if [ -z "$1" ]; then + e_warn "Stash changes with message! Don't be lazy..." + else + local msg="$*" + git stash -m "$msg" + e_succ "Succesfully stashed changes as '$msg' on branch '$(git branch --show-current)'." + fi +} + +# git apply stash by number +git_apply_stash() { + if [ $# -eq 0 ]; then + e_warn "Please specify stash number!" + return 1 + elif [ $# -gt 1 ]; then + e_warn "Please specify only one stash number!" + return 1 + else + git stash apply stash@"{$1}" + fi +} + +# git echo stasth diff by number +# useful combined with '| xc' +git_stash_patch() { + if [ $# -eq 0 ]; then + e_warn "Please specify a stash number!" + return 1 + elif [ $# -gt 1 ]; then + e_warn "Please specify only one stash number!" + return 1 + else + git stash show -p stash@"{$1}" + fi +} + +# drop stash by number with confirmation +# I accidentally dropped stashes, because I mix up gsp and gsd +git_stash_drop() { + if [ $# -eq 0 ]; then + e_warn "Please specify a stash number!" + return 1 + elif [ $# -gt 1 ]; then + e_warn "Please specify only one stash number!" + return 1 + else + printf "Are you sure you want to drop stash #$1? (Y/N): " + read -r confirm + if [[ "$confirm" =~ ^[yY]$ ]]; then + git stash drop stash@"{$1}" + e_info "Stash #$1 dropped." + else + e_warn "Aborted. Stash #$1 not dropped." + fi + return 0 + fi +} + +# git diff with optional filename from git diff --names-only | nl +git_diff_numbered() { + local files file + files=() + while IFS= read -r line; do + files+=("$line") + done < <(git diff --name-only) + if [ $# -eq 0 ]; then + printf "%s\n" "${files[@]}" | nl + elif [ $# -eq 1 ] && [[ $1 =~ ^[0-9]+$ ]]; then + if [ -n "$BASH_VERSION" ]; then + file="${files[$1 - 1]}" + else + file="${files[$1]}" + fi + [ -n "$file" ] && git diff "$file" + else + e_err "Invalid argument! Provide number from the output of 'gdn'." + fi +} + +# git restore file based on the number from git diff --names-only | nl +git_restore_by_number() { + local files file + files=() + while IFS= read -r line; do + files+=("$line") + done < <(git diff --name-only) + if [ $# -eq 0 ]; then + e_err "Missing argument! Provide number from the output of 'gdn'." + elif [ $# -eq 1 ] && [[ $1 =~ ^[0-9]+$ ]]; then + if [ -n "$BASH_VERSION" ]; then + file="${files[$1 - 1]}" + else + file="${files[$1]}" + fi + [ -n "$file" ] && git restore "$file" + else + e_err "Invalid argument! Provide number from the output of 'gdn'." + fi + +} + +# git add untracked files, commit and push +git_commit_push() { + if [ -z "$1" ]; then + e_err "No commit message provided!" + return 1 + else + local message="$*" + git add -u + git commit -m "$message" + git push + e_succ -e "\nCommit '$message' succesfully pushed to branch: '$(git branch --show-current)'." + fi +} + +# git list stashes +git_stash_list() { + printf "%s\n" "$(git stash list)" +} + +### Git aliases +# git status +fake_alias gst +if [ -n "$BASH_VERSION" ]; then + alias gst='git status' +fi +# git fetch all +fake_alias gfa +if [ -n "$BASH_VERSION" ]; then + alias gfa='git fetch --all --prune' +fi +# git diff +fake_alias gd +if [ -n "$BASH_VERSION" ]; then + alias gd='git diff' +fi +# git diff classic with less pager +alias gdc="git -c core.pager='less' diff" +# git diff numbered +alias gdn=git_diff_numbered +# (with filenum works only in repo root) +next_line +# git commit and push with message +alias gcpm=git_commit_push +# git checkout with tab completion +fake_alias gco +# git submodule update +fake_alias gsu +if [ -n "$BASH_VERSION" ]; then + alias gsu='git submodule update' +fi +# git stash with message +alias gstm=git_stash_msg +# git stash list +alias gstl=git_stash_list +# git stash patch <#> +alias gstp=git_stash_patch +# git stash apply <#> +alias gsta=git_apply_stash +# gti stash drop <#> +alias gstd=git_stash_drop +# git restore +alias grs='git restore' +# git restore by number from gdn +alias grsn=git_restore_by_number +# run pre-commit on changed files only +alias pc='pre-commit run --files $(git diff --name-only)' diff --git a/configs/aliases/.git_bash_autocomplete b/configs/aliases/.git_bash_autocomplete new file mode 100644 index 0000000..88e505e --- /dev/null +++ b/configs/aliases/.git_bash_autocomplete @@ -0,0 +1,11 @@ +gco() { + git checkout "$@" +} +# Enable git completion if available +if [ -f /usr/share/bash-completion/completions/git ]; then + . /usr/share/bash-completion/completions/git +fi +# Enable git branch completion for gc +if type __git_complete &>/dev/null; then + __git_complete gco _git_checkout +fi diff --git a/configs/bat/config b/configs/bat/config new file mode 100644 index 0000000..b1ff1da --- /dev/null +++ b/configs/bat/config @@ -0,0 +1 @@ +--theme="base16" diff --git a/configs/custom_tools/backup_dotfiles.sh b/configs/custom_tools/backup_dotfiles.sh new file mode 100644 index 0000000..962d222 --- /dev/null +++ b/configs/custom_tools/backup_dotfiles.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# TODO: update REPO_URL once remote is set up (Gitea or GitHub) +REPO_URL="https://github.com/neurobrko/server_cli_tools/archive/refs/heads/main.tar.gz" +BACKUP_DIR="$HOME/.server_cli_tools_bkp" +DRY_RUN=false + +if [[ "$1" == "--dry-run" ]]; then + DRY_RUN=true + echo "Dry run mode — no files will be moved." +fi + +curl -s -L "$REPO_URL" | \ +tar -tzf - | \ +grep -v '/$' | \ +while read -r file; do + target="${file#*/}" + + target_path="$HOME/$target" + + if [ -f "$target_path" ]; then + if $DRY_RUN; then + echo "Would move $target_path to $BACKUP_DIR/$target" + else + echo "Moving $target_path to $BACKUP_DIR/$target" + mkdir -p "$(dirname "$BACKUP_DIR/$target")" + mv "$target_path" "$BACKUP_DIR/$target" + fi + fi +done diff --git a/configs/yazi/yazi.toml b/configs/yazi/yazi.toml new file mode 100644 index 0000000..3525ef9 --- /dev/null +++ b/configs/yazi/yazi.toml @@ -0,0 +1,3 @@ +[mgr] +ratio = [ 1, 2, 5 ] +show_hidden = true diff --git a/configs/zsh/zhann-elvis.zsh-theme b/configs/zsh/zhann-elvis.zsh-theme new file mode 100644 index 0000000..85a711d --- /dev/null +++ b/configs/zsh/zhann-elvis.zsh-theme @@ -0,0 +1,29 @@ +autoload -Uz vcs_info + +zstyle ':vcs_info:*' stagedstr '%F{green}●' +zstyle ':vcs_info:*' unstagedstr '%F{yellow}●' +zstyle ':vcs_info:*' check-for-changes true +zstyle ':vcs_info:svn:*' branchformat '%b' +zstyle ':vcs_info:svn:*' formats ' [%b%F{1}:%F{11}%i%c%u%B%F{green}]' +zstyle ':vcs_info:*' enable git svn + +theme_precmd () { + if [[ -z $(git ls-files --other --exclude-standard 2> /dev/null) ]]; then + zstyle ':vcs_info:git:*' formats ' [%b%c%u%B%F{green}]' + else + zstyle ':vcs_info:git:*' formats ' [%b%c%u%B%F{red}●%F{green}]' + fi + + vcs_info +} + +setopt prompt_subst +PROMPT='%B%F{33}%c%B%F{green}${vcs_info_msg_0_}%B%F{magenta} %{$reset_color%}% ' + +autoload -U add-zsh-hook +add-zsh-hook precmd theme_precmd + +# to preview colors +# for i in {0..255}; do +# print -P "%F{$i}$i%f " +# done diff --git a/configs/zshrc b/configs/zshrc new file mode 100644 index 0000000..dd3b462 --- /dev/null +++ b/configs/zshrc @@ -0,0 +1,19 @@ +export ZSH="$HOME/.oh-my-zsh" +export ALIASES="$HOME/.config/aliases" +export CT_DIR="$HOME/.config/custom_tools" +export EDITOR="vim" +ZSH_THEME="" +DISABLE_AUTO_TITLE="true" +plugins=( + git + zsh-syntax-highlighting + zsh-autosuggestions + z +) +source $ZSH/oh-my-zsh.sh +source $HOME/.config/zsh/zhann-elvis.zsh-theme +source $ALIASES/.aliases +autoload -Uz zcalc +[ -f /usr/share/doc/fzf/examples/key-bindings.zsh ] && source /usr/share/doc/fzf/examples/key-bindings.zsh +[ -f /usr/share/doc/fzf/examples/completion.zsh ] && source /usr/share/doc/fzf/examples/completion.zsh +command -v fzf >/dev/null 2>&1 && eval "$(fzf --zsh 2>/dev/null)" || true diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..ba354c9 --- /dev/null +++ b/install.sh @@ -0,0 +1,578 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --------------------------------------------------------------------------- +# Server CLI Tools — interactive installer +# Clone this repo to any Ubuntu/Debian server and run: ./install.sh +# --------------------------------------------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIGS="$SCRIPT_DIR/configs" +BACKUP_DIR="$HOME/.server_cli_tools_bkp/$(date +%Y%m%d-%H%M%S)" + +# Colors (matching the dotfiles convention) +e_err() { echo -e "\e[1;31m$1\e[0m"; } +e_info() { echo -e "\e[1;34m$1\e[0m"; } +e_succ() { echo -e "\e[1;32m$1\e[0m"; } +e_warn() { echo -e "\e[1;38;5;208m$1\e[0m"; } + +INSTALLED=() +SKIPPED=() + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +backup_file() { + local src="$1" + if [ -e "$src" ]; then + local rel="${src#$HOME/}" + local dest="$BACKUP_DIR/$rel" + mkdir -p "$(dirname "$dest")" + cp -a "$src" "$dest" + e_info " Backed up $src" + fi +} + +copy_config() { + local src="$1" dest="$2" + backup_file "$dest" + mkdir -p "$(dirname "$dest")" + cp -a "$src" "$dest" +} + +copy_dir() { + local src="$1" dest="$2" + if [ -d "$dest" ]; then + backup_file "$dest" + fi + mkdir -p "$dest" + cp -a "$src"/. "$dest"/ +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +check_os() { + if [ ! -f /etc/os-release ]; then + e_err "Cannot detect OS. This script supports Ubuntu/Debian only." + exit 1 + fi + . /etc/os-release + case "$ID" in + ubuntu|debian) ;; + *) + e_warn "Detected $ID — this script is designed for Ubuntu/Debian." + e_warn "Package installation may not work. Configs will still be copied." + ;; + esac +} + +apt_install() { + local packages=("$@") + local to_install=() + for pkg in "${packages[@]}"; do + if dpkg -s "$pkg" &>/dev/null; then + e_info " $pkg is already installed" + else + to_install+=("$pkg") + fi + done + if [ ${#to_install[@]} -gt 0 ]; then + e_info " Installing: ${to_install[*]}" + sudo apt-get install -y "${to_install[@]}" + fi +} + +# --------------------------------------------------------------------------- +# Component installers +# --------------------------------------------------------------------------- + +install_core_packages() { + e_info "\n[1/6] Core packages" + sudo apt-get update -qq + + apt_install curl vim bat fzf htop tmux zsh ripgrep fd-find zoxide yazi + + # bat is installed as 'batcat' on Debian/Ubuntu + if command_exists batcat && ! command_exists bat; then + sudo ln -sf /usr/bin/batcat /usr/local/bin/bat + e_info " Symlinked batcat -> bat" + fi + + # eza — not in all distro repos, try apt first, fall back to cargo/binary + if ! command_exists eza; then + if apt-cache show eza &>/dev/null; then + apt_install eza + else + e_warn " eza not in apt repos. Install manually: https://github.com/eza-community/eza" + fi + else + e_info " eza is already installed" + fi + + # git-delta + if ! command_exists delta; then + if apt-cache show git-delta &>/dev/null; then + apt_install git-delta + else + e_warn " git-delta not in apt repos. Install from: https://github.com/dandavison/delta/releases" + fi + else + e_info " delta is already installed" + fi + + INSTALLED+=("core-packages") +} + +install_zsh() { + e_info "\n[2/6] Zsh + Oh-My-Zsh" + + if ! command_exists zsh; then + apt_install zsh + fi + + # Oh-My-Zsh + if [ ! -d "$HOME/.oh-my-zsh" ]; then + e_info " Installing Oh-My-Zsh..." + RUNZSH=no CHSH=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended + else + e_info " Oh-My-Zsh already installed" + fi + + # Plugins + local zsh_custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" + + if [ ! -d "$zsh_custom/plugins/zsh-autosuggestions" ]; then + e_info " Cloning zsh-autosuggestions..." + git clone --quiet https://github.com/zsh-users/zsh-autosuggestions "$zsh_custom/plugins/zsh-autosuggestions" + else + e_info " zsh-autosuggestions already installed" + fi + + if [ ! -d "$zsh_custom/plugins/zsh-syntax-highlighting" ]; then + e_info " Cloning zsh-syntax-highlighting..." + git clone --quiet https://github.com/zsh-users/zsh-syntax-highlighting.git "$zsh_custom/plugins/zsh-syntax-highlighting" + else + e_info " zsh-syntax-highlighting already installed" + fi + + # Theme + mkdir -p "$HOME/.config/zsh" + copy_config "$CONFIGS/zsh/zhann-elvis.zsh-theme" "$HOME/.config/zsh/zhann-elvis.zsh-theme" + e_info " Installed zhann-elvis.zsh-theme" + + # .zshrc + copy_config "$CONFIGS/zshrc" "$HOME/.zshrc" + e_info " Installed .zshrc" + + # Change default shell to zsh (if not already) + if [ "$(basename "$SHELL")" != "zsh" ]; then + e_info " Changing default shell to zsh..." + if command_exists chsh; then + chsh -s "$(which zsh)" || e_warn " chsh failed — change shell manually: chsh -s \$(which zsh)" + fi + fi + + INSTALLED+=("zsh") +} + +install_aliases() { + e_info "\n[3/6] Aliases + custom tools" + + copy_dir "$CONFIGS/aliases" "$HOME/.config/aliases" + chmod +x "$HOME/.config/aliases"/.aliases "$HOME/.config/aliases"/.functions.sh 2>/dev/null || true + e_info " Installed aliases to ~/.config/aliases/" + + copy_dir "$CONFIGS/custom_tools" "$HOME/.config/custom_tools" + chmod +x "$HOME/.config/custom_tools"/*.sh 2>/dev/null || true + e_info " Installed custom tools to ~/.config/custom_tools/" + + INSTALLED+=("aliases" "custom-tools") +} + +install_terminal_configs() { + e_info "\n[4/6] Terminal configs (bat, yazi)" + + # bat + mkdir -p "$HOME/.config/bat" + copy_config "$CONFIGS/bat/config" "$HOME/.config/bat/config" + e_info " Installed bat config (theme: base16)" + + # yazi + mkdir -p "$HOME/.config/yazi" + copy_config "$CONFIGS/yazi/yazi.toml" "$HOME/.config/yazi/yazi.toml" + e_info " Installed yazi config" + + INSTALLED+=("terminal-configs") +} + +install_docker_aliases() { + e_info "\n[5/6] Docker aliases" + + if command_exists docker; then + copy_config "$CONFIGS/aliases/.docker_aliases" "$HOME/.config/aliases/.docker_aliases" + e_info " Installed Docker aliases" + else + e_warn " Docker not found — aliases installed anyway (will work once Docker is available)" + copy_config "$CONFIGS/aliases/.docker_aliases" "$HOME/.config/aliases/.docker_aliases" + fi + + INSTALLED+=("docker-aliases") +} + +install_git_config() { + e_info "\n[6/6] Git config (delta pager)" + + if ! command_exists delta; then + e_warn " git-delta not installed — config will be ready when delta is available" + fi + + local gitconfig="$HOME/.gitconfig" + + # Only inject delta config if not already present + if [ -f "$gitconfig" ] && grep -q '\[delta\]' "$gitconfig" 2>/dev/null; then + e_info " Delta config already present in ~/.gitconfig" + else + e_info " Adding delta pager config to ~/.gitconfig" + cat >> "$gitconfig" << 'DELTA' + +[core] + pager = delta +[interactive] + diffFilter = delta --color-only +[delta] + side-by-side = true + navigate = true + dark = true +[merge] + conflictStyle = zdiff3 +DELTA + fi + + INSTALLED+=("git-config") +} + +# --------------------------------------------------------------------------- +# Uninstall +# --------------------------------------------------------------------------- + +uninstall() { + e_warn "This will remove configs installed by Server CLI Tools." + e_warn "Installed packages (apt) will NOT be removed." + echo "" + printf "Continue? [y/N] " + read -r confirm + case "$confirm" in + y|Y) ;; + *) echo "Aborted."; exit 0 ;; + esac + + local removed=() + + # Aliases + custom tools + if [ -d "$HOME/.config/aliases" ]; then + rm -rf "$HOME/.config/aliases" + removed+=("aliases") + fi + if [ -d "$HOME/.config/custom_tools" ]; then + rm -rf "$HOME/.config/custom_tools" + removed+=("custom-tools") + fi + + # Terminal configs + if [ -f "$HOME/.config/bat/config" ]; then + rm -f "$HOME/.config/bat/config" + removed+=("bat-config") + fi + if [ -f "$HOME/.config/yazi/yazi.toml" ]; then + rm -f "$HOME/.config/yazi/yazi.toml" + removed+=("yazi-config") + fi + + # Zsh theme + zshrc + if [ -f "$HOME/.config/zsh/zhann-elvis.zsh-theme" ]; then + rm -f "$HOME/.config/zsh/zhann-elvis.zsh-theme" + removed+=("zsh-theme") + fi + if [ -f "$HOME/.zshrc" ]; then + rm -f "$HOME/.zshrc" + removed+=("zshrc") + fi + + # Oh-My-Zsh plugins (only the ones we installed) + local zsh_custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" + if [ -d "$zsh_custom/plugins/zsh-autosuggestions" ]; then + rm -rf "$zsh_custom/plugins/zsh-autosuggestions" + removed+=("zsh-autosuggestions") + fi + if [ -d "$zsh_custom/plugins/zsh-syntax-highlighting" ]; then + rm -rf "$zsh_custom/plugins/zsh-syntax-highlighting" + removed+=("zsh-syntax-highlighting") + fi + + # Git config — strip delta block + local gitconfig="$HOME/.gitconfig" + if [ -f "$gitconfig" ] && grep -q '\[delta\]' "$gitconfig" 2>/dev/null; then + local tmp + tmp=$(mktemp) + awk ' + /^\[core\]/ { if (skip==0) { peek=1; hold=$0; next } } + peek==1 { + if ($0 ~ /pager *= *delta/) { skip=1; next } + else { print hold; peek=0 } + } + /^\[interactive\]/ && skip { next } + /^\[delta\]/ { skip=1; next } + /^\[merge\]/ && skip { skip_merge=1; next } + skip_merge==1 { skip_merge=0; skip=0; next } + /^\[/ { skip=0 } + skip { next } + { print } + ' "$gitconfig" > "$tmp" + mv "$tmp" "$gitconfig" + removed+=("git-delta-config") + fi + + echo "" + if [ ${#removed[@]} -gt 0 ]; then + e_succ "Removed:" + for item in "${removed[@]}"; do + echo " - $item" + done + else + e_info "Nothing to remove." + fi + + # Offer to restore backups + local bkp_base="$HOME/.server_cli_tools_bkp" + if [ -d "$bkp_base" ]; then + echo "" + local latest + latest=$(ls -1t "$bkp_base" 2>/dev/null | head -n1) + if [ -n "$latest" ]; then + e_info "Backup found: $bkp_base/$latest" + printf "Restore from this backup? [y/N] " + read -r restore + case "$restore" in + y|Y) + cp -a "$bkp_base/$latest"/. "$HOME"/ + e_succ "Restored from $bkp_base/$latest" + ;; + *) e_info "Skipping restore." ;; + esac + fi + fi + + echo "" +} + +# --------------------------------------------------------------------------- +# Menu +# --------------------------------------------------------------------------- + +COMPONENTS=( + "Core packages (vim, bat, eza, fzf, htop, tmux, rg, fd, zoxide, yazi, delta)" + "Zsh + Oh-My-Zsh (shell, plugins, theme)" + "Aliases + custom tools" + "Terminal configs (bat, yazi)" + "Docker aliases" + "Git config (delta pager)" +) + +INSTALLERS=( + install_core_packages + install_zsh + install_aliases + install_terminal_configs + install_docker_aliases + install_git_config +) + +show_menu() { + local count=${#COMPONENTS[@]} + local selected=() + for ((i = 0; i < count; i++)); do + selected+=(1) + done + + while true; do + clear + echo "" + e_info "Server CLI Tools — Installer" + echo "=============================" + echo "" + echo "Select components (enter number to toggle, a=all, n=none):" + echo "" + + for ((i = 0; i < count; i++)); do + local mark=" " + if [ "${selected[$i]}" -eq 1 ]; then + mark=" x " + fi + printf " [%s] %d) %s\n" "$mark" "$((i + 1))" "${COMPONENTS[$i]}" + done + + echo "" + echo " [a] Select all [n] Select none [Enter] Install [q] Quit" + echo "" + printf " > " + read -r choice + + case "$choice" in + q|Q) + echo "Aborted." + exit 0 + ;; + a|A) + for ((i = 0; i < count; i++)); do + selected[$i]=1 + done + ;; + n|N) + for ((i = 0; i < count; i++)); do + selected[$i]=0 + done + ;; + "") + # Run selected installers + local any=0 + for ((i = 0; i < count; i++)); do + if [ "${selected[$i]}" -eq 1 ]; then + any=1 + fi + done + if [ "$any" -eq 0 ]; then + e_warn "Nothing selected!" + sleep 1 + continue + fi + + echo "" + for ((i = 0; i < count; i++)); do + if [ "${selected[$i]}" -eq 1 ]; then + ${INSTALLERS[$i]} + else + SKIPPED+=("${COMPONENTS[$i]}") + fi + done + return + ;; + *) + if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then + local idx=$((choice - 1)) + if [ "${selected[$idx]}" -eq 1 ]; then + selected[$idx]=0 + else + selected[$idx]=1 + fi + fi + ;; + esac + done +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +print_summary() { + echo "" + echo "===========================================" + e_succ " Installation complete!" + echo "===========================================" + + if [ ${#INSTALLED[@]} -gt 0 ]; then + echo "" + e_succ "Installed:" + for item in "${INSTALLED[@]}"; do + echo " + $item" + done + fi + + if [ ${#SKIPPED[@]} -gt 0 ]; then + echo "" + e_warn "Skipped:" + for item in "${SKIPPED[@]}"; do + echo " - $item" + done + fi + + if [ -d "$BACKUP_DIR" ]; then + echo "" + e_info "Backups saved to: $BACKUP_DIR" + fi + + echo "" + e_info "To apply changes, start a new shell or run:" + echo " exec zsh" + echo "" + e_info "Type 'h' to see all available aliases." + echo "" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --all Install all components (non-interactive)" + echo " --update Pull latest changes and re-run installer" + echo " --update --all Pull latest and install all (non-interactive)" + echo " --uninstall Remove installed configs (keeps packages)" + echo " --help Show this help message" + echo "" + echo "Without options, an interactive menu is shown." +} + +main() { + case "${1:-}" in + --help|-h) + usage + exit 0 + ;; + --uninstall) + uninstall + exit 0 + ;; + --update) + e_info "Pulling latest changes..." + git -C "$SCRIPT_DIR" pull || { e_err "git pull failed"; exit 1; } + echo "" + check_os + if [ "${2:-}" = "--all" ] || [ "${2:-}" = "-a" ]; then + e_info "Server CLI Tools — installing all components" + for installer in "${INSTALLERS[@]}"; do + $installer + done + else + show_menu + fi + print_summary + ;; + --all|-a) + check_os + e_info "Server CLI Tools — installing all components" + for installer in "${INSTALLERS[@]}"; do + $installer + done + print_summary + ;; + "") + check_os + show_menu + print_summary + ;; + *) + e_err "Unknown option: $1" + usage + exit 1 + ;; + esac +} + +main "$@"