diff --git a/.chezmoi.toml.tmpl b/.chezmoi.toml.tmpl index 7ed58ec..267094d 100644 --- a/.chezmoi.toml.tmpl +++ b/.chezmoi.toml.tmpl @@ -29,6 +29,7 @@ personal = true rust = true ohmyzsh = true mise = true +emacs = true elixir = true erlang = true lua = true @@ -107,6 +108,51 @@ bright_magenta = "#ff7edb" bright_cyan = "#36f9f6" bright_white = "#ffffff" +# Synthwave84 Soft -- the variant Zed uses ("Synthwave84 Soft" in zed/settings.json). +# Lower contrast than the base palette above, and it carries the syntax roles the base +# block lacks. Consumed by the Emacs theme. Source of truth is the shipped Zed theme: +# ~/Library/Application Support/Zed/extensions/installed/synthwave84/themes/synthwave84.json +# ANSI/terminal colors are deliberately absent -- Zed's Soft variant reuses the base ANSI +# palette above, so terminal faces stay identical to Ghostty. +[data.theme.soft] +name = "Synthwave84 Soft" +bg = "#2e2a3e" +bg_dark = "#2a2139" +bg_light = "#332e42" +fg = "#f0f0f0" +# Flattened from Zed's alpha colors over bg: #f0f0f080 and #f0f0f073 respectively. +fg_dim = "#8f8d97" +fg_subtle = "#85838e" +accent = "#ff92e0" +accent2 = "#5af5f3" +cursor = "#ff92e0" +selection_bg = "#4e4570" +selection_fg = "#f0f0f0" +border = "#5a5f8d" +border_focus = "#9933aa" +comment = "#848bbd" +keyword = "#fee380" +string = "#ffa15a" +string_escape = "#85f3c3" +number = "#fa9890" +constant = "#fa9890" +function = "#5af5f3" +variable = "#ff92e0" +type = "#ff92e0" +property = "#ff92e0" +operator = "#fee380" +punctuation = "#f0f0f0" +preproc = "#ff92e0" +link = "#85f3c3" +error = "#fe6670" +warning = "#fee380" +info = "#33f0fc" +success = "#85f3c3" +hint = "#33f0fc" +git_added = "#85f3c3" +git_modified = "#fee380" +git_deleted = "#fe6670" + [git] autoCommit = true autoPush = false diff --git a/Brewfile b/Brewfile index f97f287..5418c2e 100644 --- a/Brewfile +++ b/Brewfile @@ -9,6 +9,7 @@ tap "hashicorp/tap" # homebrew/services is now built-in, no tap needed +tap "d12frosted/emacs-plus" tap "infisical/get-cli" tap "kurtosis-tech/tap" @@ -109,6 +110,10 @@ brew "osv-scanner" # Dependency CVE scanning # ============================================================================= brew "neovim" +# emacs-plus over core emacs: native-comp, tree-sitter, and a real macOS GUI, +# all on by default in this formula -- do not pass args, they disable bottles. +# Config lives at ~/.config/emacs (see: make setup-emacs). +brew "d12frosted/emacs-plus/emacs-plus@30" brew "autoconf" brew "automake" brew "libtool" diff --git a/CLAUDE.md b/CLAUDE.md index 477b108..f3581a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,13 +42,14 @@ make lazy-load-stats # Show lazy loading stats ``` # Identity: name, email, github, gpg_signing_key, age_recipient, brewPrefix # Shell: starship, ohmyzsh, oh_my_zsh_theme, oh_my_zsh_plugins -# Tools: mise, rust, elixir, erlang, lua, direnv, devenv, nix +# Tools: mise, rust, elixir, erlang, lua, direnv, devenv, nix, emacs # Services: tailscale, onepassword, aws, infisical, orbstack # macOS: paperwm, raycast, llvm, postgres, psql # Web3: foundry, huff, solana # Apps: work, personal # MCP: datadog, sentry, signoz, regen, coingecko, digest, recall, autoresearch, watchdog, prepper, sentinel, patchbot # Theme: [data.theme] -- full Synthwave84 palette (bg, fg, accent, ANSI colors) +# [data.theme.soft] -- Synthwave84 Soft variant + syntax roles (Zed/Emacs) ``` Use in templates: `{{- if .rust -}}...{{- end -}}`. Use `{{- -}}` to trim whitespace. @@ -108,6 +109,10 @@ Commits run: trailing-whitespace, end-of-file-fixer, check-yaml, check-added-lar Single source of truth: `[data.theme]` in `chezmoi.toml` (Synthwave84 palette). Templates reference colors as `{{ .theme.bg }}`, `{{ .theme.accent }}`, etc. Applied to: fzf (`tools.zsh.tmpl`), Starship (`starship.toml.tmpl`), Hammerspoon alerts. Static theme files in `config/theme/synthwave84.toml` for tools that can't use chezmoi templates. +**Two variants.** `[data.theme]` is the base palette (Ghostty, tmux, fzf, Starship, btop, yazi). `[data.theme.soft]` is the lower-contrast **Synthwave84 Soft** variant that Zed uses, and it is the only block carrying syntax roles (`keyword`, `string`, `function`, `type`, ...) plus diagnostic and git colors. Emacs consumes `{{ .theme.soft.* }}` for UI and syntax, but reuses the base `{{ .theme.* }}` ANSI keys for terminal faces -- Zed's Soft variant leaves `terminal.ansi.*` at base values, so shell buffers match Ghostty exactly. + +Authoritative source for both is the shipped Zed theme JSON (`~/Library/Application Support/Zed/extensions/installed/synthwave84/themes/synthwave84.json`), not `config/theme/synthwave84.toml`, which had drifted on `keyword` and `type`. + ## Adding Features **New alias**: Add to `home/dot_zsh/aliases/dev.zsh` diff --git a/Makefile b/Makefile index bdd0cb7..d87c598 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install update diff status backup clean doctor bootstrap sync sync-from-remote backup-full install-optional generate-template setup-age age-retrieve age-status setup-mise mise-status mise-upgrade mise-doctor setup-raycast raycast-export raycast-import raycast-status setup-signoz-mcp signoz-mcp-status signoz-mcp-update lint perf perf-report skills-status +.PHONY: help install update diff status backup clean doctor bootstrap sync sync-from-remote backup-full install-optional generate-template setup-age age-retrieve age-status setup-mise mise-status mise-upgrade mise-doctor setup-raycast raycast-export raycast-import raycast-status setup-emacs emacs-grammars emacs-restart emacs-status setup-signoz-mcp signoz-mcp-status signoz-mcp-update lint perf perf-report skills-status # Configuration DOTFILES_ROOT := $(shell pwd) @@ -165,6 +165,19 @@ setup-paperwm: ## Install PaperWM.spoon for Hammerspoon paperwm-status: ## Show PaperWM installation status @$(SCRIPTS_DIR)/setup/setup-paperwm.sh status +# Emacs +setup-emacs: ## Install Emacs config, packages, and grammars + @$(SCRIPTS_DIR)/setup/setup-emacs.sh install + +emacs-grammars: ## Compile missing tree-sitter grammars + @$(SCRIPTS_DIR)/setup/setup-emacs.sh grammars + +emacs-restart: ## Restart the Emacs daemon + @$(SCRIPTS_DIR)/setup/setup-emacs.sh restart + +emacs-status: ## Show Emacs installation status + @$(SCRIPTS_DIR)/setup/setup-emacs.sh status + # Raycast setup-raycast: ## Verify Raycast installation @$(SCRIPTS_DIR)/setup/setup-raycast.sh install diff --git a/README.md b/README.md index c626bdc..6b2f7ca 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,8 @@ dotfiles/ Everything runs Synthwave84. The palette lives in `config/theme/synthwave84.toml` and gets templated into Ghostty, tmux, fzf, Neovim (mona.nvim), btop, yazi, fastfetch, Starship, and Hammerspoon. +Zed and Emacs run the lower-contrast **Synthwave84 Soft** variant, driven from `[data.theme.soft]` in `chezmoi.toml`. Emacs keeps the base ANSI palette for terminal buffers, so shells inside Emacs still match Ghostty. + ## Forking This is meant to be forked. On first `chezmoi init`, you get prompted for your name, email, and GitHub username. Everything adapts from there -- git config, SSH, encrypted secrets, the works. diff --git a/chezmoi.toml b/chezmoi.toml index 715473d..99b1a28 100644 --- a/chezmoi.toml +++ b/chezmoi.toml @@ -18,6 +18,7 @@ personal = true rust = true ohmyzsh = true mise = true +emacs = true elixir = true erlang = true lua = true @@ -80,6 +81,51 @@ bright_magenta = "#ff7edb" bright_cyan = "#36f9f6" bright_white = "#ffffff" +# Synthwave84 Soft -- the variant Zed uses ("Synthwave84 Soft" in zed/settings.json). +# Lower contrast than the base palette above, and it carries the syntax roles the base +# block lacks. Consumed by the Emacs theme. Source of truth is the shipped Zed theme: +# ~/Library/Application Support/Zed/extensions/installed/synthwave84/themes/synthwave84.json +# ANSI/terminal colors are deliberately absent -- Zed's Soft variant reuses the base ANSI +# palette above, so terminal faces stay identical to Ghostty. +[data.theme.soft] +name = "Synthwave84 Soft" +bg = "#2e2a3e" +bg_dark = "#2a2139" +bg_light = "#332e42" +fg = "#f0f0f0" +# Flattened from Zed's alpha colors over bg: #f0f0f080 and #f0f0f073 respectively. +fg_dim = "#8f8d97" +fg_subtle = "#85838e" +accent = "#ff92e0" +accent2 = "#5af5f3" +cursor = "#ff92e0" +selection_bg = "#4e4570" +selection_fg = "#f0f0f0" +border = "#5a5f8d" +border_focus = "#9933aa" +comment = "#848bbd" +keyword = "#fee380" +string = "#ffa15a" +string_escape = "#85f3c3" +number = "#fa9890" +constant = "#fa9890" +function = "#5af5f3" +variable = "#ff92e0" +type = "#ff92e0" +property = "#ff92e0" +operator = "#fee380" +punctuation = "#f0f0f0" +preproc = "#ff92e0" +link = "#85f3c3" +error = "#fe6670" +warning = "#fee380" +info = "#33f0fc" +success = "#85f3c3" +hint = "#33f0fc" +git_added = "#85f3c3" +git_modified = "#fee380" +git_deleted = "#fe6670" + [age] command = "~/.config/chezmoi/age-op-decrypt.sh" recipient = "age1alqwmllzqv8pjmmn6yfjdwm0d8jz7x459clralv2xeefactyv5nq7wze36" diff --git a/config/theme/synthwave84.toml b/config/theme/synthwave84.toml index a45e6e9..75b04ce 100644 --- a/config/theme/synthwave84.toml +++ b/config/theme/synthwave84.toml @@ -32,12 +32,12 @@ comment = "#848bbd" line_highlight = "#2a2139" # Syntax colors -keyword = "#ff7edb" # Pink - keywords, operators +keyword = "#fede5d" # Yellow - keywords, operators string = "#ff8b39" # Orange - strings number = "#f97e72" # Coral - numbers function = "#36f9f6" # Cyan - functions variable = "#ff7edb" # Pink - variables -type = "#72f1b8" # Green - types +type = "#ff7edb" # Pink - types constant = "#f97e72" # Coral - constants parameter = "#fe4450" # Red - parameters diff --git a/home/dot_zsh/aliases/dev.zsh b/home/dot_zsh/aliases/dev.zsh index 29fbfe0..e424bd6 100644 --- a/home/dot_zsh/aliases/dev.zsh +++ b/home/dot_zsh/aliases/dev.zsh @@ -104,6 +104,12 @@ alias ......="cd ../../../.." alias v="nvim" alias nv="nvim" +# Emacs via the daemon (brew services runs it). The -a fallback starts a +# standalone Emacs if the daemon is down, so these never fail outright. +alias e="emacsclient -nw -a emacs" # terminal frame in this shell +alias eg="emacsclient -c -n -a emacs" # new GUI frame, returns immediately +alias ekill="emacsclient -e '(kill-emacs)'" + # Network utilities alias myip="curl -s https://ipinfo.io/ip" alias speedtest="curl -s https://raw.githubusercontent.com/sivel/speedtest-cli/master/speedtest.py | python3 -" diff --git a/home/private_dot_config/emacs/banner.txt b/home/private_dot_config/emacs/banner.txt new file mode 100644 index 0000000..991accf --- /dev/null +++ b/home/private_dot_config/emacs/banner.txt @@ -0,0 +1,7 @@ + ██████╗ ██████╗ ██████╗ ██████╗ + ██╔══██╗██╔══██╗██╔═══██╗██╔═══██╗ + ██║ ██║██████╔╝██║ ██║██║ ██║ + ██║ ██║██╔══██╗██║ ██║██║ ██║ + ██████╔╝██║ ██║╚██████╔╝╚██████╔╝ + ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ + ═══════════════════════════════════ diff --git a/home/private_dot_config/emacs/early-init.el.tmpl b/home/private_dot_config/emacs/early-init.el.tmpl new file mode 100644 index 0000000..17c79a2 --- /dev/null +++ b/home/private_dot_config/emacs/early-init.el.tmpl @@ -0,0 +1,86 @@ +;;; early-init.el --- Pre-frame initialization -*- lexical-binding: t; -*- + +;; Generated by chezmoi. Edit home/private_dot_config/emacs/early-init.el.tmpl, +;; not this file. Only the frame colors are templated, from [data.theme.soft]. + +;;; Commentary: + +;; Runs before the first frame is created and before package.el initializes. +;; Two jobs here: +;; +;; 1. Keep ~/.config/emacs free of runtime state. This directory is +;; chezmoi-managed, so anything Emacs writes into it shows up as drift in +;; `chezmoi verify' and `make test'. Packages, the native-comp cache, +;; tree-sitter grammars, and customize output all go to XDG dirs instead. +;; +;; 2. Paint the frame in the theme background immediately, so startup does not +;; flash white before init.el gets around to `load-theme'. + +;;; Code: + +;; Startup only: a huge threshold avoids dozens of collections while loading. +;; droo-defaults restores a sane steady-state value on `emacs-startup-hook'. +(setq gc-cons-threshold most-positive-fixnum + gc-cons-percentage 0.6) + +;; package.el is initialized explicitly in init.el, not implicitly before it. +(setq package-enable-at-startup nil) + +;; Skip the X resource lookup and the implied frame resize during startup. +(setq inhibit-x-resources t + frame-inhibit-implied-resize t) + +;;; XDG state redirection + +(defconst droo/xdg-data + (or (getenv "XDG_DATA_HOME") (expand-file-name "~/.local/share")) + "Base XDG data directory.") + +(defconst droo/xdg-cache + (or (getenv "XDG_CACHE_HOME") (expand-file-name "~/.cache")) + "Base XDG cache directory.") + +(defconst droo/xdg-state + (or (getenv "XDG_STATE_HOME") (expand-file-name "~/.local/state")) + "Base XDG state directory.") + +(defconst droo/data-dir (expand-file-name "emacs/" droo/xdg-data) + "Durable Emacs data: installed packages, tree-sitter grammars.") + +(defconst droo/cache-dir (expand-file-name "emacs/" droo/xdg-cache) + "Regenerable Emacs data: native-comp cache, backups, auto-saves.") + +(defconst droo/state-dir (expand-file-name "emacs/" droo/xdg-state) + "Emacs state that should survive a cache wipe: history, places, custom.el.") + +(dolist (dir (list droo/data-dir droo/cache-dir droo/state-dir)) + (make-directory dir t)) + +(setq package-user-dir (expand-file-name "elpa" droo/data-dir) + custom-file (expand-file-name "custom.el" droo/state-dir)) + +;; Native compilation writes a lot; keep it out of the managed tree. +(when (fboundp 'startup-redirect-eln-cache) + (startup-redirect-eln-cache (expand-file-name "eln-cache/" droo/cache-dir))) + +(setq native-comp-async-report-warnings-errors 'silent) + +;;; Frame appearance + +;; Set before the first frame exists so there is no white flash on startup. +(dolist (param '((background-color . "{{ .theme.soft.bg }}") + (foreground-color . "{{ .theme.soft.fg }}") + (cursor-color . "{{ .theme.soft.cursor }}") + (tool-bar-lines . 0) + (vertical-scroll-bars . nil) + (ns-transparent-titlebar . t) + (ns-appearance . dark))) + (push param default-frame-alist)) + +;; macOS keeps its system menu bar (it costs no frame real estate); elsewhere drop it. +(unless (eq system-type 'darwin) + (push '(menu-bar-lines . 0) default-frame-alist)) + +(provide 'early-init) + +;;; early-init.el ends here diff --git a/home/private_dot_config/emacs/init.el b/home/private_dot_config/emacs/init.el new file mode 100644 index 0000000..e24b22e --- /dev/null +++ b/home/private_dot_config/emacs/init.el @@ -0,0 +1,60 @@ +;;; init.el --- Personal Emacs configuration -*- lexical-binding: t; -*- + +;; Managed by chezmoi: home/private_dot_config/emacs/init.el + +;;; Commentary: + +;; Vanilla Emacs 30 with package.el and the built-in use-package. Runtime state +;; lives under XDG directories (see early-init.el); this tree stays read-only as +;; far as Emacs is concerned. +;; +;; The theme loads before any module so that a broken package still leaves a +;; correctly themed Emacs. `custom-theme-set-faces' records specs for faces that +;; do not exist yet, so magit/corfu/eglot faces are themed when those load later. + +;;; Code: + +(add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory)) +(add-to-list 'custom-theme-load-path (expand-file-name "themes" user-emacs-directory)) + +(load-theme 'synthwave84-soft t) + +;;; Packages + +(require 'package) + +(setq package-archives + '(("gnu" . "https://elpa.gnu.org/packages/") + ("nongnu" . "https://elpa.nongnu.org/nongnu/") + ("melpa" . "https://melpa.org/packages/")) + ;; Prefer the curated archives; fall back to MELPA for everything else. + package-archive-priorities + '(("gnu" . 3) ("nongnu" . 2) ("melpa" . 1))) + +(package-initialize) + +(unless package-archive-contents + (package-refresh-contents)) + +(require 'use-package) + +(setq use-package-always-ensure t + use-package-expand-minimally t + ;; `M-x use-package-report' to find slow packages. + use-package-compute-statistics t) + +;;; Modules + +(require 'droo-defaults) +(require 'droo-ui) +(require 'droo-completion) +(require 'droo-git) +(require 'droo-lang) +(require 'droo-lsp) + +;; Written by `customize'; kept out of the chezmoi-managed tree. Nothing here is +;; authoritative -- prefer editing the modules above. +(when (file-exists-p custom-file) + (load custom-file nil t)) + +;;; init.el ends here diff --git a/home/private_dot_config/emacs/lisp/droo-completion.el b/home/private_dot_config/emacs/lisp/droo-completion.el new file mode 100644 index 0000000..024c1ba --- /dev/null +++ b/home/private_dot_config/emacs/lisp/droo-completion.el @@ -0,0 +1,78 @@ +;;; droo-completion.el --- Minibuffer and in-buffer completion -*- lexical-binding: t; -*- + +;;; Commentary: + +;; The vertico/consult/corfu stack: small, composable packages built on Emacs' +;; own completion machinery rather than a parallel framework. `orderless' means +;; space-separated fragments match in any order, which is the main ergonomic win. + +;;; Code: + +(eval-when-compile (require 'use-package)) + +;;; Minibuffer + +(use-package vertico + :init + (setq vertico-cycle t + vertico-count 15) + (vertico-mode 1)) + +(use-package orderless + :init + (setq completion-styles '(orderless basic) + completion-category-defaults nil + ;; `partial-completion' keeps path expansion (/u/s/l -> /usr/share/lib). + completion-category-overrides '((file (styles partial-completion))))) + +(use-package marginalia + :init (marginalia-mode 1)) + +(use-package consult + :bind (("C-s" . consult-line) + ("C-x b" . consult-buffer) + ("C-x p b" . consult-project-buffer) + ("M-y" . consult-yank-pop) + ("M-g g" . consult-goto-line) + ("M-g i" . consult-imenu) + ("M-s r" . consult-ripgrep) + ("M-s f" . consult-find)) + :init + (setq consult-narrow-key "<" + xref-show-xrefs-function #'consult-xref + xref-show-definitions-function #'consult-xref)) + +;;; In-buffer + +(use-package corfu + :init + (setq corfu-auto t + corfu-auto-delay 0.15 + corfu-auto-prefix 2 + corfu-cycle t + corfu-preselect 'prompt + ;; Let RET insert a newline when nothing was explicitly selected. + corfu-preview-current 'insert) + (global-corfu-mode 1) + (corfu-popupinfo-mode 1)) + +(use-package cape + :init + (add-hook 'completion-at-point-functions #'cape-file) + (add-hook 'completion-at-point-functions #'cape-dabbrev)) + +;;; Actions + +(use-package embark + :bind (("C-." . embark-act) + ("C-;" . embark-dwim)) + :init + (setq prefix-help-command #'embark-prefix-help-command)) + +(use-package embark-consult + :after (embark consult) + :hook (embark-collect-mode . consult-preview-at-point-mode)) + +(provide 'droo-completion) + +;;; droo-completion.el ends here diff --git a/home/private_dot_config/emacs/lisp/droo-defaults.el b/home/private_dot_config/emacs/lisp/droo-defaults.el new file mode 100644 index 0000000..95cf914 --- /dev/null +++ b/home/private_dot_config/emacs/lisp/droo-defaults.el @@ -0,0 +1,151 @@ +;;; droo-defaults.el --- Sane built-in defaults -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Built-in behavior only, plus the two packages that exist to keep the rest of +;; the config honest: `no-littering' (state stays out of the managed tree) and +;; `exec-path-from-shell' (a launchd-started daemon inherits almost no PATH, so +;; without it eglot cannot find rust-analyzer, mise shims, or anything else). + +;;; Code: + +(eval-when-compile (require 'use-package)) + +;; Defined in early-init.el; declared here to quiet the byte-compiler. +(defvar droo/xdg-data) +(defvar droo/data-dir) +(defvar droo/cache-dir) +(defvar droo/state-dir) + +;;; Garbage collection + +;; early-init.el raised this to `most-positive-fixnum' for startup. Drop it back +;; to something that does not produce multi-second pauses on a large heap. +(add-hook 'emacs-startup-hook + (lambda () + (setq gc-cons-threshold (* 64 1024 1024) + gc-cons-percentage 0.1))) + +;;; Keep state out of ~/.config/emacs + +(use-package no-littering + :init + ;; Must be set before the package loads -- it computes paths at load time. + (setq no-littering-etc-directory (expand-file-name "etc/" droo/data-dir) + no-littering-var-directory (expand-file-name "var/" droo/state-dir)) + :config + ;; Redirects backups, auto-saves, and lockfile-adjacent state. + (no-littering-theme-backups)) + +;;; Environment + +;; A GUI or daemon Emacs on macOS starts from launchd, not from a login shell. +(use-package exec-path-from-shell + :if (or (daemonp) (memq window-system '(mac ns x))) + :config + (setq exec-path-from-shell-arguments '("-l") + exec-path-from-shell-variables + '("PATH" "MANPATH" "SSH_AUTH_SOCK" "LANG" "LC_ALL" + "XDG_DATA_HOME" "XDG_CACHE_HOME" "XDG_STATE_HOME" "XDG_CONFIG_HOME")) + (exec-path-from-shell-initialize)) + +;; mise activates from .zshrc, which a login shell never sources, so the step +;; above cannot see its shims -- and most language servers here are mise-managed. +;; Adding the directory explicitly is cheaper and more predictable than paying +;; for an interactive shell just to inherit one PATH entry. +(let ((shims (expand-file-name "mise/shims" droo/xdg-data))) + (when (file-directory-p shims) + (add-to-list 'exec-path shims) + (setenv "PATH" (concat shims path-separator (getenv "PATH"))))) + +;;; Startup and prompts + +;; The startup screen is suppressed because droo-ui points +;; `initial-buffer-choice' at the dashboard instead. +(setq inhibit-startup-screen t + inhibit-startup-echo-area-message user-login-name + initial-scratch-message + ";; *scratch* -- C-j evaluates the form before point.\n\n" + initial-major-mode 'lisp-interaction-mode + ring-bell-function #'ignore + use-short-answers t + confirm-kill-emacs #'yes-or-no-p + ;; A running daemon should not die because a client said so. + confirm-kill-processes nil) + +;;; Files + +(setq create-lockfiles nil + load-prefer-newer t + require-final-newline t + find-file-visit-truename t + ;; Large minified files should not hang Emacs. + large-file-warning-threshold (* 32 1024 1024)) + +(global-auto-revert-mode 1) +(setq global-auto-revert-non-file-buffers t + auto-revert-verbose nil) + +(save-place-mode 1) +(recentf-mode 1) +(setq recentf-max-saved-items 300 + ;; Keep scratch dirs and package sources out of the recents list -- they + ;; are noise on the dashboard and are never worth reopening. + recentf-exclude + (list "\\`/private/tmp/" "\\`/tmp/" "\\`/var/folders/" + (regexp-quote (expand-file-name "emacs/elpa" droo/xdg-data)) + "/\\.git/" "COMMIT_EDITMSG\\'" "\\.gpg\\'")) + +(savehist-mode 1) +(setq history-length 1000 + savehist-additional-variables '(kill-ring search-ring regexp-search-ring)) + +(require 'uniquify) +(setq uniquify-buffer-name-style 'forward) + +(global-so-long-mode 1) + +;;; Editing + +(setq-default indent-tabs-mode nil + tab-width 4 + fill-column 100) + +(setq sentence-end-double-space nil + tab-always-indent 'complete + kill-do-not-save-duplicates t + mouse-yank-at-point t) + +(delete-selection-mode 1) +(electric-pair-mode 1) +(show-paren-mode 1) +(setq show-paren-delay 0 + show-paren-context-when-offscreen 'overlay) + +(when (fboundp 'context-menu-mode) + (context-menu-mode 1)) + +;;; Scrolling + +(setq scroll-conservatively 101 + scroll-margin 3 + scroll-preserve-screen-position t + auto-window-vscroll nil) + +(pixel-scroll-precision-mode 1) + +;;; macOS + +(when (eq system-type 'darwin) + (setq ns-command-modifier 'super + ns-alternate-modifier 'meta + ;; Leave right-option alone so accented characters still type. + ns-right-alternate-modifier 'none + ns-use-native-fullscreen t + ;; Trash instead of unlinking, matching Finder semantics. + delete-by-moving-to-trash t + trash-directory nil)) + +(provide 'droo-defaults) + +;;; droo-defaults.el ends here diff --git a/home/private_dot_config/emacs/lisp/droo-git.el b/home/private_dot_config/emacs/lisp/droo-git.el new file mode 100644 index 0000000..a247359 --- /dev/null +++ b/home/private_dot_config/emacs/lisp/droo-git.el @@ -0,0 +1,38 @@ +;;; droo-git.el --- Version control -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Magit, plus fringe indicators for uncommitted hunks. + +;;; Code: + +(eval-when-compile (require 'use-package)) + +(use-package magit + :bind (("C-x g" . magit-status) + ("C-x M-g" . magit-dispatch)) + :init + (setq magit-diff-refine-hunk 'all + ;; Full-frame magit, restoring the previous layout on quit. + magit-display-buffer-function #'magit-display-buffer-fullframe-status-v1 + magit-bury-buffer-function #'magit-restore-window-configuration) + :config + ;; The repo signs commits (see gpg_signing_key in chezmoi.toml); make that the + ;; default in the commit popup rather than something to remember each time. + (setq magit-commit-arguments '("--gpg-sign"))) + +(use-package diff-hl + :hook ((prog-mode . diff-hl-mode) + (conf-mode . diff-hl-mode) + (dired-mode . diff-hl-dired-mode)) + :init + (setq diff-hl-draw-borders nil) + :config + ;; Without these the fringe goes stale after a magit stage/commit. + (add-hook 'magit-pre-refresh-hook #'diff-hl-magit-pre-refresh) + (add-hook 'magit-post-refresh-hook #'diff-hl-magit-post-refresh) + (diff-hl-flydiff-mode 1)) + +(provide 'droo-git) + +;;; droo-git.el ends here diff --git a/home/private_dot_config/emacs/lisp/droo-lang.el b/home/private_dot_config/emacs/lisp/droo-lang.el new file mode 100644 index 0000000..ddf10cd --- /dev/null +++ b/home/private_dot_config/emacs/lisp/droo-lang.el @@ -0,0 +1,124 @@ +;;; droo-lang.el --- Language modes and tree-sitter -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Emacs 30 ships tree-sitter modes for nearly the whole stack -- including +;; elixir-ts-mode, heex-ts-mode, and lua-ts-mode -- so only nix, markdown, and +;; solidity need packages. +;; +;; Grammars are not bundled. `droo/treesit-install-grammars' compiles the +;; missing ones; `make emacs-grammars' calls it in batch. Modes are remapped +;; only when their grammar is actually present, so a fresh machine falls back to +;; the classic major mode instead of erroring on every file open. + +;;; Code: + +(eval-when-compile (require 'use-package)) +(require 'treesit) + +;; Defined in early-init.el. +(defvar droo/data-dir) + +;;; Grammars + +(setq treesit-language-source-alist + '((bash . ("https://github.com/tree-sitter/tree-sitter-bash")) + (c . ("https://github.com/tree-sitter/tree-sitter-c")) + (css . ("https://github.com/tree-sitter/tree-sitter-css")) + (elixir . ("https://github.com/elixir-lang/tree-sitter-elixir")) + (go . ("https://github.com/tree-sitter/tree-sitter-go")) + (gomod . ("https://github.com/camdencheek/tree-sitter-go-mod")) + (heex . ("https://github.com/phoenixframework/tree-sitter-heex")) + (html . ("https://github.com/tree-sitter/tree-sitter-html")) + (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript")) + (json . ("https://github.com/tree-sitter/tree-sitter-json")) + (lua . ("https://github.com/tree-sitter-grammars/tree-sitter-lua")) + (nix . ("https://github.com/nix-community/tree-sitter-nix")) + (python . ("https://github.com/tree-sitter/tree-sitter-python")) + (rust . ("https://github.com/tree-sitter/tree-sitter-rust")) + (toml . ("https://github.com/tree-sitter/tree-sitter-toml")) + (tsx . ("https://github.com/tree-sitter/tree-sitter-typescript" + "master" "tsx/src")) + (typescript . ("https://github.com/tree-sitter/tree-sitter-typescript" + "master" "typescript/src")) + (yaml . ("https://github.com/ikatyang/tree-sitter-yaml")))) + +;; Keep compiled grammars beside the packages, not in the managed config tree. +(defvar droo/treesit-grammar-dir (expand-file-name "tree-sitter" droo/data-dir) + "Directory holding compiled tree-sitter grammars.") + +(add-to-list 'treesit-extra-load-path droo/treesit-grammar-dir) + +(defun droo/treesit-install-grammars (&optional force) + "Compile any tree-sitter grammar that is not already available. +With FORCE non-nil, reinstall every configured grammar. +Requires a C compiler and git on PATH." + (interactive "P") + (let ((out-dir droo/treesit-grammar-dir)) + (make-directory out-dir t) + (dolist (entry treesit-language-source-alist) + (let ((lang (car entry))) + (if (and (not force) (treesit-language-available-p lang)) + (message "treesit: %s already available" lang) + (message "treesit: installing %s..." lang) + (condition-case err + (treesit-install-language-grammar lang out-dir) + ;; One unreachable upstream should not abort the whole run. + (error (message "treesit: FAILED %s -- %s" + lang (error-message-string err))))))))) + +;;; Major mode remapping + +;; Only remap when the grammar exists, so files still open on a fresh checkout. +(dolist (entry '((sh-mode . bash-ts-mode) + (css-mode . css-ts-mode) + (js-mode . js-ts-mode) + (javascript-mode . js-ts-mode) + (json-mode . json-ts-mode) + (js-json-mode . json-ts-mode) + (python-mode . python-ts-mode) + (conf-toml-mode . toml-ts-mode))) + (let* ((ts-mode (cdr entry)) + (lang (intern (string-remove-suffix "-ts-mode" (symbol-name ts-mode))))) + (when (treesit-language-available-p (if (eq lang 'js) 'javascript lang)) + (add-to-list 'major-mode-remap-alist entry)))) + +;;; File associations + +;; These modes have no classic counterpart to remap from. +(dolist (entry '(("\\.ex\\'" . elixir-ts-mode) + ("\\.exs\\'" . elixir-ts-mode) + ("mix\\.lock\\'" . elixir-ts-mode) + ("\\.heex\\'" . heex-ts-mode) + ("\\.rs\\'" . rust-ts-mode) + ("\\.go\\'" . go-ts-mode) + ("/go\\.mod\\'" . go-mod-ts-mode) + ("\\.lua\\'" . lua-ts-mode) + ("\\.ts\\'" . typescript-ts-mode) + ("\\.tsx\\'" . tsx-ts-mode) + ("\\.ya?ml\\'" . yaml-ts-mode))) + (add-to-list 'auto-mode-alist entry)) + +;;; Packages + +(use-package nix-ts-mode + :mode "\\.nix\\'") + +(use-package markdown-mode + :mode ("\\.md\\'" . markdown-mode) + :init (setq markdown-fontify-code-blocks-natively t)) + +(use-package solidity-mode + :mode "\\.sol\\'" + :init (setq solidity-comment-style 'slash)) + +;;; Indentation + +(setq typescript-ts-mode-indent-offset 2 + js-indent-level 2 + css-indent-offset 2 + lua-ts-indent-offset 2) + +(provide 'droo-lang) + +;;; droo-lang.el ends here diff --git a/home/private_dot_config/emacs/lisp/droo-lsp.el b/home/private_dot_config/emacs/lisp/droo-lsp.el new file mode 100644 index 0000000..c74d17f --- /dev/null +++ b/home/private_dot_config/emacs/lisp/droo-lsp.el @@ -0,0 +1,91 @@ +;;; droo-lsp.el --- Eglot and diagnostics -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Eglot (built in since Emacs 29) over lsp-mode: it reuses xref, flymake, +;; eldoc, and completion-at-point instead of replacing them. +;; +;; Every language is configured, but `eglot-ensure' is only hooked when the +;; server binary is actually on PATH. Missing servers therefore cost nothing -- +;; the buffer opens in its normal major mode with no error -- and installing a +;; server later needs only a restart, no config change. + +;;; Code: + +(eval-when-compile (require 'use-package)) + +(defconst droo/eglot-servers + '((elixir-ts-mode-hook . "elixir-ls") + (heex-ts-mode-hook . "elixir-ls") + (rust-ts-mode-hook . "rust-analyzer") + (go-ts-mode-hook . "gopls") + (typescript-ts-mode-hook . "typescript-language-server") + (tsx-ts-mode-hook . "typescript-language-server") + (js-ts-mode-hook . "typescript-language-server") + (python-ts-mode-hook . "ruff") + (lua-ts-mode-hook . "lua-language-server") + (nix-ts-mode-hook . "nixd") + (solidity-mode-hook . "solc") + (bash-ts-mode-hook . "bash-language-server") + (yaml-ts-mode-hook . "yaml-language-server") + (json-ts-mode-hook . "vscode-json-language-server")) + "Map of major-mode hook to the executable that provides its language server.") + +(defun droo/eglot-available-servers () + "Return the subset of `droo/eglot-servers' whose executable is installed." + (seq-filter (lambda (spec) (executable-find (cdr spec))) droo/eglot-servers)) + +(use-package eglot + :ensure nil ; built in since Emacs 29 + :bind (:map eglot-mode-map + ("C-c l r" . eglot-rename) + ("C-c l a" . eglot-code-actions) + ("C-c l f" . eglot-format-buffer) + ("C-c l d" . eldoc-doc-buffer)) + :init + (setq eglot-autoshutdown t + eglot-sync-connect 1 + ;; Eglot logs every LSP message by default; that dominates memory on a + ;; long-lived daemon and is only useful when actively debugging a server. + eglot-events-buffer-config '(:size 0 :format full) + eglot-extend-to-xref t) + + (dolist (spec (droo/eglot-available-servers)) + (add-hook (car spec) #'eglot-ensure)) + + :config + ;; ruff is a linter/formatter server, not a type checker; prefer basedpyright + ;; when it is installed and fall back to ruff, which still gives diagnostics. + (add-to-list 'eglot-server-programs + (cons '(python-ts-mode python-mode) + (if (executable-find "basedpyright-langserver") + '("basedpyright-langserver" "--stdio") + '("ruff" "server"))))) + +;;; Diagnostics + +(use-package flymake + :ensure nil + :hook (prog-mode . flymake-mode) + ;; Scoped to the mode map -- M-n/M-p stay free for history elsewhere. + :bind (:map flymake-mode-map + ("M-n" . flymake-goto-next-error) + ("M-p" . flymake-goto-prev-error)) + :init + (setq flymake-no-changes-timeout 0.5 + flymake-fringe-indicator-position 'right-fringe)) + +;;; Docs + +(setq eldoc-echo-area-use-multiline-p 2 + eldoc-idle-delay 0.2) + +;;; Projects + +(setq project-vc-extra-root-markers + '("mix.exs" "Cargo.toml" "go.mod" "package.json" + "pyproject.toml" "flake.nix" "foundry.toml" "Nargo.toml")) + +(provide 'droo-lsp) + +;;; droo-lsp.el ends here diff --git a/home/private_dot_config/emacs/lisp/droo-ui.el b/home/private_dot_config/emacs/lisp/droo-ui.el new file mode 100644 index 0000000..d586450 --- /dev/null +++ b/home/private_dot_config/emacs/lisp/droo-ui.el @@ -0,0 +1,126 @@ +;;; droo-ui.el --- Fonts and visual chrome -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Fonts and UI affordances. Colors live entirely in the theme +;; (themes/synthwave84-soft-theme.el) -- nothing here names one. +;; +;; Font settings match Ghostty and Zed: Monaspace Neon at 14pt. Zed uses a 1.4 +;; line height; Emacs expresses that as `line-spacing' extra leading, so 0.2 of +;; the default ~1.2 line box lands in the same place. + +;;; Code: + +(eval-when-compile (require 'use-package)) + +;;; Fonts + +(defconst droo/font-family "Monaspace Neon" + "Primary typeface, shared with Ghostty and Zed.") + +(defconst droo/font-height 140 + "Default font height in 1/10 pt, i.e. 14pt.") + +(defun droo/apply-fonts (&optional frame) + "Apply the configured font to FRAME. +Does nothing when the family is unavailable, so a machine without +Monaspace installed still gets a usable Emacs rather than a broken one." + (when (and (display-graphic-p frame) + (member droo/font-family (font-family-list frame))) + (dolist (face '(default fixed-pitch variable-pitch)) + (set-face-attribute face frame + :family droo/font-family + :height droo/font-height)))) + +;; Under a daemon there is no frame at startup, so `font-family-list' is empty +;; and fonts must be applied per-frame as clients connect. +(if (daemonp) + (add-hook 'after-make-frame-functions #'droo/apply-fonts) + (droo/apply-fonts)) + +;; Fractional value = that fraction of the default line height, added as leading. +(setq-default line-spacing 0.2) + +;;; Chrome + +(when (fboundp 'tool-bar-mode) (tool-bar-mode -1)) +(when (fboundp 'scroll-bar-mode) (scroll-bar-mode -1)) + +(column-number-mode 1) +(setq display-line-numbers-width 3 + display-line-numbers-widen t) + +(add-hook 'prog-mode-hook #'display-line-numbers-mode) +(add-hook 'conf-mode-hook #'display-line-numbers-mode) +(add-hook 'prog-mode-hook #'hl-line-mode) + +;; Soft wrap prose, never code. +(add-hook 'text-mode-hook #'visual-line-mode) + +(setq window-divider-default-places t + window-divider-default-bottom-width 1 + window-divider-default-right-width 1) +(window-divider-mode 1) + +;; Frame title: project-relative path, or the buffer name when there is no file. +(setq frame-title-format + '(:eval (if buffer-file-name + (abbreviate-file-name buffer-file-name) + "%b"))) + +;;; Built-in discoverability + +(use-package which-key + :ensure nil ; built in since Emacs 30 + :init + (setq which-key-idle-delay 0.5 + which-key-sort-order #'which-key-key-order-alpha) + (which-key-mode 1)) + +;;; Packages + +(use-package rainbow-delimiters + :hook (prog-mode . rainbow-delimiters-mode)) + +;;; Entrypoint + +(use-package dashboard + :init + (setq dashboard-banner-logo-title "GNU Emacs 30 -- Synthwave84 Soft" + dashboard-startup-banner (expand-file-name "banner.txt" user-emacs-directory) + dashboard-center-content t + dashboard-vertically-center-content t + dashboard-items '((recents . 8) + (projects . 5) + (bookmarks . 5)) + dashboard-projects-backend 'project-el + dashboard-set-footer nil + ;; No icon packages -- keeps the dashboard ASCII and dependency-free. + dashboard-display-icons-p nil + dashboard-set-heading-icons nil + dashboard-set-file-icons nil) + :config + (dashboard-setup-startup-hook) + ;; Under a daemon, emacsclient frames never run the startup sequence that + ;; normally renders the dashboard, so they would land on *scratch*. Render it + ;; on demand instead; `initial-buffer-choice' is consulted for each new frame. + (setq initial-buffer-choice + (lambda () + (dashboard-refresh-buffer) + (get-buffer dashboard-buffer-name)))) + +(use-package pulsar + :init + (setq pulsar-pulse t + pulsar-delay 0.05 + pulsar-iterations 8 + pulsar-face 'pulsar-magenta) + :config + (pulsar-global-mode 1) + ;; Flash the landing line after a jump, so the cursor is never lost. + (add-hook 'next-error-hook #'pulsar-pulse-line) + (add-hook 'xref-after-jump-hook #'pulsar-pulse-line)) + +(provide 'droo-ui) + +;;; droo-ui.el ends here diff --git a/home/private_dot_config/emacs/themes/synthwave84-soft-theme.el.tmpl b/home/private_dot_config/emacs/themes/synthwave84-soft-theme.el.tmpl new file mode 100644 index 0000000..37b6974 --- /dev/null +++ b/home/private_dot_config/emacs/themes/synthwave84-soft-theme.el.tmpl @@ -0,0 +1,334 @@ +;;; synthwave84-soft-theme.el --- Synthwave84 Soft -*- lexical-binding: t; -*- + +;; Generated by chezmoi from [data.theme.soft] in chezmoi.toml. +;; Edit home/private_dot_config/emacs/themes/synthwave84-soft-theme.el.tmpl, not this file. + +;;; Commentary: + +;; Port of the "Synthwave84 Soft" Zed theme, the variant set in zed/settings.json. +;; UI and syntax colors come from [data.theme.soft]; the ANSI terminal palette comes +;; from the base [data.theme] block, which is what Ghostty uses -- so shell buffers +;; inside Emacs match the surrounding terminal exactly. That split mirrors the Zed +;; theme itself, whose Soft variant leaves terminal.ansi.* at the base values. + +;;; Code: + +(deftheme synthwave84-soft + "Synthwave84 Soft: a low-contrast neon dark theme matching Zed and Ghostty.") + +(let ((class '((class color) (min-colors 89))) + ;; UI + (bg "{{ .theme.soft.bg }}") + (bg-dark "{{ .theme.soft.bg_dark }}") + (bg-light "{{ .theme.soft.bg_light }}") + (fg "{{ .theme.soft.fg }}") + (fg-dim "{{ .theme.soft.fg_dim }}") + (fg-subtle "{{ .theme.soft.fg_subtle }}") + (accent "{{ .theme.soft.accent }}") + (accent2 "{{ .theme.soft.accent2 }}") + (cursor "{{ .theme.soft.cursor }}") + (sel-bg "{{ .theme.soft.selection_bg }}") + (sel-fg "{{ .theme.soft.selection_fg }}") + (border "{{ .theme.soft.border }}") + (border-focus "{{ .theme.soft.border_focus }}") + ;; Syntax + (comment "{{ .theme.soft.comment }}") + (keyword "{{ .theme.soft.keyword }}") + (string "{{ .theme.soft.string }}") + (str-escape "{{ .theme.soft.string_escape }}") + (number "{{ .theme.soft.number }}") + (constant "{{ .theme.soft.constant }}") + (function "{{ .theme.soft.function }}") + (variable "{{ .theme.soft.variable }}") + (type "{{ .theme.soft.type }}") + (property "{{ .theme.soft.property }}") + (operator "{{ .theme.soft.operator }}") + (punctuation "{{ .theme.soft.punctuation }}") + (preproc "{{ .theme.soft.preproc }}") + (link "{{ .theme.soft.link }}") + ;; Diagnostics and VC + (err "{{ .theme.soft.error }}") + (warn "{{ .theme.soft.warning }}") + (info "{{ .theme.soft.info }}") + (ok "{{ .theme.soft.success }}") + (hint "{{ .theme.soft.hint }}") + (git-add "{{ .theme.soft.git_added }}") + (git-mod "{{ .theme.soft.git_modified }}") + (git-del "{{ .theme.soft.git_deleted }}") + ;; ANSI -- base palette, shared with Ghostty + (ansi-black "{{ .theme.black }}") + (ansi-red "{{ .theme.red }}") + (ansi-green "{{ .theme.green }}") + (ansi-yellow "{{ .theme.yellow }}") + (ansi-blue "{{ .theme.blue }}") + (ansi-magenta "{{ .theme.magenta }}") + (ansi-cyan "{{ .theme.cyan }}") + (ansi-white "{{ .theme.white }}") + (ansi-br-black "{{ .theme.bright_black }}") + (ansi-br-red "{{ .theme.bright_red }}") + (ansi-br-green "{{ .theme.bright_green }}") + (ansi-br-yellow "{{ .theme.bright_yellow }}") + (ansi-br-blue "{{ .theme.bright_blue }}") + (ansi-br-magenta "{{ .theme.bright_magenta }}") + (ansi-br-cyan "{{ .theme.bright_cyan }}") + (ansi-br-white "{{ .theme.bright_white }}")) + + (custom-theme-set-faces + 'synthwave84-soft + + ;; ---------------------------------------------------------------- base ---- + `(default ((,class (:background ,bg :foreground ,fg)))) + `(cursor ((,class (:background ,cursor)))) + `(region ((,class (:background ,sel-bg :foreground ,sel-fg :extend t)))) + `(secondary-selection ((,class (:background ,bg-light :extend t)))) + `(highlight ((,class (:background ,bg-light :foreground ,accent)))) + `(hl-line ((,class (:background ,bg-light :extend t)))) + `(fringe ((,class (:background ,bg :foreground ,fg-subtle)))) + `(vertical-border ((,class (:foreground ,border)))) + `(window-divider ((,class (:foreground ,border)))) + `(window-divider-first-pixel ((,class (:foreground ,border)))) + `(window-divider-last-pixel ((,class (:foreground ,border)))) + `(minibuffer-prompt ((,class (:foreground ,accent :weight bold)))) + `(shadow ((,class (:foreground ,fg-subtle)))) + `(link ((,class (:foreground ,link :underline t)))) + `(link-visited ((,class (:foreground ,accent :underline t)))) + `(escape-glyph ((,class (:foreground ,str-escape)))) + `(homoglyph ((,class (:foreground ,warn)))) + `(error ((,class (:foreground ,err :weight bold)))) + `(warning ((,class (:foreground ,warn :weight bold)))) + `(success ((,class (:foreground ,ok :weight bold)))) + `(trailing-whitespace ((,class (:background ,err)))) + `(fill-column-indicator ((,class (:foreground ,bg-light)))) + `(separator-line ((,class (:foreground ,border)))) + + ;; -------------------------------------------------------- line numbers ---- + `(line-number ((,class (:background ,bg :foreground ,fg-subtle)))) + `(line-number-current-line ((,class (:background ,bg-light :foreground ,fg :weight bold)))) + `(line-number-major-tick ((,class (:background ,bg :foreground ,fg-dim)))) + `(line-number-minor-tick ((,class (:background ,bg :foreground ,fg-subtle)))) + + ;; ----------------------------------------------------------- mode line ---- + `(mode-line ((,class (:background ,bg-dark :foreground ,fg + :box (:line-width 1 :color ,border))))) + `(mode-line-active ((,class (:inherit mode-line)))) + `(mode-line-inactive ((,class (:background ,bg-dark :foreground ,fg-subtle + :box (:line-width 1 :color ,bg-light))))) + `(mode-line-buffer-id ((,class (:foreground ,accent :weight bold)))) + `(mode-line-emphasis ((,class (:foreground ,accent2 :weight bold)))) + `(mode-line-highlight ((,class (:background ,bg-light :foreground ,accent)))) + `(header-line ((,class (:background ,bg-dark :foreground ,fg-dim)))) + + ;; ------------------------------------------------------- tab bar/line ---- + `(tab-bar ((,class (:background ,bg-dark :foreground ,fg-dim)))) + `(tab-bar-tab ((,class (:background ,bg :foreground ,accent :weight bold)))) + `(tab-bar-tab-inactive ((,class (:background ,bg-dark :foreground ,fg-subtle)))) + `(tab-line ((,class (:background ,bg-dark :foreground ,fg-dim)))) + + ;; ----------------------------------------------------------- font-lock ---- + `(font-lock-builtin-face ((,class (:foreground ,keyword)))) + `(font-lock-comment-face ((,class (:foreground ,comment :slant italic)))) + `(font-lock-comment-delimiter-face ((,class (:foreground ,comment :slant italic)))) + `(font-lock-doc-face ((,class (:foreground ,comment :slant italic)))) + `(font-lock-doc-markup-face ((,class (:foreground ,str-escape)))) + `(font-lock-constant-face ((,class (:foreground ,constant)))) + `(font-lock-function-name-face ((,class (:foreground ,function)))) + `(font-lock-keyword-face ((,class (:foreground ,keyword)))) + `(font-lock-negation-char-face ((,class (:foreground ,operator)))) + `(font-lock-preprocessor-face ((,class (:foreground ,preproc)))) + `(font-lock-string-face ((,class (:foreground ,string)))) + `(font-lock-type-face ((,class (:foreground ,type)))) + `(font-lock-variable-name-face ((,class (:foreground ,variable)))) + `(font-lock-warning-face ((,class (:foreground ,warn :weight bold)))) + ;; Emacs 29+ / tree-sitter-aware faces + `(font-lock-bracket-face ((,class (:foreground ,punctuation)))) + `(font-lock-delimiter-face ((,class (:foreground ,punctuation)))) + `(font-lock-escape-face ((,class (:foreground ,str-escape)))) + `(font-lock-function-call-face ((,class (:foreground ,function)))) + `(font-lock-misc-punctuation-face ((,class (:foreground ,punctuation)))) + `(font-lock-number-face ((,class (:foreground ,number)))) + `(font-lock-operator-face ((,class (:foreground ,operator)))) + `(font-lock-property-name-face ((,class (:foreground ,property)))) + `(font-lock-property-use-face ((,class (:foreground ,property)))) + `(font-lock-punctuation-face ((,class (:foreground ,punctuation)))) + `(font-lock-regexp-face ((,class (:foreground ,str-escape)))) + `(font-lock-variable-use-face ((,class (:foreground ,variable)))) + + ;; --------------------------------------------------- search and parens ---- + `(isearch ((,class (:background ,accent :foreground ,bg-dark :weight bold)))) + `(isearch-fail ((,class (:background ,err :foreground ,bg-dark)))) + `(lazy-highlight ((,class (:background ,bg-light :foreground ,accent2)))) + `(match ((,class (:background ,bg-light :foreground ,accent2 :weight bold)))) + `(show-paren-match ((,class (:background ,border-focus :foreground ,fg :weight bold)))) + `(show-paren-mismatch ((,class (:background ,err :foreground ,bg-dark :weight bold)))) + + ;; --------------------------------------------------------- completions ---- + `(completions-common-part ((,class (:foreground ,accent2 :weight bold)))) + `(completions-first-difference ((,class (:foreground ,accent)))) + `(completions-annotations ((,class (:foreground ,fg-dim :slant italic)))) + + ;; ------------------------------------------------------------- vertico ---- + `(vertico-current ((,class (:background ,bg-light :foreground ,accent :extend t)))) + `(vertico-group-title ((,class (:foreground ,fg-dim :slant italic)))) + + ;; ----------------------------------------------------------- orderless ---- + `(orderless-match-face-0 ((,class (:foreground ,accent2 :weight bold)))) + `(orderless-match-face-1 ((,class (:foreground ,accent :weight bold)))) + `(orderless-match-face-2 ((,class (:foreground ,ok :weight bold)))) + `(orderless-match-face-3 ((,class (:foreground ,keyword :weight bold)))) + + ;; ---------------------------------------------------------- marginalia ---- + `(marginalia-key ((,class (:foreground ,accent2)))) + `(marginalia-documentation ((,class (:foreground ,fg-dim :slant italic)))) + + ;; --------------------------------------------------------------- corfu ---- + `(corfu-default ((,class (:background ,bg-dark :foreground ,fg)))) + `(corfu-current ((,class (:background ,bg-light :foreground ,accent)))) + `(corfu-border ((,class (:background ,border)))) + `(corfu-bar ((,class (:background ,accent)))) + + ;; ------------------------------------------------------------- consult ---- + `(consult-file ((,class (:foreground ,fg-dim)))) + `(consult-highlight-match ((,class (:foreground ,accent2 :weight bold)))) + + ;; ----------------------------------------------------------- which-key ---- + `(which-key-key-face ((,class (:foreground ,accent2 :weight bold)))) + `(which-key-command-description-face ((,class (:foreground ,fg)))) + `(which-key-group-description-face ((,class (:foreground ,accent)))) + `(which-key-separator-face ((,class (:foreground ,fg-subtle)))) + + ;; ----------------------------------------------------------- dashboard ---- + `(dashboard-banner-logo-title ((,class (:foreground ,accent2 :weight bold)))) + `(dashboard-heading ((,class (:foreground ,accent :weight bold)))) + `(dashboard-items-face ((,class (:foreground ,fg)))) + `(dashboard-no-items-face ((,class (:foreground ,fg-subtle :slant italic)))) + `(dashboard-navigator ((,class (:foreground ,accent2)))) + `(dashboard-footer ((,class (:foreground ,fg-dim :slant italic)))) + ;; The banner is rendered as plain text, so it picks up the default face -- + ;; tint it with the accent to match the Zed titlebar. + `(dashboard-text-banner ((,class (:foreground ,accent :weight bold)))) + `(widget-button ((,class (:foreground ,link :weight normal)))) + `(widget-button-pressed ((,class (:foreground ,accent)))) + + ;; ------------------------------------------------- rainbow-delimiters ---- + `(rainbow-delimiters-depth-1-face ((,class (:foreground ,accent)))) + `(rainbow-delimiters-depth-2-face ((,class (:foreground ,accent2)))) + `(rainbow-delimiters-depth-3-face ((,class (:foreground ,keyword)))) + `(rainbow-delimiters-depth-4-face ((,class (:foreground ,ok)))) + `(rainbow-delimiters-depth-5-face ((,class (:foreground ,string)))) + `(rainbow-delimiters-depth-6-face ((,class (:foreground ,number)))) + `(rainbow-delimiters-unmatched-face ((,class (:foreground ,err :weight bold)))) + + ;; --------------------------------------------------------------- diff ---- + `(diff-added ((,class (:background ,bg-light :foreground ,git-add :extend t)))) + `(diff-removed ((,class (:background ,bg-light :foreground ,git-del :extend t)))) + `(diff-changed ((,class (:background ,bg-light :foreground ,git-mod :extend t)))) + `(diff-header ((,class (:background ,bg-dark :foreground ,fg-dim :extend t)))) + `(diff-file-header ((,class (:background ,bg-dark :foreground ,accent :weight bold :extend t)))) + `(diff-hunk-header ((,class (:background ,bg-dark :foreground ,accent2 :extend t)))) + `(diff-refine-added ((,class (:background ,git-add :foreground ,bg-dark)))) + `(diff-refine-removed ((,class (:background ,git-del :foreground ,bg-dark)))) + `(diff-refine-changed ((,class (:background ,git-mod :foreground ,bg-dark)))) + + ;; ------------------------------------------------------------ diff-hl ---- + `(diff-hl-insert ((,class (:background ,git-add :foreground ,git-add)))) + `(diff-hl-change ((,class (:background ,git-mod :foreground ,git-mod)))) + `(diff-hl-delete ((,class (:background ,git-del :foreground ,git-del)))) + + ;; -------------------------------------------------------------- magit ---- + `(magit-section-heading ((,class (:foreground ,accent :weight bold)))) + `(magit-section-highlight ((,class (:background ,bg-light :extend t)))) + `(magit-branch-local ((,class (:foreground ,accent2)))) + `(magit-branch-remote ((,class (:foreground ,ok)))) + `(magit-branch-current ((,class (:foreground ,accent :weight bold :box (:line-width 1 :color ,accent))))) + `(magit-tag ((,class (:foreground ,keyword)))) + `(magit-hash ((,class (:foreground ,fg-subtle)))) + `(magit-log-author ((,class (:foreground ,type)))) + `(magit-log-date ((,class (:foreground ,fg-dim)))) + `(magit-diff-added ((,class (:background ,bg-light :foreground ,git-add :extend t)))) + `(magit-diff-added-highlight ((,class (:background ,bg-light :foreground ,git-add :weight bold :extend t)))) + `(magit-diff-removed ((,class (:background ,bg-light :foreground ,git-del :extend t)))) + `(magit-diff-removed-highlight ((,class (:background ,bg-light :foreground ,git-del :weight bold :extend t)))) + `(magit-diff-context ((,class (:foreground ,fg-dim :extend t)))) + `(magit-diff-context-highlight ((,class (:background ,bg-light :foreground ,fg :extend t)))) + `(magit-diff-hunk-heading ((,class (:background ,bg-dark :foreground ,fg-dim :extend t)))) + `(magit-diff-hunk-heading-highlight ((,class (:background ,bg-light :foreground ,accent2 :extend t)))) + `(magit-diffstat-added ((,class (:foreground ,git-add)))) + `(magit-diffstat-removed ((,class (:foreground ,git-del)))) + `(magit-process-ok ((,class (:foreground ,ok :weight bold)))) + `(magit-process-ng ((,class (:foreground ,err :weight bold)))) + + ;; ------------------------------------------------------------ flymake ---- + `(flymake-error ((,class (:underline (:style wave :color ,err))))) + `(flymake-warning ((,class (:underline (:style wave :color ,warn))))) + ;; eglot maps LSP Hint and Information severities onto flymake-note. + `(flymake-note ((,class (:underline (:style wave :color ,hint))))) + + ;; -------------------------------------------------------------- eglot ---- + `(eglot-highlight-symbol-face ((,class (:background ,bg-light :weight bold)))) + `(eglot-mode-line ((,class (:foreground ,accent2 :weight bold)))) + `(eglot-inlay-hint-face ((,class (:foreground ,fg-subtle :height 0.9 :slant italic)))) + + ;; --------------------------------------------------- compilation/dired ---- + `(compilation-info ((,class (:foreground ,info)))) + `(compilation-warning ((,class (:foreground ,warn)))) + `(compilation-error ((,class (:foreground ,err)))) + `(compilation-mode-line-run ((,class (:foreground ,keyword)))) + `(compilation-mode-line-exit ((,class (:foreground ,ok)))) + `(compilation-mode-line-fail ((,class (:foreground ,err)))) + `(dired-directory ((,class (:foreground ,accent2 :weight bold)))) + `(dired-symlink ((,class (:foreground ,link :slant italic)))) + `(dired-header ((,class (:foreground ,accent :weight bold)))) + `(dired-flagged ((,class (:foreground ,err :weight bold)))) + `(dired-marked ((,class (:foreground ,accent :weight bold)))) + + ;; ---------------------------------------------------------------- org ---- + `(org-level-1 ((,class (:foreground ,accent :weight bold :height 1.15)))) + `(org-level-2 ((,class (:foreground ,accent2 :weight bold :height 1.10)))) + `(org-level-3 ((,class (:foreground ,keyword :weight bold :height 1.05)))) + `(org-level-4 ((,class (:foreground ,ok :weight bold)))) + `(org-document-title ((,class (:foreground ,accent :weight bold :height 1.3)))) + `(org-block ((,class (:background ,bg-dark :extend t)))) + `(org-block-begin-line ((,class (:background ,bg-dark :foreground ,fg-subtle :extend t)))) + `(org-code ((,class (:foreground ,string)))) + `(org-verbatim ((,class (:foreground ,str-escape)))) + `(org-link ((,class (:foreground ,link :underline t)))) + `(org-todo ((,class (:foreground ,err :weight bold)))) + `(org-done ((,class (:foreground ,ok :weight bold)))) + `(org-table ((,class (:foreground ,accent2)))) + `(org-date ((,class (:foreground ,fg-dim :underline t)))) + + ;; -------------------------------------------------------- ANSI (base) ---- + ;; Matches Ghostty exactly -- see the commentary at the top. + `(ansi-color-black ((,class (:background ,ansi-black :foreground ,ansi-black)))) + `(ansi-color-red ((,class (:background ,ansi-red :foreground ,ansi-red)))) + `(ansi-color-green ((,class (:background ,ansi-green :foreground ,ansi-green)))) + `(ansi-color-yellow ((,class (:background ,ansi-yellow :foreground ,ansi-yellow)))) + `(ansi-color-blue ((,class (:background ,ansi-blue :foreground ,ansi-blue)))) + `(ansi-color-magenta ((,class (:background ,ansi-magenta :foreground ,ansi-magenta)))) + `(ansi-color-cyan ((,class (:background ,ansi-cyan :foreground ,ansi-cyan)))) + `(ansi-color-white ((,class (:background ,ansi-white :foreground ,ansi-white)))) + `(ansi-color-bright-black ((,class (:background ,ansi-br-black :foreground ,ansi-br-black)))) + `(ansi-color-bright-red ((,class (:background ,ansi-br-red :foreground ,ansi-br-red)))) + `(ansi-color-bright-green ((,class (:background ,ansi-br-green :foreground ,ansi-br-green)))) + `(ansi-color-bright-yellow ((,class (:background ,ansi-br-yellow :foreground ,ansi-br-yellow)))) + `(ansi-color-bright-blue ((,class (:background ,ansi-br-blue :foreground ,ansi-br-blue)))) + `(ansi-color-bright-magenta ((,class (:background ,ansi-br-magenta :foreground ,ansi-br-magenta)))) + `(ansi-color-bright-cyan ((,class (:background ,ansi-br-cyan :foreground ,ansi-br-cyan)))) + `(ansi-color-bright-white ((,class (:background ,ansi-br-white :foreground ,ansi-br-white))))) + + (custom-theme-set-variables + 'synthwave84-soft + ;; Honored by older ansi-color code paths and by some third-party term wrappers. + `(ansi-color-names-vector + [,ansi-black ,ansi-red ,ansi-green ,ansi-yellow + ,ansi-blue ,ansi-magenta ,ansi-cyan ,ansi-white]) + `(rainbow-delimiters-max-face-count 6))) + +;;;###autoload +(when load-file-name + (add-to-list 'custom-theme-load-path + (file-name-as-directory (file-name-directory load-file-name)))) + +(provide-theme 'synthwave84-soft) + +;;; synthwave84-soft-theme.el ends here diff --git a/scripts/setup/setup-emacs.sh b/scripts/setup/setup-emacs.sh new file mode 100755 index 0000000..2be987b --- /dev/null +++ b/scripts/setup/setup-emacs.sh @@ -0,0 +1,207 @@ +#!/bin/bash +# Emacs setup: XDG config, tree-sitter grammars, and the emacs-plus daemon + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +EMACS_CONFIG_DIR="$HOME/.config/emacs" +EMACS_DOTDIR="$HOME/.emacs.d" +BREW_FORMULA="emacs-plus@30" + +# Source centralized logging +# shellcheck source=../utils/logging.sh +source "$DOTFILES_ROOT/scripts/utils/logging.sh" + +# Emacs resolves ~/.emacs.d before ~/.config/emacs whenever the former merely +# EXISTS -- it never checks for an init.el (startup.el, startup--xdg-or-homedot). +# A stray directory there silently disables this entire config. +check_shadow() { + [[ -d "$EMACS_DOTDIR" ]] +} + +remove_shadow() { + if ! check_shadow; then + return 0 + fi + + warn "$EMACS_DOTDIR exists and shadows $EMACS_CONFIG_DIR" + echo " Emacs prefers ~/.emacs.d whenever it exists, so the XDG config is ignored." + echo " Contents:" + find "$EMACS_DOTDIR" -mindepth 1 -maxdepth 2 | sed 's/^/ /' | head -20 + + if command -v trash &>/dev/null; then + info "Moving $EMACS_DOTDIR to Trash (recoverable)..." + trash "$EMACS_DOTDIR" + else + error "Refusing to delete $EMACS_DOTDIR automatically." + echo " Install macos-trash (brew install macos-trash) or move it aside yourself:" + echo " mv $EMACS_DOTDIR $EMACS_DOTDIR.bak" + exit 1 + fi + + success "Shadow directory removed" +} + +install_emacs() { + if ! command -v emacs &>/dev/null; then + error "emacs not found. Install it first:" + echo " brew install d12frosted/emacs-plus/$BREW_FORMULA" + exit 1 + fi + + remove_shadow + + if [[ ! -f "$EMACS_CONFIG_DIR/init.el" ]]; then + info "Deploying Emacs config via chezmoi..." + chezmoi apply "$EMACS_CONFIG_DIR" + fi + success "Config present: $EMACS_CONFIG_DIR" + + # First run resolves and byte-compiles every package; it is slow but silent. + info "Installing packages (first run takes a few minutes)..." + emacs --batch \ + -l "$EMACS_CONFIG_DIR/early-init.el" \ + -l "$EMACS_CONFIG_DIR/init.el" \ + --eval '(princ "packages ready\n")' + + install_grammars + restart_daemon +} + +install_grammars() { + if [[ ! -f "$EMACS_CONFIG_DIR/init.el" ]]; then + error "Config not deployed. Run: make setup-emacs" + exit 1 + fi + + info "Compiling tree-sitter grammars (needs a C compiler and git)..." + emacs --batch \ + -l "$EMACS_CONFIG_DIR/early-init.el" \ + -l "$EMACS_CONFIG_DIR/init.el" \ + --eval '(droo/treesit-install-grammars)' + success "Grammar install finished" + # droo-lang.el decides major-mode remaps at load time based on which + # grammars exist, so a running daemon keeps the pre-install mapping. + info "Restart Emacs to pick up newly installed grammars: make emacs-restart" +} + +restart_daemon() { + if [[ "$(uname -s)" != "Darwin" ]]; then + info "Not macOS; skipping brew services daemon management" + return 0 + fi + + info "Restarting the Emacs daemon..." + brew services restart "$BREW_FORMULA" + success "Daemon restarted (open a frame with: emacsclient -c)" +} + +show_status() { + echo "" + echo "Emacs Status:" + echo "" + + if command -v emacs &>/dev/null; then + echo " Binary: $(command -v emacs)" + echo " Version: $(emacs --version | head -1)" + else + echo " Not installed" + echo " Run: brew install d12frosted/emacs-plus/$BREW_FORMULA" + echo "" + return 0 + fi + + # Ask Emacs itself rather than assuming -- this is the check that catches a + # reappeared ~/.emacs.d. + local resolved + resolved="$(emacs --batch --eval '(princ user-emacs-directory)' 2>/dev/null || true)" + echo " Config: ${resolved:-unknown}" + + if check_shadow; then + echo " Shadow: WARNING - $EMACS_DOTDIR exists and overrides the XDG config" + else + echo " Shadow: none (~/.emacs.d absent, XDG config active)" + fi + + if [[ -f "$EMACS_CONFIG_DIR/init.el" ]]; then + echo " init.el: present" + else + echo " init.el: MISSING (run: make setup-emacs)" + fi + + local elpa="$HOME/.local/share/emacs/elpa" + if [[ -d "$elpa" ]]; then + local count + count="$(find "$elpa" -maxdepth 1 -mindepth 1 -type d ! -name archives | wc -l | tr -d ' ')" + echo " Packages: $count installed ($elpa)" + else + echo " Packages: none installed" + fi + + local grammars="$HOME/.local/share/emacs/tree-sitter" + if [[ -d "$grammars" ]]; then + local gcount + gcount="$(find "$grammars" -maxdepth 1 -name 'libtree-sitter-*' | wc -l | tr -d ' ')" + echo " Grammars: $gcount compiled" + else + echo " Grammars: none (run: make emacs-grammars)" + fi + + if emacsclient -e '(emacs-version)' &>/dev/null; then + echo " Daemon: running" + else + echo " Daemon: not running (start: brew services start $BREW_FORMULA)" + fi + + if [[ -f "$EMACS_CONFIG_DIR/themes/synthwave84-soft-theme.el" ]]; then + echo " Theme: synthwave84-soft installed" + else + echo " Theme: MISSING" + fi + echo "" +} + +show_usage() { + cat << EOF +Usage: $0 [COMMAND] + +Setup Emacs with the XDG config, tree-sitter grammars, and the emacs-plus daemon + +Commands: + install Deploy config, install packages and grammars, restart daemon (default) + grammars Compile missing tree-sitter grammars + restart Restart the Emacs daemon + status Show current status + help Show this help + +EOF +} + +main() { + case "${1:-install}" in + install) + install_emacs + show_status + ;; + grammars) + install_grammars + ;; + restart) + restart_daemon + ;; + status) + show_status + ;; + help|--help|-h) + show_usage + ;; + *) + error "Unknown command: $1" + show_usage + exit 1 + ;; + esac +} + +main "$@" diff --git a/scripts/utils/health-check.sh b/scripts/utils/health-check.sh index c346a17..5b211b5 100755 --- a/scripts/utils/health-check.sh +++ b/scripts/utils/health-check.sh @@ -71,6 +71,19 @@ print_section "Development" check_cmd mise warn check_cmd direnv warn check_cmd nvim warn +check_cmd emacs warn + +# Emacs prefers ~/.emacs.d whenever it exists, without ever checking for an +# init.el, so a stray directory there silently disables the XDG config. +if command -v emacs &>/dev/null; then + if [[ -d "$HOME/.emacs.d" ]]; then + fail "Emacs ~/.emacs.d shadows ~/.config/emacs (remove it)" + elif [[ -f "$HOME/.config/emacs/init.el" ]]; then + ok "Emacs config active (~/.config/emacs)" + else + warn "Emacs installed but config missing (run: make setup-emacs)" + fi +fi if command -v node &>/dev/null; then ok "node $(node --version 2>/dev/null || true)"