chore: drop macOS/Windows support and offline nvim tooling

- Remove macOS/Windows clipboard branches, homebrew fzf path from .vimrc
- Remove is_macos/is_windows logic from wezterm.lua
- Drop scripts/nvim-offline-* and NVIM_OFFLINE plumbing from nvim config
- Clean macOS section from .gitignore; simplify .gitattributes to LF-only
- Trim README of offline-bundle section
This commit is contained in:
Claude 2026-06-07 16:50:33 +00:00
parent 21c315d5f2
commit 84fbb30d82
No known key found for this signature in database
12 changed files with 20 additions and 728 deletions

View file

@ -166,13 +166,7 @@ set background=dark
" hi VertSplit guibg=NONE guifg=NONE ctermbg=NONE ctermfg=NONE " hi VertSplit guibg=NONE guifg=NONE ctermbg=NONE ctermfg=NONE
" Sync clipboard with OS " Sync clipboard with OS
if has('win32') || has('win64') set clipboard=unnamedplus
set clipboard=unnamedplus " Windows
elseif has('macunix')
set clipboard=unnamed " macOS
else
set clipboard=unnamedplus " Linux/WSL
endif
" True colors " True colors
if !has('gui_running') && &term =~ '\%(screen\|tmux\)' if !has('gui_running') && &term =~ '\%(screen\|tmux\)'
@ -198,4 +192,3 @@ augroup netrw_setup | au!
au FileType netrw nmap <buffer> l <CR> au FileType netrw nmap <buffer> l <CR>
augroup END augroup END
set rtp+=/opt/homebrew/opt/fzf

View file

@ -42,10 +42,6 @@ vim.opt.iskeyword:append '-' -- hyphenated words recognized by searches
vim.opt.formatoptions:remove { 'c', 'r', 'o' } -- don't insert the current comment leader automatically for auto-wrapping comments using 'textwidth', hitting <Enter> in insert mode, or hitting 'o' or 'O' in normal mode. vim.opt.formatoptions:remove { 'c', 'r', 'o' } -- don't insert the current comment leader automatically for auto-wrapping comments using 'textwidth', hitting <Enter> in insert mode, or hitting 'o' or 'O' in normal mode.
vim.opt.runtimepath:remove '/usr/share/vim/vimfiles' -- separate vim plugins from neovim in case vim still in use vim.opt.runtimepath:remove '/usr/share/vim/vimfiles' -- separate vim plugins from neovim in case vim still in use
-- Offline mode: set NVIM_OFFLINE=1 to disable all network operations
-- (plugin update checks, Mason auto-install, Treesitter auto-install)
vim.g.offline_mode = vim.env.NVIM_OFFLINE == '1'
-- Neovim 0.12 changed directive captures from TSNode to TSNode[] (arrays). -- Neovim 0.12 changed directive captures from TSNode to TSNode[] (arrays).
-- nvim-treesitter predicates do match[capture_id] expecting a single TSNode, -- nvim-treesitter predicates do match[capture_id] expecting a single TSNode,
-- but now receive a table. The nil guard passes, then get_node_text calls -- but now receive a table. The nil guard passes, then get_node_text calls

View file

@ -22,19 +22,14 @@ if not (vim.uv or vim.loop).fs_stat(lazypath) then
end end
vim.opt.rtp:prepend(lazypath) vim.opt.rtp:prepend(lazypath)
local offline = vim.g.offline_mode
require('lazy').setup { require('lazy').setup {
spec = { spec = {
{ import = 'brian.plugins' }, { import = 'brian.plugins' },
}, },
install = { colorscheme = { 'kanagawa' } }, install = { colorscheme = { 'kanagawa' } },
checker = { enabled = not offline, notify = false }, checker = { enabled = true, notify = false },
change_detection = { change_detection = {
notify = false, notify = false,
}, },
git = {
timeout = offline and 1 or 120,
},
} }
vim.keymap.set('n', '<leader>l', '<cmd>:Lazy<cr>', { desc = 'Plugin Manager' }) vim.keymap.set('n', '<leader>l', '<cmd>:Lazy<cr>', { desc = 'Plugin Manager' })

View file

