Skip to content
assawalhyPublic

About

My config files (mostly for cli tools), AI agents configs (skills, plugins, context files), also my personal NixOS configuration.nix with all the reproducible system ready to use in a new fresh storage disk. πŸ‘‰ To continue my hobby of hopping between the distros :)

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

My Config Files

As-salamu alaykum. These are my dotfiles for macOS and Linux, kept in one repo and symlinked into $HOME.

I used to git init in $HOME directly, ignoring everything and un-ignoring the configs. That broke two things: neovim's file manager treats the directory containing .git as the workspace root, so every session rooted at $HOME; and the zsh git plugin showed branch status in every directory, including Desktop. Keeping the files in a repo and linking them out avoids both.

Layout

Directory Linked when Contents
common/ always everything portable β€” nvim, tmux, git, zsh/bash, mpv, ranger, bin/
linux/ on Linux X11 and Arch-specific β€” .xinitrc, .Xmodmap, .profile, zathura, bin/arch-scripts/
macos/ on macOS .hushlogin, and the macOS halves of the split configs

Both common/ and the overlay for the current OS are linked. On a path collision the OS overlay wins, which is how the platform-specific pieces are swapped in:

common/.bash_profile          -> ~/.bash_profile        (both platforms)
linux/.config/shell/os.sh  ─┐
macos/.config/shell/os.sh  ─┴> ~/.config/shell/os.sh    (whichever OS you're on)

~/.bash_profile sources ~/.config/shell/os.sh on its first line, so the platform-specific aliases, functions and PATH entries come from exactly one file and neither copy needs a uname branch. The same trick is used for ~/.config/git/os.gitconfig.

Neovim

The config lives in common/.config/nvim/ and loads in layers:

init.lua                   8-line manifest: leader key + 4 requires
lua/config/lazy.lua        plugin manager bootstrap
lua/config/options.lua     editor settings
lua/config/keymaps.lua     global keymaps
lua/config/autocmds.lua    global autocommands
lua/plugins/*.lua          one file per feature (ui, editor, git, lsp,
                           blink, conform, trouble, dap, treesitter,
                           ufo, neotree, telescope, competitest)
lua/config/lsp_gate.lua    per-buffer LSP attach gate, shared by the servers
lua/config/java.lua        jdtls + kotlin_lsp (mason kls fallback)
lua/config/memory.lua      RAM guard for the two JVM servers
after/plugin/lsp.lua       LspAttach native keymaps + diagnostics

LSP keymaps

Bound on LspAttach (skipped for copilot):

Key Action
;rn rename
;ac code action
gd go to definition
gp hover
gtd go to type definition
gr references
[d / ]d previous / next diagnostic
<leader>dd diagnostics list

Completion (blink.cmp)

C-n/C-p select, C-b/C-f scroll docs, C-Space complete, CR accept, Tab/S-Tab jump between snippet placeholders.

Java, Kotlin and competitive programming

The toolchain comes from mise (common/.config/mise/config.toml: temurin-21 and kotlin), so java, javac and kotlinc are on PATH without distro packages; _jdk_home in ~/.config/shell/os.sh resolves through mise where java. Maven and Gradle come from the p2 tier: setup-os --priority p2.

The servers come from mason (ensure_installed in lua/plugins/lsp.lua) except kotlin-lsp, which setup/steps/67-kotlin-lsp.sh unpacks (sha256-pinned) into ~/.local/share/kotlin-lsp/; all are gated per buffer by lua/config/lsp_gate.lua, so oversized files stay LSP-free:

  • jdtls (lua/config/java.lua) is launched as java itself β€” the equinox launcher jar plus -configuration config_linux/config_mac β€” not through the jdtls wrapper script: the wrapper would be nvim's immediate child and the JVM a grandchild that nvim never kills (nvim#29475). The heap is capped at 768 MB and every project gets its own workspace under stdpath('cache')/jdtls/<name>-<hash>. Lombok comes from mason's bundled lombok.jar; set LOMBOK_JAR to override.
  • kotlin_lsp is the official JetBrains server (Kotlin/kotlin-lsp, IntelliJ-based) β€” the only one that reads Kotlin 2.3 project metadata. Where it is not installed (macOS, non-x86_64), java.lua falls back to the mason kotlin_language_server: same 768 MB heap cap through _JAVA_OPTIONS, plus compiler.jvm.target=21 sent via workspace/didChangeConfiguration to clear its false "cannot inline" diagnostics. kls bundles Kotlin 2.1.0, so on Kotlin 2.3 projects it still reports ::class.java as unresolved (fwcd#457).

lua/config/memory.lua backs all of them: no JVM starts below 2 GiB available RAM, running ones are stopped below 1.5 GiB (20 s watchdog and on ExitPre), and JVMs orphaned by a crashed nvim are swept on the next start.

Competitive programming: CompetiTest compiles and runs Kotlin (kotlinc β†’ java -jar) as well as Java, and the shell profile grows rkotlin/wkotlin alongside rjava/wjava for running a single file.

Linking

git clone <this repo> ~/Projects/dotfiles
~/Projects/dotfiles/link-files.bash

It works from any directory, detects the OS itself, shows you exactly what it will do, and asks before touching anything.

USAGE:
  link-files.bash [--help] [--force] [--no-backup] [--dry-run] [--yes] [--refresh] [--audit] [--diff] [--fix] [filtering_pattern]
  link-files.bash --force '.*nvim/lua.*'
  link-files.bash --refresh --dry-run
  link-files.bash --audit
  link-files.bash --diff --dry-run
  link-files.bash --fix --dry-run

OPTIONS:
  -h, --help      show this message and exit
      --force     resolve conflicts: replace foreign symlinks, and back up
                  real files to <file>.bak.<timestamp> before linking
      --no-backup with --force, delete a conflicting real file instead of
                  backing it up to <file>.bak.<timestamp>; refuses to
                  replace a directory; requires --force
      --diff      show a unified diff between the home file and the repo
                  file for each conflict/relink candidate before confirming
  -n, --dry-run   show what would happen, then exit without changing anything
  -y, --yes       skip the confirmation prompt
      --refresh   capture new files that appeared inside linked dirs into the
                  repo (OS overlay first, else common) and symlink them back;
                  never overwrites repo files; --force has no effect; skips
                  files neglected for the current session context
                  (wayland/x11/headless)
      --audit     read-only link-drift report: repo files lacking a correct
                  home link (missing/relink/conflict/stale) plus real files
                  in linked dirs absent from the repo. Excludes files
                  neglected for the current session context
                  (wayland/x11/headless), same as linking. Exit 0 = clean,
                  1 = findings; never writes, no picker, no prompt
      --fix       complete linking: remove stale, ignored and session-neglected
                  links, link missing files, relink and resolve conflicts
                  (implies --force); cannot be combined with --audit or
                  --refresh
  <pattern>       extended regex; only paths matching it are considered
link-files.bash                 # everything
link-files.bash --dry-run       # preview only
link-files.bash '.*nvim/lua.*'  # just the nvim plugin files
link-files.bash --force         # also resolve conflicts

Interactive selection

Run bare, link-files.bash opens an interactive picker over the linkable files that still need linking β€” anything already linked correctly is left out β€” each tagged with a [group] label (git, shell, tmux, x11, bin, other). Type to filter, TAB toggles a file, CTRL-A selects all current matches and CTRL-D clears the selection. Marks survive when you clear the query, so you can build a set across several searches. fzf 0.48 and newer starts with everything selected; older fzf needs a CTRL-A first. Without fzf there is a numbered fallback menu with a for all and n for none.

The picker only opens when nothing else tells the script what to do: a <pattern> argument, --dry-run or --yes all skip it, so the invocations above behave exactly as before. The linker no longer needs node: the path listing is a bash script now.

What gets linked

Everything committed under common/ and the active OS directory β€” there is no include list. link-ignore.txt is the exception list: paths named there are committed but not linked. Adding an entry while its file is still linked makes --audit report it as i [ignored]; removing an entry makes the file show up as + [missing] until it is linked. link-context.txt is the session-context filter for every link operation: a relpath tagged for another context (like x11: .Xmodmap under wayland) is excluded from linking, listing, --diff, --refresh and the audit report.

Adopting a new dotfile is therefore just a move:

mv ~/.foo common/.foo && ./link-files.bash

(This replaces the old --reverse flag, which captured files from $HOME back into the repo. With symlinks the repo is the live copy, so there is nothing to capture and the flag is gone.)

Capturing new files (--refresh)

Programs sometimes write new files straight into a linked directory, a plugin adding its own config, a program creating a state file. Run with --refresh after that and the script picks those files up: anything inside a linked dir that is not already in the repo, not named in link-ignore.txt, and not matched by gitignore. Each one lands at the same relative path in the repo, in the OS overlay when that path exists there and in common/ otherwise, and is symlinked back so the live file keeps working.

--refresh never overwrites a repo file, and --force has no effect on it. New files whose relpath is neglected for the current session context are skipped, so an x11-only config directory is not scanned for captures while on a Wayland session. Like every other mode it previews what it will capture and then asks for the πŸ”₯ confirmation before moving anything. --dry-run shows the preview and stops, --yes skips the prompt, and a <pattern> narrows the scan.

link-files.bash --refresh             # capture everything new
link-files.bash --refresh --dry-run   # preview only
link-files.bash --refresh 'extra\.conf$'

Checking the link state (--audit)

--audit is a read-only report of link drift in both directions: repo files that are not correctly linked in $HOME (missing, pointing at the wrong source, a conflict, or stale), and real files inside linked dirs that are absent from the repo. It also reports links that should not be there: files still linked but listed in link-ignore.txt (i [ignored]) and files still linked but neglected for the current session context (x [neglected]). It never writes anything, opens no picker, and asks nothing. Exit 0 means the links are clean, exit 1 that there are findings.

Some files only belong to particular desktop sessions. link-context.txt lists those, per session context: a line like x11: .Xmodmap marks .Xmodmap as x11-only. The filter is applied up front for every operation, so on a Wayland session .Xmodmap is excluded from linking, listing, --diff, --refresh and this report alike, and reported only on an X11 session. --audit prints audit context: <ctx> (neglecting: ...) in its header, naming the contexts it excluded. The context is detected from the environment (wayland, x11, or headless). Stale links to repo files that were deleted are reported on any session β€” they are dangling pointers, independent of the session context.

Conflicts

Every candidate path falls into one of these:

Preview Meaning Needs --force?
(silent) already the right symlink β€”
+ nothing there yet, will be created no
* an old hard link, or a symlink pointing at the wrong file in this repo no
- a symlink into this repo that is no longer wanted; will be removed no
~ a conflict being resolved by --force yes
! a conflict β€” a real file, or a symlink pointing outside this repo yes
i ignored by link-ignore.txt but still linked; removed by --fix no
x neglected for the current session but still linked; removed by --fix no

Real files are never deleted. Under --force they are moved to <file>.bak.<timestamp> first, unless --no-backup is given, which deletes the real file instead (and refuses directory conflicts).

Add --diff to see what you would lose before deciding: it prints a unified diff of the home file against the repo's for every conflict/relink candidate that has content on both sides (foreign symlinks are dereferenced), right before the confirmation prompt. Identical files are marked (identical). On a terminal the diff is rendered colored through delta when it is installed; otherwise (or when output is piped) plain diff -u is used.

Old hard links from the previous scheme are detected by inode, so the migration to symlinks needs no flags and creates no backups β€” the content is identical by definition.

Stale links (-) are only ever removed when they point into this repo. Unrelated symlinks in $HOME are left alone.

Completing the link state (--fix)

--fix is the write counterpart of --audit: it completes linking and prunes what should not be linked. It links missing files, relinks wrong-source symlinks, resolves conflicts (backing up real files to <file>.bak.<timestamp>), and removes stale links, links ignored by link-ignore.txt (reported as i [ignored]) and links neglected for the current session context (reported as x [neglected]). It implies --force, never opens the picker, and cannot be combined with --audit or --refresh. --dry-run previews every change with the same markers as a normal run.

link-files.bash --fix --dry-run   # preview what would change
link-files.bash --fix             # complete linking and prune

Testing

The link script has a bats test suite:

sudo apt install bats            # or your distro's bats package
bats --tap tests/link-files.bats

Installing programs

setup-os              # pick from a list, then install
setup-os --list       # just show what resolves on this machine
setup-os --dry-run --all
setup-os --priority p1 -y     # temp machine: essentials + agents, no prompt
setup-os --priority p1,p2     # the dev workstation tiers in one run

Uses Homebrew on macOS and pacman / apt / dnf / zypper on Linux (plus paru or yay for AUR packages, when one is installed). Already-installed entries are hidden from the picker; --show-installed keeps them.

Every entry belongs to a priority tier, p1 to p4:

Tier Covers
p1 SSH essentials and agents: git, zsh, tmux, fzf, bat, fd, ripgrep, eza, sd, fastmod, gh, plus the rustup, oh-my-zsh, tpm, opencode and claude steps
p2 dev workstation
p3 GUI
p4 occasional

--priority selects a tier and skips the picker, so a temp machine is one command: setup-os --priority p1 -y installs the essentials with no prompt. The flag is repeatable and comma-separated, so setup-os --priority p1,p2 covers a dev workstation in one run. It also takes precedence over --all, narrowing that to the tiers you named.

Selection uses fzf --multi when available and falls back to a numbered menu otherwise, so it works on a machine where nothing is installed yet. Type a group name then Ctrl-A to select a whole group at once.

  • setup/packages.list β€” the package table. One logical name per line, with per-manager overrides only where the names differ:

    fd            apt:fd-find  dnf:fd-find    # `fd` everywhere else
    zsh           p1                          # bare pN token = priority tier
    texlive       cask:basictex  pacman:texlive-core
    xclip         !macos                      # X11 only
    megasync      cask:megasync  aur:megasync-bin  pacman:-
    bat           apt:batcat check:bat p1     # check: hides it when the command exists
    

    The bare pN token sets the priority tier; it goes at the end of the line, before any comment. Lines without one default to p4.

    A check:<command> token marks the entry installed when that command exists on PATH, even if the package manager can't see it β€” tarball installs (neovim check:nvim), cargo/snap installs, or packages whose manager name differs from the binary (bat apt:batcat check:bat). The manager lookup stays primary; the command check is only a fallback.

  • setup/steps/*.sh β€” the things that aren't packages (rustup, oh-my-zsh, tpm, volta, pnpm, macOS defaults, …). One executable file each, with # desc:, # os:, # check: and # prio: headers so setup-os can label them, filter them by platform, skip the ones already done, and include each one in its priority tier. Each is runnable on its own.

Failures don't abort the run β€” a batch that fails is retried one package at a time to isolate the culprit, and everything that failed is listed at the end.

On a fresh box with no cargo, rustup runs automatically right before the cargo packages, so you don't have to bootstrap it by hand first.

Caveats

  • The stale-link scan is a single find pass over $HOME (roughly two seconds on a 60 GB home), so every run pays that cost once.
  • Programs that save by write-and-rename replace the symlink with a real file. git config --global is the common one: it will turn ~/.gitconfig into a regular file. The next link-files.bash reports it as !; run with --force and your edits are preserved in the .bak file.
  • common/.config/nvim/lazy-lock.json is written through to the repo, so plugin updates show up as git changes. That's intended β€” it's how the lockfile stays versioned.
  • ~/.npmrc is deliberately not in this repo: it contains a registry _authToken.
  • common/bin/ is linked to ~/bin on both platforms, so scripts there must be portable. Use clip (clip to copy, clip -o to paste) instead of pbcopy/xclip/xsel directly β€” it picks the backend at runtime, and tmux and nvim both go through it. Linux-only scripts belong in linux/bin/.

Shell profiles

  • .bash_profile.
    • contains the aliases and resusable functions like lae (list all excluding ...).
    • contains the PATH changes.
  • .config/shell/os.sh.
    • contains os-specific configs like the homebrew stuff or the WSL things.
  • Python comes from uv, not the distro: setup/steps/91-uv-python.sh runs uv python install 3 --default, which links python and python3 (uv only links the versioned python3.x otherwise) into ~/.local/bin β€” a dir .bash_profile already puts on PATH, so uv python update-shell is not needed. There is deliberately no bare pip: use uv pip ... or python3 -m pip (a bare pip install is PEP 668-blocked anyway).
  • These configs should only account for the applications and packges that the setup script handles, other packages when installed will add them selfs.
  • These rules should be verified when changes happens to them in the HOME and synced to this repo.

License

MIT

About

My config files (mostly for cli tools), AI agents configs (skills, plugins, context files), also my personal NixOS configuration.nix with all the reproducible system ready to use in a new fresh storage disk. πŸ‘‰ To continue my hobby of hopping between the distros :)

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages