Initial commit for v1.0 of Simply Emacs.

This commit is contained in:
2026-07-24 09:47:24 +02:00
commit 7774e58c79
13 changed files with 1244 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
;;; init-build.el --- Project build command -*- lexical-binding: t; -*-
;;; Commentary:
;; Build via built-in `compile', running ./build.sh from the project root.
;; compile saves buffers, reuses one output buffer, and turns file:line:col
;; into clickable jumps — a thin wrapper, not a terminal-send hack.
;;; Code:
;; Always show *compilation* as a fixed right-hand panel, so the toggle below
;; hits a predictable window.
(add-to-list 'display-buffer-alist
'("\\*compilation\\*"
(display-buffer-reuse-window display-buffer-in-side-window)
(side . right)
(window-width . 0.35)))
;; If t is too aggressive (e.g. auto-scrolls while trying to read mid-output
;; that hasn't errored yet), swap to:
;; (setq compilation-scroll-output 'first-error)
(setq compilation-scroll-output t)
(defun my/project-build (&optional args)
"Run ./build.sh from the project root, appending ARGS (a string) if given.
Errors if build.sh isn't readable there. Saves all buffers first."
(let* ((root (if (project-current)
(project-root (project-current))
default-directory))
(script (expand-file-name "build.sh" root))
(default-directory root))
(unless (file-readable-p script)
(user-error "No build.sh found in %s" root))
(save-some-buffers t)
(compile (string-trim (concat "./build.sh " (or args ""))))))
(defun my/build ()
"Build the current project via `./build.sh' with no arguments."
(interactive)
(my/project-build nil))
(defun my/build-with-options ()
"Build the current project, prompting for arguments to append."
(interactive)
(my/project-build (read-string "build.sh args: ")))
(defun my/toggle-build-panel ()
"Show/hide the *compilation* window from the last run (doesn't rebuild)."
(interactive)
(let ((buf (get-buffer "*compilation*")))
(cond
((null buf)
(user-error "No build output yet — run `my/build' first"))
((get-buffer-window buf)
(delete-window (get-buffer-window buf)))
(t
(display-buffer buf)))))
;; Global (not evil-scoped) so it works from any state or buffer.
;; C-c C-c build C-c C-o build w/ options C-j toggle panel
(global-set-key (kbd "C-c C-c") #'my/build)
(global-set-key (kbd "C-c C-o") #'my/build-with-options)
(global-set-key (kbd "C-j") #'my/toggle-build-panel)
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c" "code"
"C-c c" "build"
"C-c o" "build (with options)"))
(provide 'init-build)
;;; init-build.el ends here
+248
View File
@@ -0,0 +1,248 @@
;;; init-cheatsheet.el --- In-Emacs keybinding reference -*- lexical-binding: t; -*-
;;; Commentary:
;; A static reference buffer listing this config's custom keybindings,
;; opened as a non-blocking side window — same window-management model as
;; the build panel in `init-build.el' — so it can be toggled in and out of
;; view without interrupting whatever else is on screen.
;;
;; Content is real Markdown, viewed read-only via `markdown-view-mode'
;; (from the `markdown-mode' package already pulled in by `init-dev.el')
;; so headers/tables/code spans get proper fontification for free.
;;; Code:
(require 'markdown-mode) ; markdown-view-mode; make the dep explicit
(add-to-list 'display-buffer-alist
'("\\*Cheatsheet\\*"
(display-buffer-reuse-window display-buffer-in-side-window)
(side . left)
(window-width . 0.4)))
(defconst my/cheatsheet-text
"# SimplyEmacs Cheat Sheet
Custom keybindings layered on top of vanilla Emacs. Toggle this buffer
any time with `C-c /`.
Many keys use a project/fallback pattern (`my/project-or`): one action
inside a known project, a `consult`-based fallback everywhere else.
## Navigation & Search *(Evil normal/motion state)*
| Key | In a project | Fallback |
|------------|----------------------------|-------------------|
| `C-e` | `project-dired` | `dired-jump` |
| `C-m` | `project-switch-to-buffer` | `consult-buffer` |
| `C-f` | `consult-ripgrep` | `consult-ripgrep` |
| `C-p` | `project-find-file` | `consult-fd` |
| `C-s` | `consult-line` | |
| `<return>` | `evil-ret` | |
`C-f` runs `consult-ripgrep` everywhere — it already scopes to the
project root when there is one, so it needs no project/fallback split.
## Projects
| Key | Action |
|-----------|-------------------------------------------------------------------------------|
| `C-c C-p` | Switch project — saves & closes current project's buffers first, then prompts |
Projects are rescanned from `~/Projects` on every startup (direct
children only, no symlinks, most-recently-modified first). Jumping into
a dependency's source keeps the *current project* pinned to your last
real project instead of hijacking to it.
## Build
| Key | Action |
|-----------|-----------------------------------------------------------|
| `C-c C-c` | Run `./build.sh` from project root (saves buffers first) |
| `C-c C-o` | Same, but prompts for extra args to append |
| `C-j` | Toggle the `*compilation*` output panel (right side, 35%) |
Requires `build.sh` at the project root; output is a real `compile`
buffer, so `M-g n` / `M-g p` jump between errors.
## Dev / Eglot (LSP)
Wired up for Odin (`.odin`, tree-sitter + `odin-ts-mode` + OLS).
| Key | Action |
|------------|----------------------------------|
| `C-c r` | `eglot-rename` |
| `C-c a` | `eglot-code-actions` |
| `K` norm | Full hover doc in a focused pane |
| `q` or `K` | Close the hover pane |
Add a new language: new `;;;; <Language>` section in `init-dev.el`.
## Git (Magit)
`C-c C-g` opens `magit-status` (replaces the lazygit TUI). Magit keeps
its own native keys — nothing reimplemented — evil's normal state just
shadows most of them by default, so they're restored below plus a couple
of hjkl navigation aliases.
### Workflow
- Expand only the file you're working on with `TAB`.
- Move between hunks with `j` / `k`.
- Press `s` on a hunk to stage only that hunk.
- Continue staging related hunks until they form a single logical change.
- Press `c c` to create a commit from the staged changes.
- Repeat for the remaining hunks, creating small, focused commits instead of one large commit.
> **Tip:** If a single hunk contains unrelated changes, press `e` to edit the patch before staging, allowing you to split the changes into separate commits.
| Key | Action |
|-----------|----------------------|
| `j` / `n` | next section |
| `s` | stage |
| `k` / `p` | previous section |
| `u` | unstage |
| `h` | up to parent section |
| `U` | unstage all |
| `RET` | visit thing at point |
| `x` | discard |
| `g r` | refresh |
| `q` | quit / bury buffer |
**Popups** *(transient menus)*:
| Key | Action |
|-----|----------------|
| `c` | commit |
| `m` | merge |
| `d` | diff |
| `r` | rebase |
| `b` | branch |
| `t` | tag |
| `l` | log |
| `z` | stash |
| `f` | fetch |
| `e` | ediff |
| `F` | pull |
| `$` | process buffer |
| `P` | push |
Covers the daily-driver surface, not magit's entire command set —
anything not listed falls through to evil's default on that key
(harmless, just `<escape>`).
## Dired
Evil `hjkl` grafted onto stock dired commands, remapped only so evil
doesn't swallow dired's own letters.
| Key | Action |
|-------------|---------------------------|
| `j` / `k` | next / previous line |
| `d` | flag for deletion |
| `h` | up a directory |
| `x` | execute flagged deletions |
| `l` / `RET` | open file or directory |
| `D` | delete now, no flag step |
| `q` | quit window |
| `R` | rename / move |
| `SPC` / `m` | mark |
| `C` | copy |
| `u` | unmark |
| `y` | copy filename as kill |
| `U` | unmark all |
| `!` | shell command on file(s) |
| `t` | toggle marks |
| `+` / `f` | create directory |
| `g r` | refresh listing |
| `a` | create empty file |
| `(` | toggle file details |
## Evil *(buffer editing only — dired/magit/etc. keep native binds)*
**Ex commands:**
| Command | Action |
|---------|---------------------|
| `:w` | save buffer |
| `:q` | kill current buffer |
| `:wq` | save and kill Emacs |
**Insert state:**
| Key | Action |
|-------|----------------------------------------------------|
| `TAB` | insert a literal tab (overrides major-mode indent) |
## In-buffer Completion (Corfu)
Auto-popup after 2 characters, 0.15s delay.
| Key | Action |
|-----------------|--------------------|
| `C-n` / `TAB` | next candidate |
| `C-p` / `S-TAB` | previous candidate |
| `RET` | insert selection |
| `<escape>` | quit completion |
## Minibuffer Completion
Vertico *(centered posframe)* + orderless matching + marginalia +
consult. Type fragments in any order to filter.
## Misc / Global
| Key | Action |
|----------------|-----------------------------------|
| `C-S-p` | `execute-extended-command` (M-x) |
| `<escape>` | acts like `C-g` everywhere |
| *(any prefix)* | which-key shows available options |
## Look & Feel
Theme: **gruvbox-dark-hard**, background/fringe/line-numbers darkened
to `#111111`. Font: JetBrainsMono Nerd Font @ 12pt. Line numbers and
current-line highlight on globally; scroll margin 15 lines.
"
"Reference text shown by `my/toggle-cheatsheet'.")
(define-derived-mode my/cheatsheet-mode markdown-view-mode "Cheatsheet"
"Read-only `markdown-view-mode' variant for the keybinding cheat sheet.")
(with-eval-after-load 'evil
(evil-define-key '(normal motion) my/cheatsheet-mode-map
(kbd "q") #'quit-window))
(defun my/cheatsheet-buffer ()
"Return the cheatsheet buffer, creating and populating it on first use."
(let ((buf (get-buffer-create "*Cheatsheet*")))
(with-current-buffer buf
(unless (derived-mode-p 'my/cheatsheet-mode)
(let ((inhibit-read-only t))
(erase-buffer)
(insert my/cheatsheet-text)
(my/cheatsheet-mode)
(goto-char (point-min)))))
buf))
(defun my/toggle-cheatsheet ()
"Toggle a non-blocking side window showing this config's cheat sheet.
Same show/hide-only-the-window behaviour as `my/toggle-build-panel' —
doesn't rebuild the buffer each time, just shows or hides it."
(interactive)
(let* ((buf (my/cheatsheet-buffer))
(win (get-buffer-window buf)))
(if win
(delete-window win)
(display-buffer buf))))
;; Bound globally (not evil-scoped), matching the build panel keys, so it
;; works the same from normal state, insert state, or anywhere else.
(global-set-key (kbd "C-c /") #'my/toggle-cheatsheet)
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c /" "cheatsheet"))
(provide 'init-cheatsheet)
;;; init-cheatsheet.el ends here
+123
View File
@@ -0,0 +1,123 @@
;;; init-completion.el --- Minibuffer + in-buffer completion -*- lexical-binding: t; -*-
;;; Commentary:
;; Vertico + Orderless + Marginalia + Consult for minibuffer completion.
;; Corfu provides in-buffer completion.
;; Embark adds contextual actions.
;;; Code:
;;;; Helper functions
(defun my/find-file-below (file)
"Open FILE in a split below."
(interactive "fFile: ")
(select-window (split-window-below))
(find-file file))
(defun my/find-file-right (file)
"Open FILE in a split to the right."
(interactive "fFile: ")
(select-window (split-window-right))
(find-file file))
(defun my/vertico-split-below ()
"Open the selected Vertico file candidate below."
(interactive)
(setq unread-command-events (listify-key-sequence "2"))
(embark-act))
(defun my/vertico-split-right ()
"Open the selected Vertico file candidate to the right."
(interactive)
(setq unread-command-events (listify-key-sequence "3"))
(embark-act))
;;;; Vertico
(use-package vertico
:init
(vertico-mode)
:custom
(vertico-count 25))
(use-package vertico-posframe
:after vertico
:custom
(vertico-posframe-poshandler #'posframe-poshandler-frame-center)
(vertico-posframe-border-width 2)
(vertico-posframe-width 120)
(vertico-posframe-max-width 300)
:config
(vertico-posframe-mode))
;;;; Completion styles
(use-package orderless
:custom
(completion-styles '(orderless basic))
(completion-category-defaults nil)
(completion-category-overrides
'((file (styles partial-completion)))))
(use-package marginalia
:init
(marginalia-mode))
;;;; In-buffer completion
(use-package corfu
:init
(global-corfu-mode)
:custom
(corfu-auto t)
(corfu-auto-delay 0.15)
(corfu-auto-prefix 2)
(corfu-cycle t)
(corfu-preselect 'first)
:bind
(:map corfu-map
("C-n" . corfu-next)
("C-p" . corfu-previous)
("RET" . corfu-insert)
("<escape>" . corfu-quit))
:config
(with-eval-after-load 'evil
(evil-define-key 'insert 'global
(kbd "C-n") #'corfu-next
(kbd "C-p") #'corfu-previous)))
;;;; Minibuffer commands
(use-package consult)
;;;; Contextual actions
(use-package embark
:after vertico
:bind
(("C-." . embark-act)
:map embark-file-map
("2" . my/find-file-below)
("3" . my/find-file-right)
:map vertico-map
("C-s" . my/vertico-split-below)
("C-v" . my/vertico-split-right)))
(use-package embark-consult
:after (embark consult)
:hook
(embark-collect-mode . consult-preview-at-point-mode))
;;;; Help/discovery
(use-package which-key
:config
(which-key-mode))
(provide 'init-completion)
;;; init-completion.el ends here
+95
View File
@@ -0,0 +1,95 @@
;;; init-dev.el --- Language tooling -*- lexical-binding: t; -*-
;;; Commentary:
;; Per-language setup. Currently Odin (tree-sitter + odin-ts-mode + eglot/OLS)
;; plus markdown-mode. Add each language as its own section.
;;; Code:
;;;; Markdown
(use-package markdown-mode)
;;;; Odin
(setq treesit-language-source-alist
'((odin "https://github.com/tree-sitter-grammars/tree-sitter-odin")))
(use-package odin-ts-mode
:vc (:url "https://github.com/Sampie159/odin-ts-mode")
:mode "\\.odin\\'")
;;;; Lua
(use-package lua-mode
:ensure t)
;;;; TOML
(use-package toml-mode
:ensure t)
;;;; Eglot LSP
(use-package eglot
:hook (odin-ts-mode . eglot-ensure)
:bind
(:map eglot-mode-map
("C-c r" . eglot-rename)
("C-c a" . eglot-code-actions))
:config
;; Signature-only ElDoc: OLS hover already includes the signature, so
;; showing both would duplicate it in the echo area.
(add-hook
'eglot-managed-mode-hook
(lambda ()
(setq-local eldoc-documentation-functions
'(eglot-signature-eldoc-function))))
;; K is context-aware: on a diagnostic (red underline) it shows the error
;; text, otherwise the full hover payload — both in the same focused pane
;; (dismiss with q or K). The echo area keeps the live signature.
;; ] e / [ e jump between diagnostics, echoing each as you land.
(define-derived-mode my/eglot-hover-mode special-mode "Eglot-Hover"
"Major mode for the eglot hover / diagnostic pane.")
(with-eval-after-load 'evil
(evil-define-key '(normal motion) my/eglot-hover-mode-map
(kbd "q") #'quit-window
(kbd "K") #'quit-window))
(defun my/eglot-hover--display (string &rest _)
"Show STRING in a focused *eglot-hover* pane, or message if empty."
(if (and string (not (string-empty-p string)))
(let ((buf (get-buffer-create "*eglot-hover*")))
(with-current-buffer buf
(let ((inhibit-read-only t))
(erase-buffer)
(insert string)
(goto-char (point-min)))
(my/eglot-hover-mode))
(select-window (display-buffer buf))) ; focus the pane
(message "Nothing to show at point")))
(defun my/eglot-help-at-point ()
"Show diagnostics at point if any, else eglot hover docs — in the pane."
(interactive)
(let ((diags (flymake-diagnostics (point))))
(if diags
(my/eglot-hover--display
(mapconcat #'flymake-diagnostic-text diags "\n\n"))
(eglot-hover-eldoc-function #'my/eglot-hover--display))))
(with-eval-after-load 'evil
(evil-define-key 'normal 'eglot-mode-map
(kbd "K") #'my/eglot-help-at-point
(kbd "] e") #'flymake-goto-next-error
(kbd "[ e") #'flymake-goto-prev-error))
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c r" "rename"
"C-c a" "code actions")))
(provide 'init-dev)
;;; init-dev.el ends here
+54
View File
@@ -0,0 +1,54 @@
;;; init-dired.el --- Dired with evil-flavoured navigation -*- lexical-binding: t; -*-
;;; Commentary:
;; Dired stays vanilla — every binding points at a stock dired command.
;; They're remapped only so evil's normal-state keys don't shadow dired's
;; own single-letter commands.
;;; Code:
;; Reuse the current dired buffer when descending/ascending (no pileup).
(setq dired-kill-when-opening-new-dired-buffer t)
;; Order directories first
(setq dired-listing-switches "-agho --group-directories-first")
(with-eval-after-load 'dired
(evil-define-key 'normal dired-mode-map
;; Navigation. RET and <return> both bound so evil's global <return>
;; doesn't shadow them here.
(kbd "j") #'dired-next-line
(kbd "k") #'dired-previous-line
(kbd "h") #'dired-up-directory
(kbd "l") #'dired-find-file
(kbd "RET") #'dired-find-file
[return] #'dired-find-file
(kbd "q") #'quit-window
;; Marking.
(kbd "SPC") #'dired-mark
(kbd "m") #'dired-mark
(kbd "u") #'dired-unmark
(kbd "U") #'dired-unmark-all-marks
(kbd "t") #'dired-toggle-marks
;; Deletion: d flags, x executes flags, D deletes now.
(kbd "d") #'dired-flag-file-deletion
(kbd "x") #'dired-do-flagged-delete
(kbd "D") #'dired-do-delete
;; File operations.
(kbd "R") #'dired-do-rename
(kbd "C") #'dired-do-copy
(kbd "y") #'dired-copy-filename-as-kill
(kbd "!") #'dired-do-shell-command
(kbd "+") #'dired-create-directory
(kbd "f") #'dired-create-directory
(kbd "a") #'dired-create-empty-file
;; Misc.
(kbd "g r") #'revert-buffer ; refresh listing
(kbd "(") #'dired-hide-details-mode)) ; toggle perms/size/date columns
(provide 'init-dired)
;;; init-dired.el ends here
+84
View File
@@ -0,0 +1,84 @@
;;; init-evil.el --- Evil, scoped to buffer editing -*- lexical-binding: t; -*-
;;; Commentary:
;; Evil is deliberately narrow: modal editing in text buffers only, no
;; evil-collection. Dired/Magit/etc. keep native bindings on purpose.
;; The nav keys below use `my/project-or': project-scoped command inside a
;; project, consult fallback everywhere else.
;;; Code:
(require 'project)
(defmacro my/project-or (project-cmd fallback-cmd)
"Call PROJECT-CMD inside a known project, else FALLBACK-CMD.
Both are unquoted command symbols."
`(lambda ()
(interactive)
(call-interactively
(if (project-current)
#',project-cmd
#',fallback-cmd))))
(defun my/insert-tab ()
"Insert a literal tab.
Bound to both TAB and <tab> to override major-mode indent in insert state;
`self-insert-command' can't handle the <tab> event (its arg is the symbol
`tab', not a character)."
(interactive)
(insert "\t"))
(use-package evil
:init
(setq evil-want-keybinding nil)
:config
(evil-mode 1)
;; Vim commands
(evil-ex-define-cmd "w" #'save-buffer)
(evil-ex-define-cmd "q" #'kill-current-buffer)
(evil-ex-define-cmd "wq" #'save-buffers-kill-terminal)
;; Half-page movement
(define-key evil-normal-state-map (kbd "C-d") #'evil-scroll-down)
(define-key evil-normal-state-map (kbd "C-u") #'evil-scroll-up)
;; Editor shortcuts
(dolist (map '(evil-normal-state-map
evil-motion-state-map))
(define-key (symbol-value map)
(kbd "C-e")
(my/project-or project-dired dired-jump))
(define-key (symbol-value map)
(kbd "C-s")
#'consult-line)
;; C-m is \r (= RET). Bind [return] to evil-ret so GUI Enter never falls
;; back to this C-m binding, leaving C-m free as its own key (GUI only).
(define-key (symbol-value map)
(kbd "C-m")
(my/project-or project-switch-to-buffer consult-buffer))
(define-key (symbol-value map)
[return]
#'evil-ret)
;; consult-ripgrep everywhere: it already scopes to the project root when
;; there is one, and shows hits in the posframe (RET jumps) like the other
;; finders — unlike project-find-regexp's separate xref pane.
(define-key (symbol-value map)
(kbd "C-f")
#'consult-ripgrep)
(define-key (symbol-value map)
(kbd "C-p")
(my/project-or project-find-file consult-fd))))
;; Literal tab on TAB/<tab> in insert state, overriding major-mode indent.
(with-eval-after-load 'evil
(evil-define-key 'insert 'global (kbd "TAB") #'my/insert-tab)
(evil-define-key 'insert 'global [tab] #'my/insert-tab))
(provide 'init-evil)
;;; init-evil.el ends here
+83
View File
@@ -0,0 +1,83 @@
;;; init-git.el --- Magit configuration -*- lexical-binding: t; -*-
;;; Commentary:
;; Use Magit with its native keybindings while preserving Evil's window
;; management.
;;
;; Magit buffers are displayed in a dedicated side window on the right.
;;
;; Custom keybindings:
;; C-c C-g - Open Magit
;; C-w - Evil window map
;; j - Next section
;; k - Previous section
;; q - Kill Magit buffer and close the side window
;;; Code:
(use-package magit
:after evil
:bind
(("C-c C-g" . magit-status))
:config
;; Magit appearance and behaviour.
;; Highlight the exact characters that changed.
(setq magit-diff-refine-hunk 'all)
;; Show more surrounding context in diffs.
(setq magit-diff-context-lines 5)
;; Show all transient options.
(setq transient-default-level 5)
;; Don't ask about saving modified buffers.
(setq magit-save-repository-buffers 'dontask)
;; Display Magit in a dedicated side window on the right.
(add-to-list
'display-buffer-alist
'("\\*magit"
(display-buffer-in-side-window)
(side . right)
(slot . 0)
(window-width . 0.45)
(dedicated . t)))
;; Custom commands.
(defun my/magit-quit ()
"Kill the current Magit buffer and close its window."
(interactive)
(quit-window t))
;; Keybindings.
(dolist (map '(magit-mode-map
magit-status-mode-map
magit-log-mode-map
magit-diff-mode-map
magit-revision-mode-map))
(with-eval-after-load 'magit
(when (boundp map)
(let ((keymap (symbol-value map)))
;; Keep Evil window commands.
(define-key keymap (kbd "C-w") evil-window-map)
;; Vim-style movement.
(define-key keymap (kbd "j") #'magit-section-forward)
(define-key keymap (kbd "k") #'magit-section-backward)
;; Quit Magit.
(define-key keymap (kbd "q") #'my/magit-quit)))))
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c C-g" "Magit")))
(provide 'init-git)
;;; init-git.el ends here
+113
View File
@@ -0,0 +1,113 @@
;;; init-project.el --- Project switching, scoped to ~/Projects -*- lexical-binding: t; -*-
;;; Commentary:
;; Built on built-in project.el, no third-party manager. Projects are the
;; direct children of ~/Projects, rescanned authoritatively each startup so
;; the list never accumulates stale entries. A "pin" keeps the current
;; project from hijacking to a dependency's checkout on jump-to-definition.
;;; Code:
(require 'project)
;; A directory is a project root if under VC or containing one of these.
(setq project-vc-extra-root-markers '("build.sh"))
(defvar my/project-search-dirs
'("~/Projects")
"Parent directories scanned for known projects.")
;;;; Project list: rebuilt from a fresh scan each startup
;; project-remember-project would APPEND to the persisted list and leak stale
;; entries (old paths, stdlib dirs, the config repo). Instead scan ~/Projects
;; and assign project--list directly; a real list also stops project.el
;; re-reading the stale file this session.
(defun my/scan-projects-under (parent)
"Return `project--list' entries for direct child projects of PARENT.
Ignore symlinks; don't recurse."
(when (file-directory-p parent)
(let (entries)
(dolist (dir (directory-files parent t directory-files-no-dot-files-regexp))
(when (and (file-directory-p dir)
(not (file-symlink-p dir)))
(let ((default-directory dir))
(when-let ((project (project-current nil)))
(push (list (abbreviate-file-name (project-root project)))
entries)))))
entries)))
(setq project--list
(sort (mapcan #'my/scan-projects-under
(mapcar #'expand-file-name my/project-search-dirs))
(lambda (a b)
(time-less-p
(file-attribute-modification-time (file-attributes (car b)))
(file-attribute-modification-time (file-attributes (car a)))))))
;; Persist so the on-disk file matches (no stale artifact left behind).
(project--write-project-list)
;;;; Sticky project detection
;; project.el recomputes the project from the visited buffer's directory, so
;; jumping into a dependency's VC checkout (via `gd') would make THAT the
;; project. Instead pin the last project under our roots and reuse it for any
;; buffer outside them.
(defvar my/pinned-project nil
"Last project resolved from a buffer under `my/project-search-dirs'.")
(defun my/own-project-dir-p (dir)
"Return non-nil when DIR lives under one of `my/project-search-dirs'."
(and dir
(seq-some (lambda (parent)
(file-in-directory-p dir (expand-file-name parent)))
my/project-search-dirs)))
(defun my/project-pinned (dir)
"Resolve DIR to the pinned project when DIR is outside our roots.
Runs ahead of `project-try-vc', so foreign buffers never become current."
(and my/pinned-project
(not (my/own-project-dir-p dir))
my/pinned-project))
(defun my/remember-pinned-project (project)
"Record PROJECT as the pin when the current buffer is one of ours."
(when (and project (my/own-project-dir-p default-directory))
(setq my/pinned-project project))
project)
(add-hook 'project-find-functions #'my/project-pinned) ; before project-try-vc
(advice-add 'project-current :filter-return #'my/remember-pinned-project)
;;;; Switching and cleanup
;; Skip the "what do you want to do" menu entirely.
(setq project-switch-commands #'project-dired)
;; Widen what counts as "belongs to this project" for cleanup.
(setq project-kill-buffer-conditions
'(buffer-file-name
(major-mode . compilation-mode)
(major-mode . dired-mode)
(major-mode . shell-mode)
(major-mode . eshell-mode)
(major-mode . term-mode)))
(defun my/project-switch-fresh ()
"Save and close the current project before switching."
(interactive)
(when (project-current nil)
(save-some-buffers t)
(project-kill-buffers t))
(call-interactively #'project-switch-project))
(global-set-key (kbd "C-c C-p") #'my/project-switch-fresh)
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c C-p" "switch project (fresh)"
"C-x p" "project"))
(provide 'init-project)
;;; init-project.el ends here
+21
View File
@@ -0,0 +1,21 @@
;;; init-theme.el --- Gruvbox, dark-hard variant -*- lexical-binding: t; -*-
;;; Commentary:
;; gruvbox-dark-hard, with background/fringe/line-numbers flattened to
;; #111111 and the vertico posframe matched to it.
;;; Code:
(use-package gruvbox-theme
:config
(load-theme 'gruvbox-dark-hard t)
(set-face-background 'vertico-posframe "#111111")
(set-face-background 'vertico-posframe-border "#6a9589"))
(set-face-attribute 'default nil :background "#111111")
(set-face-attribute 'fringe nil :background "#111111")
(set-face-attribute 'line-number nil :background "#111111")
(set-face-attribute 'line-number-current-line nil :background "#111111")
(provide 'init-theme)
;;; init-theme.el ends here