feat: harden dotfiles installer and add automated validation

This commit is contained in:
Brian Pooe 2026-06-11 20:38:19 +02:00
parent 394a9af367
commit db49fc1a56
34 changed files with 370 additions and 724 deletions

View file

@ -1,194 +0,0 @@
" ========================================
" Options
" ========================================
set encoding=UTF-8
set spelllang=en_us,de_de,es_es
set nohlsearch " Disable highlight on search
set number " Enable line numbers
set mouse=a " Enable mouse mode
set breakindent " Enable break indent
set undofile " Save undo history
set ignorecase " Case-insensitive searching unless \C or capital in search
set smartcase " Enable smart case
set signcolumn=yes " Keep signcolumn on by default
set updatetime=250 " Decrease update time
set timeoutlen=300 " Time to wait for a mapped sequence to complete (in milliseconds)
set nobackup " Don't create a backup file
set nowritebackup " Don't write backup before overwriting
set completeopt=menuone,noselect " Better completion experience
set whichwrap+=<,>,[,],h,l " Allow certain keys to move to the next line
set nowrap " Display long lines as one line
set linebreak " Don't break words when wrapping
set scrolloff=8 " Keep 8 lines above/below cursor
set sidescrolloff=8 " Keep 8 columns to the left/right of cursor
set relativenumber " Use relative line numbers
set numberwidth=4 " Number column width
set shiftwidth=4 " Spaces per indentation
set tabstop=4 " Spaces per tab
set softtabstop=4 " Spaces per tab during editing ops
set expandtab " Convert tabs to spaces
set nocursorline " Don't highlight the current line
set splitbelow " Horizontal splits below current window
set splitright " Vertical splits to the right
set noswapfile " Don't use a swap file
set smartindent " Smart indentation
set showtabline=2 " Always show tab line
set backspace=indent,eol,start " Configurable backspace behavior
set pumheight=10 " Popup menu height
set conceallevel=0 " Make `` visible in markdown
set fileencoding=utf-8 " File encoding
set cmdheight=1 " Command line height
set autoindent " Auto-indent new lines
set shortmess+=c " Don't show completion menu messages
set iskeyword+=- " Treat hyphenated words as whole words
set showmatch " show the matching part of pairs [] {} and ()
set laststatus=2 " Show status bar
set statusline=%f " Path to the file
set statusline+=%= " Switch to the right side
set statusline+=%l " Current line
set statusline+=/ " Separator
set statusline+=%L " Total lines
" ========================================
" Keymaps
" ========================================
" Set leader key
let mapleader = " "
let maplocalleader = " "
" Disable the spacebar key's default behavior in Normal and Visual modes
nnoremap <Space> <Nop>
vnoremap <Space> <Nop>
" Allow moving the cursor through wrapped lines with j, k
nnoremap <expr> k v:count == 0 ? 'gk' : 'k'
nnoremap <expr> j v:count == 0 ? 'gj' : 'j'
" clear highlights
nnoremap <Esc> :noh<CR>
" save file
nnoremap <C-s> :w<CR>
" save file without auto-formatting
nnoremap <leader>sn :noautocmd w<CR>
" quit file
nnoremap <C-q> :q<CR>
" delete single character without copying into register
nnoremap x "_x
" Vertical scroll and center
nnoremap <C-d> <C-d>zz
nnoremap <C-u> <C-u>zz
" Find and center
nnoremap n nzzzv
nnoremap N Nzzzv
" Resize with arrows
nnoremap <Up> :resize -2<CR>
nnoremap <Down> :resize +2<CR>
nnoremap <Left> :vertical resize -2<CR>
nnoremap <Right> :vertical resize +2<CR>
" Navigate buffers
nnoremap <Tab> :bnext<CR>
nnoremap <S-Tab> :bprevious<CR>
nnoremap <leader>sb :buffers<CR>:buffer<Space>
" increment/decrement numbers
nnoremap <leader>+ <C-a>
nnoremap <leader>- <C-x>
" window management
nnoremap <leader>v <C-w>v
nnoremap <leader>h <C-w>s
nnoremap <leader>se <C-w>=
nnoremap <leader>xs :close<CR>
" Navigate between splits
nnoremap <C-k> :wincmd k<CR>
nnoremap <C-j> :wincmd j<CR>
nnoremap <C-h> :wincmd h<CR>
nnoremap <C-l> :wincmd l<CR>
" tabs
nnoremap <leader>to :tabnew<CR>
nnoremap <leader>tx :tabclose<CR>
nnoremap <leader>tn :tabn<CR>
nnoremap <leader>tp :tabp<CR>
nnoremap <leader>x :bdelete<CR>
nnoremap <leader>b :enew<CR>
" toggle line wrapping
nnoremap <leader>lw :set wrap!<CR>
" Press jk fast to exit insert mode
inoremap jk <ESC>
inoremap kj <ESC>
" Stay in indent mode
" vnoremap < <gv
" vnoremap > >gv
" Keep last yanked when pasting
vnoremap p "_dP
" Explicitly yank to system clipboard (highlighted and entire row)
noremap <leader>y "+y
noremap <leader>Y "+Y
" Open file explorer
noremap <silent> <leader>e :Lex<CR>
" Make W w on save
noremap :W :w
" ========================================
" Other
" ========================================
" Syntax highlighting
syntax on
" Colorscheme
" colorscheme industry
colorscheme wildcharm
set background=dark
" hi Normal ctermbg=NONE guibg=NONE
" hi NonText ctermbg=NONE guibg=NONE guifg=NONE ctermfg=NONE
" hi VertSplit guibg=NONE guifg=NONE ctermbg=NONE ctermfg=NONE
" Sync clipboard with OS
set clipboard=unnamedplus
" True colors
if !has('gui_running') && &term =~ '\%(screen\|tmux\)'
let &t_8f = "\<Esc>[38;2;%lu;%lu;%lum"
let &t_8b = "\<Esc>[48;2;%lu;%lu;%lum"
endif
if has('termguicolors')
set termguicolors
endif
" Use a line cursor within insert mode and a block cursor everywhere else.
let &t_SI = "\e[6 q"
let &t_EI = "\e[2 q"
" Netrw
let g:netrw_banner = 0
let g:netrw_liststyle = 3
let g:netrw_browse_split = 4
let g:netrw_altv = 1
let g:netrw_winsize = 25
" Use 'l' instead of <CR> to open files
augroup netrw_setup | au!
au FileType netrw nmap <buffer> l <CR>
augroup END

View file

@ -174,7 +174,9 @@ local function angular_switch(section)
local is_ts = file:match '%.component%.ts$' local is_ts = file:match '%.component%.ts$'
-- If we're in a .ts file, check for inline sections before switching files -- If we're in a .ts file, check for inline sections before switching files
if is_ts and (section == 'template' or section == 'styles' or section == 'ts') then if
is_ts and (section == 'template' or section == 'styles' or section == 'ts')
then
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local pattern local pattern
if section == 'template' then if section == 'template' then
@ -213,8 +215,7 @@ local function angular_switch(section)
target = base .. '.component.html' target = base .. '.component.html'
elseif section == 'styles' then elseif section == 'styles' then
local scss = base .. '.component.scss' local scss = base .. '.component.scss'
target = vim.fn.filereadable(scss) == 1 and scss target = vim.fn.filereadable(scss) == 1 and scss or base .. '.component.css'
or base .. '.component.css'
elseif section == 'spec' then elseif section == 'spec' then
target = base .. '.component.spec.ts' target = base .. '.component.spec.ts'
end end

