Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .bash_profile
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Atuin shell history — guard against missing binary
[ -f "$HOME/.atuin/bin/env" ] && . "$HOME/.atuin/bin/env"

. "$HOME/.local/bin/env"
# Guarded like the atuin line above: written by the uv/rustup installers, and
# absent on a machine that has not run them, where sourcing it unconditionally
# aborts the shell with status 127.
[ -f "$HOME/.local/bin/env" ] && . "$HOME/.local/bin/env"

# Cargo environment
# ~/.cargo is a symlink to /Volumes/Secondary which can hang in uninterruptible
Expand Down
97 changes: 96 additions & 1 deletion .emacs.d/init.el
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,11 @@ Run for each new frame rather than once at startup, because under
:config
(setq dabbrev-case-fold-search nil))

;; Built in as of Emacs 30 -- no package needed.
;; Built in as of Emacs 30. Older builds -- Ubuntu 24.04 still ships 29 --
;; need the package, so ask for it only if the library isn't already there.
(unless (require 'which-key nil 'noerror)
(package-install 'which-key)
(require 'which-key))
(setq which-key-idle-delay 0.8
which-key-max-display-columns 4
which-key-max-description-length 25)
Expand All @@ -267,6 +271,97 @@ Run for each new frame rather than once at startup, because under
(use-package markdown-mode
:mode ("\\.md\\'" "\\.markdown\\'"))

;;; -------------------------------------------------------------- editing qol

;; project.el already binds C-x p; nothing to configure. C-x p f finds a file
;; in the current project, C-x p g greps it, C-x p p switches projects.
(electric-pair-mode 1)
(add-hook 'prog-mode-hook #'subword-mode)

;;; ----------------------------------------------------------- tree-sitter

;; Emacs 30 ships the *-ts-modes but no grammar sources, so
;; `treesit-install-language-grammar' has nothing to install from until this
;; alist is populated. bash, toml and yaml are already built here; the rest
;; install on demand with M-x treesit-install-language-grammar.
(setq treesit-language-source-alist
'((typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
(tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
(rust "https://github.com/tree-sitter/tree-sitter-rust")
(json "https://github.com/tree-sitter/tree-sitter-json")
(python "https://github.com/tree-sitter/tree-sitter-python")))

;; Opt in per language, and only where the grammar is actually present. The
;; previous config used treesit-auto with `(treesit-auto-add-to-auto-mode-alist
;; 'all)', which remapped every language whether or not its grammar existed --
;; opening any JSON file then failed with "Tree-sitter for JSON isn't
;; available". Gating on `treesit-language-available-p' makes that class of
;; error impossible: a missing grammar just means you get the plain mode.
;;
;; Two levers are needed, because they solve different problems. Where a
;; built-in mode already claims the extension, remap it:
(dolist (spec '((json js-json-mode . json-ts-mode)
(python python-mode . python-ts-mode)
(toml conf-toml-mode . toml-ts-mode)))
(when (treesit-language-available-p (car spec))
(add-to-list 'major-mode-remap-alist (cdr spec))))

;; ...and where nothing claims it at all, register the extension directly.
;; Emacs 30 ships typescript-ts-mode, tsx-ts-mode, rust-ts-mode and
;; yaml-ts-mode but puts none of them in `auto-mode-alist', so .ts, .tsx, .rs
;; and .yaml all fall through to text-mode on a stock build.
;;
;; sh-mode is deliberately left alone: it also handles .zsh, and bash-ts-mode
;; would be the wrong grammar for those.
(dolist (spec '((typescript "\\.ts\\'" . typescript-ts-mode)
(tsx "\\.tsx\\'" . tsx-ts-mode)
(rust "\\.rs\\'" . rust-ts-mode)
(yaml "\\.ya?ml\\'" . yaml-ts-mode)))
(when (treesit-language-available-p (car spec))
(add-to-list 'auto-mode-alist (cons (cadr spec) (cddr spec)))))

;;; ------------------------------------------------------------------- eglot

;; Emacs 30's built-in LSP client. Deliberately not required here: naming
;; `eglot-ensure' in a hook is enough to autoload it, so eglot stays unloaded
;; until the first file of a supported type is opened, and startup pays
;; nothing for it.

(defun tl/typescript-server (_interactive project)
"Return the language server contact for a TypeScript PROJECT.

Deno and typescript-language-server disagree about module resolution, so
this has to follow the project rather than a global preference: a tree with
a deno.json gets `deno lsp', anything else gets tsserver. Eglot has no
built-in Deno entry, which is why this exists at all."
(let ((root (and project (project-root project))))
(if (and root
(or (file-exists-p (expand-file-name "deno.json" root))
(file-exists-p (expand-file-name "deno.jsonc" root))))
'("deno" "lsp" :initializationOptions (:enable t :lint t))
'("typescript-language-server" "--stdio"))))

(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(((typescript-ts-mode :language-id "typescript")
(tsx-ts-mode :language-id "typescriptreact")
(js-ts-mode :language-id "javascript"))
. tl/typescript-server)))

;; Only the modes whose servers are actually installed. Eglot signals if it
;; can't find a server, so hooking a mode with no server is a per-file error,
;; not a silent no-op.
(dolist (hook '(rust-ts-mode-hook
typescript-ts-mode-hook
tsx-ts-mode-hook))
(add-hook hook #'eglot-ensure))

;;; --------------------------------------------------------------------- git

;; Autoloaded on the keybinding, so nothing loads until C-x g is pressed.
(use-package magit
:bind ("C-x g" . magit-status))

;;; ------------------------------------------------------------------ server

;; $EDITOR is `emacsclient -a emacs', so the server needs to be up. Started
Expand Down
59 changes: 59 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Checks

# These run bin/check-dotfiles, the same script you can run locally with
# `just check`. CI and local should never disagree about what "passing" means,
# so the logic lives in the script and this file only installs what it needs.

on:
pull_request:
push:
branches: [main]

concurrency:
group: checks-${{ github.ref }}
cancel-in-progress: true

jobs:
validate:
name: Validate dotfiles
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install check dependencies
run: |
sudo apt-get update -qq
# zsh: the shell most of this config is written in, and the one
# shellcheck could never parse.
# lua5.4: luac, for the hammerspoon and nvim configs.
# emacs-nox: 29.x on Ubuntu 24.04, deliberately older than the 30.2
# on the Macs -- if init.el works here it works there.
sudo apt-get install -y -qq zsh lua5.4 emacs-nox
python3 -m pip install --quiet --disable-pip-version-check pyyaml ansible-core

- name: Report versions
run: |
zsh --version
bash --version | head -1
emacs --version | head -1
python3 --version
ansible-playbook --version | head -1

- name: Shell syntax
run: ./bin/check-dotfiles syntax

- name: Config file syntax
run: ./bin/check-dotfiles configs

# The one that earns its keep: stages the tracked shell config into an
# empty HOME and actually starts a shell there. Catches an unguarded
# `source` of a file that exists on your machine but not a fresh one --
# which is exactly how this repo has broken before.
- name: Shell startup
run: ./bin/check-dotfiles shell

- name: Emacs cold start
run: ./bin/check-dotfiles emacs

- name: Ansible playbook
run: ./bin/check-dotfiles ansible
35 changes: 0 additions & 35 deletions .github/workflows/lint.yml

This file was deleted.

15 changes: 13 additions & 2 deletions .profile
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,19 @@ if [ -n "$BASH_VERSION" ] && [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi

. "$HOME/.local/bin/env"
. "$HOME/.cargo/env"
# Written by the uv/rustup installers; absent on a machine that has not run
# them, where sourcing it unconditionally aborts the shell with status 127.
[ -f "$HOME/.local/bin/env" ] && . "$HOME/.local/bin/env"

# Cargo environment. Deliberately not sourced, and deliberately not tested
# with [ -f ]: ~/.cargo is a symlink to /Volumes/Secondary, and any filesystem
# operation on it blocks indefinitely in uninterruptible D-state I/O wait when
# that volume is unresponsive. .zshenv and .bash_profile add the path this way
# for the same reason; this file was the one place still touching it.
case ":$PATH:" in
*":$HOME/.cargo/bin:"*) ;; # already in PATH
*) export PATH="$HOME/.cargo/bin:$PATH" ;;
esac

# Hermes Agent — ensure ~/.local/bin is on PATH
export PATH="$HOME/.local/bin:$PATH"
4 changes: 3 additions & 1 deletion .zshrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ source_if_exists "$HOME/.config/zsh/prompt.zsh"
source_if_exists "$HOME/.config/zsh/aliases.zsh"
source_if_exists "$HOME/.config/zsh/open-agent.zsh"

. "$HOME/.local/bin/env"
# Written by the uv/rustup installers; absent on a machine that has not run
# them yet, where an unguarded source aborts the whole shell with status 127.
source_if_exists "$HOME/.local/bin/env"
6 changes: 5 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ default:
check-env:
~/bin/check-env

# Lint all shell scripts with shellcheck
# Run the same checks CI runs (syntax, config parsing, shell startup, emacs, ansible)
check *SECTION:
~/bin/check-dotfiles {{ SECTION }}

# Lint all shell scripts with shellcheck (advisory; not run in CI)
lint:
~/bin/lint-shell

Expand Down
Loading
Loading