fix: correct non-TTY vim-plug install; add real plugin count check

- _vim_run non-TTY branch: remove </dev/null (caused "Error reading
  input, exiting" before downloads finished → 0 plugins installed) and
  >/dev/null (broke async job callbacks → partial install of ~13/26).
  Now only redirects stderr; stdin inherits from caller so vim's event
  loop runs to completion.
- Add post-install verification: count ~/.vim/plugged entries and die
  if empty, replacing the unconditional "ok Plugins installed" that
  masked failures silently.
- .vimrc, install.sh, QUICKSTART.md, README.md: carry forward the full
  v1.2 rewrite (checkbox module selection, solarized palette, LSP
  stack) that accumulated in the previous session.

Tested: macOS (27 plugins, exit 0) and Arch Linux SSH (26 plugins,
exit 0). shellcheck: zero warnings.
This commit is contained in:
m1ngsama 2026-04-10 18:43:52 +08:00
parent 716e7e5808
commit 0b3f631e98
4 changed files with 1363 additions and 1887 deletions

1447
.vimrc

File diff suppressed because it is too large Load diff

View file

@ -1,63 +1,47 @@
# Quick Start # Quick Start
Five minutes from zero to a working Vim engineering environment. Five minutes from zero to a working Vim environment.
> **New to Vim?** Read Step 0 first — it takes 2 minutes and prevents the most
> common beginner frustration. Already know how Vim modes work? [Skip to Step 1](#step-1-install).
--- ---
## Step 0: Vim Basics ## Step 0: Vim Basics (2 minutes)
> **When confused, press `Esc` until things feel normal again — then keep reading.** > **When confused, press `Esc` until things feel normal again.**
Vim is **modal**: the keyboard behaves differently depending on which mode you are in. Vim is **modal** — the keyboard behaves differently depending on which mode you are in.
Most people get stuck because they try to type text while in Normal mode.
### The Three Modes | Mode | Purpose | Enter | Leave |
|------|---------|-------|-------|
| **Normal** | Navigate and run commands | default on startup | — |
| **Insert** | Type text | `i` before / `a` after / `o` new line | `Esc` or `jk` |
| **Visual** | Select text | `v` char / `V` line | `Esc` |
| Mode | Purpose | How to enter | How to leave | ### 4 commands that get you out of any jam
|------|---------|--------------|--------------|
| **Normal** | Navigate and run commands | Startup default | — (you're already here) |
| **Insert** | Type text | `i` before cursor, `a` after, `o` new line below | `Esc` or `jk` |
| **Visual** | Select text | `v` char-by-char, `V` whole lines | `Esc` |
### 4 Survival Commands
Learn these before anything else. They will get you out of every stuck situation.
| Command | Action | | Command | Action |
|---------|--------| |---------|--------|
| `Esc` or `jk` | Exit insert/visual mode — return to Normal | | `Esc` or `jk` | Exit insert / visual mode → Normal |
| `:q!` then `Enter` | Force quit without saving (emergency exit) | | `:q!` then `Enter` | Force quit without saving |
| `,x` | Save and quit | | `,x` | Save and quit |
| `,w` or `Ctrl+s` | Save the file | | `,w` or `Ctrl+s` | Save |
Once in Normal mode, press `,?` to open a cheat sheet covering everything else. Once in Normal mode, press `,?` to open the cheat sheet.
--- ---
## Step 1: Install ## Step 1: Install
**One command — works on macOS and Linux:**
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash
``` ```
This clones the repo to `~/.vim` and runs the full installer. Interactive prompts Traditional:
let you choose which optional tools to install (ripgrep, Node.js, Python tools, etc.).
The installer automatically handles missing dependencies — it will offer to install
`git`, Homebrew (macOS), or Node.js via nvm if they are not found.
**Traditional install:**
```bash ```bash
git clone https://github.com/m1ngsama/chopsticks.git ~/.vim git clone https://github.com/m1ngsama/chopsticks.git ~/.vim
cd ~/.vim && ./install.sh cd ~/.vim && ./install.sh
``` ```
**Non-interactive (CI / server / scripting):** Non-interactive / CI:
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash -s -- --yes curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash -s -- --yes
``` ```
@ -67,37 +51,14 @@ curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | b
## Step 2: Open Vim ## Step 2: Open Vim
```bash ```bash
vim vim # startup dashboard (recent files + sessions)
``` vim . # startup dashboard, current directory listed
vim myfile # edit a specific file
The startup screen (vim-startify) shows recent files and sessions.
Press `Ctrl+p` to find a file, or just type a path.
To open a project:
```bash
vim . # NERDTree on left, Startify on right
vim myfile # opens file directly
``` ```
--- ---
## Step 3: Set Up LSP (pick your path) ## Step 3: Set Up LSP
### Path A: With Node.js (CoC — full LSP)
```bash
node --version # must be >= 14.14
```
Inside Vim, install language servers for your stack:
```vim
:CocInstall coc-pyright coc-tsserver coc-go coc-rust-analyzer
```
Or let `install.sh` do it — it asks during setup.
### Path B: Without Node.js (vim-lsp — no dependencies)
Open a source file, then run: Open a source file, then run:
@ -105,25 +66,38 @@ Open a source file, then run:
:LspInstallServer :LspInstallServer
``` ```
This auto-detects and installs the correct language server for the current filetype. This auto-detects the filetype and installs the correct language server.
No Node.js required — vim-lsp runs on pure VimScript.
Check status:
```vim
:LspStatus
```
**Markdown LSP** (`marksman`) needs a standalone binary:
```bash
brew install marksman # macOS
sudo pacman -S marksman # Arch
# install.sh handles this automatically
```
--- ---
## The 12 Keys That Matter ## The Keys That Matter
``` ```
, (pause 500ms) Show all keybindings (which-key) ,? Open cheat sheet (all bindings in one place)
,? Open cheat sheet inside Vim Esc / jk Exit insert mode → Normal
Esc / jk Exit insert mode → Normal (memorize this) Ctrl+s Save
Ctrl+s Save (works in normal and insert mode)
Ctrl+p Fuzzy find file Ctrl+p Fuzzy find file
Ctrl+n Toggle file tree ,e File browser (netrw)
gd Go to definition gd Go to definition
K Show documentation K Show documentation
[g / ]g Prev / next LSP diagnostic [g / ]g Prev / next LSP diagnostic
,rn Rename symbol ,rn Rename symbol
,rG Search word under cursor (ripgrep) ,rG Search word under cursor (ripgrep)
,gs Git status ,gs Git status
,mp Markdown live preview in browser
,w / ,x Save / Save+quit ,w / ,x Save / Save+quit
``` ```
@ -138,8 +112,8 @@ K Show documentation
| `gd` | Go to definition | | `gd` | Go to definition |
| `gy` | Go to type definition | | `gy` | Go to type definition |
| `gi` | Go to implementation | | `gi` | Go to implementation |
| `gr` | List all references | | `gr` | List references |
| `K` | Show docs for symbol under cursor | | `K` | Docs for symbol under cursor |
| `Ctrl+o` | Jump back | | `Ctrl+o` | Jump back |
| `Ctrl+i` | Jump forward | | `Ctrl+i` | Jump forward |
@ -152,23 +126,35 @@ K Show documentation
| `gc` | Toggle comment (visual mode too) | | `gc` | Toggle comment (visual mode too) |
| `cs"'` | Change surrounding `"` to `'` | | `cs"'` | Change surrounding `"` to `'` |
| `ds(` | Delete surrounding `(` | | `ds(` | Delete surrounding `(` |
| `s`+2ch | EasyMotion: jump anywhere | | `s` + 2 chars | EasyMotion: jump anywhere |
### Manage Errors ### Manage Errors
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| `]g` | Jump to next diagnostic | | `]g` | Jump to next LSP diagnostic |
| `[g` | Jump to previous diagnostic | | `[g` | Jump to previous diagnostic |
| `K` | Read the error message | | `K` | Read the error message |
| `,ca` | Apply code action / auto-fix | | `,ca` | Apply code action / auto-fix |
### Markdown
| Key | Action |
|-----|--------|
| `,mp` | Open live preview in browser |
| `,mt` | Table of contents |
| `zr` / `zm` | Unfold / fold all headings |
Formatting in the buffer is live: `**bold**` renders as bold,
headings hide their `#` markers. Raw syntax reappears when
the cursor enters that line.
### Git Workflow ### Git Workflow
``` ```
,gs git status (stage with 's', commit with 'cc') ,gs git status (stage with 's', commit with 'cc')
,gd diff current file ,gd diff current file
,gb blame current file ,gb blame
,gc commit ,gc commit
,gp push ,gp push
,gl pull ,gl pull
@ -176,82 +162,18 @@ K Show documentation
--- ---
## Language Workflows
### Python
```bash
# tools installed by install.sh; or manually:
pip install black flake8 pylint isort
```
Auto-formats with `black` + `isort` on save. Lint errors show as `X`/`!` in the sign column.
### JavaScript / TypeScript
```bash
npm install -g prettier eslint typescript
```
Auto-formats with `prettier` on save.
### Go
```bash
# tools installed by install.sh; or manually:
go install golang.org/x/tools/gopls@latest
go install golang.org/x/tools/cmd/goimports@latest
go install honnef.co/go/tools/cmd/staticcheck@latest
```
`gofmt` + `goimports` run on save automatically.
### Markdown
Install `marksman` for LSP support (completions, link checking):
```bash
brew install marksman # macOS
sudo pacman -S marksman # Arch
# or: ./install.sh (handles it automatically)
```
---
## Customize
Edit config live:
```vim
,ev " opens ~/.vimrc in Vim
,sv " reloads config without restarting
```
Per-project settings: create `.vimrc` in your project root.
```vim
" project/.vimrc
set shiftwidth=2
let g:ale_python_black_options = '--line-length=100'
```
Change color scheme in `~/.vimrc`:
```vim
colorscheme dracula " or: gruvbox, solarized, onedark
```
---
## Quick Reference Card ## Quick Reference Card
``` ```
BASICS (learn these first) BASICS
Esc / jk Exit insert mode → Normal Esc / jk Exit insert mode → Normal
Ctrl+s Save (normal + insert mode) Ctrl+s Save
:q! + Enter Emergency quit without saving :q! + Enter Emergency quit
,? Open cheat sheet ,? Open cheat sheet
FILES FILES
Ctrl+n File tree toggle
Ctrl+p Fuzzy find file (git-aware) Ctrl+p Fuzzy find file (git-aware)
,e File browser (netrw)
,b Search open buffers ,b Search open buffers
,rg Search file contents (ripgrep) ,rg Search file contents (ripgrep)
,rG Ripgrep word under cursor ,rG Ripgrep word under cursor
@ -260,13 +182,16 @@ FILES
,, Switch to last file ,, Switch to last file
CODE CODE
gd Go to definition gd Definition | gy Type def | gi Impl | gr References
K Show documentation K Show documentation
[g / ]g Prev / next LSP diagnostic [g / ]g Prev / next LSP diagnostic
[e / ]e Prev / next ALE error [e / ]e Prev / next ALE error
,rn Rename symbol ,rn Rename symbol
,ca Code action / auto-fix ,ca Code action
,f Format selection | ,F Format whole file ,f Format buffer / selection
MARKDOWN
,mp Live preview | ,mt Table of contents
GIT GIT
,gs Status | ,gd Diff | ,gb Blame ,gs Status | ,gd Diff | ,gb Blame
@ -275,19 +200,14 @@ GIT
WINDOWS / PANES WINDOWS / PANES
Ctrl+h/j/k/l Move between Vim windows or tmux panes Ctrl+h/j/k/l Move between Vim windows or tmux panes
,h / ,l Prev / next buffer ,h / ,l Prev / next buffer
,tv Open terminal (vertical) ,tv / ,th Terminal (vertical / horizontal)
,th Open terminal (horizontal) Esc Esc Exit terminal mode
Esc Exit terminal mode ,u Undo tree
,u Undo tree | ,tt Tag browser
SEARCH & REPLACE SEARCH
/text Search forward | ?text backward /text Forward | ?text Backward | n next | N prev
// Search for visually selected text // Search visually selected text
,* Replace word under cursor (file-wide) ,* Replace word under cursor (file-wide)
CLIPBOARD
,y / ,Y Yank / yank line to system clipboard
,p / ,P Paste from system clipboard (after / before)
``` ```
--- ---

554
README.md
View file

@ -1,7 +1,7 @@
# chopsticks # chopsticks
> A batteries-included Vim configuration for full-stack engineering. > Flowing vim for any machine — SSH servers included.
> Tiered LSP · 14 languages · TTY-aware · Zero icon fonts · One-command install. > Solarized · vim-lsp (no Node.js) · Markdown-first · One-command install.
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE)
[![Vim 8.0+](https://img.shields.io/badge/Vim-8.0%2B-brightgreen?style=flat-square)](https://www.vim.org/) [![Vim 8.0+](https://img.shields.io/badge/Vim-8.0%2B-brightgreen?style=flat-square)](https://www.vim.org/)
@ -9,18 +9,11 @@
[![Release](https://img.shields.io/github/v/release/m1ngsama/chopsticks?style=flat-square&label=release&color=orange)](https://github.com/m1ngsama/chopsticks/releases) [![Release](https://img.shields.io/github/v/release/m1ngsama/chopsticks?style=flat-square&label=release&color=orange)](https://github.com/m1ngsama/chopsticks/releases)
[![Last Commit](https://img.shields.io/github/last-commit/m1ngsama/chopsticks?style=flat-square)](https://github.com/m1ngsama/chopsticks/commits/main) [![Last Commit](https://img.shields.io/github/last-commit/m1ngsama/chopsticks?style=flat-square)](https://github.com/m1ngsama/chopsticks/commits/main)
[![Stars](https://img.shields.io/github/stars/m1ngsama/chopsticks?style=flat-square)](https://github.com/m1ngsama/chopsticks/stargazers) [![Stars](https://img.shields.io/github/stars/m1ngsama/chopsticks?style=flat-square)](https://github.com/m1ngsama/chopsticks/stargazers)
[![Issues](https://img.shields.io/github/issues/m1ngsama/chopsticks?style=flat-square)](https://github.com/m1ngsama/chopsticks/issues)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square)](https://github.com/m1ngsama/chopsticks/pulls)
[![Plugins](https://img.shields.io/badge/plugins-30%2B-blueviolet?style=flat-square)](#plugins)
[![Languages](https://img.shields.io/badge/languages-14-informational?style=flat-square)](#language-support)
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash
``` ```
> **New to Vim?** Read [Step 0 in QUICKSTART.md](QUICKSTART.md#step-0-vim-basics) first —
> a 2-minute intro to modes and the 4 commands that get you out of any jam.
--- ---
## Contents ## Contents
@ -28,14 +21,13 @@ curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | b
- [Design Principles](#design-principles) - [Design Principles](#design-principles)
- [Requirements](#requirements) - [Requirements](#requirements)
- [Installation](#installation) - [Installation](#installation)
- [LSP: Tiered Backend](#lsp-tiered-backend) - [LSP](#lsp)
- [Key Mappings](#key-mappings) - [Key Mappings](#key-mappings)
- [Markdown](#markdown)
- [Features](#features) - [Features](#features)
- [Language Support](#language-support)
- [Plugins](#plugins) - [Plugins](#plugins)
- [Customization](#customization) - [Customization](#customization)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Contributing](#contributing)
--- ---
@ -43,51 +35,44 @@ curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | b
| Principle | What it means | | Principle | What it means |
|-----------|--------------| |-----------|--------------|
| **KISS** | No icon fonts, no Nerd Font glyphs — plain ASCII everywhere | | **Flowing writing** | Every plugin earns its place by reducing interruptions to thought |
| **Tiered LSP** | CoC (full) when Node.js is available; vim-lsp (pure VimScript) otherwise | | **No Node.js** | vim-lsp runs on pure VimScript — works on any machine, including SSH servers |
| **TTY-aware** | Automatically detects SSH/console environments and degrades gracefully | | **Solarized** | One palette, everywhere — vim statusline matches tmux bar exactly |
| **Engineering-first** | Git workflow, persistent sessions, project-local config, large-file safety | | **TTY-aware** | SSH and console environments degrade gracefully without breaking |
| **Batteries included** | `install.sh` handles vim-plug, plugins, system tools, and language servers | | **KISS** | No icon fonts, no Nerd Font glyphs — plain ASCII throughout |
--- ---
## Requirements ## Requirements
| Tool | Minimum | Role | | Tool | Role |
|------|---------|------| |------|------|
| Vim | **8.0+** | Required — `install.sh` installs it if missing | | Vim 8.0+ | Required — `install.sh` installs it if missing |
| git | any | Required — `install.sh` installs it if missing | | git | Required |
| curl | any | Required — `install.sh` installs it if missing | | curl | Required |
| Node.js | 14.14+ | Optional — enables CoC LSP; `install.sh` offers nvm install | | ripgrep | Recommended — enables `,rg` project search |
| ripgrep | any | Optional — enables `,rg` / `,rG` project search | | fzf | Recommended — enables `Ctrl+p` fuzzy finder |
| fzf | any | Optional — enables `Ctrl+p` fuzzy finder | | Node.js | Optional — enables npm formatters (prettier, eslint) |
| ctags | any | Optional — enables `,tt` tag browser |
| tmux | 1.8+ | Optional — enables seamless pane navigation |
`install.sh` detects your environment and installs missing dependencies automatically. `install.sh` detects your environment and installs missing dependencies.
On macOS it will offer to install Homebrew if not present. On any platform it will
offer to install Node.js via nvm if missing.
--- ---
## Installation ## Installation
### One command (recommended) ### One command
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash
``` ```
This bootstrap script clones the repo to `~/.vim`, then runs the full installer. Non-interactive / CI:
It works correctly even when piped from curl — interactive prompts use `/dev/tty`.
For non-interactive or CI environments:
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash -s -- --yes curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash -s -- --yes
``` ```
### Traditional (git clone) ### Git clone
```bash ```bash
git clone https://github.com/m1ngsama/chopsticks.git ~/.vim git clone https://github.com/m1ngsama/chopsticks.git ~/.vim
@ -96,250 +81,196 @@ cd ~/.vim && ./install.sh
### What the installer does ### What the installer does
1. **Preflight** — checks network, detects OS and package manager, verifies or installs `curl`, `git`, and `vim` 1. Detects OS and package manager
2. **sudo** — authenticates once upfront; gracefully skips system packages when unavailable 2. Verifies or installs `curl`, `git`, `vim`
3. **macOS** — offers to install Homebrew if `brew` is not found 3. Backs up existing `~/.vimrc`, then symlinks `~/.vimrc → ~/.vim/.vimrc`
4. **Node.js** — offers to install via nvm if not found (falls back to vim-lsp if declined) 4. Installs vim-plug and runs `:PlugInstall`
5. **Python** — offers to install Python 3 if missing; bootstraps pip3 if only python3 is present 5. Offers to install system tools (ripgrep, fzf, ctags, shellcheck, hadolint, marksman)
6. **Symlinks** — backs up any existing `~/.vimrc` with a timestamp, then symlinks `~/.vimrc → ~/.vim/.vimrc` 6. Offers to install Node.js via nvm (for npm formatters — optional)
7. **Plugins** — installs vim-plug and runs `:PlugInstall` (with a progress notice during the black-screen period) 7. Offers to install npm formatters (prettier, eslint, etc.)
8. **System tools** — ripgrep, fzf, ctags, shellcheck, hadolint, marksman (verified downloads) 8. Offers to install Python formatters/linters (black, isort, flake8, etc.)
9. **Language tools** — npm formatters, pip formatters/linters, Go tools 9. Offers to install Go tools (gopls, goimports, staticcheck)
10. **CoC extensions** — all language servers in one step 10. Offers to append vim-tmux-navigator bindings to `~/.tmux.conf`
11. **tmux** — optionally appends vim-tmux-navigator bindings to `~/.tmux.conf`
**Supported platforms:** macOS (Homebrew), Debian/Ubuntu (apt), Arch Linux (pacman), Fedora (dnf). **Supported platforms:** macOS (Homebrew), Debian/Ubuntu (apt), Arch (pacman), Fedora (dnf).
### Manual
```bash
git clone https://github.com/m1ngsama/chopsticks.git ~/.vim
ln -sf ~/.vim/.vimrc ~/.vimrc
curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
vim +PlugInstall +qall </dev/null
```
--- ---
## LSP: Tiered Backend ## LSP
Code intelligence is provided by one of two backends, chosen automatically at startup: Code intelligence is provided by **vim-lsp** — a pure VimScript LSP client with no
Node.js dependency. It works on any machine, including servers accessed via SSH.
| Condition | Backend | Capabilities | Install a language server for the current file:
|-----------|---------|-------------|
| Vim 8.0.1453+ **and** Node.js 14.14+ | **CoC** | Full LSP, snippets, extension ecosystem |
| Vim 8.0+ (no Node.js) | **vim-lsp** | LSP via language server binaries, asyncomplete |
| Any Vim | **ALE** | Async linting + auto-fix (always active, both backends) |
Both CoC and vim-lsp expose the same key mappings so switching backends is transparent.
### With Node.js (CoC)
Install language server extensions from inside Vim — or let `install.sh` do it automatically:
```vim ```vim
:CocInstall coc-pyright " Python :LspInstallServer " auto-detects filetype and installs the correct server
:CocInstall coc-tsserver " JavaScript / TypeScript :LspStatus " check server status
:CocInstall coc-go " Go
:CocInstall coc-rust-analyzer " Rust
:CocInstall coc-json coc-yaml " JSON, YAML
:CocInstall coc-html coc-css " HTML, CSS/SCSS
:CocInstall coc-sh " Shell
:CocInstall coc-sql " SQL
``` ```
**Markdown LSP** uses `marksman` as an external binary (not a CoC extension): Supported languages and their servers:
| Language | Server |
|----------|--------|
| Python | pylsp / pyright |
| JavaScript / TypeScript | typescript-language-server |
| Go | gopls |
| Rust | rust-analyzer |
| C / C++ | clangd |
| Shell | bash-language-server |
| HTML | vscode-html-language-server |
| CSS / SCSS | vscode-css-language-server |
| JSON | vscode-json-language-server |
| YAML | yaml-language-server |
| Markdown | marksman |
| SQL | sqls |
**Markdown LSP** requires `marksman` as a standalone binary:
```bash ```bash
brew install marksman # macOS brew install marksman # macOS
sudo pacman -S marksman # Arch sudo pacman -S marksman # Arch
# or: ./install.sh handles it automatically # or: install.sh handles it automatically
``` ```
### Without Node.js (vim-lsp)
Open a source file, then run:
```vim
:LspInstallServer " detects filetype and installs the correct server
:LspStatus " check server status
```
Supported: Python, Go, Rust, TypeScript/JavaScript, Shell, HTML, CSS/SCSS, JSON, YAML, Markdown, SQL.
--- ---
## Key Mappings ## Key Mappings
**Leader key:** `,` (comma) **Leader key:** `,` (comma)
Press `,` and wait 500 ms to see an interactive guide to all bindings (vim-which-key). Press `,?` at any time to open the built-in cheat sheet.
Press `,?` to open the built-in cheat sheet at any time.
### Survival
| Key | Action |
|-----|--------|
| `jk` | Exit insert mode → Normal (ergonomic Escape) |
| `Esc` | Exit insert / visual mode (standard) |
| `Ctrl+s` | Save file (normal and insert mode) |
| `,w` | Save file |
| `,x` | Save and quit |
| `,q` | Quit |
| `,?` | Open cheat sheet |
> **`Ctrl+s` note:** some terminals freeze on `Ctrl+s` (XON/XOFF). Add `stty -ixon`
> to your `~/.bashrc` / `~/.zshrc` to disable this permanently.
### Files and Buffers ### Files and Buffers
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| `Ctrl+p` | Fuzzy file search — git-aware (FZF) | | `Ctrl+p` | Fuzzy file search — git-aware (FZF) |
| `Ctrl+n` | Toggle file tree (NERDTree) | | `,e` | Open netrw file browser |
| `,n` | Reveal current file in NERDTree | | `,E` | Open netrw in vertical split |
| `,b` | Search open buffers (FZF) | | `,b` | Search open buffers (FZF) |
| `,rg` | Project-wide search (ripgrep + FZF) | | `,rg` | Project-wide search (ripgrep + FZF) |
| `,rG` | Ripgrep for word under cursor (literal match) | | `,rG` | Ripgrep for word under cursor |
| `,rt` | Search tags (FZF) |
| `,gF` | Search git-tracked files (FZF) |
| `,l` | Next buffer |
| `,h` | Previous buffer |
| `,bd` | Close current buffer (preserves window layout) |
| `,,` | Switch to last file | | `,,` | Switch to last file |
| `,l` / `,h` | Next / previous buffer |
| `,bd` | Close current buffer (preserves window layout) |
### Windows, Tabs, and tmux ### Code Intelligence (vim-lsp)
| Key | Action |
|-----|--------|
| `Ctrl+h/j/k/l` | Navigate between Vim splits **and** tmux panes |
| `,=` | Increase window height |
| `,-` | Decrease window height |
| `,+` | Increase window width |
| `,_` | Decrease window width |
| `,tn` | New tab |
| `,tc` | Close tab |
| `,tl` | Toggle to last tab |
| `,tv` | Open terminal (vertical split) |
| `,th` | Open terminal (horizontal split) |
| `Esc Esc` | Exit terminal mode |
### Code Intelligence (CoC / vim-lsp)
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| `gd` | Go to definition | | `gd` | Go to definition |
| `gy` | Go to type definition | | `gy` | Go to type definition |
| `gi` | Go to implementation | | `gi` | Go to implementation |
| `gr` | Show all references | | `gr` | Show references |
| `K` | Hover documentation | | `K` | Hover documentation |
| `[g` | Previous diagnostic | | `[g` / `]g` | Previous / next LSP diagnostic |
| `]g` | Next diagnostic | | `[e` / `]e` | Previous / next ALE error |
| `,rn` | Rename symbol | | `,rn` | Rename symbol |
| `,f` | Format selection | | `,f` | Format buffer / selection |
| `,F` | Format whole file | | `,ca` | Code action |
| `,ca` | Code action (cursor position) |
| `,o` | File outline (symbols) | | `,o` | File outline (symbols) |
| `,ws` | Workspace symbols | | `,ws` | Workspace symbols |
| `,cD` | Diagnostics list | | `Tab` / `Shift+Tab` | Navigate completion popup |
| `,qf` | Quick-fix current line (CoC) |
| `Tab` | Next completion item |
| `Shift+Tab` | Previous completion item |
| `Enter` | Confirm completion | | `Enter` | Confirm completion |
Text objects (CoC): `if`/`af` (function inner/around), `ic`/`ac` (class inner/around). ### Markdown
### Linting (ALE — always active)
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| `[e` | Previous error / warning | | `,mp` | Open live preview in browser (previm) |
| `]e` | Next error / warning | | `,mt` | Table of contents |
| `,aD` | Show error detail | | `zr` / `zm` | Unfold / fold all headings |
| `,ad` | Full diagnostics list |
Signs in the gutter: `X` = error, `!` = warning.
### Git (vim-fugitive) ### Git (vim-fugitive)
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| `,gs` | Git status (stage with `s`, commit with `cc`) | | `,gs` | Git status |
| `,gc` | Git commit | | `,gc` | Git commit |
| `,gp` | Git push | | `,gp` | Git push |
| `,gl` | Git pull | | `,gl` | Git pull |
| `,gd` | Git diff | | `,gd` | Git diff |
| `,gb` | Git blame | | `,gb` | Git blame |
### Search and Replace ### Editing
| Key | Action |
|-----|--------|
| `n` / `N` | Next / previous match (cursor centered) |
| `//` | Search for visually selected text |
| `,*` | Search and replace word under cursor (file-wide) |
| `,rG` | Ripgrep word under cursor across project |
| `,<CR>` | Clear search highlight |
### Clipboard
| Key | Action |
|-----|--------|
| `,y` | Yank to system clipboard |
| `,Y` | Yank line to system clipboard |
| `,p` | Paste from system clipboard (after cursor) |
| `,P` | Paste from system clipboard (before cursor) |
### Editing and Navigation
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| `s` + 2 chars | EasyMotion — jump anywhere on screen | | `s` + 2 chars | EasyMotion — jump anywhere on screen |
| `gc` | Toggle comment (visual mode too) |
| `Space` | Toggle code fold | | `Space` | Toggle code fold |
| `Y` | Yank to end of line (consistent with `D`, `C`) | | `Y` | Yank to end of line |
| `Ctrl+d/u` | Half-page scroll (cursor stays centered) | | `Ctrl+d` / `Ctrl+u` | Half-page scroll, cursor centred |
| `>` / `<` | Indent / dedent (keeps visual selection) | | `Alt+j` / `Alt+k` | Move line down / up |
| `Alt+j/k` | Move current line down / up | | `,u` | Undo tree (visual branch history) |
| `0` | Jump to first non-blank character |
| `[q` / `]q` | Previous / next quickfix entry (vim-unimpaired) |
| `,u` | Toggle undo tree (visual branch history) |
| `,tt` | Toggle tagbar (code structure) |
| `F2` | Toggle paste mode |
| `F3` | Toggle absolute line numbers |
| `F4` | Toggle relative line numbers |
### Config and Utilities ### Survival
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| `,ev` | Edit `~/.vimrc` | | `jk` | Exit insert mode |
| `,sv` | Reload `~/.vimrc` | | `Esc` | Exit insert / visual mode |
| `,wa` | Save all open buffers | | `Ctrl+s` | Save |
| `,wd` | Change working directory to current file's location | | `,w` | Save |
| `,cp` | Copy absolute file path to clipboard | | `,x` | Save and quit |
| `,cf` | Copy filename to clipboard | | `,q` | Quit |
| `,qo` / `,qc` | Open / close quickfix list | | `,?` | Open cheat sheet |
### Windows, Tabs, tmux
| Key | Action |
|-----|--------|
| `Ctrl+h/j/k/l` | Navigate Vim splits **and** tmux panes |
| `,tv` / `,th` | Open terminal (vertical / horizontal split) |
| `Esc Esc` | Exit terminal mode |
| `,tn` / `,tc` | New tab / close tab |
| `,tl` | Toggle to last tab |
---
## Markdown
chopsticks treats Markdown as a first-class language.
### In-buffer rendering (concealment)
`vim-markdown` hides syntax markers and renders formatting inline:
- `**bold**` displays as bold text
- `# Heading` hides the `#` characters
- Tables align automatically
The raw syntax reappears when the cursor enters that line.
### Live browser preview (previm)
```vim
,mp " open rendered preview in browser — updates on every save
```
No Node.js required. Uses `open` (macOS) or `xdg-open` (Linux).
### Table of contents
```vim
,mt " open TOC in a quickfix window — press Enter to jump to heading
```
--- ---
## Features ## Features
### Startup Dashboard ### Statusline
Running `vim` (no arguments) opens a full-screen Startify dashboard showing recent A native, hand-written statusline using the Solarized palette:
files, sessions, and bookmarks. Running `vim .` opens NERDTree on the left with
the dashboard on the right.
### Keybinding Guide ```
N ~/.vimrc [+] main [vim] 42:7 68%
```
Press `,` and pause for 500 ms. A popup (vim-which-key) lists all leader bindings - Mode block changes colour by mode (Normal=yellow, Insert=blue, Visual=magenta, Replace=red)
organized into groups. No need to memorize everything upfront. - Git branch via vim-fugitive
- Background matches tmux status bar for a seamless bottom band
### Built-in Cheat Sheet
Press `,?` to open an inline reference covering modes, survival commands, search,
code intelligence, git, and clipboard — without leaving Vim.
### Session Management ### Session Management
@ -348,29 +279,23 @@ code intelligence, git, and clipboard — without leaving Vim.
:Obsess! " stop tracking :Obsess! " stop tracking
``` ```
Sessions are stored in `~/.vim/sessions/` and automatically restored by vim-prosession Sessions auto-restore when you open Vim in the same directory.
the next time you open Vim in the same directory.
### Project-Local Config ### Project-Local Config
Drop a `.vimrc` in any project root to override settings for that project: Drop a `.vimrc` in any project root to override settings:
```vim ```vim
" project/.vimrc " my-project/.vimrc
set shiftwidth=2 set shiftwidth=2
let g:ale_python_black_options = '--line-length=100' let g:ale_python_black_options = '--line-length=100'
``` ```
Loaded automatically via `set exrc`. Restricted to safe options via `set secure`.
### tmux Integration ### tmux Integration
`Ctrl+h/j/k/l` navigates seamlessly between Vim splits and tmux panes — no prefix `Ctrl+h/j/k/l` navigates seamlessly between Vim splits and tmux panes.
key, no mode switch.
**Vim side:** handled by vim-tmux-navigator (installed automatically). Add to `~/.tmux.conf` (or let `install.sh` append it):
**tmux side:** add to `~/.tmux.conf` (or let `install.sh` append it):
```tmux ```tmux
is_vim="ps -o state= -o comm= -t '#{pane_tty}' | grep -iqE '^[^TXZ ]+ +(\S+\/)?g?(view|n?vim?x?)(diff)?$'" is_vim="ps -o state= -o comm= -t '#{pane_tty}' | grep -iqE '^[^TXZ ]+ +(\S+\/)?g?(view|n?vim?x?)(diff)?$'"
@ -380,123 +305,79 @@ bind-key -n 'C-k' if-shell "$is_vim" 'send-keys C-k' 'select-pane -U'
bind-key -n 'C-l' if-shell "$is_vim" 'send-keys C-l' 'select-pane -R' bind-key -n 'C-l' if-shell "$is_vim" 'send-keys C-l' 'select-pane -R'
``` ```
Then reload: `tmux source-file ~/.tmux.conf` ### TTY / SSH Support
> **Note:** the `C-l` binding replaces the terminal's screen-clear shortcut inside Detected automatically when `$TERM` is `linux` or `screen`. In TTY mode:
> tmux. To restore it, add `bind C-l send-keys 'C-l'` — then use `prefix + C-l`.
- True colour and cursorline disabled
- FZF preview windows disabled
- IndentLine guides disabled
- Simplified statusline (no colour)
- Syntax column limit reduced to 120 characters
### Large File Handling ### Large File Handling
Files over 10 MB automatically disable syntax highlighting, undo history, and Files over 10 MB automatically disable syntax highlighting, undo history, and
linting to prevent Vim from stalling. linting to prevent stalling.
### TTY / Console Support
Detected automatically when `$TERM` is `linux` or `screen`. In TTY mode:
- True color and cursorline disabled
- Powerline separators replaced with plain ASCII
- FZF preview windows disabled
- IndentLine guides disabled
- Syntax column limit reduced to 120 characters
- Simpler built-in status line used instead of airline
---
## Language Support
| Language | Indent | Formatter | Linter | LSP |
|----------|--------|-----------|--------|-----|
| Python | 4 sp | black, isort | flake8, pylint | coc-pyright |
| JavaScript | 2 sp | prettier | eslint | coc-tsserver |
| TypeScript | 2 sp | prettier | eslint, tsserver | coc-tsserver |
| Go | tab | gofmt, goimports | staticcheck | coc-go |
| Rust | 4 sp | rustfmt | cargo | coc-rust-analyzer |
| Shell | 2 sp | — | shellcheck | coc-sh |
| YAML | 2 sp | prettier | yamllint | coc-yaml |
| HTML | 2 sp | prettier | — | coc-html |
| CSS / SCSS | 2 sp | prettier | stylelint | coc-css |
| Less | 2 sp | prettier | — | — |
| JSON | 2 sp | prettier | — | coc-json |
| Markdown | 2 sp | prettier | markdownlint | marksman |
| SQL | 4 sp | sqlfluff | sqlfluff | — |
| Dockerfile | 2 sp | — | hadolint | — |
`install.sh` installs all formatters and linters automatically.
ALE runs them asynchronously; format-on-save is active for all supported languages.
--- ---
## Plugins ## Plugins
### Navigation ### Navigation
- **NERDTree** — file tree explorer - **fzf + fzf.vim** — fuzzy finder for files, buffers, tags, ripgrep
- **fzf + fzf.vim** — fuzzy finder for files, buffers, tags, and ripgrep
### Git ### Git
- **vim-fugitive** — full Git integration inside Vim - **vim-fugitive** — full Git integration
- **vim-gitgutter** — diff signs in the sign column - **vim-gitgutter** — diff signs in the sign column
### LSP and Completion ### LSP and Completion
- **coc.nvim** — full LSP + completion via Node.js (recommended) - **vim-lsp** — pure VimScript LSP client
- **vim-lsp** — pure VimScript LSP client (Node.js-free fallback) - **vim-lsp-settings** — auto-configures language servers
- **vim-lsp-settings** — auto-configures language servers for vim-lsp - **asyncomplete.vim** — async completion engine
- **asyncomplete.vim** — async completion engine (vim-lsp mode) - **asyncomplete-lsp.vim** — LSP completion source
### Linting ### Linting and Formatting
- **ALE** — asynchronous lint engine, always active regardless of LSP backend - **ALE** — async linting and format-on-save
### UI ### Markdown
- **vim-airline** — status line and tabline - **vim-markdown** — folding, concealment, table alignment
- **vim-startify** — startup dashboard with session management - **previm** — live browser preview
- **vim-which-key** — keybinding hint popup on leader pause
- **indentLine** — indent guide lines (non-TTY only) ### Language Syntax
- **undotree** — visual undo branch history - **vim-javascript** — enhanced JS syntax
- **tagbar** — code structure sidebar via ctags - **yats.vim** — TypeScript syntax
- **vim-go** — Go syntax and tooling
### Editing ### Editing
- **vim-surround** — change surrounding quotes, brackets, and tags - **vim-surround** — change/delete/add surroundings
- **vim-commentary**`gc` to toggle comments - **vim-commentary**`gc` to toggle comments
- **auto-pairs** — auto-close brackets and quotes - **vim-repeat** — repeat plugin maps with `.`
- **vim-easymotion** — jump anywhere on screen with 2 keystrokes (`s`) - **vim-unimpaired** — bracket shortcut pairs
- **vim-unimpaired** — bracket shortcut pairs (`[q`/`]q`, etc.)
- **targets.vim** — additional text objects - **targets.vim** — additional text objects
- **vim-snippets** — snippet library (used with CoC) - **auto-pairs** — auto-close brackets and quotes
- **vim-tmux-navigator** — seamless `Ctrl+h/j/k/l` across Vim and tmux - **vim-easymotion** — `s` + 2 chars to jump anywhere
### Language Packs ### UI
- **vim-polyglot** — syntax for 100+ languages - **vim-colors-solarized** — color scheme
- **vim-go** — Go syntax and tooling (LSP handled by coc-go) - **undotree** — visual undo branch history
- **vim-startify** — startup dashboard and session list
- **indentLine** — indent guides (non-TTY)
### Session ### Session and Navigation
- **vim-obsession** — continuous session saving - **vim-obsession** — session tracking
- **vim-prosession** — project-level session management - **vim-tmux-navigator** — seamless Vim/tmux pane navigation
### Color Schemes
- **gruvbox** (default), **dracula**, **solarized**, **onedark**
--- ---
## Customization ## Customization
### Change the color scheme
In `~/.vimrc`, find and update the `colorscheme` line:
```vim
colorscheme dracula " options: gruvbox, solarized, onedark
```
True color is enabled automatically when `$COLORTERM=truecolor`. Falls back to
256-color, then 16-color in TTY.
### Per-project overrides ### Per-project overrides
Create `.vimrc` in your project root. Anything placed here overrides the global Create `.vimrc` in your project root:
config for that directory:
```vim ```vim
" my-project/.vimrc " project/.vimrc
set shiftwidth=2 set shiftwidth=2
let g:ale_python_black_options = '--line-length=120' let g:ale_python_black_options = '--line-length=120'
``` ```
@ -512,100 +393,41 @@ Edit `~/.vimrc` directly (`,ev` opens it from inside Vim). Reload with `,sv`.
**Plugins not loading** **Plugins not loading**
```vim ```vim
:PlugInstall " install any missing plugins :PlugInstall " install missing plugins
:PlugUpdate " update all plugins :PlugUpdate " update all plugins
``` ```
**CoC not working** **LSP server not starting**
```bash
node --version # must be 14.14+
```
Inside Vim: `:CocInfo` for diagnostics, `:CocInstall <extension>` to add a language server.
**vim-lsp server not starting**
```vim ```vim
:LspInstallServer " install the correct server for the current filetype :LspInstallServer " install server for current filetype
:LspStatus " check server status :LspStatus " check server status
``` ```
**Markdown LSP not starting** **Markdown preview not opening**
`marksman` must be installed as a standalone binary (not a CoC extension): `previm` uses `open` (macOS) or `xdg-open` (Linux). Make sure a browser is
set as the default handler for HTML files.
```bash
brew install marksman # macOS
sudo pacman -S marksman # Arch
# or: ./install.sh handles it automatically
```
**Colors look wrong** **Colors look wrong**
```bash ```bash
export TERM=xterm-256color # add to ~/.bashrc or ~/.zshrc export TERM=xterm-256color # add to ~/.bashrc or ~/.zshrc
export COLORTERM=truecolor # for true colour
``` ```
For true color: `export COLORTERM=truecolor`.
**ALE linters not found** **ALE linters not found**
```bash ```bash
which flake8 black prettier eslint # verify tools are on PATH which flake8 black prettier eslint # verify tools are on PATH
``` ```
If tools were installed with `pip install --user` or `npm install -g`, make sure
the respective bin directories are on `$PATH`.
**`Ctrl+s` freezes the terminal** **`Ctrl+s` freezes the terminal**
Add `stty -ixon` to your `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish`. Add `stty -ixon` to your `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish`.
This disables XON/XOFF flow control permanently.
--- ---
## Contributing
Bug reports and pull requests are welcome. Please follow these guidelines:
### Reporting a bug
1. Search [existing issues](https://github.com/m1ngsama/chopsticks/issues) before opening a new one.
2. Include your Vim version (`vim --version`), OS, and a minimal reproduction.
3. If the bug is plugin-specific, check whether it reproduces with a minimal config
(`vim -u NONE`) or only with chopsticks loaded.
### Proposing a change
1. Open an issue first to discuss the change, especially for non-trivial additions.
2. Keep the scope focused — one feature or fix per PR.
3. Follow existing conventions: augroups for autocmds, TTY guards for visual features,
conditional plugin loading where appropriate.
4. Update `CHANGELOG.md` with a summary of the change.
### Scope
Chopsticks is an opinionated configuration. Changes should align with the design
principles above — in particular, KISS (no icon fonts, minimal dependencies) and
TTY-compatibility. Neovim-only features and Lua configs are out of scope.
---
## Acknowledgements
Inspired by [amix/vimrc](https://github.com/amix/vimrc).
Built with [vim-plug](https://github.com/junegunn/vim-plug),
[coc.nvim](https://github.com/neoclide/coc.nvim),
[vim-lsp](https://github.com/prabirshrestha/vim-lsp),
and the broader Vim plugin community.
---
## Changelog
See [CHANGELOG.md](CHANGELOG.md).
## License ## License
[MIT](LICENSE) © m1ng [MIT](LICENSE) © m1ng

View file

@ -16,6 +16,7 @@ YELLOW='\033[1;33m'
RED='\033[0;31m' RED='\033[0;31m'
BOLD='\033[1m' BOLD='\033[1m'
CYAN='\033[0;36m' CYAN='\033[0;36m'
DIM='\033[2m'
NC='\033[0m' NC='\033[0m'
ok() { echo -e "${GREEN}[OK]${NC} $1"; } ok() { echo -e "${GREEN}[OK]${NC} $1"; }
@ -29,13 +30,11 @@ die() { echo -e "${RED}[FATAL]${NC} $1" >&2
step() { echo -e "\n${BOLD}==> $1${NC}"; } step() { echo -e "\n${BOLD}==> $1${NC}"; }
info() { echo " $1"; } info() { echo " $1"; }
# Track results for summary
INSTALLED=() INSTALLED=()
SKIPPED=() SKIPPED=()
FAILED=() FAILED=()
# Ask yes/no; returns 0 for yes # Ask yes/no; reads from /dev/tty so it works under: curl | bash
# Reads from /dev/tty so interactive prompts work even under: curl | bash
ask() { ask() {
[[ $AUTO_YES -eq 1 ]] && return 0 [[ $AUTO_YES -eq 1 ]] && return 0
if [[ -t 0 ]]; then if [[ -t 0 ]]; then
@ -43,7 +42,6 @@ ask() {
elif { true </dev/tty; } 2>/dev/null; then elif { true </dev/tty; } 2>/dev/null; then
read -r -p "$1 [y/N] " reply </dev/tty read -r -p "$1 [y/N] " reply </dev/tty
else else
# No terminal available — default to no (safe)
echo "$1 [y/N] N" echo "$1 [y/N] N"
return 1 return 1
fi fi
@ -52,26 +50,19 @@ ask() {
# ── Error trap ──────────────────────────────────────────────────────────────── # ── Error trap ────────────────────────────────────────────────────────────────
on_error() { on_error() {
local line="${BASH_LINENO[0]}" echo -e "\n${RED}[FATAL]${NC} Unexpected error at line ${BASH_LINENO[0]}." >&2
echo -e "\n${RED}[FATAL]${NC} Unexpected error at line $line." >&2
echo " To get a full debug log:" >&2 echo " To get a full debug log:" >&2
echo " ./install.sh 2>&1 | tee /tmp/chopsticks-install.log" >&2 echo " ./install.sh 2>&1 | tee /tmp/chopsticks-install.log" >&2
echo " Report issues: https://github.com/m1ngsama/chopsticks/issues" >&2 echo " Report issues: https://github.com/m1ngsama/chopsticks/issues" >&2
} }
trap on_error ERR trap on_error ERR
# Cleanup temp files on exit
trap 'rm -f /tmp/chopsticks-hadolint /tmp/chopsticks-marksman 2>/dev/null' EXIT trap 'rm -f /tmp/chopsticks-hadolint /tmp/chopsticks-marksman 2>/dev/null' EXIT
# ── Safe download helper ────────────────────────────────────────────────────── # ── Safe download helper ──────────────────────────────────────────────────────
# safe_download <url> <dest>
# Returns 1 if download fails or file is empty / HTML error page
safe_download() { safe_download() {
local url="$1" dest="$2" local url="$1" dest="$2"
curl -fsSL --connect-timeout 15 --retry 3 "$url" -o "$dest" 2>/dev/null || return 1 curl -fsSL --connect-timeout 15 --retry 3 "$url" -o "$dest" 2>/dev/null || return 1
# Reject empty files
[[ -s "$dest" ]] || { rm -f "$dest"; return 1; } [[ -s "$dest" ]] || { rm -f "$dest"; return 1; }
# Reject HTML error pages (GitHub 404, rate limits, etc.)
if head -c 200 "$dest" 2>/dev/null | grep -qi "<!DOCTYPE\|<html"; then if head -c 200 "$dest" 2>/dev/null | grep -qi "<!DOCTYPE\|<html"; then
rm -f "$dest"; return 1 rm -f "$dest"; return 1
fi fi
@ -79,7 +70,6 @@ safe_download() {
} }
# ── Cross-platform package install helper ───────────────────────────────────── # ── Cross-platform package install helper ─────────────────────────────────────
# pkg_install <brew> <apt> <pacman> <dnf> (pass "" to skip that pkg manager)
pkg_install() { pkg_install() {
local brew_pkg="${1:-}" apt_pkg="${2:-}" pac_pkg="${3:-}" dnf_pkg="${4:-}" local brew_pkg="${1:-}" apt_pkg="${2:-}" pac_pkg="${3:-}" dnf_pkg="${4:-}"
if [[ $HAS_BREW -eq 1 && -n "$brew_pkg" ]]; then brew install "$brew_pkg" >/dev/null 2>&1 if [[ $HAS_BREW -eq 1 && -n "$brew_pkg" ]]; then brew install "$brew_pkg" >/dev/null 2>&1
@ -90,25 +80,122 @@ pkg_install() {
fi fi
} }
# ── CPU architecture normalizer ─────────────────────────────────────────────── # ── CPU architecture helpers ──────────────────────────────────────────────────
# Normalize uname -m to the naming convention used by GitHub releases
arch_github() { arch_github() {
case "$(uname -m)" in case "$(uname -m)" in
x86_64) echo "x86_64" ;; x86_64) echo "x86_64" ;;
aarch64|arm64) echo "arm64" ;; aarch64|arm64) echo "arm64" ;;
armv7l) echo "armv7" ;; armv7l) echo "armv7" ;;
*) echo "$(uname -m)" ;; *) uname -m ;;
esac esac
} }
arch_linux_x64() { arch_linux_x64() {
# Returns x64 or arm64 style (used by marksman)
case "$(uname -m)" in case "$(uname -m)" in
x86_64) echo "x64" ;; x86_64) echo "x64" ;;
aarch64|arm64) echo "arm64" ;; aarch64|arm64) echo "arm64" ;;
*) echo "$(uname -m)" ;; *) uname -m ;;
esac esac
} }
# ============================================================================
# Checkbox selection menu
# ============================================================================
#
# Usage:
# _menu_checkbox "Title" "label|description|default(0/1)" ...
#
# Result: global MENU_SEL array — MENU_SEL[i]=1 means item i was selected.
# Globals _MENU_LABELS / _MENU_DESCS remain populated after return.
#
# In --yes mode or non-TTY: uses defaults silently.
# Controls: ↑↓ navigate · Space toggle · a all · n none · Enter confirm
# ─────────────────────────────────────────────────────────────────────────────
_MENU_LABELS=()
_MENU_DESCS=()
_MENU_SELS=()
_MENU_N=0
_MENU_TITLE=""
_MENU_CUR=0
MENU_SEL=()
_menu_draw() {
local i mark
printf "\033[2K${BOLD}%s${NC}\n" "$_MENU_TITLE"
printf "\033[2K %b%b\n" "$DIM" "↑/↓ move Space toggle a all n none Enter confirm${NC}"
printf "\033[2K\n"
for ((i = 0; i < _MENU_N; i++)); do
if [[ ${_MENU_SELS[$i]} -eq 1 ]]; then
mark="${GREEN}${NC}"
else
mark=" "
fi
if [[ $i -eq $_MENU_CUR ]]; then
printf "\033[2K ${BOLD}▶ [%b] %s${NC}\n" "$mark" "${_MENU_LABELS[$i]}"
else
printf "\033[2K [%b] %s\n" "$mark" "${_MENU_LABELS[$i]}"
fi
printf "\033[2K ${CYAN}%s${NC}\n" "${_MENU_DESCS[$i]}"
done
}
_menu_checkbox() {
_MENU_TITLE="$1"; shift
_MENU_LABELS=(); _MENU_DESCS=(); _MENU_SELS=()
_MENU_N=0; _MENU_CUR=0
while [[ $# -gt 0 ]]; do
IFS='|' read -r \
"_MENU_LABELS[$_MENU_N]" \
"_MENU_DESCS[$_MENU_N]" \
"_MENU_SELS[$_MENU_N]" <<< "$1"
shift; : $(( _MENU_N++ ))
done
# Non-interactive or --yes: use defaults, no UI
if [[ $AUTO_YES -eq 1 ]] || ! { true </dev/tty; } 2>/dev/null; then
MENU_SEL=("${_MENU_SELS[@]}")
[[ $AUTO_YES -eq 1 ]] && info "(--yes mode: using all defaults)"
return
fi
# Lines printed per _menu_draw call: 3 header + 2 per item
local _lines=$(( 3 + 2 * _MENU_N ))
local _key _esc _i
tput civis 2>/dev/null # hide cursor
_menu_draw
while true; do
tput cuu "$_lines" 2>/dev/null # move back to top of menu
_menu_draw
IFS= read -r -s -n1 _key </dev/tty
if [[ $_key == $'\x1b' ]]; then
IFS= read -r -s -n2 _esc </dev/tty
case "$_esc" in
'[A') ((_MENU_CUR > 0)) && ((_MENU_CUR--)) ;;
'[B') ((_MENU_CUR < _MENU_N - 1)) && ((_MENU_CUR++)) ;;
esac
elif [[ $_key == ' ' ]]; then
_MENU_SELS[_MENU_CUR]=$(( 1 - _MENU_SELS[_MENU_CUR] ))
elif [[ $_key == 'a' || $_key == 'A' ]]; then
for ((_i = 0; _i < _MENU_N; _i++)); do _MENU_SELS[_i]=1; done
elif [[ $_key == 'n' || $_key == 'N' ]]; then
for ((_i = 0; _i < _MENU_N; _i++)); do _MENU_SELS[_i]=0; done
elif [[ -z $_key ]]; then # Enter
break
fi
done
tput cnorm 2>/dev/null # restore cursor
echo ""
MENU_SEL=("${_MENU_SELS[@]}")
}
# Helper: was menu item at index $1 selected?
_selected() { [[ ${MENU_SEL[${1:-999}]:-0} -eq 1 ]]; }
echo -e "${BOLD}chopsticks — Vim Configuration Installer${NC}" echo -e "${BOLD}chopsticks — Vim Configuration Installer${NC}"
echo "----------------------------------------" echo "----------------------------------------"
@ -119,14 +206,10 @@ echo "----------------------------------------"
step "Detecting environment" step "Detecting environment"
OS="unknown" OS="unknown"
if [[ "$OSTYPE" == darwin* ]]; then if [[ "$OSTYPE" == darwin* ]]; then OS="macos"
OS="macos" elif [[ -f /etc/debian_version ]]; then OS="debian"
elif [[ -f /etc/debian_version ]]; then elif [[ -f /etc/fedora-release ]]; then OS="fedora"
OS="debian" elif [[ -f /etc/arch-release ]]; then OS="arch"
elif [[ -f /etc/fedora-release ]]; then
OS="fedora"
elif [[ -f /etc/arch-release ]]; then
OS="arch"
fi fi
ok "OS: $OS" ok "OS: $OS"
@ -135,44 +218,38 @@ HAS_APT=0; command -v apt >/dev/null 2>&1 && HAS_APT=1
HAS_DNF=0; command -v dnf >/dev/null 2>&1 && HAS_DNF=1 HAS_DNF=0; command -v dnf >/dev/null 2>&1 && HAS_DNF=1
HAS_PACMAN=0; command -v pacman >/dev/null 2>&1 && HAS_PACMAN=1 HAS_PACMAN=0; command -v pacman >/dev/null 2>&1 && HAS_PACMAN=1
# ── sudo ───────────────────────────────────────────────────────────────────── # sudo
HAS_SUDO=0 HAS_SUDO=0
if [[ $OS == "macos" ]]; then if [[ $OS == "macos" ]]; then
# brew handles its own privilege escalation; no sudo needed for system tools HAS_SUDO=1 # brew handles its own privilege escalation
HAS_SUDO=1
elif sudo -n true 2>/dev/null; then elif sudo -n true 2>/dev/null; then
HAS_SUDO=1 HAS_SUDO=1; ok "sudo: available (passwordless)"
ok "sudo: available (passwordless)"
elif [[ $AUTO_YES -eq 1 ]]; then elif [[ $AUTO_YES -eq 1 ]]; then
warn "sudo requires a password but running non-interactively (--yes)" warn "sudo requires a password but running non-interactively (--yes)"
warn "System package installations will be skipped" warn "System package installations will be skipped"
else else
# Prompt once for password now so later sudo calls don't interrupt flow
warn "Some steps require sudo. Authenticating now..." warn "Some steps require sudo. Authenticating now..."
if sudo true; then if sudo true; then
HAS_SUDO=1 HAS_SUDO=1; ok "sudo: authenticated"
ok "sudo: authenticated"
else else
warn "sudo not available — system package installations will be skipped" warn "sudo not available — system package installations will be skipped"
fi fi
fi fi
# ── Network ────────────────────────────────────────────────────────────────── # Network
if curl -fsSL --connect-timeout 5 https://github.com -o /dev/null 2>/dev/null; then if curl -fsSL --connect-timeout 5 https://github.com -o /dev/null 2>/dev/null; then
ok "Network: github.com reachable" ok "Network: github.com reachable"
else else
warn "Network: cannot reach github.com — plugin and binary downloads may fail" warn "Network: cannot reach github.com — plugin and binary downloads may fail"
warn "Check your internet connection or proxy settings before continuing"
fi fi
# ── Homebrew (macOS) ───────────────────────────────────────────────────────── # Homebrew (macOS)
if [[ $OS == "macos" && $HAS_BREW -eq 0 ]]; then if [[ $OS == "macos" && $HAS_BREW -eq 0 ]]; then
warn "Homebrew not found — it is the recommended package manager for macOS" warn "Homebrew not found — it is the recommended package manager for macOS"
if ask "Install Homebrew now? (strongly recommended — required for system tools)"; then if ask "Install Homebrew now? (strongly recommended — required for system tools)"; then
info "This may take a few minutes and will prompt for your password..." info "This may take a few minutes and will prompt for your password..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || \ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || \
die "Homebrew installation failed. Install manually: https://brew.sh" die "Homebrew installation failed. Install manually: https://brew.sh"
# Source brew for Apple Silicon and Intel paths
for brew_path in /opt/homebrew/bin/brew /usr/local/bin/brew; do for brew_path in /opt/homebrew/bin/brew /usr/local/bin/brew; do
[[ -x "$brew_path" ]] && eval "$("$brew_path" shellenv)" && break [[ -x "$brew_path" ]] && eval "$("$brew_path" shellenv)" && break
done done
@ -182,7 +259,7 @@ if [[ $OS == "macos" && $HAS_BREW -eq 0 ]]; then
fi fi
fi fi
# ── curl ───────────────────────────────────────────────────────────────────── # curl
if ! command -v curl >/dev/null 2>&1; then if ! command -v curl >/dev/null 2>&1; then
warn "curl not found — required to download plugins and tools" warn "curl not found — required to download plugins and tools"
if pkg_install curl curl curl curl 2>/dev/null; then if pkg_install curl curl curl curl 2>/dev/null; then
@ -196,7 +273,7 @@ if ! command -v curl >/dev/null 2>&1; then
fi fi
fi fi
# ── git ────────────────────────────────────────────────────────────────────── # git
if ! command -v git >/dev/null 2>&1; then if ! command -v git >/dev/null 2>&1; then
warn "git not found — required for vim-plug to install plugins" warn "git not found — required for vim-plug to install plugins"
if pkg_install git git git git 2>/dev/null; then if pkg_install git git git git 2>/dev/null; then
@ -210,9 +287,8 @@ if ! command -v git >/dev/null 2>&1; then
fi fi
fi fi
# ── vim ────────────────────────────────────────────────────────────────────── # vim
[ -f "$SCRIPT_DIR/.vimrc" ] || die ".vimrc not found in $SCRIPT_DIR — is this the chopsticks repo?" [ -f "$SCRIPT_DIR/.vimrc" ] || die ".vimrc not found in $SCRIPT_DIR — is this the chopsticks repo?"
if ! command -v vim >/dev/null 2>&1; then if ! command -v vim >/dev/null 2>&1; then
warn "vim not found — attempting to install" warn "vim not found — attempting to install"
if pkg_install vim vim vim vim 2>/dev/null; then if pkg_install vim vim vim vim 2>/dev/null; then
@ -225,70 +301,25 @@ if ! command -v vim >/dev/null 2>&1; then
macOS: brew install vim" macOS: brew install vim"
fi fi
fi fi
ok "Found: $(vim --version | head -n1)"
VIM_VERSION=$(vim --version | head -n1)
ok "Found: $VIM_VERSION"
vim --version | grep -q 'Vi IMproved 8\|Vi IMproved 9' || \ vim --version | grep -q 'Vi IMproved 8\|Vi IMproved 9' || \
warn "Vim 8.0+ recommended for full async/LSP support — some features may not work" warn "Vim 8.0+ recommended for full async/LSP support — some features may not work"
# ── Node.js ────────────────────────────────────────────────────────────────── # Node.js (optional — vim-lsp needs no Node.js; only npm formatters do)
HAS_NODE=0; command -v node >/dev/null 2>&1 && HAS_NODE=1 HAS_NODE=0; command -v node >/dev/null 2>&1 && HAS_NODE=1
if [[ $HAS_NODE -eq 1 ]]; then
if [[ $HAS_NODE -eq 0 ]]; then
warn "Node.js not found — CoC LSP and npm-based formatters will be unavailable"
info "Without Node.js, the config falls back to vim-lsp (pure VimScript)."
info ""
info "Install options:"
info " nvm (recommended): curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/HEAD/install.sh | bash"
info " macOS: brew install node"
info " Ubuntu/Debian: curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -"
info " sudo apt-get install -y nodejs"
info " Arch: sudo pacman -S nodejs npm"
info ""
if ask "Install Node.js via nvm now? (recommended — manages multiple Node versions)"; then
info "Fetching latest nvm release..."
NVM_VER=$(curl -fsSL https://api.github.com/repos/nvm-sh/nvm/releases/latest \
| grep '"tag_name"' | cut -d'"' -f4 2>/dev/null) || NVM_VER="v0.40.1"
[[ -z "$NVM_VER" ]] && NVM_VER="v0.40.1"
info "Installing nvm $NVM_VER + Node.js LTS..."
if curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VER}/install.sh" | bash >/dev/null 2>&1; then
export NVM_DIR="$HOME/.nvm"
# shellcheck disable=SC1091
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
if command -v nvm >/dev/null 2>&1; then
nvm install --lts >/dev/null 2>&1 && nvm use --lts >/dev/null 2>&1 || true
command -v node >/dev/null 2>&1 && HAS_NODE=1 && ok "Node.js $(node --version) installed via nvm"
fi
fi
if [[ $HAS_NODE -eq 0 ]]; then
warn "nvm install failed — CoC and npm tools will be skipped"
warn "After manually installing Node.js, re-run: ./install.sh"
fi
else
skip "Node.js — config will use vim-lsp fallback (no Node.js required)"
fi
else
ok "Node.js $(node --version) found" ok "Node.js $(node --version) found"
else
warn "Node.js not found — npm formatters (prettier, eslint) will be unavailable"
warn "LSP still works without Node.js. To add formatters later: brew install node"
fi fi
# ── Python3 ────────────────────────────────────────────────────────────────── # Python3 / pip3
HAS_PYTHON=0; command -v python3 >/dev/null 2>&1 && HAS_PYTHON=1 HAS_PYTHON=0; command -v python3 >/dev/null 2>&1 && HAS_PYTHON=1
HAS_PIP=0; command -v pip3 >/dev/null 2>&1 && HAS_PIP=1 HAS_PIP=0; command -v pip3 >/dev/null 2>&1 && HAS_PIP=1
if [[ $HAS_PYTHON -eq 0 ]]; then if [[ $HAS_PYTHON -eq 0 ]]; then
warn "python3 not found — Python formatters/linters will be unavailable" warn "python3 not found — Python formatters/linters will be unavailable"
if ask "Install Python 3?"; then
if pkg_install python3 python3 python3 python3 2>/dev/null; then
command -v python3 >/dev/null 2>&1 && HAS_PYTHON=1 && ok "Python3 installed"
else
warn "Python3 install failed — Python tools will be skipped"
fi fi
else
skip "Python3"
fi
fi
# Bootstrap pip3 when python3 exists but pip3 is absent (common on Ubuntu minimal) # Bootstrap pip3 when python3 exists but pip3 is absent (common on Ubuntu minimal)
if [[ $HAS_PYTHON -eq 1 && $HAS_PIP -eq 0 ]]; then if [[ $HAS_PYTHON -eq 1 && $HAS_PIP -eq 0 ]]; then
warn "python3 found but pip3 missing — attempting bootstrap" warn "python3 found but pip3 missing — attempting bootstrap"
@ -299,11 +330,10 @@ if [[ $HAS_PYTHON -eq 1 && $HAS_PIP -eq 0 ]]; then
warn "pip3 bootstrap failed — Python tools will be skipped" warn "pip3 bootstrap failed — Python tools will be skipped"
fi fi
fi fi
[[ $HAS_PIP -eq 1 ]] && ok "Python/pip3 found" [[ $HAS_PIP -eq 1 ]] && ok "Python/pip3 found"
[[ $HAS_PYTHON -eq 1 && $HAS_PIP -eq 0 ]] && warn "pip3 not available — Python tools will be skipped" [[ $HAS_PYTHON -eq 1 && $HAS_PIP -eq 0 ]] && warn "pip3 not available — Python tools will be skipped"
# ── Go ─────────────────────────────────────────────────────────────────────── # Go
HAS_GO=0; command -v go >/dev/null 2>&1 && HAS_GO=1 HAS_GO=0; command -v go >/dev/null 2>&1 && HAS_GO=1
[[ $HAS_GO -eq 1 ]] && ok "Go $(go version | awk '{print $3}') found" [[ $HAS_GO -eq 1 ]] && ok "Go $(go version | awk '{print $3}') found"
[[ $HAS_GO -eq 0 ]] && warn "Go not found — Go tools will be skipped (see https://go.dev/dl/)" [[ $HAS_GO -eq 0 ]] && warn "Go not found — Go tools will be skipped (see https://go.dev/dl/)"
@ -316,22 +346,29 @@ step "Setting up symlinks"
if [ -f "$HOME/.vimrc" ] && [ ! -L "$HOME/.vimrc" ]; then if [ -f "$HOME/.vimrc" ] && [ ! -L "$HOME/.vimrc" ]; then
TS=$(date +%Y%m%d_%H%M%S) TS=$(date +%Y%m%d_%H%M%S)
warn "Backing up existing ~/.vimrc → ~/.vimrc.backup.$TS" warn "Backing up existing ~/.vimrc → $HOME/.vimrc.backup.$TS"
mv "$HOME/.vimrc" "$HOME/.vimrc.backup.$TS" mv "$HOME/.vimrc" "$HOME/.vimrc.backup.$TS"
fi fi
ln -sf "$SCRIPT_DIR/.vimrc" "$HOME/.vimrc" ln -sf "$SCRIPT_DIR/.vimrc" "$HOME/.vimrc"
# Verify symlink if [[ -L "$HOME/.vimrc" ]]; then
[[ -L "$HOME/.vimrc" ]] && ok "~/.vimrc → $SCRIPT_DIR/.vimrc" || die "Failed to create ~/.vimrc symlink" ok "$HOME/.vimrc → $SCRIPT_DIR/.vimrc"
else
die "Failed to create ~/.vimrc symlink"
fi
mkdir -p "$HOME/.vim" mkdir -p "$HOME/.vim"
COC_CFG="$HOME/.vim/coc-settings.json" COC_CFG="$HOME/.vim/coc-settings.json"
if [ -f "$COC_CFG" ] && [ ! -L "$COC_CFG" ]; then if [ -f "$COC_CFG" ] && [ ! -L "$COC_CFG" ]; then
TS=$(date +%Y%m%d_%H%M%S) TS=$(date +%Y%m%d_%H%M%S)
warn "Backing up existing coc-settings.json → ~/.vim/coc-settings.json.backup.$TS" warn "Backing up existing coc-settings.json → $COC_CFG.backup.$TS"
mv "$COC_CFG" "$COC_CFG.backup.$TS" mv "$COC_CFG" "$COC_CFG.backup.$TS"
fi fi
ln -sf "$SCRIPT_DIR/coc-settings.json" "$COC_CFG" ln -sf "$SCRIPT_DIR/coc-settings.json" "$COC_CFG"
[[ -L "$COC_CFG" ]] && ok "~/.vim/coc-settings.json → $SCRIPT_DIR/coc-settings.json" || warn "coc-settings.json symlink failed (non-fatal)" if [[ -L "$COC_CFG" ]]; then
ok "$HOME/.vim/coc-settings.json → $SCRIPT_DIR/coc-settings.json"
else
warn "coc-settings.json symlink failed (non-fatal)"
fi
# ============================================================================ # ============================================================================
# 3. vim-plug + Plugins # 3. vim-plug + Plugins
@ -342,13 +379,16 @@ step "Installing vim-plug"
VIM_PLUG="$HOME/.vim/autoload/plug.vim" VIM_PLUG="$HOME/.vim/autoload/plug.vim"
if [ ! -f "$VIM_PLUG" ]; then if [ ! -f "$VIM_PLUG" ]; then
mkdir -p "$HOME/.vim/autoload" mkdir -p "$HOME/.vim/autoload"
if safe_download "https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim" "$VIM_PLUG"; then if safe_download \
"https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim" \
"$VIM_PLUG"; then
ok "vim-plug downloaded" ok "vim-plug downloaded"
else else
# Fallback: git clone
warn "curl download failed — trying git clone fallback" warn "curl download failed — trying git clone fallback"
if git clone --depth=1 https://github.com/junegunn/vim-plug.git /tmp/vim-plug-src 2>/dev/null; then if git clone --depth=1 https://github.com/junegunn/vim-plug.git \
cp /tmp/vim-plug-src/plug.vim "$VIM_PLUG" && rm -rf /tmp/vim-plug-src /tmp/vim-plug-src 2>/dev/null; then
cp /tmp/vim-plug-src/plug.vim "$VIM_PLUG"
rm -rf /tmp/vim-plug-src
ok "vim-plug installed (via git)" ok "vim-plug installed (via git)"
else else
die "vim-plug installation failed. Check your network connection and try again." die "vim-plug installation failed. Check your network connection and try again."
@ -360,44 +400,148 @@ else
fi fi
step "Installing Vim plugins" step "Installing Vim plugins"
# Use /dev/tty when available so vim properly manages the terminal (alternate
# screen buffer, cursor, colours) and restores it cleanly on exit.
# Fall back to --not-a-term for non-interactive/CI environments.
_vim_run() { _vim_run() {
if { true </dev/tty; } 2>/dev/null; then if { true </dev/tty; } 2>/dev/null; then
# Interactive terminal: let vim manage the alternate screen properly # Interactive terminal: vim uses alternate screen; user sees progress
vim "$@" </dev/tty vim "$@" </dev/tty
else else
# Non-interactive / CI: TERM=dumb suppresses all escape sequences; # No TTY (SSH batch, CI): do NOT redirect stdin (causes "Error reading input" exit)
# stdout+stderr redirected so nothing leaks into installer output # or stdout (breaks async job callbacks — partial install).
TERM=dumb vim "$@" </dev/null >/dev/null 2>&1 # Redirect only stderr; escape sequences appear on stdout but installation succeeds.
vim --not-a-term "$@" 2>/dev/null
fi fi
} }
_vim_run +PlugInstall +qall || true # post-install hooks (e.g. fzf) may exit non-zero; harmless _vim_run +'PlugClean!' +qall || true # remove plugins no longer in vimrc; ignore exit code (none expected)
ok "Plugins installed" _vim_run +'PlugInstall --sync' +qall || true # fzf post-install hook may exit non-zero; harmless
_plug_count=$(ls -1 "$HOME/.vim/plugged" 2>/dev/null | wc -l | tr -d ' ')
if [[ $_plug_count -eq 0 ]]; then
die "Plugin installation failed — ~/.vim/plugged is empty. Check network and retry."
fi
ok "Plugins installed ($_plug_count)"
# ============================================================================ # ============================================================================
# 4. System Tools # 4. Module Selection
# ============================================================================
step "Select optional components"
_ITEMS=()
_idx=0
# Index map (-1 = not in menu / unavailable)
_I_RIPGREP=-1; _I_FZF=-1; _I_CTAGS=-1; _I_SHELLCHECK=-1
_I_HADOLINT=-1; _I_MARKSMAN=-1
_I_NPM=-1; _I_PYTHON=-1; _I_GO=-1; _I_TMUX=-1
# Is any package manager available?
HAS_PKG_MGR=0
if [[ $HAS_BREW -eq 1 ]] || \
{ [[ $HAS_APT -eq 1 || $HAS_PACMAN -eq 1 || $HAS_DNF -eq 1 ]] && [[ $HAS_SUDO -eq 1 ]]; }; then
HAS_PKG_MGR=1
fi
# ── System tools ─────────────────────────────────────────────────────────────
if [[ $HAS_PKG_MGR -eq 1 ]]; then
_I_RIPGREP=$_idx
_ITEMS+=("ripgrep|,rg / ,rG project-wide search · powers Ctrl+p preview|1")
: $(( _idx++ ))
_I_FZF=$_idx
_ITEMS+=("fzf|Ctrl+p fuzzy file search · ,b buffers · ,rt tag search|1")
: $(( _idx++ ))
_I_CTAGS=$_idx
_ITEMS+=("universal-ctags|code symbol index (backing engine for ,rt tag jumps)|1")
: $(( _idx++ ))
_I_SHELLCHECK=$_idx
_ITEMS+=("shellcheck|shell script static analysis (ALE integration, on-save)|1")
: $(( _idx++ ))
_I_HADOLINT=$_idx
_ITEMS+=("hadolint|Dockerfile linting (ALE integration, on-save)|1")
: $(( _idx++ ))
_I_MARKSMAN=$_idx
_ITEMS+=("marksman|Markdown LSP — completion · go-to-definition · live diagnostics|1")
: $(( _idx++ ))
else
warn "No package manager available — system tools skipped"
fi
# ── npm tools ────────────────────────────────────────────────────────────────
if [[ $HAS_NODE -eq 1 ]]; then
_I_NPM=$_idx
_ITEMS+=("npm formatter suite|prettier / eslint / markdownlint / stylelint / tsc — ALE fix-on-save|1")
: $(( _idx++ ))
fi
# ── Python tools ─────────────────────────────────────────────────────────────
if [[ $HAS_PIP -eq 1 ]]; then
_I_PYTHON=$_idx
_ITEMS+=("Python tool suite|black / isort / flake8 / pylint / yamllint / sqlfluff — ALE fix-on-save|1")
: $(( _idx++ ))
fi
# ── Go tools ─────────────────────────────────────────────────────────────────
if [[ $HAS_GO -eq 1 ]]; then
_I_GO=$_idx
_ITEMS+=("Go tool suite|gopls (LSP) / goimports / staticcheck — completion · format · analysis|1")
: $(( _idx++ ))
fi
# ── tmux ─────────────────────────────────────────────────────────────────────
if command -v tmux >/dev/null 2>&1; then
if ! grep -q 'vim-tmux-navigator' "$HOME/.tmux.conf" 2>/dev/null; then
_I_TMUX=$_idx
_ITEMS+=("tmux integration|seamless Ctrl+h/j/k/l navigation between vim and tmux panes|1")
: $(( _idx++ ))
else
ok "tmux integration (vim-tmux-navigator already configured)"
fi
fi
if [[ ${#_ITEMS[@]} -gt 0 ]]; then
_menu_checkbox "Select components to install:" "${_ITEMS[@]}"
echo -e "${BOLD}Install plan:${NC}"
for ((_i = 0; _i < _MENU_N; _i++)); do
if [[ ${MENU_SEL[$_i]:-0} -eq 1 ]]; then
echo -e " ${GREEN}${NC} ${_MENU_LABELS[$_i]}"
else
echo -e " ${DIM}${NC} ${_MENU_LABELS[$_i]}"
fi
done
echo ""
else
warn "No optional components available for this environment"
MENU_SEL=()
fi
# ============================================================================
# 5. System Tools
# ============================================================================ # ============================================================================
step "System tools" step "System tools"
if [[ $OS == "macos" && $HAS_BREW -eq 0 ]]; then if [[ $HAS_PKG_MGR -eq 0 ]]; then
skip "system tools (Homebrew not available — install brew first, then re-run)" skip "system tools (no package manager available)"
SKIPPED+=("ripgrep" "fzf" "ctags" "shellcheck" "hadolint" "marksman") SKIPPED+=("ripgrep" "fzf" "universal-ctags" "shellcheck" "hadolint" "marksman")
elif ask "Install system tools (ripgrep, fzf, ctags, shellcheck, hadolint, marksman)?"; then else
install_sys() { # _do_sys <name> <cmd_check> <idx> <brew_pkg> <apt_pkg> <pac_pkg> [dnf_pkg]
local name="$1" check="$2"; shift 2 _do_sys() {
if command -v "$check" >/dev/null 2>&1; then local name="$1" check="$2" idx="$3"
ok "$name (already installed)" local brew_p="${4:-}" apt_p="${5:-}" pac_p="${6:-}" dnf_p="${7:-}"
return
if [[ $idx -lt 0 ]] || ! _selected "$idx"; then
skip "$name"; SKIPPED+=("$name"); return
fi fi
local installed=0 if command -v "$check" >/dev/null 2>&1; then
for cmd in "$@"; do ok "$name (already installed)"; return
if eval "$cmd" >/dev/null 2>&1; then installed=1; break; fi fi
done if pkg_install "$brew_p" "$apt_p" "$pac_p" "$dnf_p"; then
if [[ $installed -eq 1 ]]; then
ok "$name"; INSTALLED+=("$name") ok "$name"; INSTALLED+=("$name")
else else
fail "$name — could not install automatically (install manually)" fail "$name — could not install automatically (install manually)"
@ -405,111 +549,101 @@ elif ask "Install system tools (ripgrep, fzf, ctags, shellcheck, hadolint, marks
fi fi
} }
# _do_binary_apt: for tools with no apt/dnf package — download binary from GitHub
_do_binary_apt() {
local name="$1" check="$2" idx="$3" url="$4" tmp="$5"
if [[ $idx -lt 0 ]] || ! _selected "$idx"; then
skip "$name"; SKIPPED+=("$name"); return
fi
if command -v "$check" >/dev/null 2>&1; then
ok "$name (already installed)"; return
fi
if safe_download "$url" "$tmp"; then
chmod +x "$tmp" && sudo mv "$tmp" /usr/local/bin/"$check"
ok "$name"; INSTALLED+=("$name")
else
fail "$name — binary download failed (install manually)"
FAILED+=("$name")
fi
}
if [[ $OS == "macos" ]]; then if [[ $OS == "macos" ]]; then
install_sys "ripgrep" rg "brew install ripgrep" _do_sys "ripgrep" rg "$_I_RIPGREP" ripgrep "" "" ""
install_sys "fzf" fzf "brew install fzf" _do_sys "fzf" fzf "$_I_FZF" fzf "" "" ""
install_sys "universal-ctags" ctags "brew install universal-ctags" _do_sys "universal-ctags" ctags "$_I_CTAGS" universal-ctags "" "" ""
install_sys "shellcheck" shellcheck "brew install shellcheck" _do_sys "shellcheck" shellcheck "$_I_SHELLCHECK" shellcheck "" "" ""
install_sys "hadolint" hadolint "brew install hadolint" _do_sys "hadolint" hadolint "$_I_HADOLINT" hadolint "" "" ""
install_sys "marksman" marksman "brew install marksman" _do_sys "marksman" marksman "$_I_MARKSMAN" marksman "" "" ""
elif [[ $HAS_APT -eq 1 ]]; then elif [[ $HAS_APT -eq 1 ]]; then
if [[ $HAS_SUDO -eq 0 ]]; then [[ $HAS_SUDO -eq 1 ]] && sudo apt-get update -qq
warn "No sudo — skipping apt system tools" _do_sys "ripgrep" rg "$_I_RIPGREP" "" ripgrep "" ""
SKIPPED+=("ripgrep" "fzf" "ctags" "shellcheck" "hadolint" "marksman") _do_sys "fzf" fzf "$_I_FZF" "" fzf "" ""
else _do_sys "universal-ctags" ctags "$_I_CTAGS" "" universal-ctags "" ""
sudo apt-get update -qq _do_sys "shellcheck" shellcheck "$_I_SHELLCHECK" "" shellcheck "" ""
install_sys "ripgrep" rg "sudo apt-get install -y ripgrep"
install_sys "fzf" fzf "sudo apt-get install -y fzf"
install_sys "universal-ctags" ctags "sudo apt-get install -y universal-ctags"
install_sys "shellcheck" shellcheck "sudo apt-get install -y shellcheck"
# hadolint: no apt package — download binary from GitHub releases # hadolint: no apt package — binary from GitHub releases
if command -v hadolint >/dev/null 2>&1; then
ok "hadolint (already installed)"
else
HARCH=$(arch_github) HARCH=$(arch_github)
HVER=$(curl -fsSL https://api.github.com/repos/hadolint/hadolint/releases/latest \ HVER=$(curl -fsSL https://api.github.com/repos/hadolint/hadolint/releases/latest \
| grep '"tag_name"' | cut -d'"' -f4 2>/dev/null) || HVER="" | grep '"tag_name"' | cut -d'"' -f4 2>/dev/null) || HVER=""
if [[ -n "$HVER" ]] && safe_download \ _do_binary_apt "hadolint" hadolint "$_I_HADOLINT" \
"https://github.com/hadolint/hadolint/releases/download/${HVER}/hadolint-Linux-${HARCH}" \ "https://github.com/hadolint/hadolint/releases/download/${HVER}/hadolint-Linux-${HARCH}" \
/tmp/chopsticks-hadolint; then /tmp/chopsticks-hadolint
chmod +x /tmp/chopsticks-hadolint && sudo mv /tmp/chopsticks-hadolint /usr/local/bin/hadolint
ok "hadolint"; INSTALLED+=("hadolint")
else
fail "hadolint — download failed (install manually: https://github.com/hadolint/hadolint/releases)"
FAILED+=("hadolint")
fi
fi
# marksman: no apt package — download binary from GitHub releases # marksman: no apt package — binary from GitHub releases
if command -v marksman >/dev/null 2>&1; then
ok "marksman (already installed)"
else
MARCH=$(arch_linux_x64) MARCH=$(arch_linux_x64)
MVER=$(curl -fsSL https://api.github.com/repos/artempyanykh/marksman/releases/latest \ MVER=$(curl -fsSL https://api.github.com/repos/artempyanykh/marksman/releases/latest \
| grep '"tag_name"' | cut -d'"' -f4 2>/dev/null) || MVER="" | grep '"tag_name"' | cut -d'"' -f4 2>/dev/null) || MVER=""
if [[ -n "$MVER" ]] && safe_download \ _do_binary_apt "marksman" marksman "$_I_MARKSMAN" \
"https://github.com/artempyanykh/marksman/releases/download/${MVER}/marksman-linux-${MARCH}" \ "https://github.com/artempyanykh/marksman/releases/download/${MVER}/marksman-linux-${MARCH}" \
/tmp/chopsticks-marksman; then /tmp/chopsticks-marksman
chmod +x /tmp/chopsticks-marksman && sudo mv /tmp/chopsticks-marksman /usr/local/bin/marksman
ok "marksman"; INSTALLED+=("marksman")
else
fail "marksman — download failed (install manually: https://github.com/artempyanykh/marksman/releases)"
FAILED+=("marksman")
fi
fi
fi
elif [[ $HAS_PACMAN -eq 1 ]]; then elif [[ $HAS_PACMAN -eq 1 ]]; then
if [[ $HAS_SUDO -eq 0 ]]; then _do_sys "ripgrep" rg "$_I_RIPGREP" "" "" ripgrep ""
warn "No sudo — skipping pacman system tools" _do_sys "fzf" fzf "$_I_FZF" "" "" fzf ""
SKIPPED+=("ripgrep" "fzf" "ctags" "shellcheck" "hadolint" "marksman") _do_sys "universal-ctags" ctags "$_I_CTAGS" "" "" ctags ""
else _do_sys "shellcheck" shellcheck "$_I_SHELLCHECK" "" "" shellcheck ""
install_sys "ripgrep" rg "sudo pacman -S --noconfirm ripgrep" _do_sys "hadolint" hadolint "$_I_HADOLINT" "" "" hadolint ""
install_sys "fzf" fzf "sudo pacman -S --noconfirm fzf" _do_sys "marksman" marksman "$_I_MARKSMAN" "" "" marksman ""
install_sys "universal-ctags" ctags "sudo pacman -S --noconfirm ctags"
install_sys "shellcheck" shellcheck "sudo pacman -S --noconfirm shellcheck"
install_sys "hadolint" hadolint "sudo pacman -S --noconfirm hadolint"
install_sys "marksman" marksman "sudo pacman -S --noconfirm marksman"
fi
elif [[ $HAS_DNF -eq 1 ]]; then elif [[ $HAS_DNF -eq 1 ]]; then
if [[ $HAS_SUDO -eq 0 ]]; then _do_sys "ripgrep" rg "$_I_RIPGREP" "" "" "" ripgrep
warn "No sudo — skipping dnf system tools" _do_sys "fzf" fzf "$_I_FZF" "" "" "" fzf
SKIPPED+=("ripgrep" "fzf" "shellcheck" "ctags" "hadolint" "marksman") _do_sys "shellcheck" shellcheck "$_I_SHELLCHECK" "" "" "" ShellCheck
else if [[ $_I_CTAGS -ge 0 ]] && _selected "$_I_CTAGS"; then
install_sys "ripgrep" rg "sudo dnf install -y ripgrep" skip "universal-ctags — Fedora: install manually: sudo dnf install ctags"
install_sys "fzf" fzf "sudo dnf install -y fzf" SKIPPED+=("universal-ctags")
install_sys "shellcheck" shellcheck "sudo dnf install -y ShellCheck" fi
skip "universal-ctags — install manually: sudo dnf install ctags" if [[ $_I_HADOLINT -ge 0 ]] && _selected "$_I_HADOLINT"; then
SKIPPED+=("ctags") skip "hadolint — Fedora: install manually: https://github.com/hadolint/hadolint/releases"
skip "hadolint — install manually: https://github.com/hadolint/hadolint/releases"
SKIPPED+=("hadolint") SKIPPED+=("hadolint")
skip "marksman — install manually: https://github.com/artempyanykh/marksman/releases" fi
if [[ $_I_MARKSMAN -ge 0 ]] && _selected "$_I_MARKSMAN"; then
skip "marksman — Fedora: install manually: https://github.com/artempyanykh/marksman/releases"
SKIPPED+=("marksman") SKIPPED+=("marksman")
fi fi
else else
warn "Unknown distro — skipping system tools (install manually)" warn "Unknown distro — skipping system tools (install manually)"
SKIPPED+=("ripgrep" "fzf" "ctags" "shellcheck" "hadolint" "marksman") SKIPPED+=("ripgrep" "fzf" "universal-ctags" "shellcheck" "hadolint" "marksman")
fi fi
else fi # end HAS_PKG_MGR
skip "system tools"
SKIPPED+=("ripgrep" "fzf" "ctags" "shellcheck" "hadolint" "marksman")
fi
# ============================================================================ # ============================================================================
# 5. npm tools # 6. npm Tools
# ============================================================================ # ============================================================================
step "npm tools (formatters + linters)" step "npm tools (formatters + linters)"
if [[ $HAS_NODE -eq 1 ]]; then if [[ $HAS_NODE -eq 0 ]]; then
if ask "Install npm tools (prettier, markdownlint-cli, stylelint, eslint, typescript)?"; then skip "npm tools (Node.js not installed)"
SKIPPED+=("prettier" "markdownlint-cli" "stylelint" "eslint" "typescript")
elif [[ $_I_NPM -lt 0 ]] || ! _selected "$_I_NPM"; then
skip "npm formatter suite (skipped by user)"
SKIPPED+=("prettier" "markdownlint-cli" "stylelint" "eslint" "typescript")
else
npm_install() { npm_install() {
local pkg="$1"; local check="${2:-$1}" local pkg="$1" check="${2:-$1}"
if command -v "$check" >/dev/null 2>&1; then if command -v "$check" >/dev/null 2>&1; then
ok "$pkg (already installed)"; return ok "$pkg (already installed)"; return
fi fi
@ -525,25 +659,23 @@ if [[ $HAS_NODE -eq 1 ]]; then
npm_install stylelint-config-standard npm_install stylelint-config-standard
npm_install eslint npm_install eslint
npm_install typescript tsc npm_install typescript tsc
else
skip "npm tools"
SKIPPED+=("prettier" "markdownlint-cli" "stylelint" "eslint" "typescript")
fi
else
skip "npm tools (Node.js not installed)"
SKIPPED+=("prettier" "markdownlint-cli" "stylelint" "eslint" "typescript")
fi fi
# ============================================================================ # ============================================================================
# 6. Python tools # 7. Python Tools
# ============================================================================ # ============================================================================
step "Python tools (formatters + linters)" step "Python tools (formatters + linters)"
if [[ $HAS_PIP -eq 1 ]]; then if [[ $HAS_PIP -eq 0 ]]; then
if ask "Install Python tools (black, isort, flake8, pylint, yamllint, sqlfluff)?"; then skip "Python tools (pip3 not installed)"
SKIPPED+=("black" "isort" "flake8" "pylint" "yamllint" "sqlfluff")
elif [[ $_I_PYTHON -lt 0 ]] || ! _selected "$_I_PYTHON"; then
skip "Python tool suite (skipped by user)"
SKIPPED+=("black" "isort" "flake8" "pylint" "yamllint" "sqlfluff")
else
pip_install() { pip_install() {
local pkg="$1"; local check="${2:-$1}" local pkg="$1" check="${2:-$1}"
if command -v "$check" >/dev/null 2>&1; then if command -v "$check" >/dev/null 2>&1; then
ok "$pkg (already installed)"; return ok "$pkg (already installed)"; return
fi fi
@ -560,23 +692,21 @@ if [[ $HAS_PIP -eq 1 ]]; then
pip_install pylint pip_install pylint
pip_install yamllint pip_install yamllint
pip_install sqlfluff pip_install sqlfluff
else
skip "Python tools"
SKIPPED+=("black" "isort" "flake8" "pylint" "yamllint" "sqlfluff")
fi
else
skip "Python tools (pip3 not installed)"
SKIPPED+=("black" "isort" "flake8" "pylint" "yamllint" "sqlfluff")
fi fi
# ============================================================================ # ============================================================================
# 7. Go tools # 8. Go Tools
# ============================================================================ # ============================================================================
step "Go tools" step "Go tools"
if [[ $HAS_GO -eq 1 ]]; then if [[ $HAS_GO -eq 0 ]]; then
if ask "Install Go tools (gopls, goimports, staticcheck)?"; then skip "Go tools (go not installed — see https://go.dev/dl/)"
SKIPPED+=("gopls" "goimports" "staticcheck")
elif [[ $_I_GO -lt 0 ]] || ! _selected "$_I_GO"; then
skip "Go tool suite (skipped by user)"
SKIPPED+=("gopls" "goimports" "staticcheck")
else
GOBIN="$(go env GOPATH)/bin" GOBIN="$(go env GOPATH)/bin"
export PATH="$PATH:$GOBIN" export PATH="$PATH:$GOBIN"
@ -597,26 +727,24 @@ if [[ $HAS_GO -eq 1 ]]; then
echo "$PATH" | grep -q "$GOBIN" || \ echo "$PATH" | grep -q "$GOBIN" || \
warn "Add Go binaries to PATH: export PATH=\"\$PATH:$GOBIN\"" warn "Add Go binaries to PATH: export PATH=\"\$PATH:$GOBIN\""
else
skip "Go tools"
SKIPPED+=("gopls" "goimports" "staticcheck")
fi
else
skip "Go tools (go not installed — see https://go.dev/dl/)"
SKIPPED+=("gopls" "goimports" "staticcheck")
fi fi
# ============================================================================ # ============================================================================
# 8. tmux: vim-tmux-navigator integration # 9. tmux: vim-tmux-navigator integration
# ============================================================================ # ============================================================================
step "tmux: vim-tmux-navigator integration" step "tmux: vim-tmux-navigator integration"
if command -v tmux >/dev/null 2>&1; then if ! command -v tmux >/dev/null 2>&1; then
skip "tmux not found — skipping navigator config"
SKIPPED+=("tmux-navigator-config")
elif [[ $_I_TMUX -lt 0 ]]; then
: # already configured — noted earlier
elif ! _selected "$_I_TMUX"; then
skip "tmux navigator config (skipped by user)"
SKIPPED+=("tmux-navigator-config")
else
TMUX_CONF="$HOME/.tmux.conf" TMUX_CONF="$HOME/.tmux.conf"
if grep -q 'vim-tmux-navigator' "$TMUX_CONF" 2>/dev/null; then
ok "vim-tmux-navigator bindings already present in ~/.tmux.conf"
elif ask "Append vim-tmux-navigator bindings to ~/.tmux.conf (enables seamless Ctrl+h/j/k/l across vim and tmux)?"; then
cat >> "$TMUX_CONF" << 'TMUXEOF' cat >> "$TMUX_CONF" << 'TMUXEOF'
# vim-tmux-navigator: seamless Ctrl+h/j/k/l navigation between vim and tmux # vim-tmux-navigator: seamless Ctrl+h/j/k/l navigation between vim and tmux
@ -631,34 +759,21 @@ TMUXEOF
warn "Note: C-l now navigates panes instead of clearing the screen." warn "Note: C-l now navigates panes instead of clearing the screen."
warn " To restore clear: add 'bind C-l send-keys C-l' to ~/.tmux.conf" warn " To restore clear: add 'bind C-l send-keys C-l' to ~/.tmux.conf"
INSTALLED+=("tmux-navigator-config") INSTALLED+=("tmux-navigator-config")
else
skip "tmux navigator config"
SKIPPED+=("tmux-navigator-config")
fi
else
skip "tmux not found — skipping navigator config"
SKIPPED+=("tmux-navigator-config")
fi fi
# ============================================================================ # ============================================================================
# 9. CoC language server extensions # 10. LSP language servers
# ============================================================================ # ============================================================================
step "CoC language server extensions" step "LSP language servers"
info "vim-lsp installs language servers on demand — no action needed here."
if [[ $HAS_NODE -eq 1 ]]; then info ""
if ask "Install CoC language servers (LSP for all configured languages)?"; then info "To install a server: open a source file in Vim and run:"
info "(Downloading CoC extensions via npm — this may take 1-3 minutes)" info " :LspInstallServer"
# Note: coc-marksman doesn't exist on npm — markdown LSP is handled via coc-settings.json info ""
_vim_run +'CocInstall -sync coc-json coc-tsserver coc-pyright coc-sh coc-html coc-css coc-yaml coc-go coc-rust-analyzer coc-sql' +qall info "Supported: Python, JS/TS, Go, Rust, C/C++, Shell, HTML, CSS, JSON, YAML, Markdown, SQL"
ok "CoC language servers installed" info ""
else info "For Markdown LSP (marksman), the installer already handled it above."
skip "CoC language servers"
info "Install later with :CocInstall <name> inside Vim"
fi
else
warn "Node.js not found — using vim-lsp fallback (run :LspInstallServer inside Vim for each language)"
fi
# ============================================================================ # ============================================================================
# Summary # Summary
@ -689,15 +804,15 @@ echo -e "${BOLD}---------------------------------------${NC}"
echo -e "${BOLD} You're ready. Open Vim with:${NC}" echo -e "${BOLD} You're ready. Open Vim with:${NC}"
echo -e "${BOLD}---------------------------------------${NC}" echo -e "${BOLD}---------------------------------------${NC}"
echo -e " ${CYAN}vim${NC} Launch startup dashboard" echo -e " ${CYAN}vim${NC} Launch startup dashboard"
echo -e " ${CYAN}vim .${NC} Open file tree + dashboard" echo -e " ${CYAN}vim .${NC} Open dashboard in current directory"
echo -e " ${CYAN}vim myfile${NC} Edit a specific file" echo -e " ${CYAN}vim myfile${NC} Edit a specific file"
echo "" echo ""
echo -e "${BOLD} Survival Guide (first-time Vim users)${NC}" echo -e "${BOLD} First steps inside Vim${NC}"
echo -e " ${CYAN}Esc${NC} or ${CYAN}jk${NC} Exit insert mode → back to Normal" echo -e " ${CYAN}Esc${NC} or ${CYAN}jk${NC} Exit insert mode → back to Normal"
echo -e " ${CYAN}:q!${NC} + Enter Emergency quit without saving" echo -e " ${CYAN}:q!${NC} + Enter Emergency quit without saving"
echo -e " ${CYAN},x${NC} Save and quit" echo -e " ${CYAN},x${NC} Save and quit"
echo -e " ${CYAN},?${NC} Open cheat sheet inside Vim" echo -e " ${CYAN},?${NC} Open cheat sheet"
echo -e " ${CYAN},${NC} + pause Interactive keybinding guide" echo -e " ${CYAN}:LspInstallServer${NC} Install LSP for current filetype"
echo "" echo ""
echo -e "${YELLOW}[!]${NC} Ctrl+s is mapped to save in Vim." echo -e "${YELLOW}[!]${NC} Ctrl+s is mapped to save in Vim."
echo " If it freezes your terminal, add this to ~/.bashrc or ~/.zshrc:" echo " If it freezes your terminal, add this to ~/.bashrc or ~/.zshrc:"