diff --git a/.gitignore b/.gitignore index 32e46fc..55f015b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,6 @@ .config/btop .config/neofetch /.aider* -<<<<<<< HEAD -./home_config/nvim/lazy-lock.json home_config/nvim/lazy-lock.json -======= __pycache__/ ->>>>>>> 00717500056db846954b09bcfac4ebb3f1aa84e6 +*.bak diff --git a/README.md b/README.md new file mode 100644 index 0000000..18114ee --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# dotfiles + +Personal dotfiles. Configs are organized by where they get linked: + +| Folder | Links into | Examples | +| -------------- | ------------ | --------------------------------- | +| `home_root` | `~` | `.zshrc`, `.wezterm.lua`, `.nanorc` | +| `home_config` | `~/.config` | `nvim`, `ohmyposh`, `zellij`, `ghostty` | +| `lk-installers`| n/a | helper scripts to install tooling | + +## Linux / macOS (GNU stow) + +Requires [`stow`](https://www.gnu.org/software/stow/). + +```bash +./link-dotfiles +``` + +This runs `stow home_root` and `stow --target=$HOME/.config home_config`. + +## Windows (PowerShell) + +`stow` doesn't run natively on Windows, so use the PowerShell equivalent. It +mirrors stow by creating **junctions** for directories and **symlinks/hardlinks** +for files — none of which require administrator rights: + +```powershell +pwsh -File .\link-dotfiles.ps1 +``` + +- Re-running is safe: already-correct links are skipped. +- Any real file/dir that would be overwritten is moved aside to `.bak`. +- Preview without changing anything: `pwsh -File .\link-dotfiles.ps1 -DryRun` + +The repo stays the source of truth — edit files under `home_root` / +`home_config` and the linked locations update live. + +## Installers + +| Script | Platform | Purpose | +| ------------------------------- | -------- | ------------------ | +| `lk-installers/install-ohmyposh` | bash | oh-my-posh (Linux/macOS) | +| `lk-installers/install-ohmyposh.ps1` | pwsh | oh-my-posh (Windows, via winget) | + +```powershell +# Windows: install oh-my-posh, then link +pwsh -File .\lk-installers\install-ohmyposh.ps1 +pwsh -File .\link-dotfiles.ps1 +``` + +### oh-my-posh + +The prompt theme lives at `home_config/ohmyposh/lkraven.toml` and is loaded from +your shell profile, e.g. in PowerShell: + +```powershell +oh-my-posh init pwsh --config '~/.config/ohmyposh/lkraven.toml' | Invoke-Expression +``` + +It expects a Nerd Font (e.g. **VictorMono Nerd Font Mono**) to be installed and +selected in your terminal for the glyphs to render correctly. diff --git a/home_config/ohmyposh/lkraven.toml b/home_config/ohmyposh/lkraven.toml index e6704bc..b92eac0 100644 --- a/home_config/ohmyposh/lkraven.toml +++ b/home_config/ohmyposh/lkraven.toml @@ -1,126 +1,152 @@ -#:schema https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json console_title_template = '{{if .Root}}(Admin){{end}} {{.PWD}}' -version = 2 +version = 3 [palette] black = '#222531' - bright_black = '#373b46' blue = '#6388D8' + bright_70 = '#9daeb6' + bright_80 = '#b3cbcf' + bright_90 = '#cce7ec' + bright_black = '#373b46' bright_blue = '#a4c4ff' - cyan = '#00b1a8' bright_cyan = '#42dcd1' - green = '#16B866' bright_green = '#51e08a' - magenta = '#9d78ff' bright_magenta = '#d8adff' - red = '#ff491a' bright_red = '#ff854f' - white = '#a9bcc3' bright_white = '#cce7ec' - yellow = '#e1c631' bright_yellow = '#FFE14E' + cyan = '#00b1a8' dark_30 = '#414751' dark_40 = '#565f69' dark_50 = '#6e7882' dark_60 = '#86929d' - bright_70 = '#9daeb6' - bright_80 = '#b3cbcf' - bright_90 = '#cce7ec' + green = '#16B866' + magenta = '#9d78ff' + red = '#ff491a' + white = '#a9bcc3' + yellow = '#e1c631' + +[secondary_prompt] + template = '❯❯ ' + foreground = 'p:bright_green' + background = 'transparent' + +[transient_prompt] + template = '❯ ' + background = 'transparent' + foreground_templates = ['{{if gt .Code 0}}p:red{{end}}', '{{if eq .Code 0}}p:bright_green{{end}}'] [[blocks]] - alignment = 'left' type = 'prompt' + alignment = 'left' [[blocks.segments]] + template = '{{.Icon}} ' + foreground = 'p:cyan' type = 'os' style = 'plain' - foreground = 'p:cyan' - template = '{{.Icon}} ' - - [[blocks.segments]] - foreground = 'p:dark_50' - style = 'plain' - template = '{{ .Path }} {{ if not .Writable}}󰌾 {{end}}' - type = 'path' [blocks.segments.properties] + cache_duration = 'none' + + [[blocks.segments]] + template = '{{ .Path }} {{ if not .Writable}}󰌾 {{end}}' + foreground = 'p:dark_50' + type = 'path' + style = 'plain' + + [blocks.segments.properties] + cache_duration = 'none' style = 'agnoster' [[blocks]] - alignment = 'left' type = 'prompt' + alignment = 'left' [[blocks.segments]] - type = 'python' + leading_diamond = '' + trailing_diamond = ' ' + template = '  {{ .Venv }} ' foreground = 'p:blue' - background = "p:bright_black" - leading_diamond = "" - style = "diamond" - trailing_diamond = " " - template = "  {{ .Venv }} " + background = 'p:bright_black' + type = 'python' + style = 'diamond' [blocks.segments.properties] + cache_duration = 'none' display_default = false [[blocks.segments]] + leading_diamond = '' + trailing_diamond = '' + template = ' {{ .UpstreamIcon }}{{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }}  {{ .Working.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Staging.Changed }}  {{ .Staging.String }}{{ end }}{{ if gt .StashCount 0 }}  {{ .StashCount }}{{ end }} ' foreground = 'p:bright_cyan' - background = "p:bright_black" - leading_diamond = "" - style = "diamond" - trailing_diamond = "" - template = " {{ .UpstreamIcon }}{{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }}  {{ .Working.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Staging.Changed }}  {{ .Staging.String }}{{ end }}{{ if gt .StashCount 0 }}  {{ .StashCount }}{{ end }} " + background = 'p:bright_black' type = 'git' + style = 'diamond' [blocks.segments.properties] branch_icon = ' ' + cache_duration = 'none' commit_icon = ' ' + fetch_stash_count = true + fetch_status = true + fetch_upstream_icon = true merge_icon = ' ' no_commits_icon = ' ' rebase_icon = ' ' revert_icon = ' ' tag_icon = ' ' - fetch_stash_count = true - fetch_status = true - fetch_upstream_icon = true [[blocks]] - type = "prompt" - alignment = "right" + type = 'prompt' + alignment = 'right' [[blocks.segments]] - foreground = 'p:green' - style = 'plain' template = '{{ .FormattedMs }}' + foreground = 'p:green' type = 'executiontime' + style = 'plain' [blocks.segments.properties] + cache_duration = 'none' style = 'austin' [[blocks.segments]] - type = "time" - style = "plain" - foreground = "p:dark_30" + foreground = 'p:dark_30' + type = 'time' + style = 'plain' [blocks.segments.properties] - time_format = "3:04:05pm" + cache_duration = 'none' + time_format = '3:04:05pm' [[blocks.segments]] - type = "session" - style = "plain" - foreground = "p:dark_50" - template = "{{.UserName}}" + template = '{{.UserName}}' + foreground = 'p:dark_50' + type = 'session' + style = 'plain' + + [blocks.segments.properties] + cache_duration = 'none' [[blocks.segments]] - type = "session" - style = "plain" - foreground = "p:cyan" - template = " {{ if .SSHSession }}󰄙 {{ else }}@{{ end }} " + template = ' {{ if .SSHSession }}󰄙 {{ else }}@{{ end }} ' + foreground = 'p:cyan' + type = 'session' + style = 'plain' + + [blocks.segments.properties] + cache_duration = 'none' [[blocks.segments]] - type = "session" - style = "plain" - foreground = "p:blue" - template = "{{.HostName}}" + template = '{{.HostName}}' + foreground = 'p:blue' + type = 'session' + style = 'plain' + + [blocks.segments.properties] + cache_duration = 'none' [[blocks]] type = 'prompt' @@ -128,24 +154,11 @@ version = 2 newline = true [[blocks.segments]] + template = '❯ ' + background = 'transparent' type = 'text' style = 'plain' - foreground_templates = [ - "{{if gt .Code 0}}p:red{{end}}", - "{{if eq .Code 0}}p:bright_green{{end}}", - ] - background = 'transparent' - template = '❯ ' + foreground_templates = ['{{if gt .Code 0}}p:red{{end}}', '{{if eq .Code 0}}p:bright_green{{end}}'] -[transient_prompt] - foreground_templates = [ - "{{if gt .Code 0}}p:red{{end}}", - "{{if eq .Code 0}}p:bright_green{{end}}", - ] - background = 'transparent' - template = '❯ ' - -[secondary_prompt] - foreground = 'p:bright_green' - background = 'transparent' - template = '❯❯ ' + [blocks.segments.properties] + cache_duration = 'none' diff --git a/link-dotfiles.ps1 b/link-dotfiles.ps1 new file mode 100644 index 0000000..0a4a843 --- /dev/null +++ b/link-dotfiles.ps1 @@ -0,0 +1,105 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Windows equivalent of ./link-dotfiles (the GNU stow script). + +.DESCRIPTION + stow can't run natively on Windows, so this mirrors what it does: + home_root/* -> ~\ (e.g. ~\.zshrc) + home_config/* -> ~\.config\ (e.g. ~\.config\ohmyposh) + + Directories are linked with junctions and files with symlinks/hardlinks, + none of which need administrator rights (junctions and hardlinks never do; + symlinks are attempted first in case Developer Mode is on, then we fall + back). Re-running is safe: correct links are skipped, and any real file or + directory that would be clobbered is moved aside to .bak first. + +.EXAMPLE + pwsh -File .\link-dotfiles.ps1 +#> +[CmdletBinding()] +param( + # Print what would happen without changing anything. + [switch]$DryRun +) + +$ErrorActionPreference = 'Stop' +$repo = $PSScriptRoot +$home_ = $HOME + +# stow metadata files we must never link into the home tree. +$skip = @('.stow-local-ignore', '.stow-global-ignore') + +function Get-LinkTarget { + param([string]$Path) + $item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkType) { return $item.Target } + return $null +} + +function Link-Item { + param( + [string]$Source, # absolute path inside the repo + [string]$Dest # absolute path under the home tree + ) + + $isDir = (Get-Item -LiteralPath $Source -Force).PSIsContainer + + # Already linked to the right place? Nothing to do. + $existingTarget = Get-LinkTarget $Dest + if ($existingTarget -and ($existingTarget -ieq $Source)) { + Write-Host " ok $Dest" -ForegroundColor DarkGray + return + } + + if (Test-Path -LiteralPath $Dest) { + if ($existingTarget) { + # A link, but pointing elsewhere -> replace it. + if ($DryRun) { Write-Host " relink $Dest (was -> $existingTarget)" -ForegroundColor Yellow; return } + Remove-Item -LiteralPath $Dest -Force -Recurse + } else { + # A real file/dir -> back it up rather than destroy it. + $bak = "$Dest.bak" + Write-Host " backup $Dest -> $bak" -ForegroundColor Yellow + if (-not $DryRun) { Move-Item -LiteralPath $Dest -Destination $bak -Force } + } + } + + if ($DryRun) { Write-Host " link $Dest -> $Source" -ForegroundColor Cyan; return } + + $parent = Split-Path $Dest -Parent + if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } + + if ($isDir) { + New-Item -ItemType Junction -Path $Dest -Target $Source | Out-Null + Write-Host " junc $Dest -> $Source" -ForegroundColor Green + } else { + # Prefer a real symlink (needs admin or Developer Mode); fall back to a + # hardlink (same volume, no admin); finally just copy. + try { + New-Item -ItemType SymbolicLink -Path $Dest -Target $Source -ErrorAction Stop | Out-Null + Write-Host " link $Dest -> $Source" -ForegroundColor Green + } catch { + try { + New-Item -ItemType HardLink -Path $Dest -Target $Source -ErrorAction Stop | Out-Null + Write-Host " hard $Dest -> $Source" -ForegroundColor Green + } catch { + Copy-Item -LiteralPath $Source -Destination $Dest -Force + Write-Host " copy $Dest (could not link; copied)" -ForegroundColor Magenta + } + } + } +} + +Write-Host "home_root -> $home_" +Get-ChildItem -LiteralPath (Join-Path $repo 'home_root') -Force | + Where-Object { $skip -notcontains $_.Name } | + ForEach-Object { Link-Item $_.FullName (Join-Path $home_ $_.Name) } + +$configDir = Join-Path $home_ '.config' +Write-Host "home_config -> $configDir" +Get-ChildItem -LiteralPath (Join-Path $repo 'home_config') -Force | + Where-Object { $skip -notcontains $_.Name } | + ForEach-Object { Link-Item $_.FullName (Join-Path $configDir $_.Name) } + +Write-Host "Done." -ForegroundColor Green diff --git a/lk-installers/install-ohmyposh.ps1 b/lk-installers/install-ohmyposh.ps1 new file mode 100644 index 0000000..2a8a239 --- /dev/null +++ b/lk-installers/install-ohmyposh.ps1 @@ -0,0 +1,27 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Install oh-my-posh on Windows (parity with ./lk-installers/install-ohmyposh). +.DESCRIPTION + Uses winget when available; otherwise falls back to the official installer. + No administrator rights are required (installs to the current user). +#> +[CmdletBinding()] +param() +$ErrorActionPreference = 'Stop' + +if (Get-Command oh-my-posh -ErrorAction SilentlyContinue) { + Write-Host "oh-my-posh already installed: $((Get-Command oh-my-posh).Source)" + return +} + +if (Get-Command winget -ErrorAction SilentlyContinue) { + Write-Host "Installing oh-my-posh via winget..." + winget install JanDeDobbeleer.OhMyPosh -s winget --accept-source-agreements --accept-package-agreements +} else { + Write-Host "winget not found; using the official installer script..." + Set-ExecutionPolicy Bypass -Scope Process -Force + Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://ohmyposh.dev/install.ps1')) +} + +Write-Host "Done. Open a new shell so PATH refreshes, then run .\link-dotfiles.ps1"