View file

@ -42,17 +42,20 @@ 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
-- Neovim 0.12 changed directive captures from TSNode to TSNode[] (arrays). -- Temporary compatibility workaround for Neovim 0.12 directive captures.
-- nvim-treesitter predicates do match[capture_id] expecting a single TSNode, -- Keep this scoped to 0.12 so later releases use the upstream implementation.
-- but now receive a table. The nil guard passes, then get_node_text calls if vim.version().minor == 12 then
-- node:range(true) on a plain table which has no range method and crashes. local get_node_text = vim.treesitter.get_node_text
-- Unwrap the array so nvim-treesitter keeps working until it is patched upstream. vim.treesitter.get_node_text = function(node, source, opts)
local _get_node_text = vim.treesitter.get_node_text if not node then
vim.treesitter.get_node_text = function(node, source, opts) return ''
if not node then return '' end end
if type(node) == 'table' then if type(node) == 'table' then
node = node[1] node = node[1]
if not node then return '' end if not node then
return ''
end
end
return get_node_text(node, source, opts)
end end
return _get_node_text(node, source, opts)
end end

View file

@ -12,7 +12,9 @@ end
local function hover_if_supported() local function hover_if_supported()
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
for _, client in ipairs(vim.lsp.get_clients { bufnr = bufnr }) do for _, client in ipairs(vim.lsp.get_clients { bufnr = bufnr }) do
if client.server_capabilities and client.server_capabilities.hoverProvider then if
client.server_capabilities and client.server_capabilities.hoverProvider
then
vim.lsp.buf.hover { border = 'rounded' } vim.lsp.buf.hover { border = 'rounded' }
return return
end end
@ -58,12 +60,7 @@ vim.api.nvim_create_autocmd('LspAttach', {
keymap.set('n', 'gt', '<cmd>Telescope lsp_type_definitions<CR>', opts) -- show lsp type definitions keymap.set('n', 'gt', '<cmd>Telescope lsp_type_definitions<CR>', opts) -- show lsp type definitions
opts.desc = 'See available code actions' opts.desc = 'See available code actions'
keymap.set( keymap.set({ 'n', 'v' }, '<leader>ca', vim.lsp.buf.code_action, opts) -- see available code actions, in visual mode will apply to selection
{ 'n', 'v' },
'<leader>ca',
vim.lsp.buf.code_action,
opts
) -- see available code actions, in visual mode will apply to selection
opts.desc = 'Smart rename' opts.desc = 'Smart rename'
keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts) -- smart rename keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts) -- smart rename

View file