@ -1,6 +1,3 @@
local preparing = vim.env.NVIM_OFFLINE_PREPARE == '1'
local dap_auto_install = not vim.g.offline_mode and not preparing
return { return {
'mfussenegger/nvim-dap', 'mfussenegger/nvim-dap',
dependencies = { dependencies = {
@ -52,13 +49,13 @@ return {
dependencies = 'mason.nvim', dependencies = 'mason.nvim',
cmd = { 'DapInstall', 'DapUninstall' }, cmd = { 'DapInstall', 'DapUninstall' },
opts = { opts = {
automatic_installation = dap_auto_install, automatic_installation = true,
handlers = {}, handlers = {},
ensure_installed = dap_auto_install and { ensure_installed = {
'python', 'python',
'js', 'js',
'codelldb', 'codelldb',
} or {}, },
}, },
}, },
}, },

View file

@ -1,19 +1,3 @@
local offline = vim.g.offline_mode
local preparing = vim.env.NVIM_OFFLINE_PREPARE == '1'
local online = not offline
local auto_install = online and not preparing
local skip_prepare_packages = {}
do
local raw = vim.env.PREPARE_SKIP_MASON_PACKAGES or ''
for name in string.gmatch(raw, '([^,]+)') do
local trimmed = vim.trim(name)
if trimmed ~= '' then
skip_prepare_packages[trimmed] = true
end
end
end
local lsp_servers = { local lsp_servers = {
'angularls', 'angularls',
'bashls', 'bashls',
@ -68,32 +52,13 @@ local function dedupe(list)
return out return out
end end
local function filter_skips(list) local tool_installer_list = dedupe(vim.list_extend(vim.deepcopy(tools), lsp_servers))
if not preparing or vim.tbl_count(skip_prepare_packages) == 0 then
return list
end
local out = {}
for _, item in ipairs(list) do
if not skip_prepare_packages[item] then
table.insert(out, item)
end
end
return out
end
local configured_mason_packages = dedupe(
vim.list_extend(vim.deepcopy(tools), lsp_servers)
)
local prepare_tools = filter_skips(vim.deepcopy(configured_mason_packages))
local tool_installer_list = preparing and prepare_tools or configured_mason_packages
return { return {
{ {
'mason-org/mason-lspconfig.nvim', 'mason-org/mason-lspconfig.nvim',
opts = { opts = {
-- skip auto-install when offline; packages are pre-installed ensure_installed = lsp_servers,
-- during offline prepare we install LSP packages via MasonToolsInstallSync
ensure_installed = auto_install and lsp_servers or {},
-- Only auto-enable the LSP servers we explicitly manage in this config. -- Only auto-enable the LSP servers we explicitly manage in this config.
-- This avoids accidental multi-server attaches (e.g. ts_ls + vtsls), -- This avoids accidental multi-server attaches (e.g. ts_ls + vtsls),
-- which can produce duplicate Telescope definition results. -- which can produce duplicate Telescope definition results.
@ -121,14 +86,12 @@ return {
'mason-org/mason.nvim', 'mason-org/mason.nvim',
}, },
opts = { opts = {
-- skip auto-install when offline; packages are pre-installed
-- during offline prepare we install tools + lsp packages in one sync pass
ensure_installed = tool_installer_list, ensure_installed = tool_installer_list,
integrations = { integrations = {
-- keep mapping enabled so lspconfig names (e.g. ts_ls) resolve to Mason packages -- keep mapping enabled so lspconfig names (e.g. ts_ls) resolve to Mason packages
['mason-lspconfig'] = true, ['mason-lspconfig'] = true,
}, },
run_on_start = auto_install, run_on_start = true,
start_delay = 0, start_delay = 0,
}, },
}, },

View file

@ -1,12 +1,8 @@
-- Highlight, edit, and navigate code -- Highlight, edit, and navigate code
local preparing = vim.env.NVIM_OFFLINE_PREPARE == '1'
return { return {
'nvim-treesitter/nvim-treesitter', 'nvim-treesitter/nvim-treesitter',
branch = 'master', branch = 'master',
-- During offline prepare we run TSUpdateSync explicitly from the script. build = ':TSUpdate',
-- Skipping lazy build here prevents duplicate parser builds in later steps.
build = preparing and nil or ':TSUpdate',
main = 'nvim-treesitter.configs', main = 'nvim-treesitter.configs',
dependencies = { dependencies = {
'nvim-treesitter/nvim-treesitter-textobjects', 'nvim-treesitter/nvim-treesitter-textobjects',
@ -41,7 +37,7 @@ return {
'html', 'html',
'rust', 'rust',
}, },
auto_install = not vim.g.offline_mode and not preparing, auto_install = true,
highlight = { enable = true }, highlight = { enable = true },
indent = { enable = true }, indent = { enable = true },
incremental_selection = { incremental_selection = {

View file

@ -4,10 +4,6 @@ if wezterm.config_builder then
config = wezterm.config_builder() config = wezterm.config_builder()
end end
local target_triple = wezterm.target_triple or ""
local is_macos = target_triple:find("apple%-darwin") ~= nil
local is_windows = target_triple:find("windows") ~= nil
local function env_number(name, fallback) local function env_number(name, fallback)
local raw = os.getenv(name) local raw = os.getenv(name)
local parsed = raw and tonumber(raw) or nil local parsed = raw and tonumber(raw) or nil
@ -26,16 +22,10 @@ config.use_fancy_tab_bar = false
config.tab_bar_at_bottom = false config.tab_bar_at_bottom = false
config.enable_tab_bar = false config.enable_tab_bar = false
-- Font configuration -- Font configuration
local default_font_size = is_windows and 13 or 16 config.font_size = env_number("WEZTERM_FONT_SIZE", 16)
config.font_size = env_number("WEZTERM_FONT_SIZE", default_font_size)
config.font = wezterm.font("JetBrains Mono", { weight = "Regular" }) config.font = wezterm.font("JetBrains Mono", { weight = "Regular" })
config.bold_brightens_ansi_colors = true config.bold_brightens_ansi_colors = true
config.force_reverse_video_cursor = false config.force_reverse_video_cursor = false
if is_windows then
-- Windows GPUs/drivers can show transient render artifacts with the default
-- backend. OpenGL is generally more stable here.
config.front_end = "OpenGL"
end
local function resolve_theme_name() local function resolve_theme_name()
local value = (os.getenv("WEZTERM_THEME") or "dragon"):lower() local value = (os.getenv("WEZTERM_THEME") or "dragon"):lower()
local aliases = { local aliases = {
@ -128,20 +118,11 @@ config.window_padding = {
top = 8, top = 8,
bottom = 8, bottom = 8,
} }
-- Window transparency and blur -- Window transparency
local default_opacity = is_windows and 1.0 or 0.80 config.window_background_opacity = env_number("WEZTERM_WINDOW_OPACITY", 0.80)
config.window_background_opacity = env_number("WEZTERM_WINDOW_OPACITY", default_opacity)
if is_macos then
config.macos_window_background_blur = 60
end
-- Keep text cells opaque by default; transparent text backgrounds can show -- Keep text cells opaque by default; transparent text backgrounds can show
-- grid/dot artifacts on some GPU/compositor combinations. -- grid/dot artifacts on some GPU/compositor combinations.
config.text_background_opacity = env_number("WEZTERM_TEXT_OPACITY", 1.0) config.text_background_opacity = env_number("WEZTERM_TEXT_OPACITY", 1.0)
if is_windows then
-- Disable transparency on Windows to avoid compositor repaint glitches.
config.window_background_opacity = 1.0
config.text_background_opacity = 1.0
end
-- Performance -- Performance
config.scrollback_lines = 10000 config.scrollback_lines = 10000
config.enable_scroll_bar = false config.enable_scroll_bar = false
@ -151,10 +132,10 @@ config.cursor_blink_rate = 500
config.cursor_blink_ease_in = "Constant" config.cursor_blink_ease_in = "Constant"
config.cursor_blink_ease_out = "Constant" config.cursor_blink_ease_out = "Constant"
-- Key bindings -- Key bindings
local keys = { config.keys = {
{ key = "Enter", mods = "CTRL", action = wezterm.action({ SendString = "\x1b[13;5u" }) }, { key = "Enter", mods = "CTRL", action = wezterm.action({ SendString = "\x1b[13;5u" }) },
{ key = "Enter", mods = "SHIFT", action = wezterm.action({ SendString = "\x1b[13;2u" }) }, { key = "Enter", mods = "SHIFT", action = wezterm.action({ SendString = "\x1b[13;2u" }) },
-- Navigate splits (via physical Win+Shift) -- Navigate splits
{ key = "h", mods = "CTRL|SHIFT", action = wezterm.action.ActivatePaneDirection("Left") }, { key = "h", mods = "CTRL|SHIFT", action = wezterm.action.ActivatePaneDirection("Left") },
{ key = "l", mods = "CTRL|SHIFT", action = wezterm.action.ActivatePaneDirection("Right") }, { key = "l", mods = "CTRL|SHIFT", action = wezterm.action.ActivatePaneDirection("Right") },
{ key = "k", mods = "CTRL|SHIFT", action = wezterm.action.ActivatePaneDirection("Up") }, { key = "k", mods = "CTRL|SHIFT", action = wezterm.action.ActivatePaneDirection("Up") },
@ -168,70 +149,6 @@ local keys = {
{ key = "z", mods = "CTRL|SHIFT", action = wezterm.action.TogglePaneZoomState }, { key = "z", mods = "CTRL|SHIFT", action = wezterm.action.TogglePaneZoomState },
{ key = "f", mods = "SHIFT|CTRL", action = wezterm.action.ToggleFullScreen }, { key = "f", mods = "SHIFT|CTRL", action = wezterm.action.ToggleFullScreen },
} }
if is_windows then
-- Keep CapsLock->Escape only on Windows; macOS is handled by Karabiner.
table.insert(keys, {
key = "CapsLock",
mods = "NONE",
action = wezterm.action.SendKey({ key = "Escape" }),
})
end
if is_macos then
local mac_keys = {
-- =====================================================
-- NuPhy V3 fix: Physical Ctrl sends CMD to macOS.
-- Map CMD+key -> Ctrl+key so terminal apps work naturally.
-- =====================================================
-- Tmux prefix: physical Ctrl+Space
{ key = "Space", mods = "CMD", action = wezterm.action.SendKey({ key = "Space", mods = "CTRL" }) },
-- Common terminal control characters
{ key = "a", mods = "CMD", action = wezterm.action.SendKey({ key = "a", mods = "CTRL" }) },
{ key = "b", mods = "CMD", action = wezterm.action.SendKey({ key = "b", mods = "CTRL" }) },
{ key = "c", mods = "CMD", action = wezterm.action.SendKey({ key = "c", mods = "CTRL" }) },
{ key = "d", mods = "CMD", action = wezterm.action.SendKey({ key = "d", mods = "CTRL" }) },
{ key = "e", mods = "CMD", action = wezterm.action.SendKey({ key = "e", mods = "CTRL" }) },
{ key = "f", mods = "CMD", action = wezterm.action.SendKey({ key = "f", mods = "CTRL" }) },
{ key = "g", mods = "CMD", action = wezterm.action.SendKey({ key = "g", mods = "CTRL" }) },
{ key = "h", mods = "CMD", action = wezterm.action.SendKey({ key = "h", mods = "CTRL" }) },
{ key = "j", mods = "CMD", action = wezterm.action.SendKey({ key = "j", mods = "CTRL" }) },
{ key = "k", mods = "CMD", action = wezterm.action.SendKey({ key = "k", mods = "CTRL" }) },
{ key = "l", mods = "CMD", action = wezterm.action.SendKey({ key = "l", mods = "CTRL" }) },
{ key = "n", mods = "CMD", action = wezterm.action.SendKey({ key = "n", mods = "CTRL" }) },
{ key = "o", mods = "CMD", action = wezterm.action.SendKey({ key = "o", mods = "CTRL" }) },
{ key = "p", mods = "CMD", action = wezterm.action.SendKey({ key = "p", mods = "CTRL" }) },
{ key = "r", mods = "CMD", action = wezterm.action.SendKey({ key = "r", mods = "CTRL" }) },
{ key = "t", mods = "CMD", action = wezterm.action.SendKey({ key = "t", mods = "CTRL" }) },
{ key = "u", mods = "CMD", action = wezterm.action.SendKey({ key = "u", mods = "CTRL" }) },
{ key = "v", mods = "CMD", action = wezterm.action.SendKey({ key = "v", mods = "CTRL" }) },
{ key = "w", mods = "CMD", action = wezterm.action.SendKey({ key = "w", mods = "CTRL" }) },
{ key = "x", mods = "CMD", action = wezterm.action.SendKey({ key = "x", mods = "CTRL" }) },
{ key = "z", mods = "CMD", action = wezterm.action.SendKey({ key = "z", mods = "CTRL" }) },
{ key = "[", mods = "CMD", action = wezterm.action.SendKey({ key = "[", mods = "CTRL" }) },
{ key = "]", mods = "CMD", action = wezterm.action.SendKey({ key = "]", mods = "CTRL" }) },
{ key = "\\", mods = "CMD", action = wezterm.action.SendKey({ key = "\\", mods = "CTRL" }) },
-- Physical Ctrl+Shift combos (CMD+SHIFT -> WezTerm actions)
{ key = "f", mods = "CMD|SHIFT", action = wezterm.action.ToggleFullScreen },
{ key = "h", mods = "CMD|SHIFT", action = wezterm.action.ActivatePaneDirection("Left") },
{ key = "l", mods = "CMD|SHIFT", action = wezterm.action.ActivatePaneDirection("Right") },
{ key = "k", mods = "CMD|SHIFT", action = wezterm.action.ActivatePaneDirection("Up") },
{ key = "j", mods = "CMD|SHIFT", action = wezterm.action.ActivatePaneDirection("Down") },
{ key = "|", mods = "CMD|SHIFT", action = wezterm.action.SplitHorizontal({ domain = "CurrentPaneDomain" }) },
{ key = "_", mods = "CMD|SHIFT", action = wezterm.action.SplitVertical({ domain = "CurrentPaneDomain" }) },
{ key = "w", mods = "CMD|SHIFT", action = wezterm.action.CloseCurrentPane({ confirm = false }) },
{ key = "z", mods = "CMD|SHIFT", action = wezterm.action.TogglePaneZoomState },
}
for _, key in ipairs(mac_keys) do
table.insert(keys, key)
end
end
config.keys = keys
-- Hyperlink rules -- Hyperlink rules
config.hyperlink_rules = { config.hyperlink_rules = {
{ regex = "\\((\\w+://\\S+)\\)", format = "$1", highlight = 1 }, { regex = "\\((\\w+://\\S+)\\)", format = "$1", highlight = 1 },

15
.gitattributes vendored
View file

@ -1,14 +1 @@
* text=auto * text=auto eol=lf
# Keep Unix configs LF so copies into WSL don't break parsers.
*.sh text eol=lf
*.zsh text eol=lf
*.conf text eol=lf
*.lua text eol=lf
*.toml text eol=lf
*.vim text eol=lf
*.vimrc text eol=lf
*.md text eol=lf
# Keep PowerShell script native on Windows.
*.ps1 text eol=crlf

47
.gitignore vendored
View file

@ -1,38 +1,5 @@
# Created by https://www.toptal.com/developers/gitignore/api/vim,macos # Created by https://www.toptal.com/developers/gitignore/api/vim
# Edit at https://www.toptal.com/developers/gitignore?templates=vim,macos # Edit at https://www.toptal.com/developers/gitignore?templates=vim
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Vim ### ### Vim ###
# Swap # Swap
@ -64,8 +31,6 @@ tags
!.config/zed/ !.config/zed/
!.config/zed/settings.json !.config/zed/settings.json
!.config/zed/keymap.json !.config/zed/keymap.json
!.config/karabiner/
!.config/karabiner/karabiner.json
!.config/tmux/ !.config/tmux/
!.config/tmux/nord-theme.conf !.config/tmux/nord-theme.conf
!.config/tmux/dragon-theme.conf !.config/tmux/dragon-theme.conf
@ -75,9 +40,7 @@ tags
!.config/.vimrc !.config/.vimrc
!.config/.ideavimrc !.config/.ideavimrc
# Ignore subfolders in karabiner and tmux # Ignore subfolders in tmux
.config/karabiner/**
!.config/karabiner/karabiner.json
.config/tmux/** .config/tmux/**
!.config/tmux/nord-theme.conf !.config/tmux/nord-theme.conf
!.config/tmux/dragon-theme.conf !.config/tmux/dragon-theme.conf
@ -87,7 +50,5 @@ tags
!.config/.vimrc !.config/.vimrc
!.config/.ideavimrc !.config/.ideavimrc
scripts/nvim-offline-bundle-output
.nvimlog .nvimlog
# End of https://www.toptal.com/developers/gitignore/api/vim,macos # End of https://www.toptal.com/developers/gitignore/api/vim

View file

@ -49,45 +49,3 @@ Set in shell:
```bash ```bash
export KANAGAWA_THEME="wave" # or "dragon" export KANAGAWA_THEME="wave" # or "dragon"
``` ```
## Neovim Offline Setup (Corporate Proxy)
If you're behind a corporate proxy where Mason, Treesitter, or plugin installs fail, you can pre-download everything on a machine with internet access and deploy offline.
### Prerequisites
On the machine with internet access, make sure you have:
- Docker (Linux containers enabled)
### Step 1: Prepare the bundle (with internet)
```bash
./scripts/nvim-offline-prepare.sh
```
Optionally pin Neovim to a specific tag:
```bash
NVIM_RELEASE_TAG=v0.11.6 ./scripts/nvim-offline-prepare.sh
```
Output: `scripts/nvim-offline-bundle-output/nvim-offline-bundle.tar.gz`
### Step 2: Install the bundle (on the proxy machine)
```bash
./scripts/nvim-offline-install.sh ./scripts/nvim-offline-bundle-output/nvim-offline-bundle.tar.gz
```
### Step 3: Enable offline mode
```bash
export NVIM_OFFLINE=1
```
### Re-enabling network access
```bash
unset NVIM_OFFLINE
```

View file

@ -1,191 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# nvim-offline-install.sh
# =======================
# Deploys pre-downloaded Neovim dependencies on an offline / corporate proxy machine.
# Compatible with the Docker-generated offline bundle.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_DIR="$(dirname "$SCRIPT_DIR")"
NVIM_CONFIG="$DOTFILES_DIR/.config/nvim"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}==>${NC} $*"; }
warn() { echo -e "${YELLOW}WARNING:${NC} $*"; }
error() { echo -e "${RED}ERROR:${NC} $*" >&2; }
# ── Archive hygiene (defensive) ──────────────────────────────────────────────
export COPYFILE_DISABLE=1
TARBALL="${1:-}"
if [ -z "$TARBALL" ]; then
error "Usage: $0 <nvim-offline-bundle.tar.gz>"
exit 1
fi
if [ ! -f "$TARBALL" ]; then
error "File not found: $TARBALL"
exit 1
fi
copy_tree() {
local src="$1"
local dst="$2"
rsync -a --delete "$src/" "$dst/"
}
rewrite_mason_paths() {
local mason_root="$1"
local rewritten=0
if [ ! -d "$mason_root" ]; then
return 0
fi
while IFS= read -r -d '' file; do
# Only rewrite text files.
if ! LC_ALL=C grep -Iq . "$file"; then
continue
fi
if ! grep -q '/mason/packages/' "$file"; then
continue
fi
local tmp
tmp="$(mktemp)"
local escaped_root="${mason_root//&/\\&}"
sed -E "s|/[^\"'[:space:]]*/mason/packages/|$escaped_root/packages/|g" "$file" >"$tmp"
if ! cmp -s "$file" "$tmp"; then
cat "$tmp" >"$file"
chmod +x "$file" || true
rewritten=$((rewritten + 1))
fi
rm -f "$tmp"
done < <(find "$mason_root" -type f -print0)
info "Rewrote $rewritten Mason file(s) in $mason_root"
}
if ! command -v rsync >/dev/null 2>&1; then
warn "rsync not found; using cp -a fallback."
copy_tree() {
local src="$1"
local dst="$2"
rm -rf "$dst"
mkdir -p "$dst"
cp -a "$src/." "$dst/"
}
fi
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
NVIM_DATA="$XDG_DATA_HOME/nvim"
info "Installing offline Neovim bundle..."
echo " Source : $TARBALL"
echo " Target : $NVIM_DATA"
echo ""
# ── Extract to temp directory ───────────────────────────────────────────────
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
info "Extracting bundle..."
# Note: Docker tarball creates a flat structure (share/ at root)
tar \
--no-xattrs \
--no-acls \
--no-selinux \
-xzf "$TARBALL" \
-C "$TMPDIR"
# [CRITICAL CHANGE]
# The Docker script places 'share' directly in the root of the tarball.
# We no longer look for a 'nvim-offline-bundle' subdirectory.
BUNDLE_DIR="$TMPDIR"
if [ ! -d "$BUNDLE_DIR/share/nvim" ]; then
error "Invalid bundle structure: 'share/nvim' directory not found."
echo " Debug: Extracted contents of $TMPDIR:"
ls -F "$TMPDIR"
exit 1
fi
# ── Sanity check: NO AppleDouble files ──────────────────────────────────────
if find "$BUNDLE_DIR" -name '._*' | grep -q .; then
error "AppleDouble (._*) files detected in bundle. Refusing to install."
exit 1
fi
# ── Install lazy.nvim plugins ──────────────────────────────────────────────
# Path in bundle: share/nvim/lazy
SOURCE_LAZY="$BUNDLE_DIR/share/nvim/lazy"
if [ -d "$SOURCE_LAZY" ]; then
PLUGIN_COUNT=$(ls -1 "$SOURCE_LAZY" | wc -l)
info "Installing $PLUGIN_COUNT plugins..."
if [ -d "$NVIM_DATA/lazy" ]; then
warn "Existing lazy/ directory found — backing up to lazy.bak/"
rm -rf "$NVIM_DATA/lazy.bak"
mv "$NVIM_DATA/lazy" "$NVIM_DATA/lazy.bak"
fi
mkdir -p "$NVIM_DATA"
copy_tree "$SOURCE_LAZY" "$NVIM_DATA/lazy"
fi
# ── Install Mason packages ─────────────────────────────────────────────────
# Path in bundle: share/nvim/mason
SOURCE_MASON="$BUNDLE_DIR/share/nvim/mason"
if [ -d "$SOURCE_MASON" ]; then
MASON_COUNT=$(ls -1 "$SOURCE_MASON/packages" 2>/dev/null | wc -l)
info "Installing $MASON_COUNT Mason packages..."
if [ -d "$NVIM_DATA/mason" ]; then
warn "Existing mason/ directory found — backing up to mason.bak/"
rm -rf "$NVIM_DATA/mason.bak"
mv "$NVIM_DATA/mason" "$NVIM_DATA/mason.bak"
fi
copy_tree "$SOURCE_MASON" "$NVIM_DATA/mason"
rewrite_mason_paths "$NVIM_DATA/mason"
if rg -n '/tmp/runtime/share/nvim/mason/packages/' "$NVIM_DATA/mason" -S >/dev/null 2>&1; then
error "Stale Mason package paths detected after rewrite. Aborting."
exit 1
fi
fi
# ── Install lazy-lock.json ─────────────────────────────────────────────────
if [ -f "$BUNDLE_DIR/lazy-lock.json" ] && [ -d "$NVIM_CONFIG" ]; then
info "Installing lazy-lock.json..."
cp "$BUNDLE_DIR/lazy-lock.json" "$NVIM_CONFIG/lazy-lock.json"
fi
# ── Final verification ─────────────────────────────────────────────────────
if find "$NVIM_DATA" -name '._*' | grep -q .; then
error "Post-install AppleDouble files detected. Installation aborted."
exit 1
fi
# ── Summary ────────────────────────────────────────────────────────────────
echo ""
info "Installation complete!"
echo ""
echo " Plugins : $NVIM_DATA/lazy/"
echo " Mason : $NVIM_DATA/mason/"
echo ""
echo " ┌─────────────────────────────────────────────────────────┐"
echo " │ Add the following to your ~/.zshrc or ~/.bashrc: │"
echo " │ │"
echo " │ export NVIM_OFFLINE=1 │"
echo " │ │"
echo " │ Then restart your shell or run: source ~/.zshrc │"
echo " └─────────────────────────────────────────────────────────┘"

View file

@ -1,280 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# nvim-offline-prepare.sh
# =======================
# Build an offline Neovim bundle in a temporary Linux x86_64 container.
# The container downloads and compiles all dependencies, then writes only the
# final tarball to the host output directory.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_DIR="$(dirname "$SCRIPT_DIR")"
NVIM_CONFIG="$DOTFILES_DIR/.config/nvim"
OUTPUT_DIR="$SCRIPT_DIR/nvim-offline-bundle-output"
TARBALL_NAME="nvim-offline-bundle.tar.gz"
NVIM_RELEASE_TAG="${NVIM_RELEASE_TAG:-}"
NVIM_PREPARE_TIMEOUT="${NVIM_PREPARE_TIMEOUT:-75m}"
BUILDER_IMAGE="${BUILDER_IMAGE:-ubuntu:24.04}"
ALLOW_EMPTY_MASON="${ALLOW_EMPTY_MASON:-0}"
PREPARE_SKIP_MASON_PACKAGES="${PREPARE_SKIP_MASON_PACKAGES:-}"
# ── Colors & Helpers ────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}==>${NC} $*"; }
warn() { echo -e "${YELLOW}WARNING:${NC} $*"; }
error() { echo -e "${RED}ERROR:${NC} $*" >&2; }
format_duration() {
local total_seconds="$1"
local hours=$((total_seconds / 3600))
local minutes=$(((total_seconds % 3600) / 60))
local seconds=$((total_seconds % 60))
printf "%02dh:%02dm:%02ds" "$hours" "$minutes" "$seconds"
}
# ── Checks ──────────────────────────────────────────────────────────────────
command -v docker >/dev/null 2>&1 || {
error "Docker is required but not installed."
exit 1
}
if ! docker info >/dev/null 2>&1; then
error "Docker is not running."
exit 1
fi
if [ ! -d "$NVIM_CONFIG" ]; then
error "Neovim config directory not found: $NVIM_CONFIG"
exit 1
fi
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Clear previous run artifacts
rm -f "$OUTPUT_DIR/$TARBALL_NAME"
info "Starting Linux container (x86_64) to build offline bundle..."
info " Source Config : $NVIM_CONFIG"
info " Output Dir : $OUTPUT_DIR"
info " Docker Image : $BUILDER_IMAGE"
if [ -n "$NVIM_RELEASE_TAG" ]; then
info " Neovim Tag : $NVIM_RELEASE_TAG"
else
info " Neovim Tag : latest stable"
fi
info " Nvim Timeout : $NVIM_PREPARE_TIMEOUT"
if [ "$ALLOW_EMPTY_MASON" = "1" ]; then
warn "ALLOW_EMPTY_MASON=1 (will not fail if Mason installs 0 packages)"
fi
if [ -n "$PREPARE_SKIP_MASON_PACKAGES" ]; then
warn "PREPARE_SKIP_MASON_PACKAGES=$PREPARE_SKIP_MASON_PACKAGES"
fi
# ── Run Builder Container ───────────────────────────────────────────────────
# We use --platform linux/amd64 to simulate a standard corporate Linux server.
# This ensures binaries (blink.cmp) and parsers (treesitter) are compiled for x86_64.
PREPARE_STARTED_AT="$(date +%s)"
if ! docker run --rm \
--platform linux/amd64 \
-v "$NVIM_CONFIG:/input/nvim:ro" \
-v "$OUTPUT_DIR:/output" \
-e HOST_UID="$(id -u)" \
-e HOST_GID="$(id -g)" \
-e TARBALL_NAME="$TARBALL_NAME" \
-e NVIM_RELEASE_TAG="$NVIM_RELEASE_TAG" \
-e NVIM_PREPARE_TIMEOUT="$NVIM_PREPARE_TIMEOUT" \
-e ALLOW_EMPTY_MASON="$ALLOW_EMPTY_MASON" \
-e PREPARE_SKIP_MASON_PACKAGES="$PREPARE_SKIP_MASON_PACKAGES" \
"$BUILDER_IMAGE" /bin/bash -c '
set -euo pipefail
NVIM_PREPARE_TIMEOUT="${NVIM_PREPARE_TIMEOUT:-75m}"
ALLOW_EMPTY_MASON="${ALLOW_EMPTY_MASON:-0}"
echo ">> [Container] Installing dependencies..."
export DEBIAN_FRONTEND=noninteractive
# Build/runtime tools for Lazy, Treesitter, Mason, and common package installers.
apt-get update -qq && apt-get install -y -qq \
ca-certificates \
git \
curl \
wget \
unzip \
tar \
gzip \
jq \
build-essential \
python3 \
python3-venv \
python3-pip \
lua5.1 \
luarocks \
nodejs \
npm \
>/dev/null
update-ca-certificates >/dev/null 2>&1 || true
echo ">> [Container] Preparing writable config copy..."
mkdir -p /tmp/config
cp -a /input/nvim /tmp/config/nvim
chmod -R u+rwX /tmp/config/nvim
echo ">> [Container] Fetching Neovim release..."
cd /tmp
if [ -n "${NVIM_RELEASE_TAG:-}" ]; then
TAG="$NVIM_RELEASE_TAG"
else
TAG="$(curl -fsSL https://api.github.com/repos/neovim/neovim/releases/latest | jq -r .tag_name)"
fi
if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then
echo "Error: Could not determine a Neovim release tag."
exit 1
fi
echo ">> [Container] Downloading $TAG for x86_64..."
ASSET=""
for CANDIDATE in nvim-linux-x86_64.tar.gz nvim-linux64.tar.gz; do
URL="https://github.com/neovim/neovim/releases/download/${TAG}/${CANDIDATE}"
if curl -fL --retry 3 --retry-delay 2 -o nvim-linux.tar.gz "$URL"; then
ASSET="$CANDIDATE"
break
fi
done
if [ -z "$ASSET" ]; then
echo "Error: Failed to download Neovim archive for tag $TAG."
exit 1
fi
# Verify download integrity ( > 1MB )
FILESIZE=$(stat -c%s nvim-linux.tar.gz)
if [ "$FILESIZE" -lt 1000000 ]; then
echo "Error: Download failed (file too small)."
cat nvim-linux.tar.gz
exit 1
fi
tar -xf nvim-linux.tar.gz
NVIM_DIR="${ASSET%.tar.gz}"
if [ ! -x "/tmp/$NVIM_DIR/bin/nvim" ]; then
NVIM_DIR="$(find /tmp -maxdepth 1 -type d -name "nvim-linux*" | head -n1 | xargs -r basename)"
fi
if [ -z "$NVIM_DIR" ] || [ ! -x "/tmp/$NVIM_DIR/bin/nvim" ]; then
echo "Error: Neovim binary directory not found after extraction."
exit 1
fi
export PATH="/tmp/$NVIM_DIR/bin:$PATH"
# Define isolated runtime paths so host files are never added to the bundle.
export XDG_CONFIG_HOME="/tmp/config"
export RUNTIME_ROOT="/tmp/runtime"
export XDG_DATA_HOME="$RUNTIME_ROOT/share"
export XDG_STATE_HOME="$RUNTIME_ROOT/state"
export XDG_CACHE_HOME="$RUNTIME_ROOT/cache"
export NVIM_OFFLINE_PREPARE=1
mkdir -p "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME"
echo ">> [Container] 1/5 Bootstrapping lazy.nvim..."
# We bootstrap lazy.nvim manually first
git clone --filter=blob:none https://github.com/folke/lazy.nvim.git \
"$XDG_DATA_HOME/nvim/lazy/lazy.nvim" || true
echo ">> [Container] 2/5 Restoring Lazy plugins..."
if ! timeout "$NVIM_PREPARE_TIMEOUT" nvim --headless \
-c "set nomore" \
-c "Lazy! restore" \
-c "qa!"; then
echo "Error: Lazy restore failed or timed out after $NVIM_PREPARE_TIMEOUT."
exit 1
fi
echo ">> [Container] 3/5 Building Treesitter parsers..."
if ! timeout "$NVIM_PREPARE_TIMEOUT" nvim --headless \
-c "set nomore" \
-c "TSUpdateSync" \
-c "qa!"; then
echo "Error: TSUpdateSync failed or timed out after $NVIM_PREPARE_TIMEOUT."
exit 1
fi
echo ">> [Container] 4/5 Installing Mason tools..."
if ! timeout "$NVIM_PREPARE_TIMEOUT" nvim --headless \
-c "set nomore" \
-c "MasonToolsInstallSync" \
-c "qa!"; then
echo "Error: MasonToolsInstallSync failed or timed out after $NVIM_PREPARE_TIMEOUT."
echo "Tip: retry with NVIM_PREPARE_TIMEOUT=120m if needed."
exit 1
fi
# Verification (safe with pipefail even when directories are missing)
PLUGIN_COUNT=0
if [ -d "$XDG_DATA_HOME/nvim/lazy" ]; then
PLUGIN_COUNT=$(find "$XDG_DATA_HOME/nvim/lazy" -mindepth 1 -maxdepth 1 -type d | wc -l)
fi
MASON_COUNT=0
if [ -d "$XDG_DATA_HOME/nvim/mason/packages" ]; then
MASON_COUNT=$(find "$XDG_DATA_HOME/nvim/mason/packages" -mindepth 1 -maxdepth 1 -type d | wc -l)
fi
echo ">> [Container] Installed $PLUGIN_COUNT plugins and $MASON_COUNT Mason packages."
if [ "$MASON_COUNT" -eq 0 ]; then
if [ "$ALLOW_EMPTY_MASON" = "1" ]; then
echo ">> [Container] WARNING: No Mason packages detected under $XDG_DATA_HOME/nvim/mason/packages."
else
echo "Error: No Mason packages were installed. Aborting to avoid an incomplete offline bundle."
echo "Tip: set ALLOW_EMPTY_MASON=1 only if you intentionally want a plugins-only bundle."
exit 1
fi
fi
# Cleanup junk
echo ">> [Container] 5/5 Packaging & Cleaning..."
BUNDLE_ROOT="/tmp/bundle"
mkdir -p "$BUNDLE_ROOT"
cp -a "$XDG_DATA_HOME" "$BUNDLE_ROOT/share"
find "$BUNDLE_ROOT" -name ".git" -type d -exec rm -rf {} +
find "$BUNDLE_ROOT" -name "._*" -type f -delete
# Copy lockfile
if [ -f "$XDG_CONFIG_HOME/nvim/lazy-lock.json" ]; then
cp "$XDG_CONFIG_HOME/nvim/lazy-lock.json" "$BUNDLE_ROOT/"
fi
# Create the Tarball
tar --exclude="._*" -czf "/output/$TARBALL_NAME" -C "$BUNDLE_ROOT" .
# Fix ownership
chown "$HOST_UID:$HOST_GID" "/output/$TARBALL_NAME"
echo ">> [Container] Done."
'; then
PREPARE_ENDED_AT="$(date +%s)"
PREPARE_ELAPSED="$((PREPARE_ENDED_AT - PREPARE_STARTED_AT))"
error "Offline prepare container failed after $(format_duration "$PREPARE_ELAPSED"). Review the container logs above."
exit 1
fi
PREPARE_ENDED_AT="$(date +%s)"
PREPARE_ELAPSED="$((PREPARE_ENDED_AT - PREPARE_STARTED_AT))"
# ── Summary ─────────────────────────────────────────────────────────────────
TARBALL="$OUTPUT_DIR/$TARBALL_NAME"
if [ -f "$TARBALL" ]; then
SIZE=$(du -h "$TARBALL" | cut -f1)
echo ""
info "Success! Bundle created at:"
echo " $TARBALL ($SIZE)"
echo " Elapsed: $(format_duration "$PREPARE_ELAPSED")"
echo ""
echo " Target Platform: Linux (x86_64)"
else
error "Docker container finished but tarball was not found."
exit 1
fi