Archived
Initial commit for v1.0 of Simply Emacs.
This commit is contained in:
Executable
+71
@@ -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
|
||||
Executable
+248
@@ -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
|
||||
Executable
+123
@@ -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
|
||||
Executable
+95
@@ -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
|
||||
Executable
+54
@@ -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
|
||||
Executable
+84
@@ -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
|
||||
Executable
+83
@@ -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
|
||||
Executable
+113
@@ -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
|
||||
Executable
+21
@@ -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
|
||||
Reference in New Issue
Block a user