@ -184,15 +184,12 @@ return {
'DapLogPoint', 'DapLogPoint',
{ text = 'L', texthl = 'DiagnosticInfo', linehl = '', numhl = '' } { text = 'L', texthl = 'DiagnosticInfo', linehl = '', numhl = '' }
) )
vim.fn.sign_define( vim.fn.sign_define('DapStopped', {
'DapStopped', text = '>',
{ texthl = 'DiagnosticOk',
text = '>', linehl = 'DapStoppedLine',
texthl = 'DiagnosticOk', numhl = '',
linehl = 'DapStoppedLine', })
numhl = '',
}
)
vim.fn.sign_define( vim.fn.sign_define(
'DapBreakpointRejected', 'DapBreakpointRejected',
{ text = 'R', texthl = 'DiagnosticError', linehl = '', numhl = '' } { text = 'R', texthl = 'DiagnosticError', linehl = '', numhl = '' }

View file

@ -28,12 +28,11 @@ return {
html = { 'prettierd', 'prettier', stop_after_first = true }, html = { 'prettierd', 'prettier', stop_after_first = true },
htmlangular = { 'prettierd', 'prettier', stop_after_first = true }, htmlangular = { 'prettierd', 'prettier', stop_after_first = true },
lua = { 'stylua' }, lua = { 'stylua' },
rust = { 'ast_grep' }, rust = { 'rustfmt', lsp_format = 'fallback' },
python = { 'ruff', 'black' }, python = { 'ruff_format' },
dockerfile = { 'hadolint' },
}, },
format_on_save = { format_on_save = {
lsp_fallback = true, lsp_format = 'fallback',
async = false, async = false,
timeout_ms = 1000, timeout_ms = 1000,
}, },
@ -44,7 +43,7 @@ return {
vim.keymap.set({ 'n', 'v' }, '<leader>mp', function() vim.keymap.set({ 'n', 'v' }, '<leader>mp', function()
conform.format { conform.format {
lsp_fallback = true, lsp_format = 'fallback',
async = false, async = false,
timeout_ms = 1000, timeout_ms = 1000,
} }

View file

@ -52,11 +52,13 @@ local function dedupe(list)
return out return out
end end
local tool_installer_list = dedupe(vim.list_extend(vim.deepcopy(tools), lsp_servers)) local tool_installer_list =
dedupe(vim.list_extend(vim.deepcopy(tools), lsp_servers))
return { return {
{ {
'mason-org/mason-lspconfig.nvim', 'mason-org/mason-lspconfig.nvim',
cond = vim.env.DOTFILES_CI ~= '1',
opts = { opts = {
-- 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),
@ -81,6 +83,7 @@ return {
}, },
{ {
'WhoIsSethDaniel/mason-tool-installer.nvim', 'WhoIsSethDaniel/mason-tool-installer.nvim',
cond = vim.env.DOTFILES_CI ~= '1',
dependencies = { dependencies = {
'mason-org/mason.nvim', 'mason-org/mason.nvim',
}, },
@ -93,8 +96,8 @@ return {
['mason-null-ls'] = false, ['mason-null-ls'] = false,
['mason-nvim-dap'] = false, ['mason-nvim-dap'] = false,
}, },
run_on_start = true, run_on_start = vim.env.DOTFILES_CI ~= '1',
start_delay = 0, start_delay = 3000,
}, },
}, },
} }

View file

@ -8,17 +8,18 @@ return {
dependencies = { dependencies = {
'nvim-tree/nvim-web-devicons', 'nvim-tree/nvim-web-devicons',
}, },
config = function() init = function()
local nvimtree = require 'nvim-tree'
-- disable netrw at the very start of your init.lua
vim.g.loaded_netrw = 1 vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1 vim.g.loaded_netrwPlugin = 1
end,
config = function()
local nvimtree = require 'nvim-tree'
-- optionally enable 24-bit colour
vim.opt.termguicolors = true vim.opt.termguicolors = true
-- OR setup with some options
nvimtree.setup { nvimtree.setup {
disable_netrw = true,
hijack_netrw = false,
sort = { sort = {
sorter = 'case_sensitive', sorter = 'case_sensitive',
}, },

View file

@ -1,4 +1,35 @@
-- Highlight, edit, and navigate code -- Highlight, edit, and navigate code
local ci = vim.env.DOTFILES_CI == '1'
local parsers = {
'angular',
'lua',
'python',
'javascript',
'typescript',
'vimdoc',
'vim',
'regex',
'terraform',
'sql',
'dockerfile',
'toml',
'json',
'java',
'groovy',
'gitignore',
'graphql',
'yaml',
'make',
'cmake',
'markdown',
'markdown_inline',
'bash',
'tsx',
'css',
'html',
'rust',
}
return { return {
'nvim-treesitter/nvim-treesitter', 'nvim-treesitter/nvim-treesitter',
branch = 'master', branch = 'master',
@ -8,36 +39,10 @@ return {
'nvim-treesitter/nvim-treesitter-textobjects', 'nvim-treesitter/nvim-treesitter-textobjects',
}, },
opts = { opts = {
ensure_installed = { ensure_installed = ci and {} or parsers,
'angular', auto_install = not ci,
'lua', parser_install_dir = ci and (vim.fn.stdpath 'cache' .. '/treesitter')
'python', or nil,
'javascript',
'typescript',
'vimdoc',
'vim',
'regex',
'terraform',
'sql',
'dockerfile',
'toml',
'json',
'java',
'groovy',
'gitignore',
'graphql',
'yaml',
'make',
'cmake',
'markdown',
'markdown_inline',
'bash',
'tsx',
'css',
'html',
'rust',
},
auto_install = true,
highlight = { enable = true }, highlight = { enable = true },
indent = { enable = true }, indent = { enable = true },
incremental_selection = { incremental_selection = {

View file

@ -113,29 +113,3 @@ purple = '#a292a3'
gray = '#727169' gray = '#727169'
black = '#181616' black = '#181616'
white = '#c5c9c5' white = '#c5c9c5'
[palettes.nord]
dark_blue = '#5E81AC'
blue = '#81A1C1'
teal = '#88C0D0'
red = '#BF616A'
orange = '#D08770'
green = '#A3BE8C'
yellow = '#EBCB8B'
purple = '#B48EAD'
gray = '#434C5E'
black = '#2E3440'
white='#D8DEE9'
[palettes.onedark]
dark_blue='#61afef'
blue='#56b6c2'
red='#e06c75'
green='#98c379'
purple='#c678dd'
cyan='#56b6c2'
orange='#be5046'
yellow='#e5c07b'
gray='#828997'
white ='#abb2bf'
black='#2c323c'

View file

@ -113,29 +113,3 @@ purple = '#a292a3'
gray = '#727169' gray = '#727169'
black = '#181616' black = '#181616'
white = '#c5c9c5' white = '#c5c9c5'
[palettes.nord]
dark_blue = '#5E81AC'
blue = '#81A1C1'
teal = '#88C0D0'
red = '#BF616A'
orange = '#D08770'
green = '#A3BE8C'
yellow = '#EBCB8B'
purple = '#B48EAD'
gray = '#434C5E'
black = '#2E3440'
white='#D8DEE9'
[palettes.onedark]
dark_blue='#61afef'
blue='#56b6c2'
red='#e06c75'
green='#98c379'
purple='#c678dd'
cyan='#56b6c2'
orange='#be5046'
yellow='#e5c07b'
gray='#828997'
white ='#abb2bf'
black='#2c323c'

View file

@ -1,23 +0,0 @@
# bg="#3B4252"
bg="default"
default_fg="#D8DEE9"
session_fg="#A3BE8C"
session_selection_fg="#3B4252"
session_selection_bg="#81A1C1"
active_window_fg="#88C0D0"
active_pane_border="#abb2bf"
set -g status-left-length 200 # default: 10
set -g status-right-length 200 # default: 10
set -g status-left "#[fg=${session_fg},bold,bg=${bg}] #S #[fg=${default_fg},nobold,bg=${bg}] | "
set -g status-right " #{cpu -i 3}  #{mem} "
set -g status-justify left
set -g status-style "bg=${bg}"
set -g window-status-format "#[fg=${default_fg},bg=default] #I:#W"
set -g window-status-current-format "#[fg=${active_window_fg},bold,bg=default]  #[underscore]#I:#W"
set -g window-status-last-style "fg=${default_fg},bg=default"
set -g message-command-style "bg=default,fg=${default_fg}"
set -g message-style "bg=default,fg=${default_fg}"
set -g mode-style "bg=${session_selection_bg},fg=${session_selection_fg}"
set -g pane-active-border-style "fg=${active_pane_border},bg=default"
set -g pane-border-style "fg=brightblack,bg=default"

View file

@ -68,9 +68,9 @@ set -g @plugin 'tmux-plugins/tmux-continuum' # Automatically saves sessions e
set -g @plugin 'hendrikmi/tmux-cpu-mem-monitor' # CPU and memory info set -g @plugin 'hendrikmi/tmux-cpu-mem-monitor' # CPU and memory info
set -g @plugin 'tmux-plugins/tmux-yank' set -g @plugin 'tmux-plugins/tmux-yank'
# Load theme from deterministic selector file written by ~/.zshrc. # Load theme from deterministic selector file generated during shell startup.
if-shell '[ -f ~/.config/tmux/current-theme.conf ]' \ if-shell '[ -f ~/.cache/dotfiles/tmux-theme.conf ]' \
'source-file ~/.config/tmux/current-theme.conf' \ 'source-file ~/.cache/dotfiles/tmux-theme.conf' \
'source-file ~/.config/tmux/dragon-theme.conf' 'source-file ~/.config/tmux/dragon-theme.conf'
# Resurrect # Resurrect

View file

@ -13,12 +13,12 @@ local function env_number(name, fallback)
return parsed return parsed
end end
-- User-tunable settings live in ~/.zshrc, which writes them to env.lua -- User-tunable settings live in ~/.zshrc, which writes them to a cache file
-- (GUI-launched WezTerm never sees shell exports). WezTerm watches that -- (GUI-launched WezTerm never sees shell exports). WezTerm watches that
-- file and live-reloads, so editing .zshrc + opening a new shell applies. -- file and live-reloads, so editing .zshrc + opening a new shell applies.
-- Precedence: env.lua > process env > defaults. -- Precedence: cache file > process env > defaults.
local user = {} local user = {}
local env_file = wezterm.home_dir .. "/.config/wezterm/env.lua" local env_file = wezterm.home_dir .. "/.cache/dotfiles/wezterm-env.lua"
local ok, loaded = pcall(dofile, env_file) local ok, loaded = pcall(dofile, env_file)
if ok and type(loaded) == "table" then if ok and type(loaded) == "table" then
user = loaded user = loaded

View file

@ -1,107 +0,0 @@
// Zed keymap
//
// This carries over the Neovim shortcuts that map cleanly onto Zed.
// Leader mappings use Zed's Vim mode sequence support.
[
{
"context": "Editor && !menu && vim_mode == normal",
"bindings": {
"ctrl-s": "workspace::SaveAll",
"ctrl-q": "pane::CloseActiveItem",
"tab": "pane::ActivateNextItem",
"shift-tab": "pane::ActivatePreviousItem",
"ctrl-h": "vim::ToggleProjectPanelFocus",
"ctrl-j": "workspace::ActivatePaneDown",
"ctrl-k": "workspace::ActivatePaneUp",
"ctrl-l": "workspace::ActivatePaneRight",
"shift-k": "editor::Hover",
"alt-j": "editor::MoveLineDown",
"alt-k": "editor::MoveLineUp",
"[ d": "editor::GoToPreviousDiagnostic",
"] d": "editor::GoToDiagnostic",
"g d": "editor::GoToDefinition",
"g i": "editor::GoToImplementation",
"g t": "editor::GoToTypeDefinition",
"g shift-d": "editor::GoToDeclaration",
"g shift-r": "editor::FindAllReferences"
}
},
{
"context": "ProjectPanel",
"bindings": {
"ctrl-h": "project_panel::ToggleFocus",
"ctrl-l": "project_panel::ToggleFocus",
"o": "project_panel::Open"
}
},
{
"context": "Editor && !menu && vim_mode == visual",
"bindings": {
"alt-j": "editor::MoveLineDown",
"alt-k": "editor::MoveLineUp",
"space c a": "editor::ToggleCodeActions",
"space g h": "vim::StartOfLine",
"space g l": "vim::EndOfLine",
"space s t": "editor::SortLinesCaseSensitive",
"space ~": "editor::ToggleCase"
}
},
{
"context": "Editor && !menu && vim_mode == normal",
"bindings": {
"space space": "tab_switcher::Toggle",
"space tab": "tab_switcher::Toggle",
"space /": "buffer_search::Deploy",
"space +": "vim::Increment",
"space -": "vim::Decrement",
"space b": "workspace::NewFile",
"space c a": "editor::ToggleCodeActions",
"space c f": "editor::Format",
"space d b": "editor::ToggleBreakpoint",
"space d c": "debugger::Continue",
"space d f": "editor::Hover",
"space d i": "debugger::StepInto",
"space d o": "debugger::StepOut",
"space d p": "debugger::Pause",
"space d r": "debugger::Rerun",
"space d shift-o": "debugger::StepOver",
"space d t": "debugger::Stop",
"space d u": "debug_panel::ToggleFocus",
"space e": "project_panel::ToggleFocus",
"space g b": "git::Branch",
"space g c f": "git::FileHistory",
"space g h": "vim::StartOfLine",
"space g l": "vim::EndOfLine",
"space g s": "git::Diff",
"space h": "pane::SplitHorizontal",
"space l g": "git_panel::ToggleFocus",
"space l w": "editor::ToggleSoftWrap",
"space m p": "editor::Format",
"space q": "diagnostics::Deploy",
"space q t": "pane::CloseOtherItems",
"space r n": "editor::Rename",
"space r s": "editor::RestartLanguageServer",
"space s /": "workspace::NewSearch",
"space s a": "workspace::SaveAll",
"space s b": "tab_switcher::Toggle",
"space s d": "diagnostics::Deploy",
"space s d s": "project_symbols::Toggle",
"space s f": "file_finder::Toggle",
"space s g": "workspace::NewSearch",
"space s n": "workspace::SaveWithoutFormat",
"space shift-d": "diagnostics::DeployCurrentFile",
"space t o": "terminal_panel::ToggleFocus",
"space t r": "task::Rerun",
"space t s": "zed::OpenTasks",
"space t t": "editor::SpawnNearestTask",
"space u d": "editor::ToggleInlineDiagnostics",
"space v": "pane::SplitVertical",
"space w h": "workspace::ActivatePaneLeft",
"space w j": "workspace::ActivatePaneDown",
"space w k": "workspace::ActivatePaneUp",
"space w l": "workspace::ActivatePaneRight",
"space w p": "workspace::ActivatePreviousPane",
"space x": "pane::CloseActiveItem"
}
}
]

View file

@ -1,160 +0,0 @@
// Zed settings
//
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// This translates the main Neovim behaviors from `.config/n into
// the Zed features that have direct equivalents.
{
"telemetry": {
"diagnostics": false,
"metrics": false,
},
"vim_mode": true,
"base_keymap": "JetBrains",
"auto_install_extensions": {
"html": true,
"angular": true,
"lua": true,
"sql": true,
"kanagawa-themes": true,
},
"icon_theme": {
"mode": "dark",
"light": "Zed (Default)",
"dark": "Zed (Default)",
},
"theme": {
"mode": "dark",
"light": "Kanagawa Lotus",
"dark": "New Darcula",
},
"ui_font_size": 16,
"buffer_font_size": 15,
"gutter": {
"line_numbers": true,
"breakpoints": true,
"folds": true,
"min_line_number_digits": 4,
},
"relative_line_numbers": "enabled",
"soft_wrap": "none",
"show_wrap_guides": false,
"tab_size": 4,
"hard_tabs": false,
"vertical_scroll_margin": 4,
"horizontal_scroll_margin": 8,
"use_smartcase_search": true,
"show_whitespaces": "selection",
"hover_popover_delay": 300,
"auto_signature_help": true,
"remove_trailing_whitespace_on_save": true,
"format_on_save": "on",
"diagnostics_max_severity": null,
"diagnostics": {
"inline": {
"enabled": false,
},
},
"inlay_hints": {
"enabled": true,
"show_type_hints": true,
"show_parameter_hints": true,
"show_other_hints": true,
"show_background": false,
},
"git": {
"git_gutter": "tracked_files",
"inline_blame": {
"enabled": false,
},
},
"tabs": {
"file_icons": true,
"git_status": true,
"show_close_button": "always",
"show_diagnostics": "all",
},
"project_panel": {
"default_width": 320,
"git_status": true,
"show_diagnostics": "all",
"hide_hidden": false,
"auto_reveal_entries": true,
"indent_guides": {
"show": "always",
},
},
"toolbar": {
"breadcrumbs": true,
"quick_actions": true,
"selections_menu": true,
"code_actions": true,
},
"file_types": {
"Groovy": ["**/*.pipeline", "**/*.multibranch"],
"Terraform": ["**/*.tfvars"],
},
"languages": {
"JavaScript": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
},
"TypeScript": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
},
"TSX": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
},
"Lua": {
"formatter": {
"external": {
"command": "stylua",
"arguments": [
"--respect-ignores",
"--stdin-filepath",
"{buffer_path}",
"-",
],
},
},
},
"Python": {
"formatter": {
"external": {
"command": "ruff",
"arguments": [
"format",
"--stdin-filename",
"{buffer_path}",
"-",
],
},
},
},
"Shell Script": {
"tab_size": 4,
"hard_tabs": false,
"formatter": {
"external": {
"command": "shfmt",
"arguments": [
"--filename",
"{buffer_path}",
"--indent",
"4",
],
},
},
},
},
}

View file

@ -27,13 +27,13 @@ today = !git all --since='12am'
pom = push origin main pom = push origin main
pos = push origin staging pos = push origin staging
pod = push origin develop pod = push origin develop
pou = push origin uat pou = push origin uat
ruo = remote update origin ruo = remote update origin
cma = commit --amend cma = commit --amend
cmna = commit -n --amend cmna = commit -n --amend
del = branch -D del = branch -D
cm = commit -m cm = commit -m
pf = push -f origin pf = push --force-with-lease origin
po = push origin po = push origin
rv = remote -v rv = remote -v
rao = remote add origin rao = remote add origin
@ -48,7 +48,7 @@ latest-tag = tag -l --sort=-version:refname
back = !f() { git reset --soft HEAD~$1; }; f back = !f() { git reset --soft HEAD~$1; }; f
[push] [push]
default = matching default = simple
[core] [core]
editor=nvim editor=nvim

30
.github/workflows/validate.yml vendored Normal file
View file

@ -0,0 +1,30 @@
name: Validate
on:
push:
pull_request:
jobs:
dotfiles:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install validation tools
run: |
sudo apt-get update
sudo apt-get install --yes gnupg shellcheck shfmt zsh
source install/lib/common.sh
source install/lib/packages.sh
install_wezterm
brew install neovim starship stylua
- name: Restore Neovim plugins
env:
DOTFILES_CI: "1"
XDG_CONFIG_HOME: ${{ github.workspace }}/.config
run: nvim --headless "+Lazy! restore" +qa
- name: Validate
run: bin/check-dotfiles

37
.gitignore vendored
View file

@ -1,56 +1,33 @@
# Created by https://www.toptal.com/developers/gitignore/api/vim # Vim
# Edit at https://www.toptal.com/developers/gitignore?templates=vim
### Vim ###
# Swap
[._]*.s[a-v][a-z] [._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p] [._]*.sw[a-p]
[._]s[a-rt-v][a-z] [._]s[a-rt-v][a-z]
[._]ss[a-gi-z] [._]ss[a-gi-z]
[._]sw[a-p] [._]sw[a-p]
# Session
Session.vim Session.vim
Sessionx.vim Sessionx.vim
# Temporary
.netrwhist .netrwhist
*~ *~
# Auto-generated tag files
tags tags
# Persistent undo
[._]*.un~ [._]*.un~
.nvimlog
nvim.log
.config/zed/
# Track only the managed application configurations.
.config/* .config/*
!.config/nvim/ !.config/nvim/
!.config/lazygit/ !.config/lazygit/
!.config/starship/ !.config/starship/
!.config/wezterm/ !.config/wezterm/
# machine-local, generated by .zshrc
.config/wezterm/env.lua .config/wezterm/env.lua
!.config/zed/
!.config/zed/settings.json
!.config/zed/keymap.json
!.config/tmux/ !.config/tmux/
!.config/tmux/nord-theme.conf
!.config/tmux/dragon-theme.conf
!.config/tmux/wave-theme.conf
!.config/tmux/tmux-cheatsheet.md
!.config/tmux/tmux.conf
!.config/.vimrc
!.config/.ideavimrc !.config/.ideavimrc
# Ignore subfolders in tmux # Ignore generated or downloaded tmux content.
.config/tmux/** .config/tmux/**
!.config/tmux/nord-theme.conf .config/tmux/current-theme.conf
!.config/tmux/dragon-theme.conf !.config/tmux/dragon-theme.conf
!.config/tmux/wave-theme.conf !.config/tmux/wave-theme.conf
!.config/tmux/tmux-cheatsheet.md !.config/tmux/tmux-cheatsheet.md
!.config/tmux/tmux.conf !.config/tmux/tmux.conf
!.config/.vimrc
!.config/.ideavimrc
.nvimlog
# End of https://www.toptal.com/developers/gitignore/api/vim

View file

@ -1 +0,0 @@
source-file ~/.config/tmux/tmux.conf

43
.zshrc
View file

@ -3,13 +3,13 @@
if [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then if [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
fi fi
export PATH="$HOME/.local/bin:$PATH"
# ===================== User-tunable environment ======================== # ===================== User-tunable environment ========================
# Single place to control the look of the whole stack. Everything below # Single place to control the look of the whole stack.
# (and the wezterm env.lua writer further down) derives from these.
export KANAGAWA_THEME="wave" # wave | dragon export KANAGAWA_THEME="wave" # wave | dragon
export WEZTERM_FONT_SIZE="13" export WEZTERM_FONT_SIZE="13"
export WEZTERM_WINDOW_OPACITY="0.80" export WEZTERM_WINDOW_OPACITY="0.9"
export WEZTERM_TEXT_OPACITY="1.0" export WEZTERM_TEXT_OPACITY="1.0"
# ======================================================================== # ========================================================================
@ -31,32 +31,11 @@ case "${KANAGAWA_THEME}" in
;; ;;
esac esac
# Write a deterministic tmux theme selector file so tmux servers don't depend # Generate the theme files consumed by GUI-launched WezTerm and tmux.
# on inherited shell env vars. if command -v dotfiles-apply-theme >/dev/null 2>&1; then
export TMUX_THEME_FILE="$HOME/.config/tmux/current-theme.conf" dotfiles-apply-theme
mkdir -p "$HOME/.config/tmux"
if [ "$TMUX_THEME" = "wave" ]; then
printf 'source-file %s\n' "$HOME/.config/tmux/wave-theme.conf" >| "$TMUX_THEME_FILE"
else
printf 'source-file %s\n' "$HOME/.config/tmux/dragon-theme.conf" >| "$TMUX_THEME_FILE"
fi fi
# Write WezTerm settings file (same pattern as tmux above). GUI-launched
# WezTerm never sees shell exports, so it watches this file and live-reloads
# when the values change.
_wez_env_file="$HOME/.config/wezterm/env.lua"
_wez_env_content="return {
theme = \"$WEZTERM_THEME\",
font_size = $WEZTERM_FONT_SIZE,
window_opacity = $WEZTERM_WINDOW_OPACITY,
text_opacity = $WEZTERM_TEXT_OPACITY,
}"
mkdir -p "$HOME/.config/wezterm"
if [ ! -f "$_wez_env_file" ] || [ "$(cat "$_wez_env_file")" != "$_wez_env_content" ]; then
printf '%s\n' "$_wez_env_content" >| "$_wez_env_file"
fi
unset _wez_env_file _wez_env_content
# Starship # Starship
if command -v starship >/dev/null 2>&1; then if command -v starship >/dev/null 2>&1; then
eval "$(starship init zsh)" eval "$(starship init zsh)"
@ -81,6 +60,9 @@ ZSH_HIGHLIGHT_STYLES[path]=none
ZSH_HIGHLIGHT_STYLES[path_prefix]=none ZSH_HIGHLIGHT_STYLES[path_prefix]=none
# Autosuggestions # Autosuggestions
# The plugin default is intentionally dim and becomes unreadable with a
# transparent terminal background.
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=#938aa9'
for _p in \ for _p in \
"$HOMEBREW_PREFIX/share/zsh-autosuggestions/zsh-autosuggestions.zsh" \ "$HOMEBREW_PREFIX/share/zsh-autosuggestions/zsh-autosuggestions.zsh" \
/usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh
@ -124,7 +106,12 @@ alias sudo="sudo "
alias lg="lazygit" alias lg="lazygit"
alias update="brew upgrade && sudo apt update && sudo apt upgrade -y" alias update="brew upgrade && sudo apt update && sudo apt upgrade -y"
if command -v tmux &>/dev/null && [ -z "$TMUX" ]; then export DOTFILES_AUTO_TMUX="${DOTFILES_AUTO_TMUX:-0}"
if [ "$DOTFILES_AUTO_TMUX" = "1" ] \
&& command -v tmux &>/dev/null \
&& [ -z "$TMUX" ] \
&& [ -z "$SSH_CONNECTION" ]
then
tmux attach -t default 2>/dev/null || tmux new -s default tmux attach -t default 2>/dev/null || tmux new -s default
fi fi

View file

@ -5,21 +5,12 @@ Linux dotfiles oriented around **Pop!_OS 24.04** (Cosmic DE).
## Quick Setup ## Quick Setup
```bash ```bash
# Clone git clone https://github.com/brianpooe/dotfiles.git ~/dotfiles
git clone <repo-url> ~/dotfiles ~/dotfiles/install/install.sh --dotfiles-only
# Symlink configs
ln -sfn ~/dotfiles/.config/nvim ~/.config/nvim
ln -sfn ~/dotfiles/.config/tmux ~/.config/tmux
ln -sfn ~/dotfiles/.config/starship ~/.config/starship
ln -sfn ~/dotfiles/.config/lazygit ~/.config/lazygit
ln -sfn ~/dotfiles/.config/wezterm ~/.config/wezterm
ln -sfn ~/dotfiles/.config/zed ~/.config/zed
ln -sfn ~/dotfiles/.config/.ideavimrc ~/.ideavimrc
ln -sfn ~/dotfiles/.zshrc ~/.zshrc
ln -sfn ~/dotfiles/.gitconfig ~/.gitconfig
``` ```
The installer backs up existing targets before linking replacements.
## Install ## Install
On Pop!_OS / Ubuntu 24.04, run the bundled installer. It uses **apt** for On Pop!_OS / Ubuntu 24.04, run the bundled installer. It uses **apt** for
@ -33,7 +24,8 @@ starship, gh, zsh plugins) so those tools stay current — one
``` ```
See [`install/README.md`](install/README.md) for flags and the full See [`install/README.md`](install/README.md) for flags and the full
package list. The `update` alias runs `brew upgrade && sudo apt upgrade`. package list. The `update` alias runs
`brew upgrade && sudo apt update && sudo apt upgrade -y`.
## Theme ## Theme
@ -44,3 +36,20 @@ Set in shell:
```bash ```bash
export KANAGAWA_THEME="wave" # or "dragon" export KANAGAWA_THEME="wave" # or "dragon"
``` ```
Opening a new shell runs `dotfiles-apply-theme`, which writes the generated
tmux and WezTerm settings to `~/.cache/dotfiles`.
Automatic tmux startup is opt-in:
```bash
export DOTFILES_AUTO_TMUX=1
```
## Validate
```bash
bin/check-dotfiles
```
The same checks run in GitHub Actions.

60
bin/check-dotfiles Executable file
View file

@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
export PATH="$HOME/.local/share/nvim/mason/bin:$PATH"
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
printf 'Required validation tool not found: %s\n' "$1" >&2
exit 1
}
}
for command in bash git nvim shellcheck shfmt starship stylua wezterm zsh; do
require_cmd "$command"
done
shell_files=(bin/dotfiles-apply-theme bin/check-dotfiles install/install.sh install/lib/*.sh)
git diff --check
bash -n "${shell_files[@]}"
zsh -n .zshrc
git config --file .gitconfig --list >/dev/null
shellcheck --exclude=SC1091 "${shell_files[@]}"
shfmt -d -i 2 -ci "${shell_files[@]}"
stylua --check .config/nvim
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/data/nvim"
ln -s "$HOME/.local/share/nvim/lazy" "$tmp/data/nvim/lazy"
HOME="$tmp/home" \
KANAGAWA_THEME=wave \
WEZTERM_FONT_SIZE=13 \
WEZTERM_WINDOW_OPACITY=.9 \
WEZTERM_TEXT_OPACITY=1 \
bin/dotfiles-apply-theme
STARSHIP_CONFIG="$repo_root/.config/starship/starship.toml" starship print-config >/dev/null
STARSHIP_CONFIG="$repo_root/.config/starship/starship-wave.toml" starship print-config >/dev/null
wezterm --config-file "$repo_root/.config/wezterm/wezterm.lua" show-keys --lua >/dev/null
nvim_output="$(
DOTFILES_CI=1 \
XDG_CONFIG_HOME="$repo_root/.config" \
XDG_CACHE_HOME="$tmp/cache" \
XDG_DATA_HOME="$tmp/data" \
XDG_STATE_HOME="$tmp/state" \
nvim --headless '+sleep 100m' '+lua assert(vim.v.errmsg == "", vim.v.errmsg)' '+qa' 2>&1
)"
if [[ -n $nvim_output ]]; then
printf 'Neovim startup produced unexpected output:\n%s\n' "$nvim_output" >&2
exit 1
fi
printf 'All dotfile checks passed.\n'

46
bin/dotfiles-apply-theme Executable file
View file

@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
theme="${KANAGAWA_THEME:-wave}"
font_size="${WEZTERM_FONT_SIZE:-13}"
window_opacity="${WEZTERM_WINDOW_OPACITY:-0.9}"
text_opacity="${WEZTERM_TEXT_OPACITY:-1.0}"
state_dir="$HOME/.cache/dotfiles"
case "$theme" in
wave | kanagawa-wave) theme=wave ;;
dragon | kanagawa-dragon) theme=dragon ;;
*) theme=dragon ;;
esac
for value in "$font_size" "$window_opacity" "$text_opacity"; do
[[ $value =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]] || {
printf 'Invalid numeric theme value: %s\n' "$value" >&2
exit 1
}
done
mkdir -p "$state_dir"
write_if_changed() {
local destination="$1" content="$2" temporary
if [[ -f $destination ]] && [[ $(<"$destination") == "$content" ]]; then
return
fi
temporary="$(mktemp "$state_dir/.theme.XXXXXX")"
printf '%s\n' "$content" >"$temporary"
mv "$temporary" "$destination"
}
write_if_changed "$state_dir/tmux-theme.conf" \
"source-file \"$HOME/.config/tmux/$theme-theme.conf\""
write_if_changed "$state_dir/wezterm-env.lua" "return {
theme = \"$theme\",
font_size = $font_size,
window_opacity = $window_opacity,
text_opacity = $text_opacity,
}"

View file

@ -12,6 +12,10 @@ Ubuntu 24.04 / Debian-derivative) to the state these dotfiles assume.
| **nvm** | Node.js runtime | Installs the latest LTS release and sets it as the default; `.zshrc` sources `~/.nvm/nvm.sh`. | | **nvm** | Node.js runtime | Installs the latest LTS release and sets it as the default; `.zshrc` sources `~/.nvm/nvm.sh`. |
| **tpm** | tmux plugin manager | Clone of `tmux-plugins/tpm` so the bundled `tmux.conf` works on first launch. | | **tpm** | tmux plugin manager | Clone of `tmux-plugins/tpm` so the bundled `tmux.conf` works on first launch. |
Downloaded installer scripts, apt signing keys, and the Nerd Font archive
are pinned and verified before use. Updating their upstream versions requires
updating the corresponding SHA-256 hashes or fingerprints in `install/lib/`.
GitHub-release-binary downloads (the previous approach for nvim/lazygit/ GitHub-release-binary downloads (the previous approach for nvim/lazygit/
starship) have been retired — they ran latest at install time then went starship) have been retired — they ran latest at install time then went
stale. Brew solves that. stale. Brew solves that.
@ -46,7 +50,7 @@ install/install.sh --help
## Keeping current ## Keeping current
```bash ```bash
update # alias: brew upgrade && sudo apt upgrade update # alias: brew upgrade && sudo apt update && sudo apt upgrade -y
``` ```
The CLI tools come from brew, so they stay on bleeding-edge stable. Brave The CLI tools come from brew, so they stay on bleeding-edge stable. Brave
@ -63,6 +67,7 @@ install/
lib/packages.sh apt base + WezTerm + Brave Beta vendor repos lib/packages.sh apt base + WezTerm + Brave Beta vendor repos
lib/brew.sh install Homebrew + `brew bundle` lib/brew.sh install Homebrew + `brew bundle`
lib/extras.sh nvm + tpm lib/extras.sh nvm + tpm
lib/fonts.sh verified JetBrainsMono Nerd Font install
lib/dotfiles.sh symlink dotfiles + chsh zsh lib/dotfiles.sh symlink dotfiles + chsh zsh
``` ```
@ -75,8 +80,9 @@ install/
5. **Homebrew install** (NONINTERACTIVE) + `brew bundle` against `Brewfile`. 5. **Homebrew install** (NONINTERACTIVE) + `brew bundle` against `Brewfile`.
6. **nvm** (pinned `$NVM_VERSION`, default `v0.40.1`) + latest LTS Node.js. 6. **nvm** (pinned `$NVM_VERSION`, default `v0.40.1`) + latest LTS Node.js.
7. **tpm** clone to `~/.tmux/plugins/tpm`. 7. **tpm** clone to `~/.tmux/plugins/tpm`.
8. **Symlink** `nvim tmux starship lazygit wezterm zed → ~/.config/...`; 8. **Symlink** `nvim tmux starship lazygit wezterm → ~/.config/...`;
`.config/.ideavimrc → ~/.ideavimrc`; `.zshrc`, `.gitconfig → ~`. `.config/.ideavimrc → ~/.ideavimrc`; `.zshrc`, `.gitconfig → ~`;
`dotfiles-apply-theme → ~/.local/bin/`.
9. **chsh** login shell to zsh. 9. **chsh** login shell to zsh.
Pre-existing config targets get backed up to `*.pre-install.<timestamp>`, Pre-existing config targets get backed up to `*.pre-install.<timestamp>`,

View file

@ -36,7 +36,10 @@ for arg in "$@"; do
case $arg in case $arg in
--packages-only) MODE=packages ;; --packages-only) MODE=packages ;;
--dotfiles-only) MODE=dotfiles ;; --dotfiles-only) MODE=dotfiles ;;
-h|--help) usage; exit 0 ;; -h | --help)
usage
exit 0
;;
*) die "Unknown argument: $arg (try --help)" ;; *) die "Unknown argument: $arg (try --help)" ;;
esac esac
done done
@ -46,6 +49,7 @@ preflight() {
((EUID != 0)) || die "Do not run as root; run as your regular user." ((EUID != 0)) || die "Do not run as root; run as your regular user."
require_cmd sudo require_cmd sudo
require_cmd curl require_cmd curl
require_cmd sha256sum
} }
main() { main() {
@ -76,7 +80,7 @@ main() {
info "Done. Open a new shell (or log out + back in) to pick up the new login shell." info "Done. Open a new shell (or log out + back in) to pick up the new login shell."
info "First tmux launch: prefix + I to fetch tpm plugins." info "First tmux launch: prefix + I to fetch tpm plugins."
info "Keep tools current with: brew upgrade && sudo apt upgrade" info "Keep tools current with: brew upgrade && sudo apt update && sudo apt upgrade -y"
} }
main main

View file

@ -3,14 +3,25 @@
# Brew owns the CLI toolchain so `brew upgrade` always gets latest versions. # Brew owns the CLI toolchain so `brew upgrade` always gets latest versions.
BREW_PREFIX="/home/linuxbrew/.linuxbrew" BREW_PREFIX="/home/linuxbrew/.linuxbrew"
HOMEBREW_INSTALL_COMMIT="93a25fd2d2fdb86422303c292b9223f84ef23eaf"
HOMEBREW_INSTALL_SHA256="17f204b128869ec71e6b650051f1e65ef5b74cf994c2ff8b919103241554dd2f"
install_homebrew() { install_homebrew() {
if [[ -x $BREW_PREFIX/bin/brew ]]; then if [[ -x $BREW_PREFIX/bin/brew ]]; then
info "Homebrew already installed" info "Homebrew already installed"
else else
info "Installing Homebrew..." info "Installing Homebrew..."
NONINTERACTIVE=1 bash -c \ local installer
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" installer="$(mktemp)"
download_verified \
"https://raw.githubusercontent.com/Homebrew/install/$HOMEBREW_INSTALL_COMMIT/install.sh" \
"$HOMEBREW_INSTALL_SHA256" \
"$installer"
if ! NONINTERACTIVE=1 bash "$installer"; then
rm -f "$installer"
die "Homebrew installer failed"
fi
rm -f "$installer"
fi fi
eval "$("$BREW_PREFIX/bin/brew" shellenv)" eval "$("$BREW_PREFIX/bin/brew" shellenv)"
} }

View file

@ -8,12 +8,49 @@ APT_LISTS_DIR="/etc/apt/sources.list.d"
info() { printf '\033[0;32m==>\033[0m %s\n' "$*"; } info() { printf '\033[0;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mWARNING:\033[0m %s\n' "$*" >&2; } warn() { printf '\033[1;33mWARNING:\033[0m %s\n' "$*" >&2; }
error() { printf '\033[0;31mERROR:\033[0m %s\n' "$*" >&2; } error() { printf '\033[0;31mERROR:\033[0m %s\n' "$*" >&2; }
die() { error "$*"; exit 1; } die() {
error "$*"
exit 1
}
require_cmd() { require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1" command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
} }
download_verified() {
local url="$1" sha256="$2" destination="$3"
if ! curl -fsSL --retry 3 "$url" -o "$destination"; then
rm -f "$destination"
die "Failed to download $url"
fi
if ! printf '%s %s\n' "$sha256" "$destination" | sha256sum --check --status -; then
rm -f "$destination"
die "SHA-256 verification failed for $url"
fi
}
key_fingerprints_match() {
local key="$1" expected_fingerprints="$2"
local actual_fingerprints normalized_expected
actual_fingerprints="$(
gpg --show-keys --with-colons "$key" 2>/dev/null |
awk -F: '$1 == "fpr" { print $10 }' |
sort -u |
tr '\n' ' '
)"
normalized_expected="$(
for fingerprint in $expected_fingerprints; do
printf '%s\n' "$fingerprint"
done |
sort -u |
tr '\n' ' '
)"
[[ -n $actual_fingerprints && $actual_fingerprints == "$normalized_expected" ]]
}
apt_install() { apt_install() {
sudo apt-get install -y --no-install-recommends "$@" sudo apt-get install -y --no-install-recommends "$@"
} }
@ -45,15 +82,14 @@ backup_and_link() {
info "Linked $dest -> $src" info "Linked $dest -> $src"
} }
# Add a signed apt repo. Args: name keyring_url repo_line # Add a signed apt repo. Args: name keyring_url repo_line expected_fingerprints
# Downloads the key to a temp file first and verifies it, so a failed
# fetch never leaves an empty keyring or partial list file behind.
add_apt_repo() { add_apt_repo() {
local name="$1" keyring_url="$2" repo_line="$3" local name="$1" keyring_url="$2" repo_line="$3" expected_fingerprints="$4"
local keyring="$KEYRINGS_DIR/$name.gpg" local keyring="$KEYRINGS_DIR/$name.gpg"
local list="$APT_LISTS_DIR/$name.list" local list="$APT_LISTS_DIR/$name.list"
if [[ -f $list && -s $keyring ]]; then if [[ -f $list && -s $keyring ]] &&
key_fingerprints_match "$keyring" "$expected_fingerprints"; then
return 0 return 0
fi fi
info "Adding apt repo: $name" info "Adding apt repo: $name"
@ -68,7 +104,10 @@ add_apt_repo() {
fi fi
[[ -s $tmp_key ]] || die "Downloaded key for $name is empty" [[ -s $tmp_key ]] || die "Downloaded key for $name is empty"
if ! gpg --yes --dearmor -o "$tmp_gpg" < "$tmp_key" 2>/dev/null; then key_fingerprints_match "$tmp_key" "$expected_fingerprints" ||
die "Signing-key fingerprint verification failed for $name"
if ! gpg --yes --dearmor -o "$tmp_gpg" <"$tmp_key" 2>/dev/null; then
# Already binary (.gpg) keys can't be dearmored; use the raw bytes instead. # Already binary (.gpg) keys can't be dearmored; use the raw bytes instead.
cp "$tmp_key" "$tmp_gpg" cp "$tmp_key" "$tmp_gpg"
fi fi
@ -76,5 +115,5 @@ add_apt_repo() {
sudo install -d -m 0755 "$KEYRINGS_DIR" sudo install -d -m 0755 "$KEYRINGS_DIR"
sudo install -m 0644 "$tmp_gpg" "$keyring" sudo install -m 0644 "$tmp_gpg" "$keyring"
echo "$repo_line" | sudo tee "$list" >/dev/null echo "$repo_line" | sudo tee "$list" >/dev/null
unset _APT_UPDATED # force re-update so the new repo is seen unset _APT_UPDATED # force re-update so the new repo is seen
} }

View file

@ -3,15 +3,15 @@
link_dotfiles() { link_dotfiles() {
info "Linking dotfiles from $DOTFILES_DIR..." info "Linking dotfiles from $DOTFILES_DIR..."
backup_and_link "$DOTFILES_DIR/.config/nvim" "$HOME/.config/nvim" backup_and_link "$DOTFILES_DIR/.config/nvim" "$HOME/.config/nvim"
backup_and_link "$DOTFILES_DIR/.config/tmux" "$HOME/.config/tmux" backup_and_link "$DOTFILES_DIR/.config/tmux" "$HOME/.config/tmux"
backup_and_link "$DOTFILES_DIR/.config/starship" "$HOME/.config/starship" backup_and_link "$DOTFILES_DIR/.config/starship" "$HOME/.config/starship"
backup_and_link "$DOTFILES_DIR/.config/lazygit" "$HOME/.config/lazygit" backup_and_link "$DOTFILES_DIR/.config/lazygit" "$HOME/.config/lazygit"
backup_and_link "$DOTFILES_DIR/.config/wezterm" "$HOME/.config/wezterm" backup_and_link "$DOTFILES_DIR/.config/wezterm" "$HOME/.config/wezterm"
backup_and_link "$DOTFILES_DIR/.config/zed" "$HOME/.config/zed"
backup_and_link "$DOTFILES_DIR/.config/.ideavimrc" "$HOME/.ideavimrc" backup_and_link "$DOTFILES_DIR/.config/.ideavimrc" "$HOME/.ideavimrc"
backup_and_link "$DOTFILES_DIR/.zshrc" "$HOME/.zshrc" backup_and_link "$DOTFILES_DIR/.zshrc" "$HOME/.zshrc"
backup_and_link "$DOTFILES_DIR/.gitconfig" "$HOME/.gitconfig" backup_and_link "$DOTFILES_DIR/.gitconfig" "$HOME/.gitconfig"
backup_and_link "$DOTFILES_DIR/bin/dotfiles-apply-theme" "$HOME/.local/bin/dotfiles-apply-theme"
} }
set_zsh_as_shell() { set_zsh_as_shell() {

View file

@ -2,15 +2,22 @@
# Per-user helpers not managed by brew: nvm (official installer) + tpm. # Per-user helpers not managed by brew: nvm (official installer) + tpm.
NVM_VERSION="${NVM_VERSION:-v0.40.1}" NVM_VERSION="${NVM_VERSION:-v0.40.1}"
NVM_INSTALL_SHA256="${NVM_INSTALL_SHA256:-abdb525ee9f5b48b34d8ed9fc67c6013fb0f659712e401ecd88ab989b3af8f53}"
install_nvm() { install_nvm() {
if [[ -s $HOME/.nvm/nvm.sh ]]; then if [[ -s $HOME/.nvm/nvm.sh ]]; then
info "nvm already installed" info "nvm already installed"
else else
info "Installing nvm ($NVM_VERSION)..." info "Installing nvm ($NVM_VERSION)..."
PROFILE=/dev/null bash -c \ local installer
"curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/$NVM_VERSION/install.sh | bash" || installer="$(mktemp)"
download_verified \
"https://raw.githubusercontent.com/nvm-sh/nvm/$NVM_VERSION/install.sh" \
"$NVM_INSTALL_SHA256" \
"$installer"
PROFILE=/dev/null bash "$installer" ||
warn "nvm install failed (install manually if you need Node)" warn "nvm install failed (install manually if you need Node)"
rm -f "$installer"
fi fi
if [[ -s $HOME/.nvm/nvm.sh ]]; then if [[ -s $HOME/.nvm/nvm.sh ]]; then

View file

@ -6,6 +6,8 @@
NERD_FONT_NAME="JetBrainsMono" NERD_FONT_NAME="JetBrainsMono"
NERD_FONT_DIR="$HOME/.local/share/fonts/${NERD_FONT_NAME}NerdFont" NERD_FONT_DIR="$HOME/.local/share/fonts/${NERD_FONT_NAME}NerdFont"
NERD_FONT_VERSION="v3.4.0"
NERD_FONT_SHA256="ef552a3e638f25125c6ad4c51176a6adcdce295ab1d2ffacf0db060caf8c1582"
install_nerd_font() { install_nerd_font() {
if fc-list 2>/dev/null | grep -qi "JetBrainsMono Nerd Font"; then if fc-list 2>/dev/null | grep -qi "JetBrainsMono Nerd Font"; then
@ -14,15 +16,12 @@ install_nerd_font() {
fi fi
info "Installing ${NERD_FONT_NAME} Nerd Font..." info "Installing ${NERD_FONT_NAME} Nerd Font..."
local url="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${NERD_FONT_NAME}.tar.xz" local url="https://github.com/ryanoasis/nerd-fonts/releases/download/$NERD_FONT_VERSION/${NERD_FONT_NAME}.tar.xz"
local tmp local tmp
tmp="$(mktemp -d)" tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' RETURN trap 'rm -rf "$tmp"' RETURN
if ! curl -fsSL --retry 3 -o "$tmp/font.tar.xz" "$url"; then download_verified "$url" "$NERD_FONT_SHA256" "$tmp/font.tar.xz"
warn "Nerd Font download failed; install manually from https://www.nerdfonts.com"
return
fi
mkdir -p "$NERD_FONT_DIR" mkdir -p "$NERD_FONT_DIR"
tar -xJf "$tmp/font.tar.xz" -C "$NERD_FONT_DIR" tar -xJf "$tmp/font.tar.xz" -C "$NERD_FONT_DIR"

View file

@ -44,7 +44,8 @@ install_wezterm() {
fi fi
add_apt_repo wezterm-fury \ add_apt_repo wezterm-fury \
https://apt.fury.io/wez/gpg.key \ https://apt.fury.io/wez/gpg.key \
"deb [signed-by=$KEYRINGS_DIR/wezterm-fury.gpg] https://apt.fury.io/wez/ * *" "deb [signed-by=$KEYRINGS_DIR/wezterm-fury.gpg] https://apt.fury.io/wez/ * *" \
"0CA603116C960BAFB2BF310BD7BA31CF90C4B319 82FA179A44D70EEF9CDEB52659C703F427E1A6C9"
apt_update_once apt_update_once
apt_install wezterm apt_install wezterm
} }
@ -59,7 +60,8 @@ install_brave_beta() {
# "beta" in the name. # "beta" in the name.
add_apt_repo brave-browser-beta \ add_apt_repo brave-browser-beta \
https://brave-browser-apt-beta.s3.brave.com/brave-browser-beta-archive-keyring.gpg \ https://brave-browser-apt-beta.s3.brave.com/brave-browser-beta-archive-keyring.gpg \
"deb [signed-by=$KEYRINGS_DIR/brave-browser-beta.gpg arch=amd64] https://brave-browser-apt-beta.s3.brave.com/ stable main" "deb [signed-by=$KEYRINGS_DIR/brave-browser-beta.gpg arch=amd64] https://brave-browser-apt-beta.s3.brave.com/ stable main" \
"56F49901AB19BAF099A95A76C3DE1DD4F661CDCB 8C1F16AB24DF8F75C1CF56595929A141E0E87F1F B721E073B7EF8E56ACC6B23ECBC67D2399225CCF"
apt_update_once apt_update_once
apt_install brave-browser-beta apt_install brave-browser-beta
} }