Personal shell, terminal and editor configuration, aimed primarily at headless Linux hosts reached over SSH (Debian, Ubuntu, Fedora), with a desktop profile, Termux (Android) support and partial Windows support. Everything is symlinked from this repo, so edits here take effect immediately.
On a fresh machine:
curl -fsSL https://raw.githubusercontent.com/SennLabs/_dotfiles/main/bootstrap.sh | shThat installs git, clones the repo to ~/_dotfiles, and links the configs. Add
packages too with:
curl -fsSL .../bootstrap.sh | DOTFILES_APPS=1 shIf you already have the repo:
git clone https://github.com/SennLabs/_dotfiles.git ~/_dotfiles
cd ~/_dotfiles
./Init.sh --profile server # or: --profile desktop
./Init.sh --profile server --apps # also install packagesOn Windows, from PowerShell in the cloned repo:
.\InstallApps.ps1 -InstallProfile desktop # packages via winget
.\SetupSimLink.ps1 -InstallProfile desktop # link the configsOn Termux, the same commands work: pkg install git first, then the quick-start
curl | sh line or the manual clone. See On Termux below.
Re-running is safe. SetupSimLink.sh is idempotent, and anything already at a
target path is moved to ~/.dotfiles-backup/<timestamp>/ rather than deleted.
Cumulative — server includes core, desktop includes both.
| Profile | Packages | Configs |
|---|---|---|
core |
git, curl, tmux, neovim, bat, fd, fzf, less, column | all non-GUI |
server |
+ fish, htop, jq, ripgrep, rsync, ncdu, unzip, yazi, python, rust, gh, docker | all non-GUI |
desktop |
+ VSCode, WezTerm, browsers, graphics apps | everything, incl. WezTerm & KZones |
server is the default, since most installs here are headless. It deliberately
does not link WezTerm or KZones.
| Script | Purpose |
|---|---|
bootstrap.sh |
One-shot setup on a bare machine (POSIX sh; installs git) |
Init.sh |
Records the repo path, then links configs |
SetupSimLink.sh |
Does the linking; idempotent, backs up what it replaces |
InstallApps.sh |
Installs packages from apps/app_list.csv via apt, dnf or Termux's pkg |
InstallApps.ps1 |
Windows equivalent, via winget, same CSV |
SetupSimLink.ps1 |
Windows equivalent of SetupSimLink.sh (see below) |
SetupCron.sh |
Installs the 2-hourly pull-only sync |
AutoGitSync.sh |
Pulls only. Never commits or pushes |
apps/app_list.csv is the single source of truth for packages, with per-manager
columns (Apt, Dnf, Winget, Termux). A - means "not available here".
Packages missing from a host's repos are reported and skipped, not fatal.
| Repo path | Target | Profile |
|---|---|---|
scripts/ |
~/.config/scripts |
all |
config/ssh/config |
~/.ssh/config |
all |
config/git/config |
~/.config/git/config |
all |
config/readline/inputrc |
~/.inputrc |
all |
config/fish/config.fish |
~/.config/fish/config.fish |
all |
config/fish/functions/ |
~/.config/fish/functions |
all |
config/nvim/ |
~/.config/nvim |
all |
config/yazi/ |
~/.config/yazi |
all |
config/tmux/tmux.conf |
~/.tmux.conf |
all |
config/bash/.bash_aliases |
~/.bash_aliases |
all |
config/bash/.bash_functions |
~/.bash_functions |
all |
config/misc_env_var.sh |
~/.config/dotfiles_env.sh |
all |
config/wezterm/.wezterm.lua |
~/.wezterm.lua |
desktop |
config/kzones/layouts.json |
~/.local/share/kzones/layouts.json |
desktop |
SetupSimLink.ps1 links only what native Windows uses; bash, fish, tmux,
readline and KZones are skipped. It honours XDG_CONFIG_HOME the same way each
tool does.
| Repo path | Target | Profile |
|---|---|---|
config/ssh/config |
%USERPROFILE%\.ssh\config |
all |
config/git/config |
%USERPROFILE%\.config\git\config |
all |
config/nvim/ |
%LOCALAPPDATA%\nvim |
all |
config/yazi/ |
%APPDATA%\yazi\config |
all |
config/wezterm/.wezterm.lua |
%USERPROFILE%\.config\wezterm\wezterm.lua |
desktop |
Real symlinks need an elevated shell, or Developer Mode plus PowerShell 7.
Without that, directories become junctions and files are copied. Copies do
not follow later edits, so re-run the script after pulling. -DryRun shows what
it would do.
Termux ships its own apt, so InstallApps.sh detects it (via TERMUX_VERSION)
and reads the Termux column of the CSV, because package names differ from
Debian's (fd not fd-find, python not python3, util-linux for
column). It never tries sudo there: Termux installs unprivileged into its own
prefix. Docker has no Termux package and is skipped.
The symlinks are the same as on Linux. Termux starts bash as a login shell, so
the ~/.profile hook below is what loads the environment and chains to
~/.bashrc. The terminal supports OSC 52, so the tmux and Neovim clipboard
setup works without termux-api.
Cron is not installed by default; SetupCron.sh tells you what to add
(cronie plus termux-services) if you want the pull-only sync there.
SSH gives you a login shell, which reads ~/.profile or ~/.bash_profile —
never ~/.bashrc directly. Ubuntu's stock ~/.bashrc also returns early when
non-interactive. So:
- Environment (
dotfiles_env.sh) is sourced from the login file, soEDITOR,LANGandPAGERare set even forssh host -t <cmd>. - Aliases and functions are sourced from
~/.bashrc, because they are only meaningful in an interactive shell.
Both hooks are marker-delimited and added once. Check the environment loaded
with ssh host 'echo $DOTFILES_ENV' (prints 1).
Clipboard. A headless host has no X11 or Wayland selection, so yanking
normally goes nowhere. tmux is set to set-clipboard on and Neovim emits OSC 52,
which the terminal turns into a copy on your local machine. Neovim ≥ 0.10 does
this itself when $SSH_TTY is set; the config wires it up explicitly for 0.9.
Reconnecting. ta attaches to the main tmux session, creating it if
needed — the normal way back in after a dropped link. prefix + S does the same
for a named session, prefix + R reloads the config.
Readline. ~/.inputrc gives case-insensitive completion and ↑/↓ history
search on prefix, which is most of what makes a bare SSH prompt tolerable.
Nothing secret belongs in a public repo:
- SSH —
config/ssh/configstarts withInclude ~/.ssh/config.local. Put private hosts and identity files there; it is gitignored, and because the include comes first it wins over the shared defaults. - Git identity —
config/git/configincludes~/.config/git/config.local, which is whereuser.nameanduser.emailgo:git config -f ~/.config/git/config.local user.email "you@example.com"
- Anything else —
.gitignorecovers*.local,.env*and key material.
bat and fd are packaged as batcat and fdfind on Debian/Ubuntu but as
bat and fd on Fedora. Both shells detect which exists and export
BAT_CMD / FD_CMD. Similarly, column comes from bsdextrautils on Debian
and util-linux on Fedora, and lsh degrades gracefully when it is absent.
EDITOR falls back through nvim → vim → vi → nano and stays unset if none
exist, rather than pointing at a missing binary.
./SetupCron.shAdds a 2-hourly cron job running AutoGitSync.sh, which only pulls — it
never commits or pushes, and skips entirely if the tree is dirty or the branch
has diverged. Committing stays manual.
Two caveats: cron is absent from most containers (use a systemd timer instead)
and from Termux by default (pkg install cronie termux-services, then
sv-enable crond), and cron has no SSH agent, so this only works while the
repo is public over HTTPS.
.github/workflows/ci.yml runs shellcheck, parses the fish and Lua configs, and
then installs the dotfiles inside minimal debian:stable-slim, ubuntu:latest
and fedora:latest containers — asserting that the run is idempotent, that
every symlink resolves, that a login shell picks up the environment, and that
the package installer actually resolves real packages on both apt and dnf. A
further job does the same inside termux/termux-docker.
Core: git, bash. Everything else is optional and detected at runtime.
- Neovim plugin management
- Per-host overrides beyond the
.localfiles
MIT — see LICENSE.