+;;; init.el --- Built-in-first Emacs configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Configuration that prefers Emacs' built-in features (Emacs 32) over external
+;; packages, following the "Emacs Solo" approach where practical.
+;;
+;; Packages removed in favour of built-ins:
+;; vertico/consult/marginalia/orderless/embark -> icomplete-vertical + completion-styles
+;; corfu/cape -> completion-preview-mode
+;; moody/minions -> custom mode-line (lisp/emacs-solo-mode-line.el)
+;; exec-path-from-shell -> lisp/emacs-solo-exec-path-from-shell.el
+;; blamer -> git-gutter (lisp/emacs-solo-gutter.el)
+;; rg -> built-in grep / project-find-regexp + ripgrep xref
+;; browse-at-remote -> emacs-solo/vc-browse-remote (built-in vc)
+;; ormolu -> eglot-format
+;; markdown-mode -> markdown-ts-mode (built-in)
+;; jinx -> flyspell (built-in)
+;; yasnippet -> abbrev (built-in)
+;; pulsar -> pulse.el (built-in)
+;; doom-themes -> modus-themes (built-in)
+;;
+;; Packages removed entirely:
+;; aider, claude-code-ide, vterm (AI tooling), magit-delta, treemacs, drag-stuff
+;;
+;; Packages kept (no good built-in equivalent):
+;; magit, envrc, multiple-cursors, nerd-icons, haskell-ts-mode (no built-in
+;; Haskell mode exists), nix-ts-mode, ediprolog, org-modern, org-super-agenda,
+;; dashboard
+;;
+;; crosshairs (line+column highlight) -> global-hl-line-mode (built-in, line
+;; only); the column overlay conflicted with eglot's inline overlays.
+;;
+;; External setup this config assumes:
+;; - language servers on PATH (haskell-language-server-wrapper, rust-analyzer, nil/nixd)
+;; - a spell checker (aspell or hunspell) + dictionary for flyspell
+;; - tree-sitter grammars (M-x treesit-install-language-grammar; see "Language modes")
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Package management (straight.el + use-package)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Bootstrap straight.el: download and load it on first run.
+(defvar bootstrap-version)
+(let ((bootstrap-file
+ (expand-file-name
+ "straight/repos/straight.el/bootstrap.el"
+ (or (bound-and-true-p straight-base-dir)
+ user-emacs-directory)))
+ (bootstrap-version 7))
+ (unless (file-exists-p bootstrap-file)
+ (with-current-buffer
+ (url-retrieve-synchronously
+ "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
+ 'silent 'inhibit-cookies)
+ (goto-char (point-max))
+ (eval-print-last-sexp)))
+ (load bootstrap-file nil 'nomessage))
+
+(straight-use-package 'org)
+(straight-use-package 'use-package)
+(use-package straight
+ :custom
+ ;; Treat these as built-in "pseudo packages" so straight.el doesn't download
+ ;; a separate copy than the one Emacs already ships.
+ (straight-built-in-pseudo-packages
+ '(emacs nadvice python image-mode project flymake xref eglot which-key
+ icomplete completion-preview markdown-ts-mode modus-themes flyspell vc))
+ ;; Make `use-package' install packages via straight.el automatically.
+ (straight-use-package-by-default t))
+
+;; Local lisp modules (vendored built-in helpers, cabal-source-repo, ...).
+(add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Custom file
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Keep machine-generated `customize' settings out of this file.
+(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
+(load custom-file 'noerror)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; UI & appearance
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(set-fringe-mode 10) ; Give some breathing room
+(global-subword-mode 1) ; Navigate inside CamelCaseWords
+(column-number-mode t) ; Show column number in the mode line
+(setq visible-bell t) ; Flash instead of the audible bell
+(xterm-mouse-mode 1) ; Enable mouse support in the terminal
+(blink-cursor-mode 0)
+(setq-default fill-column 80)
+(global-display-fill-column-indicator-mode t) ; Draw a line at `fill-column'
+
+;; Built-in theme (replaces doom-themes). modus-vivendi is a polished dark
+;; theme; swap for modus-vivendi-tinted / modus-operandi (light) to taste.
+(require-theme 'modus-themes)
+(setq modus-themes-italic-constructs t
+ modus-themes-bold-constructs t
+ modus-themes-mixed-fonts t)
+(load-theme 'modus-vivendi-tritanopia t)
+
+;; Inherit PATH/exec-path from the login shell (replaces exec-path-from-shell).
+;; Runs asynchronously so it never blocks startup.
+(require 'emacs-solo-exec-path-from-shell)
+(add-hook 'after-init-hook #'emacs-solo/set-exec-path-from-shell-PATH)
+
+;; Briefly highlight the current line after big movements (replaces pulsar).
+(require 'pulse)
+(defun my/pulse-line (&rest _)
+ "Pulse the current line."
+ (pulse-momentary-highlight-one-line (point)))
+(dolist (cmd '(scroll-up-command scroll-down-command
+ recenter-top-bottom other-window))
+ (advice-add cmd :after #'my/pulse-line))
+
+;; Compact built-in mode line (replaces moody + minions).
+(require 'emacs-solo-mode-line)
+
+;; Vendored emacs-solo modules (built-in font-lock/overlays, no external deps).
+(require 'emacs-solo-highlight-keywords) ; TODO/FIXME/HACK/NOTE highlighting
+(require 'emacs-solo-rainbow-delimiters) ; depth-colored () [] {}
+(require 'emacs-solo-ace-window) ; M-O: jump to a window by number
+(require 'emacs-solo-dired-gutter) ; git status marks in Dired
+(require 'emacs-solo-abbrev) ; snippet-like abbrev expansions
+
+;; Icons (kept for the dashboard). First run: M-x nerd-icons-install-fonts
+(use-package nerd-icons)
+
+;; Multiple cursors (no built-in equivalent).
+(use-package multiple-cursors)
+
+;; Highlight the current line (built-in). The column crosshair (crosshairs /
+;; col-highlight / vline) is dropped: its full-height vertical overlay clashes
+;; badly with eglot's inline overlays (diagnostics, inlay hints, eldoc-at-point).
+(global-hl-line-mode 1)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Editing & files
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Keep versioned backups in a dedicated directory instead of next to files.
+(setq
+ backup-by-copying t ; don't clobber symlinks
+ backup-directory-alist
+ '(("." . "~/.emacs.d/backups/")) ; don't litter my fs tree
+ delete-old-versions t
+ kept-new-versions 6
+ kept-old-versions 2
+ version-control t) ; use versioned backups
+
+;; Delete trailing whitespace on save, globally.
+(add-hook 'before-save-hook 'delete-trailing-whitespace)
+
+(delete-selection-mode t) ; Typing replaces the active region
+(setq-default indent-tabs-mode nil) ; Indent with spaces, never tabs
+(setq use-short-answers t) ; Accept short y/n answers
+
+(global-set-key (kbd "C-x k") #'kill-current-buffer) ; Kill buffer without prompting
+
+;; Revert buffers when their files change on disk.
+(setq global-auto-revert-non-file-buffers t)
+(global-auto-revert-mode 1)
+
+;; Snippets via built-in abbrev (replaces yasnippet). Expansions live in the
+;; abbrev table (see lisp/emacs-solo-abbrev.el); edit with M-x edit-abbrevs.
+(setq save-abbrevs 'silently)
+(add-hook 'text-mode-hook #'abbrev-mode)
+(add-hook 'prog-mode-hook #'abbrev-mode)
+
+;; Smarter completion of the word/line/filename before point from open buffers,
+;; the kill ring, file names, etc. (built-in).
+(global-set-key (kbd "M-/") #'hippie-expand)
+
+;; Jump to Dired at the current file's directory, point on the file.
+(global-set-key (kbd "C-x C-j") #'dired-jump)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Quality-of-life (built-in; many new in Emacs 31/32)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; --- Kill/yank & pairs ---
+(setq kill-region-dwim 'emacs-word ; E31: C-w with no region kills a word back
+ kill-do-not-save-duplicates t
+ save-interprogram-paste-before-kill t
+ delete-pair-blink-delay 0
+ delete-pair-push-mark t ; E31: easy C-x C-x after delete-pair
+ delete-by-moving-to-trash t) ; deletions go to the system trash
+
+;; --- Expanded editing/movement verbs (upper-case siblings of the defaults) ---
+(global-set-key (kbd "C-x ;") #'comment-line)
+(global-set-key (kbd "C-M-z") #'delete-pair)
+(global-set-key (kbd "M-J") #'duplicate-dwim)
+(global-set-key (kbd "M-K") #'kill-paragraph)
+(global-set-key (kbd "M-Z") #'zap-up-to-char)
+(global-set-key (kbd "M-F") #'forward-to-word)
+(global-set-key (kbd "M-B") #'backward-to-word)
+(global-set-key [remap capitalize-word] #'capitalize-dwim) ; act on the region
+(global-set-key [remap downcase-word] #'downcase-dwim)
+(global-set-key [remap upcase-word] #'upcase-dwim)
+(global-set-key [remap delete-horizontal-space] #'cycle-spacing)
+
+;; --- Window management (Emacs 31) + undo/redo of window layouts ---
+(global-set-key (kbd "C-x w t") #'window-layout-transpose)
+(global-set-key (kbd "C-x w r") #'window-layout-rotate-clockwise)
+(global-set-key (kbd "C-x w f h") #'window-layout-flip-leftright)
+(global-set-key (kbd "C-x w f v") #'window-layout-flip-topdown)
+(winner-mode 1) ; C-c <left> / C-c <right>
+
+;; --- Movement, scrolling, windows ---
+(setq scroll-conservatively 8
+ ;; scroll-margin must be 0 with pixel-scroll-precision-mode: a non-zero
+ ;; margin makes the wheel unable to reach the top/bottom of the buffer
+ ;; ("Beginning of buffer", cursor stuck ~`scroll-margin' lines down).
+ scroll-margin 0
+ set-mark-command-repeat-pop t ; C-u C-SPC C-SPC ... cycles the mark ring
+ split-width-threshold 170) ; prefer vertical splits
+(pixel-scroll-precision-mode 1)
+(repeat-mode 1) ; e.g. C-x o o o ..., C-x ^ ^ ... to repeat
+
+;; --- Pairs & parens ---
+(electric-pair-mode 1)
+(setq show-paren-context-when-offscreen 'overlay) ; preview an off-screen match
+
+;; --- Display & buffers ---
+(setq display-line-numbers-type 'relative)
+(add-hook 'text-mode-hook #'display-line-numbers-mode)
+(setq ibuffer-human-readable-size t ; E31
+ view-lossage-auto-refresh t ; E31: live C-h l
+ uniquify-buffer-name-style 'forward
+ uniquify-after-kill-buffer-flag t) ; E31
+(global-set-key (kbd "C-x C-b") #'ibuffer) ; richer buffer list
+
+;; --- History persistence ---
+(setq history-length 300
+ savehist-additional-variables
+ '(kill-ring register-alist mark-ring global-mark-ring
+ search-ring regexp-search-ring)
+ recentf-exclude (list "^/\\(?:ssh\\|su\\|sudo\\)?:"))
+
+;; --- vc (built-in; used by vc-dir / vc-diff / C-x v ... alongside magit) ---
+(with-eval-after-load 'vc
+ (setq vc-allow-rewriting-published-history t ; E31
+ vc-git-print-log-follow t))
+
+;; --- dired ---
+(with-eval-after-load 'dired
+ (setq dired-dwim-target t ; default copy target = other dired pane
+ dired-hide-details-hide-absolute-location t ; E31
+ dired-listing-switches "-alh"))
+
+;; --- Smarter C-g: one quit dismisses an active minibuffer from anywhere ---
+(define-advice keyboard-quit
+ (:around (quit) quit-current-context)
+ "Quit the active minibuffer (from any window) before quitting in-buffer."
+ (if (active-minibuffer-window)
+ (if (minibufferp) (minibuffer-keyboard-quit) (abort-recursive-edit))
+ (unless (or defining-kbd-macro executing-kbd-macro)
+ (funcall-interactively quit))))
+
+;; --- Terminal niceties (Emacs 31) ---
+(when (fboundp 'tty-tip-mode) (tty-tip-mode 1)) ; tooltips in the terminal
+
+;; --- Search (isearch) ---
+(setq isearch-lazy-count t
+ lazy-count-prefix-format "(%s/%s) "
+ isearch-allow-motion t ; M-< / M-> move between matches in isearch
+ search-ring-max 200
+ regexp-search-ring-max 200)
+
+;; --- Long lines, URLs, mouse menu, encoding ---
+(global-so-long-mode 1) ; stay responsive in files with very long lines
+(global-goto-address-mode 1) ; C-c RET opens URLs / emails at point
+(context-menu-mode 1) ; right-click context menu
+(minibuffer-electric-default-mode 1)
+(modify-coding-system-alist 'file "" 'utf-8) ; don't re-prompt for encoding on tsx/etc.
+
+;; --- Flymake diagnostic navigation + lists ---
+;; (Flymake's "error list" is `flymake-show-buffer-diagnostics'; the project-wide
+;; one is `flymake-show-project-diagnostics' -- routed to a side window above.)
+(with-eval-after-load 'flymake
+ (define-key flymake-mode-map (kbd "M-n") #'flymake-goto-next-error)
+ (define-key flymake-mode-map (kbd "M-p") #'flymake-goto-prev-error)
+ (define-key flymake-mode-map (kbd "C-c ! l") #'flymake-show-buffer-diagnostics)
+ (define-key flymake-mode-map (kbd "C-c ! P") #'flymake-show-project-diagnostics))
+
+;; --- C-x s / C-x C-c: press "d" to preview the diff of a buffer being saved ---
+(add-to-list 'save-some-buffers-action-alist
+ (list ?d
+ (lambda (buf) (diff-buffer-with-file (buffer-file-name buf)))
+ "show diff between the buffer and its file"))
+
+;; --- Show elisp eval results (C-x C-e) inline as an overlay ---
+(defun my/eval-last-sexp-overlay (arg)
+ "Eval the sexp before point and show the result inline for a few seconds.
+With prefix ARG, insert the result into the buffer instead."
+ (interactive "P")
+ (let ((val (elisp--eval-last-sexp nil)))
+ (if arg
+ (insert (format " ; => %S" val))
+ (let ((ov (make-overlay (point) (point))))
+ (overlay-put ov 'after-string
+ (propertize (format " ; => %S" val) 'face 'font-lock-comment-face))
+ (run-with-timer 3 nil #'delete-overlay ov)))))
+(global-set-key (kbd "C-x C-e") #'my/eval-last-sexp-overlay)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Minibuffer completion (built-in)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Vertical minibuffer completion (replaces vertico + marginalia).
+(setq icomplete-delay-completions-threshold 0
+ icomplete-compute-delay 0
+ icomplete-show-matches-on-no-input t
+ icomplete-hide-common-prefix nil
+ icomplete-prospects-height 10
+ icomplete-separator " . "
+ icomplete-with-completion-tables t
+ icomplete-in-buffer t
+ icomplete-max-delay-chars 0
+ icomplete-scroll t
+ icomplete-vertical-in-buffer-adjust-list t
+ icomplete-vertical-render-prefix-indicator t)
+(fido-mode -1)
+(icomplete-vertical-mode 1)
+
+;; vertico-directory-style file navigation for icomplete: TAB/RET descend into
+;; the selected directory (and the list refreshes to its contents) instead of
+;; opening it in Dired; DEL goes up a level.
+(defun my/icomplete--file-completion-p ()
+ "Non-nil when the minibuffer is completing file names."
+ (eq (completion-metadata-get
+ (completion-metadata
+ (buffer-substring (minibuffer-prompt-end) (point))
+ minibuffer-completion-table minibuffer-completion-predicate)
+ 'category)
+ 'file))
+
+(defun my/icomplete--selected ()
+ "The currently selected candidate string, or nil."
+ (car (bound-and-true-p completion-all-sorted-completions)))
+
+(defun my/icomplete--selected-directory-p ()
+ "Non-nil when the selected candidate is a real subdirectory.
+Excludes the `./' pseudo-entry (descending into it would loop)."
+ (and (my/icomplete--file-completion-p)
+ (let ((cand (my/icomplete--selected)))
+ (and (stringp cand) (string-suffix-p "/" cand) (not (equal cand "./"))))))
+
+(defun my/icomplete-tab ()
+ "Descend into the selected directory; otherwise complete as usual."
+ (interactive)
+ (if (my/icomplete--selected-directory-p)
+ (icomplete-force-complete)
+ (minibuffer-complete)))
+
+(defun my/icomplete-ret ()
+ "Descend into a subdirectory; open the dir on `./'; else accept and exit."
+ (interactive)
+ (cond
+ ((my/icomplete--selected-directory-p) (icomplete-force-complete))
+ ;; `./' = open the directory currently typed (don't insert it, just accept).
+ ((and (my/icomplete--file-completion-p)
+ (equal (my/icomplete--selected) "./"))
+ (exit-minibuffer))
+ (t (icomplete-force-complete-and-exit))))
+
+(defun my/icomplete-del ()
+ "Delete the last path component (go up a level) when completing files."
+ (interactive)
+ (if (and (my/icomplete--file-completion-p) (eq (char-before) ?/))
+ (save-excursion
+ (goto-char (1- (point)))
+ (when (search-backward "/" (minibuffer-prompt-end) t)
+ (delete-region (1+ (point)) (point-max))))
+ (call-interactively #'delete-backward-char)))
+
+(let ((m icomplete-minibuffer-map))
+ (define-key m (kbd "C-n") #'icomplete-forward-completions)
+ (define-key m (kbd "C-p") #'icomplete-backward-completions)
+ (define-key m (kbd "<down>") #'icomplete-forward-completions)
+ (define-key m (kbd "<up>") #'icomplete-backward-completions)
+ (define-key m (kbd "C-v") #'icomplete-vertical-mode)
+ (define-key m (kbd "TAB") #'my/icomplete-tab)
+ (define-key m (kbd "RET") #'my/icomplete-ret)
+ (define-key m (kbd "DEL") #'my/icomplete-del)
+ (define-key m (kbd "C-j") #'exit-minibuffer)) ; accept raw input as typed
+;; Hide the *Completions* buffer after an in-buffer completion.
+(advice-add 'completion-at-point :after #'minibuffer-hide-completions)
+
+;; Completion behaviour (replaces orderless with built-in flex matching).
+(setq completion-styles '(flex partial-completion basic)
+ completion-category-overrides '((file (styles basic partial-completion)))
+ completion-ignore-case t
+ read-buffer-completion-ignore-case t
+ read-file-name-completion-ignore-case t
+ completions-detailed t
+ completion-eager-update t ; E31: update *Completions* as you type
+ minibuffer-visible-completions 'up-down ; E31: navigate completions in place
+ enable-recursive-minibuffers t
+ tab-always-indent 'complete) ; TAB completes when already indented
+(minibuffer-depth-indicate-mode 1)
+(file-name-shadow-mode 1) ; type a new path without deleting the old
+
+(savehist-mode) ; Persist minibuffer history across sessions
+(save-place-mode 1) ; Remember point position in visited files
+
+(recentf-mode 1) ; Track recently opened files
+(setq recentf-max-menu-items 25
+ recentf-max-saved-items 1000000)
+(global-set-key (kbd "M-g r") #'recentf-open)
+
+;; A few search/navigation bindings using built-ins (replaces consult cmds).
+(global-set-key (kbd "M-s g") #'grep)
+(global-set-key (kbd "M-s r") #'rgrep)
+(global-set-key (kbd "M-s f") #'find-name-dired)
+(global-set-key (kbd "M-s l") #'occur)
+(global-set-key (kbd "M-g i") #'imenu)
+
+;; Show available key bindings after a prefix key (built-in which-key).
+(which-key-mode)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Version control & project
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package magit)
+
+;; Use ripgrep for grep/xref when available (replaces the rg package).
+(when (executable-find "rg")
+ (setq xref-search-program 'ripgrep
+ grep-command "rg -nS --no-heading "
+ grep-find-template "rg <C> --null -nH -e <R> <D>"))
+
+(use-package project :straight nil)
+
+;; Nicer git diffs in vc (word-level histogram), replacing magit-delta.
+(with-eval-after-load 'vc-git
+ (setq vc-git-diff-switches '("--patch-with-stat" "--histogram")))
+
+;; Open the current file/line on the remote forge (replaces browse-at-remote).
+(defun emacs-solo/vc-browse-remote (&optional current-line)
+ "Open the repository's remote URL in the browser.
+With prefix CURRENT-LINE, point at the current branch, file, and line."
+ (interactive "P")
+ (require 'vc-git)
+ (let* ((remote-url (string-trim (vc-git--run-command-string nil "config" "--get" "remote.origin.url")))
+ (branch (string-trim (vc-git--run-command-string nil "rev-parse" "--abbrev-ref" "HEAD")))
+ (file (and (buffer-file-name)
+ (string-trim (file-relative-name (buffer-file-name) (vc-root-dir)))))
+ (line (line-number-at-pos)))
+ (if (and remote-url (string-match "\\(?:git@\\|https://\\)\\([^:/]+\\)[:/]\\(.+?\\)\\(?:\\.git\\)?$" remote-url))
+ (let ((host (replace-regexp-in-string "^git@" "" (match-string 1 remote-url)))
+ (path (match-string 2 remote-url)))
+ (browse-url
+ (if (and current-line file)
+ (format "https://%s/%s/blob/%s/%s#L%d" host path branch file line)
+ (format "https://%s/%s" host path))))
+ (message ">>> Could not determine repository URL"))))
+(global-set-key (kbd "C-x p y") #'emacs-solo/vc-browse-remote)
+(with-eval-after-load 'vc
+ (define-key vc-prefix-map (kbd "B") #'emacs-solo/vc-browse-remote)
+ (define-key vc-prefix-map (kbd "o")
+ (lambda () (interactive) (emacs-solo/vc-browse-remote 1))))
+
+;; Git diff indicators in the left margin (replaces blamer).
+(setq-default left-margin-width 1) ; room for the gutter marks
+(require 'emacs-solo-gutter)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Windows, popups & sidebars (built-in)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Predictable popups: Help/Ibuffer on the right; grep/xref/Flymake/Completions
+;; at the bottom; inferior REPLs at the bottom. (display-buffer-in-side-window)
+(setq switch-to-buffer-obey-display-actions t)
+(setq display-buffer-alist
+ '(("\\*\\(Backtrace\\|Warnings\\|Compile-Log\\|Messages\\|Bookmark List\\|Occur\\|eldoc\\)\\*"
+ (display-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 0))
+ ("\\*\\([Hh]elp\\)\\*"
+ (display-buffer-in-side-window) (window-width . 80) (side . right) (slot . 0))
+ ("\\*\\(Ibuffer\\)\\*"
+ (display-buffer-in-side-window) (window-width . 100) (side . right) (slot . 1))
+ ("\\*\\(Flymake diagnostics\\|Completions\\)"
+ (display-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 2))
+ ("\\*\\(grep\\|xref\\|find\\)\\*"
+ (display-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 1))
+ ("\\*inferior.*"
+ (display-buffer-in-side-window) (window-height . 0.5) (side . bottom) (slot . 1))))
+
+;; Built-in speedbar as a file/project tree sidebar (replaces treemacs).
+;; M-I toggles/focuses it on a side window.
+(use-package speedbar
+ :straight nil
+ :bind (("M-I" . (lambda ()
+ (interactive)
+ (speedbar-window) ; E31: open speedbar in a side window
+ (let ((win (get-buffer-window speedbar-buffer)))
+ (when win (select-window win))))))
+ :custom
+ (speedbar-window-default-width 25) ; E31
+ (speedbar-window-max-width 25) ; E31
+ (speedbar-show-unknown-files t)
+ (speedbar-directory-unshown-regexp "^$")
+ (speedbar-indentation-width 2)
+ (speedbar-use-images t)
+ (speedbar-update-flag nil))
+
+;; Grouped buffer list (org / vc / dired / terminal / help ...).
+(setq ibuffer-saved-filter-groups
+ '(("default"
+ ("org" (or (mode . org-mode)
+ (name . "^\\*Org Agenda\\*$")))
+ ("dired" (mode . dired-mode))
+ ("vc" (or (name . "^\\*vc-") (name . "^magit")))
+ ("terminal" (or (mode . term-mode) (mode . shell-mode) (mode . eshell-mode)))
+ ("help" (or (name . "^\\*Help\\*$") (name . "^\\*info\\*$")))
+ ("emacs" (or (name . "^\\*scratch\\*$")
+ (name . "^\\*Messages\\*$")
+ (name . "^\\*Warnings\\*$"))))))
+(setq ibuffer-show-empty-filter-groups nil)
+(add-hook 'ibuffer-mode-hook
+ (lambda () (ibuffer-switch-to-saved-filter-groups "default")))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Programming (general)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(add-hook 'prog-mode-hook #'display-line-numbers-mode) ; Line numbers in code buffers
+
+;; Spell checking via built-in flyspell (replaces jinx). Needs aspell/hunspell.
+(add-hook 'text-mode-hook #'flyspell-mode)
+(add-hook 'prog-mode-hook #'flyspell-prog-mode)
+;; M-$ runs `ispell-word' by default; C-M-$ to change dictionary.
+(global-set-key (kbd "C-M-$") #'ispell-change-dictionary)
+
+;; LSP performance knobs (relevant to eglot).
+(setq read-process-output-max (* 1024 1024))
+
+;; In-buffer completion popup -> built-in inline preview (replaces corfu+cape).
+;; Grey suggestion text completed with TAB; cycle with M-n / M-p.
+(setq completion-preview-minimum-symbol-length 2)
+(add-hook 'prog-mode-hook #'completion-preview-mode)
+(add-hook 'text-mode-hook #'completion-preview-mode)
+(with-eval-after-load 'completion-preview
+ (define-key completion-preview-active-mode-map (kbd "M-n") #'completion-preview-next-candidate)
+ (define-key completion-preview-active-mode-map (kbd "M-p") #'completion-preview-prev-candidate)
+ (define-key completion-preview-active-mode-map (kbd "TAB") #'completion-preview-insert))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; LSP (eglot, built-in)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Show eldoc for the symbol at point automatically, not just in the echo area.
+(setq eldoc-help-at-pt t) ; E31
+
+(use-package eglot
+ :straight nil ; use the built-in version
+ :hook ((haskell-ts-mode . eglot-ensure)
+ (haskell-literate-ts-mode . eglot-ensure)
+ (rust-ts-mode . eglot-ensure)
+ (nix-ts-mode . eglot-ensure))
+ :bind (:map eglot-mode-map
+ ("C-c o r" . eglot-rename)
+ ("C-c o a" . eglot-code-actions)
+ ("C-c o f" . eglot-format)
+ ("C-c o o" . eglot-code-action-organize-imports)
+ ("C-c o h" . eglot-inlay-hints-mode)) ; toggle inline type/param hints
+ :custom
+ (eglot-events-buffer-size 0) ; don't log LSP traffic (faster)
+ (eglot-code-action-indications nil) ; E31: no inline code-action hints
+ (eglot-documentation-renderer 'markdown-ts-view-mode) ; E31: render docs via markdown-ts
+ :config
+ ;; Tell haskell-language-server to format with fourmolu.
+ (setq-default eglot-workspace-configuration
+ '(:haskell (:formattingProvider "fourmolu"))))
+;; Workspace/document symbols via built-in imenu/xref (replaces consult-eglot):
+;; M-g i -> imenu C-M-. -> xref-find-apropos
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Language modes
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Tree-sitter grammar *sources* (the URL map). This is still required: there
+;; is no built-in registry, and the auto-install fallback for an unlisted
+;; language is an interactive prompt. nix (non-standard repo) and markdown
+;; (needs a branch + subdir recipe) in particular cannot be auto-guessed.
+(setq treesit-language-source-alist
+ '((rust . ("https://github.com/tree-sitter/tree-sitter-rust"))
+ (nix . ("https://github.com/nix-community/tree-sitter-nix"))
+ (haskell . ("https://github.com/tree-sitter/tree-sitter-haskell"))
+ (haskell-literate . ("https://github.com/LaurentRDC/tree-sitter-haskell-literate"))
+ (markdown . ("https://github.com/tree-sitter-grammars/tree-sitter-markdown"
+ "split_parser" "tree-sitter-markdown/src"))
+ (markdown-inline . ("https://github.com/tree-sitter-grammars/tree-sitter-markdown"
+ "split_parser" "tree-sitter-markdown-inline/src"))))
+;; Auto-enable every available built-in tree-sitter mode (rust, markdown,
+;; python, c, js, go, json, ...): this populates `major-mode-remap-alist' so
+;; .rs/.md/etc. are routed to their *-ts-mode automatically -- no per-language
+;; auto-mode-alist entries needed for built-in modes. External ts packages
+;; (nix-ts-mode, haskell-ts-mode) are NOT covered here and still set :mode.
+;; NOTE: must be `setopt' -- this defcustom's :set is what does the remapping.
+(setopt treesit-enabled-modes t)
+;; And auto-install a grammar (from the sources above) the first time a
+;; tree-sitter mode needs it.
+(setopt treesit-auto-install-grammar 'always)
+
+;; External ts-modes (haskell-ts-mode, nix-ts-mode) check the grammar with
+;; `treesit-ready-p', which never auto-installs -- so `treesit-auto-install-
+;; grammar' is not consulted and you'd just get an error. This helper installs
+;; the grammar first (honoring that option), then enables the mode.
+(defun my/ensure-ts-grammar-then (lang mode)
+ "Install LANG's tree-sitter grammar if missing, then call MODE."
+ (treesit-ensure-installed lang)
+ (funcall mode))
+
+;; Tree-sitter Haskell mode (external; no built-in Haskell mode exists). IDE
+;; features (rename, format-via-fourmolu, code actions) come from eglot/HLS.
+(use-package haskell-ts-mode
+ :commands haskell-ts-mode
+ :init
+ (defun my/haskell-ts-mode ()
+ "Ensure the Haskell grammar, then enable `haskell-ts-mode'."
+ (interactive)
+ (my/ensure-ts-grammar-then 'haskell #'haskell-ts-mode))
+ (add-to-list 'auto-mode-alist '("\\.hs\\'" . my/haskell-ts-mode)))
+
+;; Literate Haskell (.lhs): the `haskell-literate' grammar parses the document
+;; and the `haskell' grammar is injected into code blocks (see
+;; lisp/haskell-literate-ts-mode.el). Needs both grammars.
+(use-package haskell-literate-ts-mode
+ :straight nil
+ :commands haskell-literate-ts-mode
+ :init
+ (defun my/haskell-literate-ts-mode ()
+ "Ensure the haskell + haskell-literate grammars, then enable the mode."
+ (interactive)
+ (treesit-ensure-installed 'haskell)
+ (treesit-ensure-installed 'haskell-literate)
+ (require 'haskell-literate-ts-mode)
+ (haskell-literate-ts-mode))
+ (add-to-list 'auto-mode-alist '("\\.lhs\\'" . my/haskell-literate-ts-mode)))
+
+;; eglot/HLS supports literate Haskell; register the mode with the HLS contact
+;; and the "literate haskell" language id (HLS keys off the .lhs extension too).
+(with-eval-after-load 'eglot
+ (add-to-list 'eglot-server-programs
+ '((haskell-literate-ts-mode :language-id "literate haskell")
+ . ("haskell-language-server-wrapper" "--lsp"))))
+
+;; Format Haskell (and other eglot-managed buffers) via the language server,
+;; which is configured above to use fourmolu (replaces the ormolu package).
+(with-eval-after-load 'eglot
+ (define-key eglot-mode-map (kbd "C-c r") #'eglot-format-buffer))
+
+;; Opt-in format-on-save (replaces ormolu-format-on-save-mode). Enable it
+;; per-project from .dir-locals.el; it formats via eglot only when a language
+;; server is actually connected, so saves never error without one.
+(defun my/eglot-format-buffer-maybe ()
+ "Format the buffer via eglot when managed; otherwise report why it skipped."
+ (if (and (fboundp 'eglot-managed-p) (eglot-managed-p))
+ (eglot-format-buffer)
+ (message ">>> format-on-save skipped: no eglot server connected to %s"
+ (buffer-name))))
+
+(define-minor-mode my/eglot-format-on-save-mode
+ "Format the current buffer with eglot before each save."
+ :lighter " Fmt"
+ (if my/eglot-format-on-save-mode
+ (add-hook 'before-save-hook #'my/eglot-format-buffer-maybe nil t)
+ (remove-hook 'before-save-hook #'my/eglot-format-buffer-maybe t)))
+
+;; Direct fourmolu/ormolu formatting (the true replacement for the ormolu
+;; package, which ran the binary itself -- independent of HLS). Use this when
+;; the project's HLS lacks the fourmolu/ormolu formatter plugin. Picks up the
+;; binary from the buffer's environment, so envrc/nix-shell PATH is honored.
+(defun my/fourmolu-format-buffer ()
+ "Format the current Haskell buffer with the external `fourmolu' (or `ormolu')."
+ (interactive)
+ (let ((exe (or (executable-find "fourmolu") (executable-find "ormolu"))))
+ (if (not exe)
+ (message ">>> fourmolu/ormolu not found on PATH for this buffer")
+ ;; Capture stdout (the formatted source) in a buffer and stderr (fourmolu's
+ ;; "Loaded config from: ..." diagnostics) in a separate file, so the
+ ;; diagnostics never get mixed into the buffer contents.
+ (let ((out (generate-new-buffer " *fourmolu-out*"))
+ (errfile (make-temp-file "fourmolu-err"))
+ (pt (point))
+ (wstart (window-start)))
+ (unwind-protect
+ (if (zerop (call-process-region
+ (point-min) (point-max) exe nil (list out errfile) nil
+ "--stdin-input-file" (or buffer-file-name "Main.hs")))
+ (progn
+ (replace-buffer-contents out)
+ (goto-char pt)
+ (set-window-start (selected-window) wstart))
+ (message ">>> %s failed:\n%s"
+ (file-name-nondirectory exe)
+ (with-temp-buffer (insert-file-contents errfile) (buffer-string))))
+ (kill-buffer out)
+ (delete-file errfile))))))
+
+(define-minor-mode my/fourmolu-format-on-save-mode
+ "Run `fourmolu'/`ormolu' on the buffer before each save."
+ :lighter " 4mol"
+ (if my/fourmolu-format-on-save-mode
+ (add-hook 'before-save-hook #'my/fourmolu-format-buffer nil t)
+ (remove-hook 'before-save-hook #'my/fourmolu-format-buffer t)))
+
+;; Look up / insert the source-repository-package stanza for a Cabal dependency.
+(use-package cabal-source-repo
+ :straight nil
+ :load-path "~/.emacs.d/lisp"
+ :bind ("C-c h r" . cabal-source-repo-upsert))
+
+(use-package nix-ts-mode
+ :commands nix-ts-mode
+ :init
+ (defun my/nix-ts-mode ()
+ "Ensure the Nix grammar, then enable `nix-ts-mode'."
+ (interactive)
+ (my/ensure-ts-grammar-then 'nix #'nix-ts-mode))
+ (add-to-list 'auto-mode-alist '("\\.nix\\'" . my/nix-ts-mode)))
+
+;; rust-ts-mode (.rs) and markdown-ts-mode (.md/.markdown/.mdx) are built-in and
+;; auto-routed by `treesit-enabled-modes' above -- no auto-mode-alist needed.
+
+;; Prolog: treat .pl files as Prolog (not Perl) and add an interaction helper.
+(add-to-list 'auto-mode-alist '("\\.pl\\'" . prolog-mode))
+(use-package ediprolog)
+
+;; Build a self-contained HTML preview of a Markdown buffer (used with
+;; impatient-mode / strapdown for live preview in a browser).
+(defun markdown-html (buffer)
+ (princ (with-current-buffer buffer
+ (format "<!DOCTYPE html><html><title>Impatient Markdown</title><xmp theme=\"united\" style=\"display:none;\"> %s </xmp><script src=\"http://ndossougbe.github.io/strapdown/dist/strapdown.js\"></script></html>" (buffer-substring-no-properties (point-min) (point-max))))
+ (current-buffer)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Environment & toolchains
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Make the locally-built Cardano toolchain (binaries, pkg-config, shared libs)
+;; visible to Emacs and to subprocesses it launches.
+(setenv "PATH" (concat "/usr/local/opt/cardano/bin:" (getenv "PATH")))
+(setenv "PKG_CONFIG_PATH" (concat "/usr/local/opt/cardano/lib/pkgconfig:" (getenv "PKG_CONFIG_PATH")))
+(setenv "LD_LIBRARY_PATH" "/usr/local/opt/cardano/bin")
+
+;; Buffer-local direnv environment (no built-in equivalent).
+(use-package envrc
+ :hook (after-init . envrc-global-mode))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Fonts (emoji & symbols)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Pick the first available emoji font for emoji codepoints.
+(set-fontset-font
+ t
+ 'emoji
+ (cond
+ ((member "Apple Color Emoji" (font-family-list)) "Apple Color Emoji")
+ ((member "Noto Color Emoji" (font-family-list)) "Noto Color Emoji")
+ ((member "Noto Emoji" (font-family-list)) "Noto Emoji")
+ ((member "Segoe UI Emoji" (font-family-list)) "Segoe UI Emoji") ; 🧗
+ ((member "Symbola" (font-family-list)) "Symbola")))
+
+;; Pick the first available font for generic symbol codepoints.
+(set-fontset-font
+ t
+ 'symbol
+ (cond
+ ((member "Segoe UI Symbol" (font-family-list)) "Segoe UI Symbol")
+ ((member "Apple Symbols" (font-family-list)) "Apple Symbols")
+ ((member "Symbola" (font-family-list)) "Symbola")))
+
+;; On Windows, route the Misc Symbols and Pictographs block to Segoe UI Symbol.
+(cond
+ ((eq system-type 'windows-nt)
+ (set-fontset-font t '(#x1F300 . #x1F5FF) "Segoe UI Symbol")))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Clipboard (OSC-52 / tmux)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun tmux-osc52-direct-copy (text)
+ "Copy TEXT to the clipboard.
+In a terminal frame, use the OSC-52 escape sequence through tmux's passthrough.
+In a graphical frame, use the normal system clipboard."
+ (if (display-graphic-p)
+ ;; Graphical frame: ordinary clipboard, no terminal escape sequences.
+ (gui-select-text text)
+ ;; Terminal frame: send OSC-52 to the selected frame's terminal.
+ (let* ((b64 (base64-encode-string text t))
+ (sequence (format "\ePtmux;\e\e]52;c;%s\a\e\\" b64)))
+ (send-string-to-terminal sequence (frame-terminal)))))
+
+;; Always install the hook; the function decides what to do at runtime.
+(setq interprogram-cut-function #'tmux-osc52-direct-copy)
+(setq tty-select-enable-set-clipboard t)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Server & org-protocol
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Start the Emacs server so emacsclient can reuse this instance.
+(require 'server)
+(unless (server-running-p) (server-start))
+
+;; Custom org-protocol handler: open external file:// links at a given line,
+;; bypassing org's project-alist path remapping.
+(require 'org-protocol)
+(defun my/org-protocol-open-source (fname)
+ "Open a file:// URL with line, bypassing the project-alist remap."
+ (let* ((data (org-protocol-parse-parameters fname nil '(:url :line)))
+ (uri (plist-get data :url))
+ (line (plist-get data :line))
+ (path (cond
+ ((string-prefix-p "file://" uri) (url-unhex-string (substring uri 7)))
+ (t (url-unhex-string uri)))))
+ (find-file path)
+ (when line
+ (goto-char (point-min))
+ (forward-line (1- (string-to-number line))))
+ nil)) ; nil = handled, don't pass through
+
+(add-to-list 'org-protocol-protocol-alist
+ '("open-source-local"
+ :protocol "open-source"
+ :function my/org-protocol-open-source
+ :kill-client nil))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Org mode, agenda & dashboard
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; --- 1. Modern look (org-modern) ---
+(use-package org-modern
+ :ensure t
+ :hook ((org-mode . org-modern-mode)
+ (org-agenda-finalize . org-modern-agenda))
+ :config
+ (setq org-modern-star '("◉" "○" "◈" "◇")
+ org-modern-todo nil ; Let org-super-agenda handle TODO colors
+ org-modern-priority nil
+ org-modern-keyword nil))
+
+;; --- 2. Organization engine (org-super-agenda) ---
+(use-package org-super-agenda
+ :ensure t
+ :config
+ (org-super-agenda-mode 1)
+ ;; Defines how the default agenda is grouped/displayed.
+ (setq org-agenda-custom-commands
+ '(("a" "Agenda"
+ ;; --- Date-based block: only SCHEDULED / DEADLINE / timestamped items ---
+ ((agenda "" ((org-agenda-span 'week)
+ (org-super-agenda-groups
+ '((:name "🔥 PRIORIDAD ALTA"
+ :priority "A")
+ (:name "📅 PARA HOY"
+ :time-grid t
+ :date today)
+ (:name "⚠️ VENCIDO"
+ :deadline past)
+ (:discard (:anything t))))))
+ ;; --- All TODOs, grouped by state/tag, regardless of scheduling ---
+ (alltodo "" ((org-agenda-overriding-header "")
+ (org-super-agenda-groups
+ '((:name "🛠️ EN PROGRESO"
+ :todo ("WAITING" "STRT" "INPROGRESS" "NEXT"))
+ (:name "💼 TRABAJO"
+ :tag "work")
+ (:name "🏠 PERSONAL"
+ :tag "home")
+ (:name "📋 OTROS"
+ :anything t))))))))))
+
+;; Colors for custom TODO keywords.
+(setq org-todo-keyword-faces
+ '(("INPROGRESS" . "orange")
+ ("NEXT" . "cyan")
+ ("WAITING" . "yellow")
+ ("OVERSEE" . "magenta")))
+
+(setq org-agenda-sorting-strategy
+ '((agenda todo-state-up priority-down time-up)
+ (todo todo-state-up priority-down)))
+
+;; --- 3. Org mode settings ---
+(setq org-agenda-files '("~/org/")) ; Folder scanned for agenda entries
+(setq org-agenda-format-date "\n%A, %d de %B") ; More readable agenda date headers
+;; Hide finished/cancelled entries from the agenda.
+(setq org-agenda-skip-function-global
+ '(org-agenda-skip-entry-if 'todo '("DONE" "CANCELLED")))
+
+;; --- 3b. Time tracking (org-clock) ---
+(setq org-clock-into-drawer t) ; keep CLOCK lines in a :LOGBOOK: drawer
+(setq org-clock-persist 'history) ; remember the running clock across restarts
+(org-clock-persistence-insinuate)
+(setq org-clock-out-remove-zero-time-clocks t)
+(setq org-clock-report-include-clocking-task t)
+(setq org-clock-history-length 15) ; keep enough recent tasks for org-mru-clock
+(setq org-duration-format '(h:mm)) ; show 1:30, not 1.5h
+;; Clock report defaults for the weekly review (C-c C-c on a #+BEGIN: clocktable).
+(setq org-clock-clocktable-default-properties
+ '(:scope agenda :block thisweek :maxlevel 3 :compact t))
+;; Terminal-safe clock bindings (C-i is indistinguishable from TAB in a tty).
+(global-set-key (kbd "C-c c o") #'org-clock-out) ; clock OUT
+(global-set-key (kbd "C-c c j") #'org-clock-goto) ; JUMP to the running clock
+(global-set-key (kbd "C-c c q") #'org-clock-cancel) ; cancel/abort the running clock
+(global-set-key (kbd "C-c c r") #'org-clock-report) ; insert/refresh a clock table
+
+;; Pick a project from a completing-read list of recent tasks, from anywhere.
+(use-package org-mru-clock
+ :bind (("C-c c i" . org-mru-clock-in) ; clock IN via recent-task menu
+ ("C-c c s" . org-mru-clock-select-recent-task))
+ :config
+ (setq org-mru-clock-how-many 20
+ org-mru-clock-completing-read #'completing-read))
+
+;; --- 4. Startup screen (dashboard) ---
+(defun my/dashboard-agenda-actionable ()
+ "Dashboard agenda filter: keep only actionable entries.
+This is used as the SKIP predicate of `org-map-entries', so the sense is
+inverted: return the point to EXCLUDE an entry, return nil to INCLUDE it.
+An entry is kept when it is an open TODO or carries a scheduled/deadline
+date; plain container headings -- such as the work.org project list --
+are skipped."
+ (let ((kw (org-get-todo-state)))
+ (unless (or (and kw (not (member kw '("DONE" "CANCELLED"))))
+ (org-get-scheduled-time (point))
+ (org-get-deadline-time (point)))
+ (point))))
+
+(use-package dashboard
+ :ensure t
+ :config
+ (dashboard-setup-startup-hook)
+ ;; Visual configuration.
+ (setq dashboard-banner-logo-title "Bienvenido a tu Segundo Cerebro")
+ (setq dashboard-startup-banner 'official) ; Or a path to a PNG/SVG
+ (setq dashboard-center-content t)
+ (setq dashboard-show-shortcuts t)
+ (setq dashboard-set-heading-icons t)
+ (setq dashboard-set-file-icons t)
+ (setq dashboard-icon-type 'nerd-icons)
+
+ ;; Sections shown on the dashboard.
+ (setq dashboard-items '((recents . 5)
+ (agenda . 10) ; Shows the super-agenda here
+ (projects . 5)))
+
+ ;; Make the dashboard agenda use org-super-agenda's grouping.
+ (setq dashboard-week-agenda t)
+ (setq dashboard-filter-agenda-entry 'my/dashboard-agenda-actionable)
+ (setq dashboard-agenda-sort-strategy '(todo-state-up priority-down time-up)))
+
+(defun my/dashboard-fix-client (frame)
+ "Show the dashboard in a new emacsclient FRAME, then raise and focus it.
+The dashboard refresh is wrapped so that an error there (e.g. agenda
+parsing) can never abort frame creation -- which would otherwise leave
+the launcher with no visible window."
+ (when (and (daemonp) (frame-parameter frame 'client))
+ (with-selected-frame frame
+ ;; Never let a dashboard/agenda error escape this frame hook.
+ (with-demoted-errors "dashboard: %S"
+ (dashboard-refresh-buffer)
+ (switch-to-buffer "*dashboard*")))
+ ;; Always make the new frame visible and focused, even if the above failed.
+ (select-frame-set-input-focus frame)
+ (raise-frame frame)))
+
+;; Runs every time an emacsclient frame is created.
+(add-hook 'after-make-frame-functions #'my/dashboard-fix-client)
+(setq initial-buffer-choice (lambda () (get-buffer-create "*dashboard*")))
+
+;;; init.el ends here