Emacs config

Table of Contents

Vanilla Emacs setup

This is required to enable lexical binding in the resulting .el file, which will be used by emacs at startup.

;; -*- lexical-binding: t -*-
(setq inhibit-startup-message t)
(setq default-directory (expand-file-name "~/"))

;; Default font-family, this will used in all the function that manipulates font settings
(defvar my/font-family "JetBrainsMono Nerd Font")

;; Change backups directory to emacs folder
;; this avoid to save backup files in the same directory of the original files
(setq backup-directory-alist `(("." . ,(concat user-emacs-directory
                                               "backups"))))

;; Revert dired and other buffers when there are changes on disk
(setq global-auto-revert-non-file-buffers t)

;; Hide the bell in the center of screen
(setq ring-bell-function 'ignore)
(column-number-mode t)
(global-hl-line-mode 1)

(when (member my/font-family (font-family-list))
  (set-face-attribute 'default nil :font (format "%s 13" my/font-family)))

(global-set-key (kbd "C-x -") 'my/decrease-font-height)
(global-set-key (kbd "C-x =") 'my/increase-font-height)
(global-set-key (kbd "C-x +") 'my/increase-font-height)

;; Navigate through buffers
(global-set-key (kbd "M-[") 'previous-buffer)
(global-set-key (kbd "M-]") 'next-buffer)

;; Fix unicode errors
(setenv "LANG" "en_US.UTF-8")
(setenv "LC_ALL" "en_US.UTF-8")
(setenv "LC_CTYPE" "en_US.UTF-8")

;; Fix size of scroll
(setq scroll-step 1
      scroll-conservatively  10000)

;; Show a marker when the line has empty characters at the end
(setq-default show-trailing-whitespace t)

;; disable `show-trailing-whitespace' for modes based in comint-mode
;; make prompt readonly
(add-hook 'comint-mode-hook #'(lambda ()
                                (setq-local show-trailing-whitespace nil)
                                (setq-local comint-prompt-read-only t)))

;; Avoid close emacs by mistake
(global-unset-key (kbd "C-x C-c"))

(defalias 'yes-or-no-p 'y-or-n-p)
(defalias 'run-elisp 'ielm)

;; place custom code generated for emacs in a separate file
(defconst custom-file (expand-file-name ".customize.el" user-emacs-directory))
(load custom-file :noerror)

;; disable mouse gestures to avoid scaling text by mistake
(global-set-key (kbd "<pinch>") 'ignore)
(global-set-key (kbd "<C-wheel-up>") 'ignore)
(global-set-key (kbd "<C-wheel-down>") 'ignore)

Show a message with emacs startup time

(defun my/display-startup-time ()
  (message "🚀 Emacs loaded in %s with %d garbage collections."
           (float-time (time-subtract after-init-time before-init-time))
           gcs-done))

(add-hook 'emacs-startup-hook #'my/display-startup-time)

Setup ligatures, by default it has support for JetBrainsMono.

(use-package ligature
  :ensure t
  :config
  ;; Enable ligatures in programming modes
  (ligature-set-ligatures 'prog-mode '("www" "**" "***" "**/" "*>" "*/" "\\\\" "\\\\\\" "{-" "::"
                                       ":::" ":=" "!!" "!=" "!==" "-}" "----" "-->" "->" "->>"
                                       "-<" "-<<" "-~" "#{" "#[" "##" "###" "####" "#(" "#?" "#_"
                                       "#_(" ".-" ".=" ".." "..<" "..." "?=" "??" ";;" "/*" "/**"
                                       "/=" "/==" "/>" "//" "///" "&&" "||" "||=" "|=" "|>" "^=" "$>"
                                       "++" "+++" "+>" "=:=" "==" "===" "==>" "=>" "=>>" "<="
                                       "=<<" "=/=" ">-" ">=" ">=>" ">>" ">>-" ">>=" ">>>" "<*"
                                       "<*>" "<|" "<|>" "<$" "<$>" "<!--" "<-" "<--" "<->" "<+"
                                       "<+>" "<=" "<==" "<=>" "<=<" "<>" "<<" "<<-" "<<=" "<<<"
                                       "<~" "<~~" "</" "</>" "~@" "~-" "~>" "~~" "~~>" "%%"))
  (global-ligature-mode 't))

Adjust font size to screen resolution, increase font size for 4K screens

(when (and (display-graphic-p) (>= (x-display-pixel-width) 3840))
  (set-face-attribute 'default nil :font (format "%s 14" my/font-family)))
(define-minor-mode big-font-mode
  "Switch between a regular font size and a presentation font size."
  :init-value nil
  :global t
  (if big-font-mode
      (progn
        ;; save current font size in a temp variable to be able to restore it
        ;; after this minor mode is disabled
        (setq big-font-mode--tmp (/ (face-attribute 'default :height) 10))
        (set-face-attribute 'default nil :font (format "%s 24" my/font-family)))
    (set-face-attribute 'default nil :font (format "%s %d" my/font-family big-font-mode--tmp))))

Parrot mode FTW!

(defun my/parrot-animate-when-compile-success (buffer result)
  (if (string-match "^finished" result)
      (parrot-start-animation)))

(use-package parrot
  :ensure t
  :config
  (parrot-mode)
  (add-hook 'before-save-hook 'parrot-start-animation)
  (add-to-list 'compilation-finish-functions 'my/parrot-animate-when-compile-success))

Open scratch buffer

(defun my/scratch-buffer()
  "Switch to scratch buffer."
  (interactive)
  (let ((buffer (get-buffer "*scratch*")))
    (if buffer
        (switch-to-buffer buffer)
      (switch-to-buffer (get-buffer-create "*scratch*"))
      (insert (substitute-command-keys initial-scratch-message)))
    (lisp-interaction-mode)))

Ansi term

For some reason ansi-term doesn't respect the global keybinding for M-] so this have to be setup in term-raw-map as well.

(with-eval-after-load 'term
  (define-key term-raw-map (kbd "M-]") 'next-buffer))

xref

Enable evil emacs state when entering a xref buffer

(with-eval-after-load 'xref
  (add-hook 'xref-after-update-hook #'(lambda () (evil-emacs-state))))

Occur

;; occur is part of replace.el file
(with-eval-after-load 'replace
  (define-key occur-mode-map (kbd "C-c C-e") 'occur-edit-mode)
  ;; this will run every time search results are shown
  (setq occur-hook #'(lambda ()
                       (window-buffer)
                       (select-window (get-buffer-window "*Occur*"))
                       (evil-normal-state))))

Popper.el

Display some special buffers in a little window always at bottom

(use-package popper
  :ensure t
  :init
  (setq popper-reference-buffers
        '("\\*eldoc\\*"
          help-mode))
  (popper-mode +1))

Compilation

Disable h key-binding, this has a conflict with evil-mode left navigation key-binding.

(with-eval-after-load 'compile
  ;; set cursor to follow compilation output
  (setq compilation-scroll-output t)
  ;; for elixir testing output test filename use black color which makes it ineligible
  (set-face-foreground 'ansi-color-bold "magenta")
  (define-key compilation-mode-map (kbd "g") nil)
  (define-key compilation-mode-map (kbd "r") 'recompile)
  (define-key compilation-mode-map (kbd "h") nil))

Translate ANSI escape sequences into faces, for example to show colors.

(require 'ansi-color)

(add-hook 'compilation-filter-hook 'ansi-color-compilation-filter)

Custom function to select a base directory before running compilation. M-x compile always use the base directory of the buffer from where it was called as default-directory, this is awful when you want to run a project compilation command from a nested file buffer.

(defun my/compile ()
  "Run compilation process but ask for a `default-directory' before."
  (interactive)
  (let ((default-directory (read-directory-name "Base directory: " (my/project-root)))
        (cmd (read-string "Compile command: ")))
    ;; we need to "export" this variable to be able to re-run `compile' command
    (setq compile-command cmd)
    (compile compile-command)))

Async shell commands

Helper function to run async-shell-command with some tweaks.

(defun my/async-shell-command ()
  "Run `async-shell-command' but at any location.
By default it will use project root but this can be changed"
  (interactive)
  (let* ((default-directory (read-directory-name "Base directory: " (my/project-root)))
         (action-name (read-string "Action name: "))
         (command (read-string "shell command: "))
         (buffer-name (format "(%s)*%s*" (my/project-name) action-name)))
    (async-shell-command command (get-buffer-create buffer-name))))

Narrowing

(defun my/toggle-narrowing ()
  "Toggle narrow on the selected region."
  (interactive)
  (if (buffer-narrowed-p)
      (widen)
    (if (region-active-p)
        (narrow-to-region (region-beginning) (region-end))
      (user-error "No active selection"))))

Ediff

(with-eval-after-load 'ediff
  (setq ediff-split-window-function 'split-window-horizontally)
  ;; put ediff buffer in a buffer at the bottom instead of in a new frame
  (setq ediff-window-setup-function 'ediff-setup-windows-plain))

Theme and styles

Doom theme

(use-package doom-themes
  :ensure t
  :config
  (load-theme 'doom-badger t))

Doom modeline

Enable display-battery-mode after doom-modeline is loaded.

This is required for GitHub notifications segment

(use-package async
  :ensure t)
(use-package doom-modeline
  :ensure t
  :defer t
  :custom
  (doom-modeline-modal-icon nil)
  (doom-modeline-buffer-file-name-style 'relative-from-project)
  (doom-modeline-github t)
  (doom-modeline-github-interval (* 30 60))
  :hook
  (after-init . doom-modeline-mode)
  (doom-modeline-mode . display-battery-mode))

Emoji support

(use-package unicode-fonts
  :ensure t
  :config
  (unicode-fonts-setup))

Dired

(with-eval-after-load "dired"
  (add-hook 'dired-mode-hook 'evil-emacs-state)
  (define-key dired-mode-map (kbd "C-c C-e") 'wdired-change-to-wdired-mode))

Dired preview

(use-package dired-preview
  :ensure t
  :after dired
  :config
  (define-key dired-mode-map (kbd "P") #'(lambda ()
                                           (interactive)
                                           (dired-preview-display-file (dired-file-name-at-point)))))

Nerd icons dired

(use-package nerd-icons-dired
  :ensure t
  :defer t
  :hook
  (dired-mode . nerd-icons-dired-mode))

Dired subtree

(use-package dired-subtree
  :ensure t
  :after dired
  :config
  (define-key dired-mode-map (kbd "<tab>") 'dired-subtree-toggle))

Editor enhancements

Whitespace

Show special markers for tab and endline characters in prog-mode

(use-package whitespace-mode
  :custom
  (whitespace-style '(tab-mark newline-mark))
  (whitespace-display-mappings '((newline-mark ?\n    [?¬ ?\n]  [?$ ?\n])
                                 (tab-mark     ?\t    [?» ?\t] [?\\ ?\t])))
  :hook
  (prog-mode . whitespace-mode))

Deactivate extended region in visual mode

Allow to visual mode work more like vim visual highlighting.

(set-face-attribute 'region nil :extend nil)

Dark and transparent title bar in macOS

(when (memq window-system '(mac ns))
  (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
  (add-to-list 'default-frame-alist '(ns-appearance . dark)))

Share clipoard with OS

(use-package pbcopy
  :ensure t)

Highlight TODO, FIXME, etc

(defun my/highlight-todo-like-words ()
  (font-lock-add-keywords
   nil `(("\\<\\(FIXME\\|TODO\\)"
          1 font-lock-warning-face t))))

(add-hook 'prog-mode-hook 'my/highlight-todo-like-words)

Auto fill mode

Use auto-fill-mode only for comments and only with programming buffers

(setq comment-auto-fill-only-comments t)

(add-hook 'prog-mode-hook #'(lambda ()
                              (auto-fill-mode 1)))

Load PATH environment

exec-path-from-shell by default uses ("-l" "-i") when starts a new shell to get the PATH, -i option was removed to open a non interactive shell so it can be faster at startup.

(use-package exec-path-from-shell
  :ensure t
  :custom
  (exec-path-from-shell-arguments '("-l"))
  (exec-path-from-shell-check-startup-files nil)
  :config
  (when (memq window-system '(mac ns))
    (exec-path-from-shell-initialize)))

Editorconfig

(if (>= emacs-major-version 30)
    (use-package editorconfig
      :straight (:type built-in)
      :config
      (editorconfig-mode 1))
  (use-package editorconfig
    :ensure t
    :config
    (editorconfig-mode 1)))

Snippets

(use-package yasnippet
  :ensure t
  :hook ((prog-mode . yas-minor-mode)
         (conf-mode . yas-minor-mode)
         (text-mode . yas-minor-mode)
         (snippet-mode . yas-minor-mode)))

(use-package yasnippet-snippets
  :ensure t
  :after (yasnippet))

Wakatime

(use-package wakatime-mode
  :ensure t
  :if (executable-find "wakatime-cli")
  :init
  (setq wakatime-cli-path (executable-find "wakatime-cli"))
  :config
  (global-wakatime-mode))

Highlight thing

(use-package highlight-thing
  :ensure t
  :hook
  (prog-mode . highlight-thing-mode))

Various changes

Disable lock files

(setq create-lockfiles nil)

Reformatter

(use-package reformatter
  :ensure t)

Hydra for major modes

(use-package major-mode-hydra
  :ensure t
  :config
  (with-eval-after-load 'evil
    (evil-define-key nil 'global (kbd "<leader>\\") #'(lambda ()
                                                        (interactive)
                                                        (major-mode-hydra)))))

(use-package hydra-posframe
  :straight (hydra-posframe
             :type git
             :host github
             :repo "Ladicle/hydra-posframe")
  :hook (after-init . hydra-posframe-mode))

Vterm

Function to search into zsh history

(defun my/select-from-zsh-history ()
  "Selectt one option from ~/.zsh_history file."
  (with-temp-buffer
    (insert-file-contents (expand-file-name "~/.zsh_history"))
    (let* ((raw-content (buffer-substring-no-properties (point-min) (point-max)))
           (lines (string-split raw-content "\n"))
           (choices (mapcar (lambda (line) (second (string-split line ";"))) lines)))
      (completing-read "Select command: " choices))))

(defun my/insert-from-zsh-history ()
  "Search into zsh history and insert the selected choice into buffer."
  (interactive)
  (when-let ((selected-choice (my/select-from-zsh-history)))
    (vterm-insert selected-choice)))
(use-package vterm
  :ensure t
  :defer t
  :hook
  (vterm-mode . (lambda ()
                  (setq-local show-trailing-whitespace nil)))
  :mode-hydra
  (vterm-mode
   (:title (concat (nerd-icons-icon-for-buffer) " Vterm commands"))
   ("History"
    (("i" my/insert-from-zsh-history "Insert command from history"))))
  :custom
  (vterm-module-cmake-args "-DUSE_SYSTEM_LIBVTERM=yes")
  (vterm-always-compile-module t))

Toggle terminal

'project use always the same terminal per project, this way we avoid to create a new terminal for each call to vterm-toggle. 'reset-window-configration yes, it's suppose to be configration, for some reason it was defined like this instead of configuration

Also for easy access insert mode is activated right away after vterm is shown

(use-package vterm-toggle
  :ensure t
  :custom
  (vterm-toggle-scope 'project)
  (vterm-toggle-hide-method 'reset-window-configration)
  :hook
  (vterm-toggle-show . evil-insert-state))

Jinx

This require to have enchant library to be able to compile and load a module

(use-package jinx
  :ensure t
  :hook (emacs-startup . global-jinx-mode)
  :config
  (define-key jinx-mode-map (kbd "M-\\") 'jinx-correct))

iSpell

Avoid check spelling in markdown code blocks

(with-eval-after-load 'ispell
  (setq ispell-program-name "aspell")
  (add-to-list 'ispell-skip-region-alist
               '("^```" . "^```")))

When editing a commit message ispell should ignore lines that start with #, these lines are diff details about the commit.

(defun my/setup-ispell-for-commit-message ()
  "Setup `ispell-skip-region-alist' to avoid lines starting with #.
  This way diff code will be ignored when ispell run."
  (setq-local ispell-skip-region-alist (cons '("^#" . "$") ispell-skip-region-alist)))

Tree sitter

Incremental code parsing for better syntax highlighting

(use-package treesit-auto
  :ensure t
  :custom (treesit-auto-install t)
  :config
  (global-treesit-auto-mode))

Run ispell in text nodes

(use-package treesit-ispell
  :ensure t
  :defer t
  :bind (("C-x C-s" . treesit-ispell-run-at-point)))

Set maximum value for font-locking in treesit native syntax highlighting

(with-eval-after-load 'treesit
  (setq treesit-font-lock-level 4))

Install combobulate just to have combobulate-query-builder is like tree-sitter-query-builder but using native treesit package.

(use-package combobulate
  :straight (combobulate
             :type git
             :host github
             :repo "mickeynp/combobulate")
  :commands (combobulate-query-builder))

Evil

(defun my/find-file-under-cursor ()
  "Check it the filepath under cursor is an absolute path otherwise open `project-find-file' and insert the filepath."
  (interactive)
  (let ((file-path (thing-at-point 'filename t)))
    (if (file-name-absolute-p file-path)
        (find-file-at-point file-path)
      (minibuffer-with-setup-hook #'(lambda () (insert file-path))
        (project-find-file)))))
(use-package evil
  :ensure t
  :init
  (setq evil-emacs-state-cursor '("white" box)
        evil-normal-state-cursor '("green" box)
        evil-visual-state-cursor '("orange" box)
        evil-insert-state-cursor '("red" bar)
        evil-want-keybinding nil
        ;; use emacs-28 undo system
        evil-undo-system 'undo-redo)
  :config
  (evil-mode 1)
  (modify-syntax-entry ?_ "w")
  (define-key evil-normal-state-map (kbd "C-p") 'diff-hl-previous-hunk)
  (define-key evil-normal-state-map (kbd "C-n") 'diff-hl-next-hunk)
  (define-key evil-normal-state-map "gf" 'my/find-file-under-cursor)
  (define-key evil-motion-state-map "gd" 'my/goto-definition-dumb-jump-fallback)
  (add-hook 'prog-mode-hook #'(lambda ()
                                (modify-syntax-entry ?_ "w")))

  ;; Setup leader key only for `normal', `visual' and `motion' modes
  (evil-set-leader '(normal visual motion) (kbd "\\"))

  (evil-define-key nil 'global (kbd "<leader>SPC") #'(lambda ()
                                                       (interactive)
                                                       (call-interactively #'execute-extended-command)))
  (evil-define-key nil 'global (kbd "<leader>a") #'(lambda ()
                                                     (interactive)
                                                     (if (region-active-p)
                                                         (my/grep-in-project (buffer-substring-no-properties (region-beginning) (region-end)))
                                                       (my/grep-in-project (thing-at-point 'symbol)))))
  (evil-define-key nil 'global (kbd "<leader>A") 'my/grep-in-project)
  (evil-define-key nil 'global (kbd "<leader>ba") 'my/add-bookmark)
  (evil-define-key nil 'global (kbd "<leader>bb") 'my/bookmark-switch)
  (evil-define-key nil 'global (kbd "<leader>B") #'(lambda ()
                                                     (interactive)
                                                     (call-interactively #'switch-to-buffer)))
  (evil-define-key nil 'global (kbd "<leader>c") 'vterm-toggle)
  (evil-define-key nil 'global (kbd "<leader>e") 'my/find-file-in-project)
  (evil-define-key nil 'global (kbd "<leader>f") 'find-file)
  (evil-define-key nil 'global (kbd "<leader>g") 'magit-status)
  (evil-define-key nil 'global (kbd "<leader>G") 'magit-file-dispatch)
  (evil-define-key nil 'global (kbd "<leader>i") 'consult-imenu)
  (evil-define-key nil 'global (kbd "<leader>hs") 'diff-hl-stage-current-hunk)
  (evil-define-key nil 'global (kbd "<leader>hk") 'diff-hl-revert-hunk)
  (evil-define-key nil 'global (kbd "<leader>k") 'kill-buffer)
  (evil-define-key nil 'global (kbd "<leader>l") 'display-line-numbers-mode)
  (evil-define-key nil 'global (kbd "<leader>n") 'evil-buffer-new)
  (evil-define-key nil 'global (kbd "<leader>N") 'my/toggle-narrowing)
  (evil-define-key nil 'global (kbd "<leader>pa") 'my/copy-abs-path)
  (evil-define-key nil 'global (kbd "<leader>pr") 'my/copy-relative-path)
  (evil-define-key nil 'global (kbd "<leader>q") 'consult-line)
  (evil-define-key nil 'global (kbd "<leader>r") 'my/replace-at-point-or-region)
  (evil-define-key nil 'global (kbd "<leader>R") '(lambda ()
                                                    (interactive)
                                                    (save-excursion)
                                                    (with-current-buffer "*compilation*"
                                                      (recompile))))
  (evil-define-key nil 'global (kbd "<leader>s") 'my/toggle-spanish-characters)
  (evil-define-key nil 'global (kbd "<leader>t") 'persp-switch)
  (evil-define-key nil 'global (kbd "<leader>w") 'my/toggle-maximize)
  (evil-define-key nil 'global (kbd "<leader>x") 'my/resize-window)
  (evil-define-key nil 'global (kbd "<leader>y") 'consult-yank-from-kill-ring)

  (face-spec-set
   'evil-ex-substitute-matches
   '((t :foreground "red"
        :strike-through t
        :weight bold)))

  (face-spec-set
   'evil-ex-substitute-replacement
   '((t
      :foreground "green"
      :weight bold))))

(use-package evil-commentary
  :ensure t
  :after (evil)
  :config
  (evil-commentary-mode))

(use-package evil-surround
  :ensure t
  :after (evil)
  :config
  (global-evil-surround-mode 1))

(defun my/replace-at-point-or-region ()
  "Setup buffer replace string for word at point or active region using evil ex mode."
  (interactive)
  (let ((text (if (region-active-p)
                  (buffer-substring-no-properties (region-beginning) (region-end))
                (word-at-point))))
    (evil-ex (concat "%s/" text "/"))))

(use-package evil-matchit
  :ensure t
  :config (global-evil-matchit-mode 1))

Evil collection

Allow to use default terminal keybinding in vterm without losing evil features

(use-package evil-collection
  :ensure t
  :config
  (evil-collection-init '(vterm)))

IA models integration

Integration with different "backends", ollama, openai, and so on.

(use-package llm
  :ensure t)

UI to interact with models, relies on llm

(use-package ellama
  :ensure t
  :custom
  (ellama-language "English")
  :config
  (require 'llm-ollama)
  (with-eval-after-load 'llm-ollama)
  (setopt ellama-provider (make-llm-ollama
                           :host "localhost"
                           :chat-model "zephyr")))

Custom functions to better management of models

(defun my/switch-ollama-provider ()
  "Switch ollama provider by using the installed local models."
  (interactive)
  (let* ((raw-result (shell-command-to-string "ollama list | awk '{print $1}' | tail -n+2"))
         (choices (string-split (string-trim raw-result) "\n"))
         (choices (mapcar (lambda (choice) (car (string-split choice ":"))) choices))
         (model (completing-read "Choose model" choices)))
    (setopt ellama-provider (make-llm-ollama :host "localhost" :chat-model model))
    (message "Model %s configured as ollama provider." (propertize model 'face '(:foreground "magenta")))))

Utils

Which-key

(if (>= emacs-major-version 30)
    (use-package which-key
      :straight (:type built-in)
      :config
      (which-key-mode)
      (which-key-setup-minibuffer))
  (use-package which-key
    :ensure t
    :config
    (which-key-mode)
    (which-key-setup-minibuffer)))

Auto pair

Complete parenthesis, square brackets, etc

Enable it globally and disable it just for org-mode to avoid having a conflict with <s

(electric-pair-mode)
(add-hook 'org-mode-hook #'(lambda ()
                             (electric-pair-local-mode -1)))

Restclient

(use-package restclient
  :ensure t
  :defer t
  :mode (("\\.http\\'" . restclient-mode))
  :mode-hydra
  (restclient-mode
   (:title (concat (nerd-icons-icon-for-buffer) " restclient commands"))
   ("Format"
    ;; TODO: change to only apply json formatting when the content-type is application/json
    (("f" jsonian-format-region))
    "Secrets"
    (("h" cloak-mode "Toggle cloak" :toggle t))
    "Request"
    (("r" restclient-http-send-current-stay-in-window "Execute request")
     ("c" restclient-copy-curl-command "Copy CURL command")))))

(use-package company-restclient
  :ensure t
  :after (restclient)
  :config
  (add-to-list 'company-backends 'company-restclient))

Rainbow delimiters

(use-package rainbow-delimiters
  :ensure t
  :hook
  (prog-mode . rainbow-delimiters-mode))

XML formatter

(with-eval-after-load 'reformatter
  (reformatter-define xml-format
    :program "xmlformat"
    :group 'xml))

(with-eval-after-load 'nxml-mode
  (define-key nxml-mode-map (kbd "C-c C-f") 'xml-format-buffer))

SQL formatter

Install pgformatter using homebrew brew install pgformatter

(with-eval-after-load 'reformatter
  (reformatter-define sql-format
    :program "pg_format"))

(defun my/format-sql ()
  "Format active region otherwise format the entire buffer."
  (interactive)
  (if (region-active-p)
      (sql-format-region (region-beginning) (region-end))
    (sql-format-buffer)))

(with-eval-after-load 'sql
  (add-hook 'sql-mode-hook 'flymake-sqlfluff-load)
  (add-hook 'sql-mode-hook 'flymake-mode)
  (define-key sql-mode-map (kbd "C-c C-f") 'my/format-sql))

SQL linter using sqlfluff

(use-package flymake-sqlfluff
  :ensure t)

Common packages

Used in every major mode

Company

(use-package company
  :ensure t
  :init
  (setq company-idle-delay 0.1
        company-tooltip-limit 10
        company-minimum-prefix-length 3)
  :hook (after-init . global-company-mode)
  :config
  (define-key company-active-map (kbd "C-n") 'company-select-next)
  (define-key company-active-map (kbd "C-p") 'company-select-previous))

Flymake

Only activate flymake for actual projects and for prog-mode

(defun my/setup-flymake ()
  "Activate flymake only if we are inside a project."
  (when (functionp 'my/project-p)
    (flymake-mode 1)))

(add-hook 'prog-mode-hook 'my/setup-flymake)

(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))

Direnv

Handle environment variables per buffer using a .envrc file.

(use-package envrc
  :ensure t
  :config
  (envrc-global-mode))

Hydra commands, this have to be defined this way instead of using :mode-hydra inside use-package because for some reason it mess up with environment variables loading

(with-eval-after-load 'major-mode-hydra
  (major-mode-hydra-define envrc-file-mode
    (:title (concat (nerd-icons-icon-for-buffer) " envrc commands"))
    ("Secrets"
     (("h" cloak-mode "Toggle cloak" :toggle t)))))

Cloak mode

Hide values that match regex patterns in .envrc and restclient files

(use-package cloak-mode
 :ensure t
 :custom
 (cloak-mode-patterns '((envrc-file-mode . "[a-zA-Z0-9_]+[ \t]*=[ \t]*\\(.*+\\)$")
                        (restclient-mode . "^:[^: ]+[ \t]*=[ \t]*\\(.+?\\)$")))
 (cloak-mode-mask "🙈🙈🙈")
 :config
 (global-cloak-mode))

Avy

(use-package avy
  :ensure t
  :config
  (with-eval-after-load 'evil
    (define-key evil-normal-state-map (kbd "SPC SPC") 'avy-goto-char-2)))

perspective.el

(use-package perspective
  :ensure t
  :custom
  ;; disable warnings
  (persp-suppress-no-prefix-key-warning t)
  :config
  (persp-mode)
  ;; setup vim tab like key-bindings
  (define-key evil-normal-state-map (kbd "gt") 'persp-next)
  (define-key evil-normal-state-map (kbd "gT") 'persp-prev))

Project.el

project.el default prefix is C-x

(defun my/project-edit-dir-locals ()
  "Edit .dir-locals.el file in project root."
  (interactive)
  (find-file (expand-file-name ".dir-locals.el" (my/project-root))))

(defun my/project-edit-direnv ()
  "Edit .envrc file in project root."
  (interactive)
  (find-file (expand-file-name ".envrc" (my/project-root))))

(use-package project
  :straight (:type built-in)
  :bind (:map project-prefix-map
              ("D" . 'my/project-edit-direnv)
              ("d" . 'project-dired)
              ("e" . 'my/project-edit-dir-locals)
              ("k" . 'my/project-kill-buffers)
              ("n" . 'my/project-open-new-project)
              ("p" . 'my/project-switch)))

Define helper functions to be used by other packages

(defun my/project-root ()
  "Return project root path."
  (project-root (project-current)))

(defun my/project-p ()
  (project-current))

(defun my/project-name ()
  "Get project name extracting latest part of project path."
  (if (my/project-p)
      (second (reverse (split-string (my/project-root) "/")))
    nil))

perspective.el integration, a new perspective should be "attached" to a project so it's easy to switch between them.

(defun my/project-switch ()
  "Switch to a project and trigger switch action."
  (interactive)
  ;; make sure all the projects list is available to be used
  (project--ensure-read-project-list)
  (let* ((projects (mapcar 'car project--list))
         (choice (completing-read "Switch to project: " projects))
         (default-directory choice))
    ;; `default-directory' must be defined so `project.el' can know is in a new project
    (my/project-switch-action)))

(defun my/project-switch-action ()
  "Switch to a new perspective which name is project's name and open `project-find-file'."
  (interactive)
  (persp-switch (my/project-name))
  (project-find-file))

(defun my/project-kill-buffers ()
  "Kill all the related buffers to the current project and delete its perspective as well."
  (interactive)
  (let* ((project-name (my/project-name))
         (project (project-current))
         (buffers-to-kill (project--buffers-to-kill project)))
    (when (yes-or-no-p (format "Kill %d buffers in %s?" (length buffers-to-kill) (my/project-root)))
      ;; in case we're using eglot we shutdown its server
      (if (and (featurep 'eglot) (eglot-managed-p))
          (eglot-shutdown (eglot-current-server)))
      (mapc #'kill-buffer buffers-to-kill)
      (persp-kill project-name))))

(defun my/project-open-new-project ()
  "Open a project for the first time and add it to `project.el' projects list."
  (interactive)
  (let* ((project-path-abs (read-directory-name "Enter project root: "))
         ;; we need to define `default-directory' to be able to get the new project when `project-current' is called
         (default-directory (replace-regexp-in-string (expand-file-name "~") "~" project-path-abs)))
    (project-remember-project (project-current))
    (my/project-switch-action)))

Completion

UI for completion

(use-package vertico
  :ensure t
  :hook
  (after-init . vertico-mode)
  :custom
  ;; fixed height
  (vertico-resize nil)
  ;; show max 15 elements
  (vertico-count 15)
  :config
  ;; `C-;' will open embark and `o' with execute `find-file-other-window'
  (define-key vertico-map (kbd "C-<return>") (kbd "C-; o")))

Load vertico-multiform which is required for vertico-posframe

(use-package vertico-multiform
  :after vertico
  :straight nil
  :load-path "straight/repos/vertico/extensions/")

Vertico posframe, show all the candidates in a child-frame, it will activated only for GUI version.

(use-package vertico-posframe
  :ensure t
  :if (display-graphic-p)
  :init
  (vertico-posframe-mode 1))

Add annotations to results shown by vertico

(use-package marginalia
  :ensure
  :init
  (marginalia-mode))

Icons support

(use-package nerd-icons-completion
  :ensure t
  :after marginalia
  :config
  (nerd-icons-completion-mode)
  (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup))

Enable better completion styles

(use-package orderless
  :ensure t
  :config
  (setq completion-styles '(orderless basic)
        completion-category-overrides '((file (styles basic partial-completion)))))

Disable orderless completion style in company to keep previous behaviour which I like, this was copied from orderless documentation.

;; We follow a suggestion by company maintainer u/hvis:
;; https://www.reddit.com/r/emacs/comments/nichkl/comment/gz1jr3s/
(defun company-completion-styles (capf-fn &rest args)
  (let ((completion-styles '(basic partial-completion)))
    (apply capf-fn args)))

(advice-add 'company-capf :around #'company-completion-styles)

Search into project source

(use-package consult
  :ensure t
  :config
  ;; Use consult for completion inside minibuffer, for example when
  ;; searching for a file.
  (setq completion-in-region-function #'consult-completion-in-region))

Integration with yasnippets

(use-package consult-yasnippet
  :ensure t
  :defer t)

Helpers to search term at point and general search into project

(defun my/grep-in-project (&optional term)
  "Run grep in current project.
If TERM is not nil it will be used as initial value."
  (interactive)
  (let* ((pattern (read-string "Pattern: " (or term "")))
         ;; add an extra space to be able to start typing more filters
         (pattern (concat pattern " ")))
    (call-interactively #'(lambda ()
                            (interactive)
                            (consult-ripgrep (my/project-root) pattern)))))

Switch fonts like consult-theme

(defun my/consult-font (font)
  "Replace current font with FONT from `font-family-list'."
  (interactive
   (list
    (let ((saved-font (symbol-name (font-get (face-attribute 'default :font) :family))))
      (consult--read
       (font-family-list)
       :prompt "Font: "
       :require-match t
       :state (lambda (action font)
                (pcase action
                  ('return (my/consult-font (or font saved-font)))
                  ((and 'preview (guard font)) (my/consult-font font))))
       ))))
  (when font
    ;; size doesn't change during scrolling so we can reuse that to
    ;; configure new selected font
    (set-face-attribute 'default nil :font (format "%s %d" font (font-get (face-attribute 'default :font) :size)))))

Integration with embark

(use-package embark
  :ensure t
  :bind
  ("C-;" . embark-act)
  :config
  ;; grep exported data can have a lot of white spaces so we don't want
  ;; them to be shown while editing their content
  (setq-mode-local embark-collect-mode show-trailing-whitespace nil))

(use-package embark-consult
  :ensure t
  :defer t)

(defun my/edit-completing-results ()
  "Use results origin to execute an action after export them with `embark-export'."
  (interactive)
  ;; call of `project-find-file'
  (when (cl-search "Find file in" (buffer-string))
    (run-at-time 0 nil #'embark-export)
    (run-at-time 0 nil #'wdired-change-to-wdired-mode)
    (run-at-time 0 nil #'evil-normal-state))
  ;; call of `consult-ripgrep'
  (when (cl-search "Ripgrep" (buffer-string))
    ;; we use `run-at-time' to ensure all of these steps
    ;; will be executed in order
    (run-at-time 0 nil #'embark-export)
    (run-at-time 0 nil #'wgrep-change-to-wgrep-mode)
    (run-at-time 0 nil #'evil-normal-state)))

(define-key minibuffer-mode-map (kbd "C-c C-e") #'my/edit-completing-results)

Edit grep buffer

(use-package wgrep
  :ensure t
  :custom
  (wgrep-auto-save-buffer t)
  :bind
  ("C-c C-c" . 'wgrep-finish-edit)
  ("C-c C-k" . 'wgrep-abort-changes))

Emacs process list

(defun my/kill-emacs-process ()
  "Show a list of current Emacs processes and kill the selected one."
  (interactive)
  (let* ((names (mapcar #'process-name (process-list)))
         (process-name (completing-read "Choose process: " names)))
    (delete-process (get-process process-name))
    (message "%s process killed" (propertize process-name 'face '(:foreground "magenta")))))

(global-set-key (kbd "C-x c p") 'my/kill-emacs-process)

Git backup

Save a backup on every save, also allow to recover any version of a file

(defvar my/backup-dir (expand-file-name "~/.git-backup"))

(defun my/git-backup-versioning ()
  "Save a version of the current file."
  (unless (featurep 'git-backup)
    (require 'git-backup))
  (git-backup-version-file (executable-find "git") my/backup-dir '() (buffer-file-name)))

(defun my/git-backup-run-action (command commit-hash)
  "Execute COMMAND with COMMIT-HASH using another defaults arguments."
  (apply command `(,(executable-find "git") ,my/backup-dir ,commit-hash ,(buffer-file-name))))

(defun my/git-backup-sort (completions)
  "Given COMPLETIONS define a custom sort function."
  (lambda (string pred action)
    (if (eq action 'metadata)
        '(metadata (display-sort-function . identity))
      (complete-with-action action completions string pred))))

(defun my/git-backup ()
  "Navigate in versions of the current file."
  (interactive)
  (unless (featurep 'git-backup)
    (require 'git-backup))
  ;; for some reason an extra space after `%h|' is required to avoid an error when
  ;; the shell command is executed
  (let* ((candidates (git-backup-list-file-change-time (executable-find "git") my/backup-dir "%cI|%h| %ar" (buffer-file-name)))
         (selection (completing-read "Pick revision: " (my/git-backup-sort candidates)))
         (commit-hash (nth 1 (string-split selection "|")))
         (action (completing-read "Choose action: " '("diff" "new buffer" "replace current buffer"))))
    (cond ((string-equal action "diff") (my/git-backup-run-action 'git-backup-create-ediff commit-hash))
          ((string-equal action "new buffer") (my/git-backup-run-action 'git-backup-open-in-new-buffer commit-hash))
          ((string-equal action "replace current buffer") (my/git-backup-run-action 'git-backup-replace-current-buffer commit-hash))
          (t (message "Not valid option")))))

(use-package git-backup
  :ensure t
  :hook (after-save . my/git-backup-versioning))

Meme

This package requires to have svg support in emacs, this feature relies on librsvg at compilation time

(defun my/meme-from-clipboard ()
  "Create a meme using an image from clipboard"
  (interactive)
  (unless (executable-find "pngpaste")
    (user-error "please install pngpaste"))

  (let* ((filepath (make-temp-file "clipboard" nil ".png"))
         (command (format "pngpaste %s" filepath))
         (command-stdout (shell-command-to-string command)))
    ;; pngpaste returns "" when found a valid image in the clipboard
    (unless (string-equal command-stdout "")
      (user-error (string-trim command-stdout)))

    (switch-to-buffer (get-buffer-create "*meme*"))
    (meme-mode)
    (meme--setup-image filepath)))
(use-package imgur
  :ensure t
  :defer t
  :straight (imgur
             :type git
             :host github
             :repo "myuhe/imgur.el"))

(use-package meme
  :ensure t
  :defer t
  :commands (meme-mode meme)
  :straight (meme
             :type git
             :host github
             :repo "larsmagne/meme")
  :config
  ;; fix to be able to read images, straight.el put files in a different directory so we have to
  ;; move them to the right one
  (let ((images-dest-dir (concat user-emacs-directory "straight/build/meme/images"))
        (images-source-dir (concat user-emacs-directory "straight/repos/meme/images")))
    (unless (file-directory-p images-dest-dir)
      (shell-command (format "cp -r %s %s" images-source-dir images-dest-dir)))))

Orgmode

Configured variables:

  • org-latex-caption-above puts table captions at the bottom
  • org-clock-persist persists time even if emacs is closed
  • org-src-fontify-natively enables syntax highlighting for code blocks
  • org-log-done saves the timestamp when a task is done
  • org-src-preserve-indentation when is t avoid to insert a left indentation in source blocks
(defun my/org-insert-image-from-clipboard ()
  "Insert image from clipboard using an org tag"
  (interactive)
  (let* ((image-name (read-string "Filename: " "image.png"))
         (images-folder "./images")
         (image-path (format "%s/%s" images-folder image-name)))
    (unless (file-directory-p images-folder)
      (shell-command (format "mkdir -p %s" images-folder)))
    (shell-command (format "pngpaste %s" image-path))
    (insert (format "[[file:%s]]" image-path))))

(with-eval-after-load 'evil
  (evil-define-key nil org-mode-map (kbd "<leader>mii") 'my/org-insert-image-from-clipboard))

When I read books on Apple Books and I want to insert some quote Apple Books insert some text I don't want in my notes, this function delete that and just insert the meaning part using org quote syntax.

(defun my/org-insert-quote-from-apple-books ()
  "Take quote from clipboard and remove all the unnecesary text and insert
    an org quote in the current position"
  (interactive)
  (let* ((raw-value (current-kill 0 t))
         (tmp (second (split-string raw-value "“")))
         (quote-value (car (split-string tmp "”"))))
    (insert "#+begin_quote\n")
    (insert (concat quote-value "\n"))
    (insert "#+end_quote\n")))
(defvar my/org-src-block-tmp-window-configuration nil)

(defun my/org-edit-special (&optional arg)
  "Save current window configuration before a org-edit buffer is open."
  (setq my/org-src-block-tmp-window-configuration (current-window-configuration)))

(defun my/org-edit-src-exit ()
  "Restore the window configuration that was saved before org-edit-special was called."
  (set-window-configuration my/org-src-block-tmp-window-configuration))

(with-eval-after-load 'org
  (setq org-latex-caption-above nil
        org-clock-persist 'history
        org-src-fontify-natively t
        org-src-preserve-indentation t
        ;; use tectonic for export to PDF
        org-latex-pdf-process '("tectonic -X compile --outdir=%o -Z shell-escape %f")
        org-log-done t)
  (org-clock-persistence-insinuate)

  (add-hook 'org-mode-hook #'(lambda ()
                               ;; Since emacs 27 this is needed to use shortcuts like <s to create source blocks
                               (unless (featurep 'org-tempo)
                                 (require 'org-tempo))
                               (org-indent-mode t)))

  (advice-add 'org-edit-special :before 'my/org-edit-special)
  (advice-add 'org-edit-src-exit :after 'my/org-edit-src-exit)

  (org-babel-do-load-languages 'org-babel-load-languages
                               '((python . t)
                                 (shell . t)
                                 (lisp . t)
                                 (sql . t)
                                 (dot . t)
                                 (plantuml . t)
                                 (emacs-lisp . t))))

(use-package htmlize
  :ensure t
  :after (org))

Fix error with TAB in evil-mode in org-mode with org elements.

(defun my/org-tab ()
  "Run `org-cycle' only at point of an org element."
  (interactive)
  (if (org-element-at-point)
      (org-cycle)
    (evil-jump-forward)))

(with-eval-after-load 'org
  (define-key org-mode-map (kbd "<tab>") 'my/org-tab))

Org modern

(use-package org-modern
  :ensure t
  :custom
  (org-modern-fold-stars '(("▶" . "▼") ("▷" . "▽") ("▹" . "▿") ("▸" . "▾")))
  :config
  (with-eval-after-load 'org (global-org-modern-mode)))

Org tree slide

A tool to show org file as an slideshow

hide-mode-line hide the modeline to allow to have a clean screen while using org-tree-slide-mode

(use-package hide-mode-line
  :ensure t)

Some tweaks to have a better looking while presenting slides

(defun my/org-tree-slide-setup ()
  (org-display-inline-images)
  (hide-mode-line-mode 1))

(defun my/org-tree-slide-end ()
  (org-display-inline-images)
  (hide-mode-line-mode 0))

(use-package org-tree-slide
  :ensure t
  :defer t
  :custom
  (org-image-actual-width nil)
  (org-tree-slide-activate-message "Presentation started!")
  (org-tree-slide-deactivate-message "Presentation finished!")
  :hook ((org-tree-slide-play . my/org-tree-slide-setup)
         (org-tree-slide-stop . my/org-tree-slide-end))
  :bind (:map org-tree-slide-mode-map
              ("C-<" . org-tree-slide-move-previous-tree)
              ("C->" . org-tree-slide-move-next-tree)))

Denote

Note taking using denote

(use-package denote
  :ensure t
  :custom (denote-directory "~/Documents/wiki")
  :hook ((dired-mode . denote-dired-mode)))

(defun my/wiki ()
  "Open personal wiki and launch Dired."
  (interactive)
  (dired (expand-file-name "~/Documents/wiki"))
  (dired-hide-details-mode t))

Latex

(use-package auctex
  :ensure t
  :defer t)

(use-package latex-preview-pane
  :ensure t
  :defer t)

Git

Git-link

Open selected region in remote repo page

(use-package git-link
  :ensure t
  :defer t)

Git modes

This pacakge includes gitignore-mode, gitconfig-mode and gitattributes-mode

(use-package git-modes
  :defer t
  :ensure t)

Magit

(defun my/magit-blame-quit ()
  "Restore evil state after magit blame mode is closed."
  (evil-exit-emacs-state))

(use-package magit
  :ensure t
  :custom
  ;; restore previous window configuration after a buffer is closed
  (magit-bury-buffer-function 'magit-restore-window-configuration)
  ;; open magit status buffer in the whole frame
  (magit-display-buffer-function 'magit-display-buffer-fullframe-status-v1)
  :defer t
  :config
  (advice-add 'magit-blame-quit :after 'my/magit-blame-quit)
  (add-hook 'git-commit-mode-hook 'my/setup-ispell-for-commit-message)
  (add-hook 'magit-blame-mode-hook #'(lambda () (evil-emacs-state))))

Magit delta

Use delta tool to show diffs in magit

(use-package magit-delta
  :ensure t
  :if (executable-find "delta")
  :hook (magit-mode . magit-delta-mode))

Forge

(use-package forge
  :ensure t
  :after (magit closql)
  :config
  (add-hook 'forge-topic-mode-hook #'(lambda () (evil-emacs-state))))

Git diff-hl

(use-package diff-hl
  :ensure t
  :custom
  (diff-hl-show-staged-changes nil)
  ;; for some reason the :hook form doesn't work so we have to use :init
  :init
  (add-hook 'magit-pre-refresh-hook 'diff-hl-magit-pre-refresh)
  (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh)
  (add-hook 'dired-mode-hook 'diff-hl-dired-mode)
  :config
  (global-diff-hl-mode))

Timemachine

(use-package git-timemachine
  :ensure t
  :config
  (add-hook 'git-timemachine-mode-hook #'(lambda () (evil-emacs-state))))

Gist

(defun my/gist ()
  "Create a public gist."
  (interactive)
  (my/create-gist t))

(defun my/private-gist ()
  "Create a private gist."
  (interactive)
  (my/create-gist :false))

(defun my/create-gist (public)
  "Create a gist, given PUBLIC value it will set as private or public."
  (let* ((oauth-token (my/gist-get-oauth-token))
         (content (my/gist-get-content))
         (payload (make-hash-table))
         (filename (or (and (buffer-file-name) (file-name-nondirectory (buffer-file-name))) (buffer-name)))
         (files (make-hash-table))
         (file (make-hash-table)))
    (puthash "description" "" payload)
    (puthash "public" public payload)
    (puthash "content" content file)
    (puthash filename file files)
    (puthash "files" files payload)
    (let ((url-request-method "POST")
          (url-request-extra-headers `(("Content-Type" . "application/json") ("User-Agent" . "gist.el") ("Authorization" . ,(concat "Bearer " oauth-token))))
          (url-request-data (json-encode payload)))
      (url-retrieve "https://api.github.com/gists" 'my/gist-handle-response))))

(defun my/gist-handle-response (response)
  "Process content of RESPONSE and extract link."
  (let* ((status-line (buffer-substring-no-properties (line-beginning-position) (line-end-position)))
         (status-code (nth 1 (split-string status-line " "))))

    (unless (string-equal status-code "201")
      (user-error "There was a problem with GitHub API, try again later"))

    (search-forward "\n\n")
    (let* ((raw-response (buffer-substring-no-properties (point) (point-max)))
           (data (json-parse-string raw-response))
           (gist-link (gethash "html_url" data)))
      (kill-new gist-link)
      (message "Paste created: %s" gist-link))))

(defun my/gist-get-oauth-token ()
  "Fetch oauth token from ~/.authinfo."
  (let ((entry (car (auth-source-search :host "gist"))))
    (unless entry
      (user-error "GitHub oauth token must be in ~/.authinfo using 'gist' as host"))
    (funcall (plist-get entry :secret))))

(defun my/gist-get-content ()
  "Create payload using current region or the whole buffer."
  (if (region-active-p)
      (buffer-substring-no-properties (region-beginning) (region-end))
    (buffer-substring-no-properties (point-min) (point-max))))

Linkode

(use-package linkode
  :ensure t
  :defer t)

Web

Web mode

(defun my/web-mode-hook ()
  (emmet-mode)
  (rainbow-delimiters-mode-disable))

(use-package web-mode
  :ensure t
  :custom
  (web-mode-enable-current-element-highlight t)
  (web-mode-enable-current-column-highlight t)
  :mode (("\\.html\\'" . web-mode)
         ("\\.html.eex\\'" . web-mode)
         ("\\.html.leex\\'" . web-mode)
         ("\\.hbs\\'" . web-mode))
  :config
  (add-hook 'web-mode-hook 'my/web-mode-hook))

Emmet

(use-package emmet-mode
  :ensure t)

CSS mode

(use-package css-ts-mode
  :straight (:type built-in)
  :mode "\\.css\\'"
  :defer t)

Rainbow

(use-package rainbow-mode
  :ensure t
  :hook
  ((css-ts-mode . rainbow-mode)
   (sass-mode . rainbow-mode)
   (heex-ts-mode . rainbow-mode)
   (scss-mode . rainbow-mode)))

Telegram

In case on error when compiling tdlib the first time we need to specify where it's installed, for macOS we can specify:

(telega-server-libs-prefix "/opt/homebrew/Cellar/tdlib/HEAD-fd3154b")

The last part can change depending of the version installed

(use-package telega
  :ensure t
  :defer t
  :custom
  ;; enable markdown for code snippets
  (telega-chat-input-markups '("markdown2" "org" nil))
  ;; use vertico for completion
  (telega-completing-read-function 'completing-read)
  :config
  ;; use shift enter to make multi line messages and enter to send it
  (define-key telega-chat-mode-map (kbd "S-<return>") #'newline)
  ;; disable copy message link when moving over text with evil-mode
  (define-key telega-msg-button-map (kbd "l") nil)
  ;; avoid showing blank spaces highlighted
  (add-hook 'telega-chat-mode-hook #'(lambda ()
                                       (setq-local show-trailing-whitespace nil))))

(defun my/start-telega ()
  "Start `telega' inside a new perspective and activate 'telega-mode-line-mode'"
  (interactive)
  (persp-switch "*telega*")
  (telega)
  (telega-mode-line-mode))

Miscellaneous

(use-package writeroom-mode
  :ensure t)

(use-package csv-mode
  :ensure t
  :defer t)

(defun my/json-format ()
  "Format region or buffer."
  (interactive)
  (if (region-active-p)
      (jsonian-format-region (region-beginning) (region-end))
    (jsonian-format-region (point-min) (point-max))))

(use-package jsonian
  :ensure t
  :defer t
  :mode (("\\.json\\'" . jsonian-mode))
  :mode-hydra
  (jsonian-mode
   (:title (concat (nerd-icons-icon-for-buffer) " JSON commands"))
   ("Format"
    (("f" my/json-format)))))

(use-package request
  :ensure t
  :defer t)

(use-package graphql-mode
  :ensure t
  :defer t)

(with-eval-after-load 'reformatter
  (reformatter-define terraform-format
    :program "terraform"
    :args '("fmt" "-")
    :group 'terraform))

(use-package terraform-mode
  :ensure t
  :defer t
  :bind (:map terraform-mode-map
              ("C-c C-f" . 'terraform-format-buffer)))

(defun my/k8s-apply ()
  "Apply current yaml file to the current kubernetes context."
  (interactive)
  (let ((default-directory (file-name-directory buffer-file-name)))
    (compile (format "kubectl apply -f %s" buffer-file-name))))

(defun my/k8s-delete ()
  "Delete current yaml file to the current kubernetes context."
  (interactive)
  (let ((default-directory (file-name-directory buffer-file-name)))
    (compile (format "kubectl delete -f %s" buffer-file-name))))

(use-package yaml-ts-mode
  :straight (:type built-in)
  :mode "\\.ya?ml\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " YAML Commands"))
   ("Kubernetes"
    (("a" my/k8s-apply)
     ("k" my/k8s-delete)))))

(use-package flymake-yamllint
  :ensure t
  :defer t
  :hook
  (yaml-ts-mode . flymake-mode)
  (yaml-ts-mode . flymake-yamllint-setup))

(use-package yaml-pro
  :ensure t
  :defer t
  :hook
  (yaml-ts-mode . yaml-pro-ts-mode)
  :config
  (define-key yaml-pro-ts-mode-map (kbd "C-c C-f") 'yaml-pro-format)
  ;; this binding conflicts with org indirect mode
  (define-key yaml-pro-ts-mode-map (kbd "C-c '") nil))

(use-package hcl-mode
  :ensure t)

;; Used for gherkin files (.feature)
(use-package feature-mode
  :ensure t
  :defer t)

(use-package toml-ts-mode
  :straight (:type built-in)
  :mode "\\.toml\\'"
  :defer t)

(use-package slint-mode
  :ensure t
  :hook (slint-mode . eglot-ensure)
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Slint Commands"))
   ("Format"
    (("f" eglot-format-buffer)))))

(use-package nix-mode
  :ensure t
  :defer t
  :custom
  (nix-nixfmt-bin "nixpkgs-fmt")
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Nix commands"))
   ("Format"
    (("f" nix-format-buffer))))
  :mode "\\.nix\\'")

(use-package d2-mode
  :ensure t
  :defer t
  :mode "\\.d2\\'")

(use-package ob-d2
  :defer t
  :ensure t
  :config
  (with-eval-after-load 'org
    (org-babel-do-load-languages
     'org-babel-load-languages
     '((d2 . t)))))

(use-package mermaid-mode
  :ensure t
  :defer t)

(defun my/preview-mermaid ()
  "Render region inside a webit embebed browser."
  (interactive)
  (unless (region-active-p)
    (user-error "Select a region first"))
  (let* ((path (concat (make-temp-file (temporary-file-directory)) ".html"))
         (mermaid-code (buffer-substring-no-properties (region-beginning) (region-end))))
    (save-excursion
      (with-temp-buffer
        (insert "<body>
  <pre class=\"mermaid\">")
        (insert mermaid-code)
        ;; js script copied from mermaid documentation
        (insert "</pre>
  <script type=\"module\">
    import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
    mermaid.initialize({ startOnLoad: true });
  </script>
</body>")
        (write-file path)))
    (xwidget-webkit-browse-url (format "file://%s" path))))

(use-package markdown-mode
  :ensure t
  :defer t
  :custom
  (markdown-fontify-code-blocks-natively t)
  ;; use different sizes for headings, like org-mode
  (markdown-header-scaling t)
  :config
  (add-hook 'markdown-mode-hook #'(lambda ()
                                    (setq-local fill-column 120)
                                    (auto-fill-mode 1))))

(use-package edit-indirect
  :ensure t
  :defer t)

(use-package dockerfile-ts-mode
  :straight (:type built-in)
  :defer t
  :mode (("\\Dockerfile\\'" . dockerfile-ts-mode)
         ("\\.dockerignore\\'" . dockerfile-ts-mode)))

(use-package dumb-jump
  :ensure t
  :defer t
  :custom
  (dumb-jump-force-searcher 'rg)
  (dumb-jump-selector 'completing-read))

helpful, enhance help functions

(use-package helpful
  :ensure t
  :defer t)

;; these function have autoload annotation so they will load `helpful' package when they are called
;; because we're defined just keybindings we can just use the symbol even the function is not loaded yet
(global-set-key (kbd "C-h f") #'helpful-callable)
(global-set-key (kbd "C-h v") #'helpful-variable)
(global-set-key (kbd "C-h k") #'helpful-key)

Use ESC key instead C-g to close and abort

Copied from somewhere

(defun minibuffer-keyboard-quit ()
  "Abort recursive edit.
  In Delete Selection mode, if the mark is active, just deactivate it;
  then it takes a second \\[keyboard-quit] to abort the minibuffer."
  (interactive)
  (if (and delete-selection-mode transient-mark-mode mark-active)
      (setq deactivate-mark  t)
    (when (get-buffer "*Completions*") (delete-windows-on "*Completions*"))
    (abort-recursive-edit)))

(with-eval-after-load 'evil
  (define-key evil-normal-state-map [escape] 'keyboard-quit)
  (define-key evil-visual-state-map [escape] 'keyboard-quit))

(define-key minibuffer-local-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-ns-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-completion-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-must-match-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-isearch-map [escape] 'minibuffer-keyboard-quit)
(global-set-key [escape] 'evil-exit-emacs-state)

Emacs Start Up Profiler

(use-package esup
  :ensure t)

websockets client

(use-package websocket
  :ensure t
  :defer t)

LSP

(use-package eglot
  :ensure nil
  :defer t
  :straight (:type built-in)
  :bind (:map eglot-mode-map
              ("C-c C-d" . 'eldoc-doc-buffer)
              ("<M-return>" . 'eglot-code-actions)
              ("C-c C-s" . 'xref-find-references))
  :config
  ;; tailwind LSP support
  (add-to-list 'eglot-server-programs
               '(((heex-ts-mode :language-id "html")
                  (web-mode :language-id "html")) . ("tailwindcss-language-server" "--stdio")))
  ;; elixir LSP server
  ;; we first look up for a lexical binary, this will be in the case
  ;; we're using nix, otherwise we relay on a cloned repository which
  ;; previously was compiled
  (setf (alist-get 'elixir-ts-mode eglot-server-programs) `(,(if (executable-find "lexical") "lexical" (expand-file-name "~/Code/oss/lexical/release/bin/start_lexical.sh")))))

In case we don't have eglot running we can relay on dumb-jump

(defun my/goto-definition-dumb-jump-fallback ()
  "Go to definition using eglot when is active otherwise use dumb-jump."
  (interactive)
  (if (and (featurep 'eglot) (eglot-managed-p))
      (xref-find-definitions (thing-at-point 'symbol))
    (if (member major-mode '(emacs-lisp-mode lisp-interaction-mode))
        (xref-find-definitions (thing-at-point 'symbol))
      (dumb-jump-go))))

Programming languages

Shell scripts

(use-package flymake-shellcheck
  :ensure t
  :defer t
  :if (executable-find "shellcheck")
  :commands flymake-shellcheck-load
  :init
  (add-hook 'bash-ts-mode-hook 'flymake-shellcheck-load))

Bash formatter using shfmt

(with-eval-after-load 'reformatter
  (reformatter-define sh-format
    :program "shfmt"
    :args '("-i" "2" "-")
    :group 'sh))

(with-eval-after-load 'sh-script
  (define-key bash-ts-mode-map (kbd "C-c C-f") 'sh-format-buffer))

C

clang-format is required for this, we can install it with brew install clang-format

(with-eval-after-load 'reformatter
  (reformatter-define c-format
    :program "clang-format"))

(with-eval-after-load 'cc-mode
  (define-key c-mode-map (kbd "C-c C-f") 'c-format-buffer))

Python

For LSP support pyright is required

brew install pyright

Install flymake-ruff

(use-package flymake-ruff
  :ensure t)

Generate a pyrightconfig.json file

(defun my/gen-pyright-config-file ()
  "Generate a configuration file for PyRight."
  (interactive)
  (let* ((input-dir (read-directory-name "Virtualenv directory: "))
         (venv-path (file-name-parent-directory input-dir))
         (venv (file-name-base (replace-regexp-in-string "/$" "" input-dir)))
         (data (make-hash-table)))
    (puthash "venvPath" venv-path data)
    (puthash "venv" venv data)
    (with-temp-buffer
      (insert (json-encode data))
      (write-file (file-name-concat venv-path "pyrightconfig.json")))))
(with-eval-after-load 'reformatter
  (reformatter-define python-ruff-format
    :program "ruff"
    :args '("format" "-")
    :group 'python)

  (reformatter-define python-sort-imports
    :program "ruff"
    :args '("check" "--fix" "--select" "I001" "-")
    :group 'python))

(setq python-shell-completion-native-enable nil)

(use-package python-ts-mode
  :straight (:type built-in)
  :defer t
  :mode "\\.py\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Python commands"))
   ("Format"
    (("f" python-ruff-format-buffer)
     ("si" python-sort-imports-region))
    "Testing"
    (("tt" pythontest-test-at-point)
     ("tf" pythontest-test-file)
     ("ta" pythontest-test-all))))
  :hook (python-ts-mode . flymake-ruff-load))

Testing package

(use-package pythontest
  :ensure t
  :defer t
  :custom
  (pythontest-pytest-command "pytest -s -vv"))

Show a list of the available django commands and run the selected one using a compilation buffer.

(defun my/run-django-command ()
  "Run a django command."
  (interactive)
  (let* ((python-bin (concat (getenv "VIRTUAL_ENV") "/bin/python"))
         (manage-py-file (concat (my/project-root) "manage.py"))
         (default-directory (my/project-root))
         (raw-help (shell-command-to-string (concat python-bin " " manage-py-file " help")))
         (splited-lines (split-string raw-help "\n"))
         (options (seq-filter #'(lambda (line) (cl-search "    " line)) splited-lines))
         (selection (completing-read "Pick django command: " (mapcar 'string-trim options)))
         (command (concat python-bin " " manage-py-file " " selection)))
    (compile command)))

Erlang

(use-package erlang-ts-mode
  :straight (erlang-ts-mode
             :type git
             :host github
             :repo "sebastiw/erlang-ts-mode"))

Elixir

LSP server building

To have support for LSP we need to compile lexical and setup the generated release into eglot-server-programs. lexical must use minimal versions so it can be used by more types of projects, recommended versions are:

elixir: 1.13.4-otp-24
erlang: 24.3.4.12

Prepare release of lexical

MIX_ENV=prod mix package --path release

Compilation integration

Add regex to match mix test execution output and be able to navigate between errors.

(with-eval-after-load 'compile
  (push 'mix compilation-error-regexp-alist)
  (push '(mix "^\\ \\ \\ \\ \\ \\([a-zA-Z/_/.]+\\):\\([0-9]+\\)$" 1 2) compilation-error-regexp-alist-alist))

Config

(with-eval-after-load 'reformatter
  (reformatter-define elixir-format
    :program "mix"
    :args '("format" "-")
    :group 'elixir))

(defun my/elixir-format-buffer ()
  "Format elixir buffers using eglot when is active otherwise use reformatter function."
  (interactive)
  ;; eglot formatter is preferred because it will use project .formatter.exs file
  ;; regular formatter generated by reformatted will ignore that file
  (if (and (featurep 'eglot) (eglot-managed-p))
      (eglot-format-buffer)
    (elixir-format-buffer)))

(use-package elixir-ts-mode
  :straight (:type built-in)
  :mode (("\\.ex\\'" . elixir-ts-mode)
         ("\\.exs\\'" . elixir-ts-mode)
         ("\\mix.lock\\'" . elixir-ts-mode))
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Elixir Commands"))
   ("Format"
    (("f" my/elixir-format-buffer))
    "Testing"
    (("tt" my/mix-run-test-at-point)
     ("tf" my/mix-run-test-file)
     ("ta" my/mix-run-test-all)))))

heex templates support

(defun my/heex-wrap-text-in-get-text ()
  "Convert region content into a tag using gettext."
  (interactive)
  (unless (region-active-p)
    (user-error "Must select some text first"))
  (save-excursion
    (let* ((text (buffer-substring-no-properties (region-beginning) (region-end)))
           (new-text (concat "<%= " "gettext(\"" text "\") %>")))
      (kill-region (region-beginning) (region-end))
      (insert new-text))))
(use-package heex-ts-mode
  :ensure t
  :hook (heex-ts-mode . emmet-mode)
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Heex Commands"))
   ("Format"
    (("f" my/heex-format-file))
    "Text"
    (("wg" my/heex-wrap-text-in-get-text)))))

Set up hydra commands

(defun my/heex-format-file ()
  "Format heex file using mix command."
  (interactive)
  ;; avoid to show *Async-Shell-Command* buffer
  (let* ((display-buffer-alist (list (cons "\\*Async Shell Command\\*.*" (cons #'display-buffer-no-window nil))))
         (mix-file (expand-file-name (concat (locate-dominating-file (buffer-file-name) "mix.exs") "mix.exs")))
         (default-directory (file-name-directory mix-file)))
    (async-shell-command (format "mix format %s" (buffer-file-name)) nil)
    (revert-buffer (current-buffer) :ignore-auto :no-confirm)))

Custom functions to run elixir tests.

elixir-extra-test-env can be set up on .dir-locals.el

(defun my/mix-run-test (&optional scope)
  "Run elixir test for the given SCOPE."
  (interactive)
  ;; we need to get absolute path of mix binary because when running
  ;; on nix this path will be different per project, version, etc
  (let* ((mix-binary (executable-find "mix"))
         (current-file (buffer-file-name))
         (current-line (line-number-at-pos))
         (mix-file (expand-file-name (concat (locate-dominating-file (buffer-file-name) "mix.exs") "mix.exs")))
         (default-directory (file-name-directory mix-file))
         (extra-env (if (boundp 'elixir-extra-test-env) elixir-extra-test-env ""))
         (mix-env (concat "MIX_ENV=test " extra-env)))

    (cond
     ((string-equal scope "file")
      (compile (format "%s %s test %s" mix-env mix-binary current-file)))

     ((string-equal scope "at-point")
      (compile (format "%s %s test %s:%s" mix-env mix-binary current-file current-line)))

     (t
      (compile (format "%s %s test" mix-env mix-binary))))))

(defun my/mix-run-test-file ()
  "Run mix test over the current file."
  (interactive)
  (my/mix-run-test "file"))

(defun my/mix-run-test-at-point ()
  "Run mix test at point."
  (interactive)
  (my/mix-run-test "at-point"))

(defun my/mix-run-test-all ()
  "Run mix test at point."
  (interactive)
  (my/mix-run-test))
(defun my/phx-insert-hero-icon ()
  "Insert icon component in a Elixir Phoenix application."
  (interactive)
  (let* ((base-dir (expand-file-name (locate-dominating-file (buffer-file-name) "assets")))
         (icons-dir (format "%sassets/vendor/heroicons/optimized/20/solid/" base-dir))
         (option (completing-read "Select icon: " (directory-files icons-dir nil "svg")))
         (no-extension (string-replace ".svg" "" (string-trim option) )))
    (insert (format "<.icon name=\"hero-%s\" />" no-extension))))

LFE

(use-package lfe-mode
  :ensure t
  :if (executable-find "lfe")
  :bind (:map lfe-mode-map
              ("C-c C-c" . lfe-eval-buffer))
  :init
  (defun lfe-eval-buffer ()
    "Send current buffer to inferior LFE process."
    (interactive)
    (if (eq (get-buffer-window "*inferior-lfe*") nil)
        (run-lfe nil))
    (lfe-eval-region (point-min) (point-max) nil)))

Elm

Install Elm

npm -g install elm elm-format elm-oracle
(use-package elm-mode
  :ensure t
  :if (executable-find "elm")
  :bind (:map elm-mode-map
              ("C-c C-d" . elm-oracle-doc-at-point))
  :config
  (add-hook 'elm-mode-hook #'elm-oracle-setup-completion)
  (add-to-list 'company-backends 'company-elm))

Haskell

Install haskell binaries hlint and hindent and make sure ~/.local/bin/ is loaded in PATH.

stack install hlint
stack install hindent
(with-eval-after-load 'reformatter
  (reformatter-define haskell-format
    :program "hindent"
    :group 'haskell))

(use-package haskell-mode
  :ensure t
  :bind (:map haskell-mode-map
              ("C-c C-f" . haskell-format-buffer)
              ("C-c C-l" . haskell-process-load-file)))

(defun my/run-hlint ()
  "Run  hlint over the current project."
  (interactive)
  (let ((default-directory (my/project-root)))
    (compile "hlint .")))

(defun my/run-hlint-buffer ()
  "Run  hlint over the current buffer."
  (interactive)
  (let* ((current-file (buffer-file-name))
         (default-directory (my/project-root)))
    (compile (concat "hlint " current-file))))

Lua

(use-package lua-mode
  :ensure t
  :bind (:map lua-mode-map
              ("C-c C-b" . compile)
              ("C-c C-f" . lua-format-buffer)))

Define formatter using StyLua

(with-eval-after-load 'reformatter
  (reformatter-define lua-format
    :program "stylua"
    :args '("-")
    :group 'lua))

Javascript

Linter using biome

(use-package flymake-biome
  :ensure t)
(use-package js-ts-mode
  :straight (:type built-in)
  :defer t
  :mode "\\.jsx?\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Javascript Commands"))
   ("Format"
    (("f" js-format-buffer))))
  :hook (js-ts-mode . flymake-biome-load))

Formattter

We need to use --stdin-file-path a.js to tell biome to use stdin.

(with-eval-after-load 'reformatter
  (reformatter-define js-format
    :program "biome"
    :args '("format" "--stdin-file-path" "a.js" "--javascript-formatter-indent-style" "space")))

Typescript

(use-package typescript-ts-mode
  :straight (:type built-in)
  :defer t
  :mode "\\.tsx?\\'")

Zig

(use-package zig-mode
  :ensure t
  :custom
  (zig-format-on-save nil))

Rust

Install rust analyzer, this should be installed when rustup-init is executed but in case is not we can execute:

rustup component add rust-analyzer

Install rust source code, it is required by rust-analyzer, in case it's not installed automatically

rustup component add rust-src
(with-eval-after-load 'reformatter
  (reformatter-define rust-format
    :program "rustfmt"))

(use-package rust-ts-mode
  :straight (:type built-in)
  :mode "\\.rs\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Rust Commands"))
   ("Format"
    (("f" rust-format-buffer)))))
(defun my/rust-run-file ()
  "Compile and rust current file."
  (interactive)
  (unless (buffer-file-name)
    (user-error "Save file before"))
  (let* ((path (buffer-file-name))
         (default-directory (file-name-directory path))
         (filename (buffer-name))
         (command (format "rustc %s && ./%s" filename (string-replace ".rs" "" filename))))
    (compile command)))

Golang

Install dependencies: goimports

go install golang.org/x/tools/cmd/goimports@latest

Install gopls to have LSP support using eglot

brew install gopls
(use-package go-ts-mode
  :straight (:type built-in)
  :hook (go-ts-mode . eglot-ensure)
  :mode (("\\.go\\'" . go-ts-mode))
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Go Commands"))
   ("Format"
    (("f" gofmt))
    "Execute"
    (("r" go-run))
    "Testing"
    (("tt" go-test-current-test)
     ("tf" go-test-current-file)
     ("ta" go-test-current-project))))
  :config
  ;; by default tab width is 8, that's too much space so we define 4
  ;; only for go buffers
  (add-hook 'go-ts-mode-hook #'(lambda ()
                              (setq-local tab-width 4)))
  (setq gofmt-command "goimports"))

;; we install this package to have access to their helper functions
(use-package go-mode
  :ensure t
  :after go-ts-mode)

(use-package gotest
  :ensure t
  :after go-ts-mode)

Common lisp

(defconst inferior-lisp-program (executable-find "sbcl"))

(use-package sly
  :ensure t
  :defer t)

Clojure

(defun my/clj-format-code ()
  "Format clojure code using cider commands."
  (interactive)
  (if (region-active-p)
      (cider-format-region (region-beginning) (region-end))
    (cider-format-buffer)))

(defun my/cider-repl-reset ()
  "Call (reset) in the active repl and return to the position where was called."
  (interactive)
  (save-window-excursion
    (cider-insert-in-repl "(reset)" t)))

(use-package cider
  :ensure t
  :bind (:map cider-mode-map
              ("C-c C-f" . my/clj-format-code)
              ("C-c C-r" . my/cider-repl-reset)))

(with-eval-after-load 'evil
  (evil-set-initial-state 'cider-stacktrace-mode 'emacs))
(use-package clj-refactor
  :ensure t
  :after cider
  :bind (:map clojure-mode-map
              ("C-c C-a" . cljr-add-project-dependency))
  :hook (clojure . clj-refactor))

Emacs lisp

Disable indentation with tabs for emacs-lisp-mode

(defun my/emacs-lisp-hook-setup ()
  (setq indent-tabs-mode nil))

(add-hook 'emacs-lisp-mode-hook 'my/emacs-lisp-hook-setup)

Enable flymake

(add-hook 'emacs-lisp-mode-hook 'flymake-mode-on)

package-lint, used for packages development

(use-package package-lint
  :ensure t
  :defer t)

OCaml

(use-package tuareg
  :ensure t
  :defer t)

(use-package merlin
  :ensure t
  :hook ((tuareg-mode caml-mode) . merlin-mode))

(use-package merlin-eldoc
  :ensure t
  :hook ((reason-mode tuareg-mode caml-mode) . merlin-eldoc-setup))

Dart

(with-eval-after-load 'reformatter
  (reformatter-define dart-format
    :program "dart"
    :args '("format")
    :group 'dart))

(defun my/dart-run-file ()
  "Execute the code of the current file."
  (interactive)
  (compile (format "dart %s" (buffer-file-name))))

(use-package dart-mode
  :ensure t
  :if (or (executable-find "dart") (executable-find "flutter"))
  :bind (:map dart-mode-map
              ("C-c C-f" . dart-format-buffer)
              ("C-c C-c" . my/dart-run-file)))

Flutter

(defun my/flutter-goto-logs-buffer()
  "Go to buffer logs buffer."
  (interactive)
  (let ((buffer (get-buffer flutter-buffer-name)))
    (unless buffer
      (user-error "flutter is not running."))
    (switch-to-buffer buffer)
    (goto-line (point-max))))

(use-package flutter
  :ensure t
  :after dart-mode
  :bind (:map dart-mode-map
              ("C-c C-r" . #'flutter-run-or-hot-reload)
              ("C-c C-l" . #'my/flutter-goto-logs-buffer))
  :hook (dart-mode . flutter-test-mode)
  :custom
  ;; sdk path will be the parent-parent directory of flutter cli
  (flutter-sdk-path (directory-file-name
                     (file-name-directory
                      (directory-file-name
                       (file-name-directory (executable-find "flutter")))))))

Writing

Custom functions to speed up writing process

Hugo

Insert org-link image using clipboard value, if the current file is blog/demo.org it will place the resulting image into static/images/blog/demo/image.png.

(defun my/hugo-insert-image-from-clipboard ()
  "Use clipoard image and put it in a generated images folder for the current file."
  (interactive)
  (let* ((absolute-path (buffer-file-name))
         (splitted (reverse (split-string absolute-path "/")))
         (filename (replace-regexp-in-string ".org" "" (car splitted)))
         (dir (nth 1 splitted))
         (base-image-path (concat (my/project-root) "static/images"))
         (result-image-dir (format "%s/%s/%s" base-image-path dir filename))
         (result-image-name (read-string "Filename: " "image.png"))
         (full-path-result-image (format "%s/%s" result-image-dir result-image-name)))

    (shell-command (format "mkdir -p %s" result-image-dir))
    (shell-command (format "pngpaste %s" full-path-result-image))
    (insert (format "[[file:%s]]" (car (cdr (split-string full-path-result-image "static")))))))

Insert docsy link

(defun my/docsy-insert-ref ()
  "Insert a link using docsy ref helper."
  (interactive)
  (let* ((filename (read-file-name "Select file: " (my/project-root)))
         (prefix-to-remove (concat (my/project-root) "content/"))
         (relative-path (string-replace prefix-to-remove "" filename)))
    (insert (format "{{< ref \"%s\" >}}" relative-path))))

Custom functions

Simple bookmarks management

(defvar my/bookmarks (make-hash-table :test 'equal))

(defun my/bookmark-switch ()
  "Switch to selected bookmark."
  (interactive)
  (let* ((key (my/project-name))
         (items (gethash key my/bookmarks (make-hash-table :test 'equal)))
         (options (hash-table-keys items))
         (selected (completing-read "Pick buffer: " options))
         (selected-buffer (gethash selected items)))
    (when selected-buffer

      (switch-to-buffer selected-buffer))))

(defun my/add-bookmark ()
  "Add current buffer to bookmark list."
  (interactive)
  (let* ((project-key (my/project-name))
         (buffer-key (buffer-name))
         (items (gethash project-key my/bookmarks (make-hash-table :test 'equal))))
    (puthash buffer-key (current-buffer) items)
    (puthash project-key items my/bookmarks)
    (message "%s added to bookmarks" buffer-key)))

Manage window configurations, allows to save a "snapshot" of the current windows configuration. Also allows to restore a saved "snapshot".

(defvar my/window-snapshots '())

(defun my/save-window-snapshot ()
  "Save the current window configuration into `window-snapshots` alist."
  (interactive)
  (let ((key (read-string "Enter a name for the snapshot: ")))
    (setf (alist-get key my/window-snapshots) (current-window-configuration))
    (message "%s window snapshot saved!" key)))

(defun my/get-window-snapshot (key)
  "Given a KEY return the saved value in `window-snapshots` alist."
  (let ((value (assoc key my/window-snapshots)))
    (cdr value)))

(defun my/restore-window-snapshot ()
  "Restore a window snapshot from the window-snapshots alist."
  (interactive)
  (let* ((snapshot-name (completing-read "Choose snapshot: " (mapcar #'car my/window-snapshots)))
         (snapshot (my/get-window-snapshot snapshot-name)))
    (if snapshot
        (set-window-configuration snapshot)
      (message "Snapshot %s not found" snapshot-name))))

Manipulate frame font height.

(defun my/change-font-height (delta)
  "Use DELTA to increase/decrease the frame font height."
  (let* ((current-height (face-attribute 'default :height))
         (new-height (+ current-height delta)))
    (set-face-attribute 'default nil :height new-height)))

(defun my/decrease-font-height ()
  "Decrease font height by 10."
  (interactive)
  (my/change-font-height -10))

(defun my/increase-font-height ()
  "Increase font height by 10."
  (interactive)
  (my/change-font-height +10))
(defun my/find-file-in-project ()
  "Custom find file function."
  (interactive)
  (if (my/project-p)
      (project-find-file)
    (call-interactively #'find-file)))

(defun my/fold-buffer-when-is-too-big (max-lines)
  "Fold buffer is max lines if grater than as MAX-LINES."
  (if (> (count-lines (point-min) (point-max)) max-lines)
      (hs-hide-all)))

;; used to store all the configuration per perspective
(defvar my/toggle-window-configurations (make-hash-table))

(defun my/toggle-maximize ()
  "Toggle maximization of current window."
  (interactive)
  (let* ((key (persp-name (persp-curr)))
         (value (gethash key my/toggle-window-configurations)))
    (if (eq value nil)
        (progn
          (puthash key (current-window-configuration) my/toggle-window-configurations)
          (delete-other-windows))
      (progn
        (set-window-configuration value)
        (puthash key nil my/toggle-window-configurations)))))

(defun my/venv-workon (name)
  "Active virtualenv NAME only is not setup yet."
  (unless pyvenv-virtual-env
    (pyvenv-workon name)))

(defun my/config-file ()
  "Open config file."
  (interactive)
  (find-file (expand-file-name "~/.emacs.d/bootstrap.org")))

(defun my/toggle-spanish-characters ()
  "Enable/disable alt key to allow insert spanish characters."
  (interactive)
  (if (eq ns-alternate-modifier 'meta)
      (setq ns-alternate-modifier nil)
    (setq ns-alternate-modifier 'meta)))

(defun my/change-font-size()
  "Change frame font size."
  (interactive)
  (let* ((size (read-number "New size: ")))
    (set-face-attribute 'default nil :font (format "%s %d" my/font-family size))))

Function to extract clocks from org buffer and filter them by month

(defun my/collect-clocks ()
  "Collect all the clocks of current buffer."
  (org-element-map (org-element-parse-buffer) 'clock #'(lambda (clock) clock)))

(defun my/filter-clocks-by-month (clocks month)
  "Filter CLOCKS using MONTH value."
  (seq-filter #'(lambda (clock)
                  (eq (org-element-property :month-end (org-element-property :value clock)) month)) clocks))

(defun my/org-filter-clocks-report ()
  "Create a buffer with the tasks filtered by month."
  (interactive)
  (let* ((month (read-number "Insert month: "))
         (clocks (my/collect-clocks))
         (filtered-clocks (my/filter-clocks-by-month clocks month))
         (buffer (get-buffer-create "*clocks report*")))
    (switch-to-buffer buffer)
    (org-mode)
    (insert "* Report\n")
    (seq-map #'(lambda (clock)
                 (insert (format "CLOCK: %s\n" (org-element-property :raw-value (org-element-property :value clock))))) filtered-clocks)
    (org-clock-display)))

Copy absolute and relative path to clipboard

(defun my/copy-abs-path ()
  "Copy absolute path of the buffer to clipboard"
  (interactive)
  (if buffer-file-name
      (progn
        (kill-new buffer-file-name)
        (message (format "%s copied to clipboard" buffer-file-name)))
    (message "File not saved yet")))

(defun my/copy-relative-path ()
  "Copy relative path of the buffer to clipboard"
  (interactive)
  (if (and (my/project-p) buffer-file-name)
      (let ((path (file-relative-name buffer-file-name (my/project-root))))
        (kill-new path)
        (message (format "%s copied to clipboard" path)))
    (message "File not saved yet or not inside project")))

Create a temp file with the current buffer content and render it with eww.

(defun my/preview-buffer-in-eww ()
  "Preview buffer content in EWW."
  (interactive)
  (let* ((temp-file (make-temp-name (temporary-file-directory)))
         (path (concat temp-file ".html")))
    (write-file path)
    (kill-buffer)
    (eww-open-file path)))

(defun my/preview-buffer-in-xwidget-browser ()
  "Preview buffer content in EWW."
  (interactive)
  (let* ((temp-file (make-temp-name (temporary-file-directory)))
         (path (concat temp-file ".html")))
    (write-file path)
    (kill-buffer)
    (xwidget-webkit-browse-url (format "file://%s" path))))

Resize window: allow create a "resize mode" and use hjkl to increase/decrease width/height of the current window

(defun my/resize-window ()
  "Resize window using j k h l keys."
  (interactive)
  (let ((keys-map '((?h . evil-window-decrease-width)
                    (?j . evil-window-decrease-height)
                    (?k . evil-window-increase-height)
                    (?l . evil-window-increase-width)))
        (overlay (make-overlay (point-min) (point-max) (window-buffer))))
    (let ((is-reading t))
      (overlay-put overlay 'face '((t (:foreground "gray40"))))
      (while is-reading
        (let ((action (alist-get (read-key) keys-map)))
          (if action
              (apply action '(1))
            (setq is-reading nil)
            (delete-overlay overlay)))))))

Kill the current buffer and delete the related file

(defun my/delete-close-file ()
  "Delete the current file and kill its buffer."
  (interactive)
  (when buffer-file-name
    (delete-file buffer-file-name)
    (kill-buffer)))

Copy json text from clipboard in a new buffer and format it

(defun my/copy-and-format-json-from-clipboard ()
  "Copy content from clipboard and format it in a new buffer."
  (interactive)
  (let ((buffer (generate-new-buffer "tmp.json")))
    (with-current-buffer buffer
      (yank)
      (jsonian-mode)
      (jsonian-format-region (point-min) (point-max)))
    (set-window-buffer nil buffer)))

MacOS

Functions to open Finder using current file or current project.

(defun my/open-finder-at (path)
  "Open Finder app with the given PATH."
  (let* ((finder (executable-find "open"))
         (command (format "%s %s" finder path)))
    (shell-command command)))

(defun my/open-project-in-finder ()
  "Open current project in Finder app."
  (interactive)
  (if (my/project-p)
      (my/open-finder-at (my/project-root))
    (message "There is no active project.")))

(defun my/open-current-file-in-finder ()
  "Open current file in Finder."
  (interactive)
  (let ((file (buffer-file-name)))
    (if file
        (my/open-finder-at (file-name-directory file))
      (message "Buffer has not been saved yet!"))))

Open current file with an macOS app. Installed macOS apps will be listed using completing-read

(defun my/macos-open-file-with ()
  "Open current file with and macOS installed app."
  (interactive)
  (let* ((apps-list (directory-files "/Applications" nil "\\.app$"))
         (selected-app (completing-read "Choose an application: " apps-list)))
    (shell-command (format "open %s -a '%s'" (buffer-file-name) selected-app))))

Open the current file with macOS open command. This will open the file with the default app configured for the type of file.

(defun my/macos-open-current-file ()
  (interactive)
  (shell-command (format "open %s" (buffer-file-name))))

Save image from clipboard to path.

(defun my/save-image-from-clipboard ()
  "Save image from clipboard to the given path."
  (interactive)
  (unless (executable-find "pngpaste")
    (user-error "Install pngpaste to continue"))
  (let* ((path (read-file-name ""))
         (command (format "pngpaste %s" path)))
    (shell-command command)
    (kill-new path)))

Author: Erick Navarro

Created: 2024-11-21 Thu 16:58

Validate