My Emacs configuration

Table of Contents

Table of Contents

1. Emacs stuff

Here be dragons.

1.1. early-init.el

Starting from emacs 27 (I guess) there is an additional configuration file called early-init.el. As the name suggest, this is called before the usual init.el or .emacs.

Even if when it's called there is no frame available yet, I found that there's a small boost in the startup by disabling all this cruft before it gets even rendered:

(when (fboundp 'menu-bar-mode)
  (menu-bar-mode -1))
(when (fboundp 'tool-bar-mode)
  (tool-bar-mode -1))
(when (fboundp 'scroll-bar-mode)
  (scroll-bar-mode -1))
(when (fboundp 'horizontal-scroll-bar-mode)
  (horizontal-scroll-bar-mode -1))

I also use it to add two very important paths to the the load-path: the lisp directory, which holds various smallish packages and the mu4e source code.

(add-to-list 'load-path
             (expand-file-name "lisp" user-emacs-directory))
(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e")

Another thing do here is load my custom modeline and theme, now that the load-path is correctly populated.

(require 'my-modeline)
(load-theme 'minimal-light t)

Even if I'll be called out for cargo culting, I've seen this suggestion in various configuration and, so far, it hasn't caused any drawbacks:

;; Resizing the Emacs frame can be a terribly expensive part of
;; changing the font. By inhibiting this, we easily halve startup
;; times with fonts that are larger than the system default.
(setq frame-inhibit-implied-resize t)

The last trick is to bump the GC threshold during the initialisation. Beware: while this is usually fine, Emacs will die from OOM error if bootstrapping straight.el and fetching all the packages! So remember to disable this when moving this configuration to another place!.

(defvar op/default-gc-cons-threshold gc-cons-threshold
  "Backup of the default GC threshold.")

(defvar op/default-gc-cons-percentage gc-cons-percentage
  "Backup of the default GC cons percentage.")

;; boost the gc during the load
(setq gc-cons-threshold most-positive-fixnum ; 2^61 bytes
      gc-cons-percentage 0.6)

;; and reset it to "normal" when done
(add-hook 'emacs-startup-hook
          (lambda ()
            (setq gc-cons-threshold op/default-gc-cons-threshold
                  gc-cons-percentage op/default-gc-cons-percentage)))

1.2. Preamble

;; -*- lexical-binding: t; -*-

(require 'cl-lib)

(message "here be dragons")

A bit of clojure-ness is always accepted! I copied the idea of a comment macro from there. In elisp there is a ignore function, but that evaluates its argument, yielding always nil, whereas comment doesn't evaluate its body. It's useful to temporary disable bits of code.

(defmacro comment (&rest _body)
  "Ignore BODY, just like `ignore', but this is a macro."
  '())

One drawbacks of using a literate init file is that customisation get lost every time it get tangled, because they're usually appended at the end of the user-init-file. The simplest solution I found is to move the custom stuff in another file, so it gets persisted:

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file)

<2021-06-12 Sat> $CDPATH produce strange effects on eshell

$ echo $CDPATH
.:/home/op/w:/usr/ports:/usr/ports/mystuff:/home/op/quicklisp/local-projects
$ pwd
~
$ cd .
$ pwd
~/.emacs.d/
$ wtf?

So just disable it for the time being. It's not particularly useful inside eshell anyway

(setenv "CDPATH" nil)

1.3. straight

I'm currently using straight.el and use-package to manage external packages. It makes easy to pull packages from git, so one can do local modifications and test right away. I don't know if it's better than other similar tools (cask, qelpa, …), it's the first one I tried and so far I like it.

<2021-09-11 Sat> straight keeps loading project from ELPA instead of using the one bundled with Emacs. Since I'm using Emacs from the master branch it's pretty annoying. I've found that requiring project here will avoid loading the ELPA one.

(require 'project)
(defvar bootstrap-version)
(defvar straight-use-package-by-default t)
(defvar straight-disable-native-compile t)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 5))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(straight-use-package 'use-package)

1.4. org

One day I'll split this manegeable chunks, but today it's not that day.

(use-package org
  :straight nil
  :bind (("C-c c" . org-capture)
         ("C-c a" . org-agenda)
         ("<f7> s" . org-store-link)
         :map org-src-mode-map
         ("C-x w" . org-edit-src-exit)
         ("C-x C-s" . org-edit-src-exit))
  :hook ((org-mode . op/org-setup))
  :custom ((org-todo-keywords '((sequence "TODO" "WAITING" "|" "DONE")
                                (sequence "IDEA" "WRITING" "|" "POSTED")
                                (sequence "REPORT" "BUG" "KNOWCAUSE" "|" "FIXED")
                                (sequence "|" "CANCELLED")))
           (org-capture-templates '(("n" "annotate something" entry (file "~/org/personal.org")
                                     "* %? :note:\n  %a")
                                    ("t" "something to do" entry (file "~/org/personal.org")
                                     "* TODO %?\n %a")
                                    ("b" "bug" entry (file "~/org/personal.org")
                                     "* REPORT %?\n  %a")))
           (org-adapt-indentation t)
           (org-ellipsis " [+]")
           (org-imenu-depth 4)
           (org-startup-folded t)
           (org-startup-with-inline-images t)
           (org-fontify-quote-and-verse-blocks t)
           (org-use-speed-commands t)
           (org-src-window-setup 'current-window)
           (org-directory "~/org")
           (org-agenda-files '("~/org"))
           (org-refile-use-outline-path t)
           (org-outline-path-complete-in-steps nil)
           (org-refile-targets '((nil :maxlevel . 3)
                                 (org-agenda-files :maxlevel . 3)))
           (org-src-fontify-natively t)
           (org-clock-out-remove-zero-time-clocks t)
           (org-clock-out-when-done t)
           (org-clock-auto-clock-resolution '(when-no-clock-is-running))
           (org-clock-report-include-clocking-task t)
           (org-time-stamp-rounding-minutes '(1 1))
           (org-clock-history-length 23)
           (org-clock-in-resume t)
           (org-confirm-babel-evaluate nil))
  :config
  (require 'org-protocol)

  (defun op/org-setup ()
    (hl-line-mode +1)
    (auto-fill-mode +1)
    (whitespace-mode -1)
    (setq-local cursor-type 'bar)
    (setq-local delete-trailing-lines t)
    (add-hook 'before-save-hook #'delete-trailing-whitespace nil t))

  (org-link-set-parameters "gemini"
                           :follow (lambda (p) (elpher-go (concat "gemini:" p)))
                           :display 'full)

  (org-babel-do-load-languages
   'org-babel-load-languages
   '((emacs-lisp . t)
     (C . t)
     (R . t)
     (sql . t)
     (lisp . t)
     (shell . t)
     (sqlite . t)
     (python . t)
     (gnuplot . t)))
  (setq org-babel-lisp-eval-fn #'sly-eval)

  <<org-roam>>
  <<org-tree-slide>>)

I'm having some problems with org, in particular C-c C-e ... doesn't export. Probably it's because I'm ending up with org from Emacs and not from straight, or something like that. This seems to fix the problem, but I'd like to avoid this workaround

(add-hook 'after-init-hook
          #'org-reload)

Org uses htmlize to prettify the code when exporting:

(use-package htmlize)

To fix some "alignment" problem with unicode characters in tables (but not also) there is a valign package!

(use-package valign
  :straight (:type git :host github :repo "casouri/valign")
  :defer t
  :hook ((org-mode . valign-mode))
  :custom ((valign-fancy-bar t)))
1.4.0.1. TODO wasn't valign included into ELPA?

1.4.1. org-roam

;; NOTE: needs sqlite3
(use-package org-roam
  :init (setq org-roam-v2-ack t) ; yeah, I know I'm on v2
  :custom ((org-roam-directory "~/org-roam"))
  :hook ((after-init . org-roam-setup))
  :bind (("C-z r l" . org-roam-buffer-toggle)
         ("C-z r f" . org-roam-node-find)
         ("C-z r i" . org-roam-node-insert))
  :config
  (comment
   (make-directory org-roam-directory)))

1.4.2. Presentations in org-mode

(use-package org-tree-slide
  :custom ((org-image-actual-width nil))
  :config
  (defun op/org-present-frame ()
    (let ((frame (make-frame `( ;(minibuffer . nil)
                               (title . "Presentation")
                               (menu-bar-lines . 0)
                               (tool-bar-lines . 0)
                               (vertical-scroll-bars . nil)
                               (left-fringe . 0)
                               (right-fringe . 0)
                               (internal-border-width . 10)
                               ;(cursor-type . nil)
                               ))))
      (select-frame-set-input-focus frame)
      (toggle-frame-fullscreen)
      (raise-frame frame)
      frame))

  (defun op/org-present ()
    (interactive)
    (let ((name "*presentazione*"))
      (ignore-errors
        (kill-buffer name))
      ;; (with-current-buffer (make-indirect-buffer (current-buffer)
      ;;                                            name))
      (op/org-present-frame)
      (org-display-inline-images)
      (olivetti-mode)
      (olivetti-set-width 90)
      (call-interactively #'org-tree-slide-mode)
      (text-scale-adjust 3))))

1.4.3. Org publish

Org publish is a library that allows to generate sets of documents from a directory tree. It provides some basic mechanisms to copy files around, converting org files to other formats (HTML for instance). I know some people use it to generate static websites, I'm using it to publish my dots repo on the web (and soon on Gemini!)

The variable org-publish-project-alist as an alist of ("name" props...).

To publish org files as another file and copy files as-is, the best way I found is to define multiple targets, one for org and one for the copy, and require with the :components props from another target.

(with-eval-after-load 'org
  (setq org-publish-project-alist
        '(("dots-org"
           :base-directory "~/dots"
           :base-extension "org"
           :publishing-directory "~/w/blog/resources/dots/"
           :recursive t
           :publishing-function org-html-publish-to-html)
          ("dots-org-gmi"
           :base-directory "~/dots"
           :base-extension "org"
           :publishing-directory "~/w/blog/resources/dots/"
           :recursive t
           :publishing-function org-gemini-publish-to-gemini)
          ("dots-static"
           :base-directory "~/dots"
           :base-extension "css\\|png\\|jpg\\|jpeg"
           :publishing-directory "~/w/blog/resources/dots/"
           :recursive t
           :publishing-function org-publish-attachment)
          ("dots" :components ("dots-org" "dots-org-gmi" "dots-static"))))

  (define-key global-map (kbd "C-z p p") #'org-publish)
  (define-key global-map (kbd "C-z p P") #'org-publish-all))

1.5. Misc

The following are some misc customizations. They can't be split in their own blocks, either because are variables defined in C or are defined in lisp files that we can't require. Either the way, it's probably self-explanatory.

(use-package emacs
  :straight nil
  :custom ((use-dialog-box nil)
           (x-stretch-cursor t)
           (sentence-end-double-space t)
           (require-final-newline t)
           (visible-bell nil)
           (load-prefer-newer t))
  :bind (("M-z" . zap-up-to-char))
  :config
  ;; free the C-z key
  (define-key global-map (kbd "C-z") nil)

  ;; these becomes buffer-local when set
  (setq-default scroll-up-aggressively 0.0
                scroll-down-aggressively 0.0
                scroll-preserve-screen-position t
                next-screen-context-lines 1)

  ;; fix hangs due to pasting from xorg -- workaround, not a solution :/
  (setq x-selection-timeout 1)
  (add-hook 'after-make-frame-functions
            (lambda (_frame)
              (setq x-selection-timeout 1)))

  (fset 'yes-or-no-p 'y-or-n-p))

I'm using a custom keyboard layout, where the numbers are actually symbols, and to type numbers I have to hold shift. Normally, this is not a problem, I type symbols more frequently than numbers anyway, but it's handy to have a quick shortcut for C-u 0, instead of doing C-u s-! or C-s-! (0 is s-! here). Introducing C-!

(defun op/digit-argument-zero ()
  "Like `digit-argument', but set the arg to 0 unconditionally."
  (interactive)
  (prefix-command-preserve-state)
  (setq prefix-arg 0))

(define-key global-map (kbd "C-!") #'op/digit-argument-zero)

I always end up trying to execute unload-theme instead of disable-theme when I want to get rid of a theme. If to load a theme I have to M-x load-theme, why the dual operation is disable-theme? Who knows, but I'll keep the alias.

(defalias 'unload-theme #'disable-theme)

Pasting from the primary selection is handy in various situations, but having to press mouse2 with a surgical-precision is not something I like. Taken from a conversation with cage, here's a better way:

(defun paste-at-point ()
  (interactive)
  (insert (gui-get-primary-selection)))

(define-key global-map (kbd "<mouse-2>") #'paste-at-point)
(define-key global-map (kbd "S-<insert>") #'paste-at-point)

1.5.1. Font

I discovered this font thanks to a submission on the ports@ mailing list. I'm just trying it for now, I'm not sure if I really like it.

<2021-07-29 Thu> I'm trying iosevka again. Mononoki is cool, but I like fonts that takes as little horizontal space as possible, and iosevka seems tiny, yet readable.

(let ((font "Iosevka Term Curly Medium 9"))
  (add-to-list 'default-frame-alist `(font . ,font))
  (set-face-attribute 'default t :font font :height 100)
  (set-face-attribute 'default nil :font font :height 100)
  (set-frame-font font nil t))

Also, I'd like emojis to be rendered…

(set-fontset-font "fontset-default" 'unicode "Noto Emoji" nil 'prepend)

1.5.2. tab-bar

I initially thought I would never used the tab-bar, but now here we are. How ironic. Anyway, please don't show the tab-bar when there is only one tab:

(setq tab-bar-show 1)

1.5.3. bookmarks

Emacs lets one keep bookmarks on various places (usually files) to quickly jump around.

(use-package bookmark
  :straight nil
  :bind (("C-z b b" . bookmark-jump)
         ("C-z b a" . bookmark-set)
         ("C-z b l" . list-bookmarks)))

1.5.4. save the place

save-place-mode remembers the position of the point in a buffer and, when re-opening it, restores the point. I don't know how it handles the fact that a buffer can be viewed in different window, each one with its point, but anyway it seems handy.

(use-package saveplace
  :straight nil
  :config (save-place-mode 1))

1.5.5. history

savehist is similar to saveplace, but save history. I don't know exactly what histories it saves, but when it doubt, save it!

(use-package savehist
  :straight nil
  :config (savehist-mode))

1.5.6. Uniquify

Buffer names must be unique. This package permits to tweak the rules that Emacs uses to uniquify those names. The following seems pretty handy, especially wrt project structures like Clojure

(use-package uniquify
  :straight nil
  :custom ((uniquify-buffer-name-style 'forward)
           (uniquify-strip-common-suffix t)))

1.5.7. Hydra

I use hydra for various thing, hence why it's in the "misc" section.

These are some general hydras that I find useful. They are used mostly to quickly "repeat" the last command.

(use-package hydra
  :config
  (defhydra hydra-windowsize (global-map "C-x")
    ("{" shrink-window-horizontally)
    ("}" enlarge-window-horizontally))

  (defhydra hydra-grep-like (global-map "M-g")
    ("n" next-error "next")
    ("p" previous-error "prev")
    ("RET" nil :exit t)
    ("C-l" recenter-top-bottom)
    ("q" nil :exit t))

  (defhydra hydra-other-window (global-map "C-x")
    ("o" other-window "next window")
    ("O" (other-window -1) "previous window"))
  (hydra-set-property 'hydra-other-window :verbosity 0)

  (defhydra hydra-other-tab (global-map "C-x t")
    ("o" tab-next)
    ("O" tab-previous)
    ("q" nil :exit t))
  (hydra-set-property 'hydra-other-tab :verbosity 0))

1.5.8. desktop.el

The desktop package saves and restore the emacs session. This is especially useful when using the emacs daemon. Truth to be told, I'm thinking of getting rid of this in favour of something like recentf.

<2021-06-16 Wed> I've disabled desktop.el in favour of recentf, let's see how it goes!

(use-package desktop
  :straight nil
  :hook ((after-init . desktop-read)
         (after-init . desktop-save-mode))
  :custom ((desktop-base-file-name ".desktop")
           (desktop-base-lock-name ".desktop.lock")
           (desktop-restore-eager 8)
           (desktop-restore-frames nil)))

1.5.9. recentf

(require 'recentf)
(recentf-mode t)

(setq recentf-max-saved-items 80)

(defun op/find-recentf (file)
  "Use `completing-read' to open a recent FILE."
  (interactive (list (completing-read "Find recent file: "
                                      recentf-list)))
  (when file
    (find-file file)))

(define-key global-map (kbd "C-x C-r") #'op/find-recentf)

1.5.10. Gemini for thingatpoint

I don't exactly remember why, but this should enable the gemini:// scheme in some kind of buffers.

(use-package thingatpt
  :config
  (add-to-list 'thing-at-point-uri-schemes "gemini://"))

1.5.11. browse-url

Browse URLs, and add Gemini support.

(use-package browse-url
  :bind ("<f9>" . browse-url)
  :config
  (add-to-list 'browse-url-default-handlers
               '("\\`gemini:" . op/browse-url-elpher))
  (defun op/browse-url-elpher (url &rest _args)
    "Open URL with `elpher-go'."
    (elpher-go url)))

1.5.12. variable pitch mode (aka non monospace)

I like to use variable-pitch-mode in some text buffers (org and gemini usually), but sometimes I'd like a way to toggle it. While M-x variable-pitch-mode RET is a solution, binding a key is faster:

(define-key global-map (kbd "C-z V") #'variable-pitch-mode)

1.5.13. form-feed

The form-feed ASCII character (0x0C or 12) was used to signal the end of the page. It's still used (albeit not that frequently) in code to divide a file into logical "pages".

The form-feed packages changes how these ^L characters are rendered, it turns them into a line spanning the entire window width.

(use-package form-feed
  :config (global-form-feed-mode))

1.6. Minibuffer

all hail the minibuffer

This allows to launch a command that uses the minibuffer while already inside the minibuffer.

(setq enable-recursive-minibuffers t)

I'm generally pretty lazy, so why pressing shift to get the case right?

(setq completion-ignore-case t
      read-file-name-completion-ignore-case t
      read-buffer-completion-ignore-case t)

Misc enhancement to the minibuffer behaviour.

;; add prompt inidcator to `completing-read-multiple'.
(defun op/crm-indicator (args)
  (cons (concat "[CRM] " (car args))
        (cdr args)))
(advice-add #'completing-read-multiple :filter-args #'op/crm-indicator)

(setq minibuffer-prompt-properties
      '(read-only true cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

1.6.1. Marginalia

Enhances the minibuffer completions with additional informations

(use-package marginalia
  :custom (marginalia-annotators
           '(marginalia-annotators-heavy marginalia-annotators-light nil))
  :init (marginalia-mode))

1.6.2. Orderless

Controls the sorting of the minibuffer completions. I still have to tweak it a little bit, but I'm overall happy.

(use-package orderless
  :custom ((completion-styles '(orderless))
           (completion-category-defaults nil)
           (completion-category-overrides '((file (styles . (partial-completion)))))))

1.6.3. Consult

Consult enhances various command by using the minibuffer.

(use-package consult
  :bind (("C-c h" . consult-history)
         ("C-c m" . consult-mode-command)
         ("C-c b" . consult-bookmark)
         ("C-c k" . consult-kmacro)
         ("C-x M-:" . consult-complex-command)
         ("C-x b" . consult-buffer)
         ("C-x 4 b" . consult-buffer-other-window)
         ("C-x 5 b" . consult-buffer-other-frame)
         ("M-#" . consult-register-load)
         ("M-'" . consult-register-store)
         ("C-M-#" . consult-register)
         ("M-g e" . consult-compile-error)
         ("M-g g" . consult-goto-line)
         ("M-g M-g" . consult-goto-line)
         ("M-g o" . consult-outline)
         ("M-g m" . consult-mark)
         ("M-g k" . consult-global-mark)
         ("M-g i" . consult-imenu)
         ("M-g I" . consult-project-imenu)
         ("M-s f" . op/consult-find)
         ("M-s g" . consult-grep)
         ("M-s l" . consult-line)
         ("M-s k" . consult-keep-lines)
         ("M-s u" . consult-focus-lines)
         ("M-s e" . consult-isearch))
  :custom ((register-preview-delay 0)
           (register-preview-function #'consult-register-format)
           ;; use consult to select xref locations with preview
           (xref-show-xrefs-function #'consult-xref)
           (xref-show-definitions-function #'consult-xref)
           (consult-narrow-key "<")
           (consult-project-root #'project-roots)
           (consult-find-args "find .")
           (consult-grep-args "grep --null --line-buffered --ignore-case -RIn"))
  :init
  (advice-add #'register-preview :override #'consult-register-window)

  :config
  ;; make narrowing help available in the minibuffer.
  (define-key consult-narrow-map (vconcat consult-narrow-key "?")
              #'consult-narrow-help)

  ;; a find-builder that works with OpenBSD' find
  (defun op/consult--find-builder (input)
  "Build command line given INPUT."
  (pcase-let* ((cmd (split-string-and-unquote consult-find-args))
               (type (consult--find-regexp-type (car cmd)))
               (`(,arg . ,opts) (consult--command-split input))
               (`(,re . ,hl) (funcall consult--regexp-compiler arg type)))
    (when re
      (list :command
            (append cmd
                    (cdr (mapcan
                          (lambda (x)
                            `("-and" "-iname"
                              ,(format "*%s*" x)))
                          re))
                    opts)
            :highlight hl))))

  (defun op/consult-find (&optional dir)
    (interactive "P")
    (let* ((prompt-dir (consult--directory-prompt "Find" dir))
           (default-directory (cdr prompt-dir)))
      (find-file (consult--find (car prompt-dir) #'op/consult--find-builder "")))))

1.6.4. Affe

This is a new-ish package from the same author of consult and marginalia. Honestly, I still have to use it, so this is more a remainder of its existance.

(use-package affe
  :straight (:type git :host github :repo "minad/affe")
  :after orderless
  :custom ((affe-regexp-function #'orderless-pattern-compiler)
           (affe-highlight-function #'orderless-highlight-matches)))

1.6.5. Vertico

Vertico is just like selectrum or icomplete-vertical. It's written by the same author of consult, so at this point I thought of keeping the streak and using this

<2021-06-08 Tue> vertico is too damn slow here: M-x halts emacs for like 3-4 seconds before any UI show up. I should spend some time profiling it, but for the time being switch back to Selectrum.

<2021-11-04 Thu> giving it another try

(use-package vertico
  :config (vertico-mode))

1.6.6. Selectrum

<2021-11-04 Thu> I'm gonna give vertico another try, this is not tangled anymore.

(use-package selectrum
  :custom ((selectrum-highlight-candidates-function #'orderless-highlight-matches)
           (orderless-skip-highlighting (lambda () selectrum-is-active)))
  :config
  (selectrum-mode +1)

  <<selectrum-embark>>)

Unlike vertico, selectrum needs something more to integrate with embark. This is taken from the Embark wiki:

(defun op/refresh-selectrum ()
  (setq selectrum--previous-input-string nil))
(add-hook 'embark-pre-action-hook #'op/refresh-selectrum)

1.6.7. embark

Embark provides custom actions on the minibuffer (technically everywhere, but I only use it in the minibuffer.)

embark-become is a command I should use more. It provides a way to "change" the minibuffer while retaining the input. For instance, I often do C-x b <something> just to see that I haven't a buffer, and then C-x C-f to open it. With embark-become I can transform the switch-buffer command to the find-file command without the abort C-g in between and retain the input.

(use-package embark
  :straight (:type git :host github :repo "oantolin/embark")
  :bind (("C-." . embark-act)
         :map minibuffer-local-completion-map
              ("M-t" . embark-act)
              ("M-h" . embark-become)
              :map minibuffer-local-map
              ("M-t" . embark-act)
              ("M-h" . embark-become)))

1.7. Completions

I'm trying corfu at the moment. It has still some bugs for me, but I haven't found a way to reproduce, so I can't report them.

(use-package corfu
  :custom (corfu-cycle t)
  :config
  (corfu-global-mode +1))

1.8. Window management

This is a bit topic for me, and the only thing that I'm not completely happy with. Fortunately, as time goes, I'm less annoyed with it, bit by bit.

1.8.1. The window package

This does a lot of stuff, from the split logic to customising the thresholds. One of these days I'll split in multiple pieces.

(use-package window
  :straight nil
  :bind (("C-x +" . balance-windows-area))
  :custom
  ((window-combination-resize t)
   (even-window-sizes 'heigth-only)
   (window-sides-vertical nil)
   (switch-to-buffer-in-dedicated-window 'pop)
   (split-height-threshold 160)
   (split-width-threshold 110)
   (split-window-preferred-function #'op/split-window-sensibly))
  :config
  (defun op/split-window-prefer-horizontal (&optional window)
    "Based on `split-window-sensibly', but designed to prefer a horizontal split.
It prefers windows tiled side-by-side.  Taken from
emacs.stackexchange.com.  Optional argument WINDOW is the current
window."
    (let ((window (or window (select-window))))
      (or (and (window-splittable-p window t)
               ;; split window horizontally
               (with-selected-window window
                 (split-window-right))))
      (and (window-splittable-p window)
           ;; split window vertically
           (with-selected-window window
             (split-window-below)))
      (and
       ;; if window is the only usable window on its frame and is not
       ;; the minibuffer window, try to split it horizontally
       ;; disregarding the value of `split-height-threshold'.
       (let ((frame (window-frame window)))
         (or (eq window (frame-root-window frame))
             (catch 'done
               (walk-window-tree (lambda (w)
                                   (unless (or (eq w window)
                                               (window-dedicated-p w))
                                     (throw 'done nil)))
                                 frame)
               t)))
       (not (window-minibuffer-p window))
       (let ((split-width-threshold 0))
         (when (window-splittable-p window t)
           (with-selected-window window
             (split-window-right)))))))

  (defun op/split-window-sensibly (&optional window)
    "Splitting window function.
Intended to use as `split-window-preferred-function'.  Also taken
from stackexchange with edits.  Optional argument WINDOW is the
window."
    (let ((window (or window (selected-window))))
      (with-selected-window window
        (if (> (window-total-width window)
               (* 2 (window-total-width window)))
            (op/split-window-sensibly window)
          (split-window-sensibly window))))))

1.8.2. Placement with shackle

Shackle is an easy way to customise the display rules for windows rather than messing up with display-buffer-alist.

(use-package shackle
  :custom
  ((shackle-rules
    (let ((repls "\\*\\(cider-repl\\|sly-mrepl\\|ielm\\)")
          (godot "\\*godot - .*\\*")
          (vcs   "\\*\\(Flymake\\|Package-Lint\\|vc-\\(git\\|got\\) :\\).*")
          (elfeed "\\*elfeed-entry\\*")
          (vmd    "\\*vmd console .*"))
      `((compilation-mode :noselect t
                          :align above
                          :size 0.2)
        ("*Async Shell Command*" :ignore t)
        (,repls :regexp t
                :align below
                :size 0.3)
        (,godot :regexp t
                :align t
                :size 0.3)
        (occur-mode :select t
                    :align right
                    :size 0.3)
        (diff-mode :select t)
        (help-mode :select t
                   :align left
                   :size 0.3)
        (,vcs :regexp t
              :align above
              :size 0.15
              :select t)
        (,elfeed :regexp t
                 :align t
                 :select t
                 :size 0.75)
        (,vmd :regexp t
              :align below
              :select t
              :size 0.3))))
   (shackle-default-rule nil ; '(:inhibit-window-quit t)
                         ))
  :config (shackle-mode))

1.8.3. History

Winner saves the window placement and allows to travel back and forth in time. Also add an hydra for that for extra comfort.

(use-package winner
  :straight nil
  :config
  (winner-mode 1)
  (defhydra hydra-winner (winner-mode-map "C-c")
    ("<left>" (progn (winner-undo)
                     (setq this-command 'winner-undo))
     "undo")
    ("h" (progn (winner-undo)
                (setq this-command 'winner-undo))
     "undo")
    ("<right>" winner-redo "redo")
    ("l" winner-redo "redo")
    ("q" nil :exit nil)))

1.8.4. Switch window

The builtin windmove package provides function to move between windows in the same frame easily. Unfortunately, I don't use this package often enough, I usually C-x o.

(defhydra hydra-windmove (global-map "M-r")
  ("h" windmove-left)
  ("j" windmove-down)
  ("k" windmove-up)
  ("l" windmove-right)
  ("q" nil :exit nil))
(hydra-set-property 'hydra-windmove :verbosity 0)

1.8.5. Layouts

transpose-frame provides various function to change the window layout in the current frame. Since my memory is pretty limited, an hydra is needed.

(use-package transpose-frame
  :bind ("C-#" . my/hydra-window/body)
  :commands (transpose-frame flip-frame flop-frame
                             rotate-frame rotate-frame-clockwise
                             rotate-frame-anti-anticlockwise)
  :config
  (defhydra hydra-window (:hint nil)
    "
^File/Buffer^      ^Movements^        ^Misc^              ^Transpose^
^^^^^^^^------------------------------------------------------------------------------
_b_ switch buffer  ^ ^ hjkl           _0_   delete        _t_     transpose frame
_f_ find file      _o_ other window   _1_   delete other  _M-f_   flip frame
_s_ save conf      _O_ OTHER window   _2_   split below   _M-C-f_ flop frame
_r_ reload conf    ^ ^                _3_   split right   _M-s_   rotate frame
^ ^                ^ ^                _SPC_ balance       _M-r_   rotate clockw.
^^^^-------------------------------   _v_   split horiz.  _M-C-r_ rotate anti clockw.
_?_ toggle help    ^ ^                _-_   split vert.
^ ^                ^ ^                _C-l_ recenter line
"
    ("?" (hydra-set-property 'hydra-window :verbosity
                             (if (= (hydra-get-property 'hydra-window :verbosity) 1)
                                 0 1)))

    ("b" switch-to-buffer)
    ("f" (call-interactively #'find-file))

    ("s" window-configuration-to-register)
    ("r" jump-to-register)

    ("k" windmove-up)
    ("j" windmove-down)
    ("h" windmove-left)
    ("l" windmove-right)

    ("o" (other-window 1))
    ("O" (other-window -1))

    ("C-l" recenter-top-bottom)

    ("0" delete-window)
    ("1" delete-other-windows)
    ("2" split-window-below)
    ("3" split-window-right)

    ;; v is like a |, no?
    ("v" split-window-horizontally)
    ("-" split-window-vertically)

    ("SPC" balance-windows)

    ("t" transpose-frame)
    ("M-f" flip-frame)
    ("M-C-f" flop-frame)
    ("M-s" rotate-frame)
    ("M-r" rotate-frame-clockwise)
    ("M-C-r" rotate-frame-anti-anticlockwise)

    ("q" nil :exit nil)
    ("RET" nil :exit nil)
    ("C-g" nil :exit nil))

  (defun my/hydra-window/body ()
    (interactive)
    (hydra-set-property 'hydra-window :verbosity 0)
    (hydra-window/body)))

1.8.6. Side windows

Side windows are an interesting concept. Emacs reserve an optional space at the top, bottom, left and right of the frame for these side windows. You can think of them as a dockable space, akin to the panels in IDEs.

I'm finding useful to keep an IRC buffer at the bottom of the frame, to avoid jumping from the "code" frame to the "chat" frame or switch buffers continuously.

The following functions helps achieve this:

(defun op/buffer-to-side-window (place)
  "Place the current buffer in the side window at PLACE."
  (interactive (list (intern
                      (completing-read "Which side: "
                                       '(top left right bottom)))))
  (let ((buf (current-buffer)))
    (display-buffer-in-side-window
     buf `((window-height . 0.15)
           (side . ,place)
           (slot . -1)
           (window-parameters . ((no-delete-other-windows . t)
                                 (no-other-window t)))))
    (delete-window)))

See that no-other-window? it means that the side window won't be accessible by other-window means (i.e. C-x o). Which brings us to Ace Windows.

1.8.7. Ace window

(use-package ace-window
  :bind (("C-z o" . ace-window))
  :custom ((aw-keys '(?a ?o ?e ?u ?i ?d ?h ?t ?n ?s))
           (aw-dispatch-always t)
           (aw-minibuffer-flag t)))

1.9. Text editing

1.9.1. Misc

Usually I don't need to waste space for a column with the line numbers, it's something that it's just not useful. Anyway, there are specific times where this is handy, so reserve a key for it.

(define-key global-map (kbd "C-z n") #'display-line-numbers-mode)

Better defaults

(define-key global-map (kbd "M-SPC") #'cycle-spacing)
(define-key global-map (kbd "M-u")   #'upcase-dwim)
(define-key global-map (kbd "M-l")   #'downcase-dwim)
(define-key global-map (kbd "M-c")   #'capitalize-dwim)

Scroll-lock is sometimes useful to re-read the code. The idea is that command that usually moves the point (e.g. next-line) now scroll the buffer keeping the point in the same "visual" position. I've also got a keyboard with a Scr Lk key, so why don't use it?

The only small annoyance is that I've bound <up> and <down> to some variants of copy-from-above-command, so revert that when we're on scroll-lock mode.

Unfortunately, this doesn't seem to work :/

(use-package scroll-lock
  :straight nil
  :bind (:map scroll-lock-mode
              ("<down>" . scroll-lock-next-line)
              ("<up>" . scroll-lock-previous-line)))

1.9.2. Auto-saving

I have a problem with compulsive saving. I type C-x C-s every few keystroke to write the buffer I'm editing.

I'm trying to make emacs do that for me, so make it save early instead of waiting me to press the combination. Normally emacs uses an auto-save file, but if the global minor mode auto-save-visited-mode is active, it actually saves the file.

(auto-save-visited-mode +1)

This is still not enough. By default it saves every 5 seconds, which is obviously wrong. Five seconds are like an eternity! I'm auto-saving every two seconds, but I'm tempted to drop to one second.

(setq auto-save-visited-interval 2)

I'm only scared of the consequences of this over TRAMP. I don't use it very often, but I guess that something to disable locally auto-save-visited-mode could be implemented.

1.9.3. imenu

Imenu is a mean of navigation in a buffer. It can act like a TOC, for instance.

Prevent stale entries by always rescan the buffer

(setq imenu-auto-rescan t)

1.9.4. Filling

This is a useful function copied from somewhere I don't remember, sorry unknown author!

It makes fill-paragraph "toggable": M-q once to fill, M-q again to un-fill!

(defun op/fill-or-unfill (fn &optional justify region)
  "Meant to be an adviced :around `fill-paragraph'.
FN is the original `fill-column'.  If `last-command' is
`fill-paragraph', unfill it, fill it otherwise.  Inspired from a
post on endless parentheses.  Optional argument JUSTIFY and
REGION are passed to `fill-paragraph'."
  (let ((fill-column
         (if (eq last-command 'fill-paragraph)
             (progn (setq this-command nil)
                    (point-max))
           fill-column)))
    (funcall fn justify region)))
(advice-add 'fill-paragraph :around #'op/fill-or-unfill)

1.9.5. Transpose

This is an idea that I stole from prot' dotemacs. It augments the various transpose-* commands so they respect the region: if (use-region-p) then transpose the thing at the extremes of the region, otherwise operates as usual.

(the code is somewhat different from prot, but the idea is the same)

(defmacro op/deftranspose (name scope key doc)
  "Macro to produce transposition functions.
NAME is the function's symbol.  SCOPE is the text object to
operate on.  Optional DOC is the function's docstring.

Transposition over an active region will swap the object at
mark (region beginning) with the one at point (region end).

It can optionally define a key for the defined function in the
`global-map' if KEY is passed.

Originally from protesilaos' dotemacs."
  (declare (indent defun))
  `(progn
     (defun ,name (arg)
       ,doc
       (interactive "p")
       (let ((x (intern (format "transpose-%s" ,scope))))
         (if (use-region-p)
             (funcall x 0)
           (funcall x arg))))
     ,(when key
        `(define-key global-map (kbd ,key) #',name))))

(op/deftranspose op/transpose-lines "lines" "C-x C-t"
  "Transpose lines or swap over active region.")

(op/deftranspose op/transpose-paragraphs "paragraphs" "C-S-t"
  "Transpose paragraph or swap over active region.")

(op/deftranspose op/transpose-sentences "sentences" "C-x M-t"
  "Transpose sentences or swap over active region.")

(op/deftranspose op/transpose-sexps "sexps" "C-M-t"
  "Transpose sexps or swap over active region.")

(op/deftranspose op/transpose-words "words" "M-t"
  "Transpose words or swap over active region.")

A command I have to try to use more is transpose-regions

(define-key global-map (kbd "C-x C-M-t") #'transpose-regions)
1.9.5.1. TODO cycle-region is worth a try

1.9.6. Narrow to what I mean

Narrowing is really a powerful mechanism of Emacs. It lets one show only a part of a buffer. Unfortunately, the default keys aren't that great, and there's space for a do what I mean command. The following is adapted from a post on endless parentheses.

(defun op/narrow-or-widen-dwim (p)
  "Widen if the buffer is narrowed, narrow-dwim otherwise.
Dwim means: region, org-src-block, org-subtree or defun,
whichever applies first.  Narrowing to org-src-blocks actually
calls `org-edit-src-code'.

With prefix P, don't widen, just narrow even if buffer is already
narrowed.  With P being -, narrow to page instead of to defun.

Taken from endless parentheses."
  (interactive "P")
  (declare (interactive-only))
  (cond ((and (buffer-narrowed-p) (not p)) (widen))
        ((region-active-p)
         (narrow-to-region (region-beginning)
                           (region-end)))
        ((derived-mode-p 'org-mode)
         ;; `org-edit-src-code' isn't a real narrowing
         (cond ((ignore-errors (org-edit-src-code) t))
               ((ignore-errors (org-narrow-to-block) t))
               (t (org-narrow-to-subtree))))
        ((eql p '-) (narrow-to-page))
        (t (narrow-to-defun))))

(define-key global-map (kbd "C-c w") #'op/narrow-or-widen-dwim)

1.9.7. White spaces

Nothing bothers me more than trailing white spaces, so enable whitespace-mode for programming and text buffers.

Also, I like to use TAB to trigger the completions-at-point, and while there customize tab behaviours.

Furthermore, use hard tabs by default; op/disable-tabs will be added as mode hook for buffers that needs "soft" tabs.

(use-package whitespace
  :straight nil
  :custom ((whitespace-style '(face trailing))
           (backward-delete-char-untabify-method 'hungry)
           (tab-always-indent 'complete)
           (tab-width 8))
  :hook ((conf-mode . op/enable-tabs)
         (text-mode . op/enable-tabs)
         (prog-mode . op/enable-tabs)
         (prog-mode . whitespace-mode)
         (text-mode . whitespace-mode))
  :config
  (setq-default indent-tabs-mode t)

  (defun op/enable-tabs ()
    "Enable `indent-tabs-mode' in the current buffer."
    (interactive)
    (setq-local indent-tabs-mode t))

  (defun op/disable-tabs ()
    "Disable `indent-tabs-mode' in the current buffer."
    (interactive)
    (setq-local indent-tabs-mode nil))

  ;; TODO: remove
  (dolist (hook '(emacs-lisp-mode-hook))
    (add-hook hook 'op/disable-tabs)))

1.9.8. Version Control

1.9.8.1. Backups

Albeit not exactly a version control system, the backup system is indeed very usefuly. By defaults backup are created alongside the original files. I don't like that, and prefer to move everything into a separate backup directory.

By the way, it's incredibly useful to keep backups. I once deleted a file, and manage to recover it because of Emacs' backups!

(defconst op/backup-dir
  (expand-file-name "backups" user-emacs-directory))

(unless (file-exists-p op/backup-dir)
  (make-directory op/backup-dir))

(setq backup-directory-alist `(("." . ,op/backup-dir)))
1.9.8.2. Log

It's handy to have auto-fill-mode enabled while writing the commit message inside a log-edit-mode buffer. It saves a few M-q

(use-package log-edit
  :straight nil
  :hook ((log-edit-mode . auto-fill-mode)))
1.9.8.3. Got

Game of Trees is a version control system written by Stefan Sperling.

Game of Trees (Got) is a version control system which prioritizes ease of use and simplicity over flexibility.

Got is still under development; it is being developed on OpenBSD and its main target audience are OpenBSD developers.

Got uses Git repositories to store versioned data. Git can be used for any functionality which has not yet been implemented in Got. It will always remain possible to work with both Got and Git on the same repository.

I'm trying to complete vc-got, a VC backend for Got.

(use-package vc-got
  :straight nil
  :load-path "~/w/vc-got/"
  :defer t
  :init
  (add-to-list 'vc-handled-backends 'Got)
  (add-to-list 'vc-directory-exclusion-list ".got"))

1.9.9. Auto insert mode

auto-insert-mode is an elisp library that automatically inserts text into new buffers based on the file extension or major mode. For instance, trying to open a .el (Emacs LISP) file will insert the entire GPL notice, and also other stuff. This automatic insert can be interactive, too.

(add-hook 'after-init-hook #'auto-insert-mode)

(with-eval-after-load 'autoinsert
  <<c-skeleton>>
  <<go-skeleton>>
  <<clojure-skeleton>>
  <<perl-skeleton>>
  <<svg-skeleton>>)

I prefer the ISC license, and tend to use that for almost all the C I write:

(define-auto-insert '("\\.c\\'" . "C skeleton")
  '("Description: "
    "/*" \n
    > "* Copyright (c) " (format-time-string "%Y") " " user-full-name " <" user-mail-address ">" \n
    > "*" \n
    > "* Permission to use, copy, modify, and distribute this software for any" \n
    > "* purpose with or without fee is hereby granted, provided that the above" \n
    > "* copyright notice and this permission notice appear in all copies." \n
    > "*" \n
    > "* THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES" \n
    > "* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF" \n
    > "* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR" \n
    > "* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES" \n
    > "* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN" \n
    > "* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF" \n
    > "* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE." \n
    > "*/" \n
    \n
    > _ \n
    \n))

I added a skeleton for go files:

(define-auto-insert
  '("\\.go\\'" . "Go skeleton")
  '("Short description: "
    "package "
    (completing-read "Go package: "
                     `("main" ,(file-name-nondirectory
                                (directory-file-name default-directory))))
    \n \n > _ \n))

The clojure skeleton inserts the correct ns form at the top of the buffer:

(defun op/cloj-ns ()
  "Return the clojure namespace (as string) for the current file.
Stolen from the ``ns'' yasnippet from yasnippet-snippets."
  (cl-flet ((try-src-prefix
             (path src-prfx)
             (let ((parts (split-string path src-prfx)))
               (when (= (length parts) 2)
                 (cadr parts)))))
    (let* ((p (buffer-file-name))
           (p2 (cl-first
                (cl-remove-if-not #'identity
                                  (mapcar (lambda (prfx)
                                            (try-src-prefix p prfx))
                                          '("/src/cljs/" "/src/cljc/" "/src/clj/" "/src/" "/test/")))))
           (p3 (file-name-sans-extension p2))
           (p4 (mapconcat #'identity
                          (split-string p3 "/")
                          ".")))
      (replace-regexp-in-string "_" "-" p4))))

(define-auto-insert
  '("\\.\\(clj\\|cljs\\|cljc\\)\\'" . "Clojure skeleton")
  '("Short description: "
    "(ns " (op/cloj-ns) ")" \n \n
    > _ \n))
(define-auto-insert '("\\.pl\\'" . "Perl skeleton")
  '("Name: "
    "#!/usr/bin/env perl" \n
    "#" \n
    "# Copyright (c) " (format-time-string "%Y") " " user-full-name " <" user-mail-address ">" \n
    "#" \n
    "# Permission to use, copy, modify, and distribute this software for any" \n
    "# purpose with or without fee is hereby granted, provided that the above" \n
    "# copyright notice and this permission notice appear in all copies." \n
    "#" \n
    "# THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES" \n
    "# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF" \n
    "# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR" \n
    "# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES" \n
    "# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN" \n
    "# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF" \n
    "# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE." \n
    \n
    "use v5.10;" \n
    "use strict;" \n
    "use warnings;" \n \n
    _ \n \n
    "__END__" "\n\n"
    "=head1 NAME" "\n\n"
    str "\n\n"
    "=head1 SYNOPSIS" "\n\n\n"
    "=head1 DESCRIPTION" "\n\n\n"
    "=cut" "\n"))

I'm also writing some small SVGs in Emacs, and I keep forgetting the right xmlns

(define-auto-insert '("\\.svg\\'" . "SVG Skeleton")
  '("Name: "
    "<svg xmlns=\"http://www.w3.org/2000/svg\"" \n
    "    version=\"1.1\"" \n
    "    width=\"\"" \n
    "    height=\"\">"
    "  " _ \n
    "</svg>"))

1.9.10. DIRED

By default dired will show, other than the files, also various other data about every file (like owner, permissions, …) in a format similar to ls -lah. This is indeed useful, but usually I don't need to see all that informations, and they steal precious space, hence dired-hide-details-mode.

In the same spite, most of the time I'm not interested in certain kinds of files (like object files or similar garbage), so hide them too by default with dired-omit-mode.

Finally, wdired is awesome, reserve a key for it!

(use-package dired
  :straight nil
  :hook ((dired-mode . dired-hide-details-mode)
         (dired-mode . dired-omit-mode))
  :bind (:map dired-mode-map
              ("C-c w" . wdired-change-to-wdired-mode))
  :config
  (require 'dired-x)
  (setq dired-listing-switches "-lahF"
        dired-dwim-target t
        dired-deletion-confirmer 'y-or-n-p
        dired-omit-files "\\`[.]?#\\|\\`[.][.]?\\'\\|*\\.o\\`\\|*\\.log\\`"))

1.9.11. Project

(with-eval-after-load 'project
  <<project-try-local>>)

This is a bulit-in package to manage "projects" (that is, directory trees commonly called "projects")

It provides various commands that operate on the project, like project-find-file and project-query-replace-regexp.

By default a project is something that is managed by a VCS, such as git. However, sometimes is useful to mark something as a project without actually create a repo for it. This code, adapted from something that I found online I don't remember where, adds another implementation for the project backend that consider a project something that has a .project file.

(defun op/project-try-local (dir)
  "Determine if DIR is a local project.
DIR must include a .project file to be considered a project."
  (when-let (root (locate-dominating-file dir ".project"))
    (cons 'local root)))

(add-to-list 'project-find-functions #'op/project-try-local)

(cl-defmethod project-root ((project (head local)))
  (cdr project))
1.9.11.1. TODO add some mechanism to ignore files

1.9.12. Scratchpads

Scratchpads are useful. I wrote a small package to create custom scratchpads on-the-fly. By default it creates a *scratch*<n> buffer in the current major-mode, but the starting mode can be chosen by invoking scratchpad-new-scratchpad with a prefix argument.

(use-package scratchpads
  :bind ("C-z s" . scratchpads-new-scratchpad)
  :straight nil)

1.9.13. Occur & loccur

Occur is a grep-like functionality for Emacs. It populates the *occur* buffer with the lines matching a certain regexp in the current buffer. It's super-useful.

(use-package replace
  :straight nil
  :bind (("C-c o" . occur)))

loccur is similar, but instead of using a separate buffer, it visually hides all the non-matching lines, also super useful!

(use-package loccur
  :bind (("C-c O" . loccur)))

1.9.14. hideshow

Hideshow is a built-in package to fold section of code. It has some really awkward keybindings under C-c @, but otherwise is nice, sometimes.

(add-hook 'prog-mode-hook #'hs-minor-mode)

1.9.15. Smartparens

Smartparens has become my go-to package for managing parethesis and the like. The peculiar thing is that, unlike packages such as paredit, it works on any language, not only lisp-y ones.

(use-package smartparens
  :bind (:map smartparens-mode-map
              ("C-M-f" . sp-forward-sexp)
              ("C-M-b" . sp-backward-sexp)

              ("C-M-a" . sp-beginning-of-sexp)
              ("C-M-e" . sp-end-of-sexp)
              ("C-M-n" . sp-next-sexp)
              ("C-M-p" . sp-previous-sexp)

              ("C-(" . sp-forward-barf-sexp)
              ("C-)" . sp-forward-slurp-sexp)
              ("C-{" . sp-backward-barf-sexp)
              ("C-}" . sp-backward-slurp-sexp)

              ("C-k" . sp-kill-hybrid-sexp)

              ("C-," . sp-rewrap-sexp)

              :map emacs-lisp-mode-map
              (";" . sp-comment)

              :map lisp-mode-map
              (";" . sp-comment))
  :hook ((prog-mode . turn-on-smartparens-strict-mode)
         (web-mode . op/sp-web-mode)
         (LaTeX-mode . turn-on-smartparens-strict-mode))
  :custom ((sp-highlight-pair-overlay nil))
  :config
  (require 'smartparens-config)

  (with-eval-after-load 'clojure-mode
    (define-key clojure-mode-map ";" #'sp-comment))

  (with-eval-after-load 'scheme-mode
    (define-key scheme-mode-map ";" #'sp-comment))

  (sp-with-modes 'org-mode
    (sp-local-pair "=" "=" :wrap "C-="))

  (bind-key [remap c-electric-backspace] #'sp-backward-delete-char
            smartparens-strict-mode-map)

  (sp-local-pair 'log-edit-mode "`" "'")

  (defun op/sp-web-mode ()
    (setq web-mode-enable-auto-pairing nil))

  (defun op/newline-indent (&rest _ignored)
    (split-line)
    (indent-for-tab-command))

  (let ((c-like '(awk-mode c++mode cc-mode c-mode css-mode go-mode java-mode
                           js-mode json-mode python-mode web-mode es-mode
                           perl-mode lua-mode)))
    (dolist (x `(("{" . ,c-like)
                 ("[" . ,c-like)
                 ("(" . (sql-mode ,@c-like))))
      (dolist (mode (cdr x))
        (sp-local-pair mode (car x) nil :post-handlers
                       '((op/newline-indent "RET")
                         (op/newline-indent "<return>"))))))

  (defun op/inside-comment-or-string-p ()
    "T if point is inside a string or comment."
    (let ((s (syntax-ppss)))
      (or (nth 4 s)                     ;comment
          (nth 3 s))))

  (defun op/current-line-str ()
    "Return the current line as string."
    (buffer-substring-no-properties (line-beginning-position)
                                    (line-end-position)))

  (defun op/maybe-add-semicolon-paren (_id action _ctx)
    "Insert semicolon after parens when appropriat.
Mainly useful in C and derived, and only when ACTION is insert."
    (when (eq action 'insert)
      (save-excursion
        ;; caret is between parens (|)
        (forward-char)
        (let ((line (op/current-line-str)))
          (when (and (looking-at "\\s-*$")
                     (not (string-match-p
                           (regexp-opt '("if" "else" "switch" "for" "while"
                                         "do" "define")
                                       'words)
                           line))
                     (string-match-p "[\t ]" line)
                     (not (op/inside-comment-or-string-p)))
            (insert ";"))))))

  (let ((c-like-modes-list '(c-mode c++-mode java-mode perl-mode)))
    (sp-local-pair c-like-modes-list "(" nil
                   :post-handlers
                   '(:add op/maybe-add-semicolon-paren)))

  (defhydra hydra-sp (:hint nil)
    "
 Moving^^^^                       Slurp & Barf^^   Wrapping^^            Sexp juggling^^^^               Destructive
------------------------------------------------------------------------------------------------------------------------
 [_a_] beginning  [_n_] down      [_h_] bw slurp   [_R_]   rewrap        [_S_] split   [_t_] transpose   [_c_] change inner  [_w_] copy
 [_e_] end        [_N_] bw down   [_H_] bw barf    [_u_]   unwrap        [_s_] splice  [_A_] absorb      [_C_] change outer
 [_f_] forward    [_p_] up        [_l_] slurp      [_U_]   bw unwrap     [_r_] raise   [_E_] emit        [_k_] kill          [_g_] quit
 [_b_] backward   [_P_] bw up     [_L_] barf       [_(__{__[_] wrap (){}[]   [_j_] join    [_o_] convolute   [_K_] bw kill       [_q_] quit"
    ("?" (hydra-set-property 'hydra-sp :verbosity 1))

    ;; moving
    ("a" sp-beginning-of-sexp)
    ("e" sp-end-of-sexp)
    ("f" sp-forward-sexp)
    ("b" sp-backward-sexp)
    ("n" sp-down-sexp)
    ("N" sp-backward-down-sexp)
    ("p" sp-up-sexp)
    ("P" sp-backward-up-sexp)

    ;; slurping & barfing
    ("h" sp-backward-slurp-sexp)
    ("H" sp-backward-barf-sexp)
    ("l" sp-forward-slurp-sexp)
    ("L" sp-forward-barf-sexp)

    ;; wrapping
    ("R" sp-rewrap-sexp)
    ("u" sp-unwrap-sexp)
    ("U" sp-backward-unwrap-sexp)
    ("(" sp-wrap-round)
    ("[" sp-wrap-square)
    ("{" sp-wrap-curly)

    ;; sexp juggling
    ("S" sp-split-sexp)
    ("s" sp-splice-sexp)
    ("r" sp-raise-sexp)
    ("j" sp-join-sexp)
    ("t" sp-transpose-sexp)
    ("A" sp-absorb-sexp)
    ("E" sp-emit-sexp)
    ("o" sp-convolute-sexp)

    ;; destructive editing
    ("c" sp-change-inner :exit t)
    ("C" sp-change-enclosing :exit t)
    ("k" sp-kill-sexp)
    ("K" sp-backward-kill-sexp)
    ("w" sp-copy-sexp)

    ("q" nil)
    ("g" nil))

  (define-key global-map (kbd "s-c")
    (lambda ()
      (interactive)
      (hydra-set-property 'hydra-sp :verbosity 0)
      (hydra-sp/body))))
1.9.15.1. TODO the configuration is quite long, can it be made modular?

1.9.16. Flymake

Flymake marks errors in buffer, using various means. LSP is one of those. For starters, enable it for every prog-mode buffer

(add-hook 'prog-mode-hook #'flymake-mode)

Tweak its settings a bit

(setq flymake-fringe-indicator-position 'left-fringe
      flymake-suppress-zero-counters t
      flymake-start-on-flymake-mode t
      flymake-no-changes-timeout nil
      flymake-start-on-save-buffer t
      flymake-proc-compilation-prevents-syntax-check t
      flymake-wrap-around nil)

and make a hydra for it

(with-eval-after-load 'flymake
  (defhydra hydra-flymake (flymake-mode-map "C-c !")
    ("n" flymake-goto-next-error)
    ("p" flymake-goto-prev-error)
    ("RET" nil :exit t)
    ("q" nil :exit t)))

1.9.17. Flyspell and friends

Flyspell is Flymake, but for natural languages! /s

(add-hook 'text-mode-hook #'flyspell-mode)
1.9.17.1. guess language

One annoying thing of not being a native English speaker is that I need Emacs to handle more than one language. That means constantly M-x ispell-change-dictionary, or one cane use guess-language!

It uses a statistical method to detect the language, which seems to work pretty well for English and Italian. It even supports multiple languages in the same buffer (as long as they appear in different paragraphs). The only drawback is that sometimes Emacs gets stuck executing ispell, but a pkill -USR2 on the server pid fixes it.

(use-package guess-language
  :hook (text-mode . guess-language-mode)
  :config
  (setq guess-language-langcodes '((en . ("en_GB" "English"))
                                   (it . ("it" "Italian")))
        guess-language-languages '(en it)
        guess-language-min-paragraph-length 45))

1.9.18. Typo(graphical stuff)

Typo transforms certain character into their "typographical" counterpart. I like to use it when writing in my blog, so enable it for gemini-mode.

(use-package typo
  :hook ((gemini-mode . typo-mode))
  :config
  (push '("Italian" "“" "”" "‘" "’" "«" "»")
        typo-quotation-marks))

Olivetti mode "centers" the buffer, it's nice when writing text:

(use-package olivetti
  :hook ((gemini-mode . olivetti-mode)
         (markdown-mode . olivetti-mode)))

I also do typos pretty often, and abbrev is handy for those occasions and accents (like "perchè" instead of "perché").

my-abbrev is a package-like file where I store the abbreviations I need.

(use-package my-abbrev
  :straight nil)

1.9.19. hippie expand

This is a "dumb" completion method. It tries a couple of method to complete the word before the cursor. Turns out, for how rudimentary it may be, it's often precise.

(define-key global-map (kbd "M-/") #'hippie-expand)

(setq hippie-expand-try-functions-list
      '(try-expand-dabbrev
        try-expand-dabbrev-all-buffers
        try-expand-dabbrev-from-kill
        try-complete-file-name-partially
        try-complete-file-name
        try-expand-all-abbrevs
        try-expand-list
        try-expand-line
        try-complete-lisp-symbol-partially
        try-complete-lisp-symbol))

1.9.20. isearch

Some very small tweaks for isearch

(setq isearch-lazy-count t
      search-whitespace-regexp ".*?"
      isearch-allow-scroll 'unlimited)

1.9.21. etags

Reload tags without asking

(setq tags-revert-without-query 1)

1.9.22. view mode

Sometimes it's handy to make a buffer read-only. Also, define some key to easily navigate in read-only buffers.

(use-package view
  :straight nil
  :bind (("C-x C-q" . view-mode)
         :map view-mode-map
         ("n" . next-line)
         ("p" . previous-line)
         ("l" . recenter-top-bottom)))

1.9.23. pdf-tools

Not really text-related, but still.

(use-package pdf-tools
  :bind (:map pdf-view-mode-map
              ("C-s" . isearch-forward))
  :custom (pdf-annot-activate-created-annotations t)
  :init
  (pdf-tools-install))

Works great on OpenBSD. It would be cool to make a package out of it, but since it requires tablist from melpa it may be a problem?

<2021-06-23 Wed> see this post to hints on how to integrate it with AucTeX.

1.9.24. avy

I definitely need to use it more. It allows to quickly jump around, both in the same and in other buffers.

(use-package avy
  :custom ((avy-keys '(?s ?n ?t ?h ?d ?i ?u ?e ?o ?a)))
  :bind (("M-g c" . avy-goto-char)
         ("M-g C" . avy-goto-char-2)
         ("M-g w" . avy-goto-word-1)
         ("M-g f" . avy-goto-line)
         :map isearch-mode-map
         ("C-'" . avy-isearch)))

1.9.25. iedit

I tried to use multiple-cursor, but I just fail. iedit does 99% of what I need.

The following is a small tweak for it, maybe it's unnecessary as I haven't read the documentation in depth.

(use-package iedit
  :bind (("C-;" . op/iedit-dwim))
  :config
  (defun op/iedit-dwim (arg)
    "Start iedit but do what I mean.
With a prefix (i.e. non-nil ARG) just execute `iedit-mode'; if
the region is active start iedit in the current defun (as by
`narrow-to-defun') with the current selection as replacement
search string.  if a region is not active, do the same but with
`current-word'.  Inspired, but modified, by the
masteringemacs.org article."
    (interactive "P")
    (if arg
        (iedit-mode)
      (let (beg end)
        (save-excursion
          (save-restriction
            (widen)
            (narrow-to-defun)
            (setq beg (point-min)
                  end (point-max))))
        (cond (iedit-mode (iedit-done))
              ((use-region-p) (iedit-start (regexp-quote
                                            (buffer-substring-no-properties (mark)
                                                                            (point)))
                                           beg end))
              (t (iedit-start (concat "\\<"
                                      (regexp-quote (current-word))
                                      "\\>")
                              beg end)))))))

1.9.26. editorconfig

I don't use it very often, so this bit not actually included in the configuration, but when you need it, it's handy:

(use-package editorconfig
  :config (editorconfig-mode +1))

1.9.27. Compilation

M-x compile RET (or recompile) spawn a buffer with the output of make. Generally speaking, auto scroll on that is useless, but I keep this bit here in case I'll ever change my mind.

(setq compilation-scroll-output nil)

Even if, to be completely honest, keeping it at the top means I can M-g n=/=p easily…

1.9.28. Languages

1.9.28.1. jump to matching paren

The idea behind this is really cool. Pressing % with the cursor on (or before) a parenthesis (of any kind) will jump to the other side. Unfortunately, it doesn't play well with Clojure, where % is used for the "terse" lambda syntax (i.e. #(assoc foo :bar %))

(use-package paren
  :straight nil
  ;; :bind (("%" . op/match-paren))
  :config
  (show-paren-mode +1)

  ;; thanks, manual
  (defun op/match-paren (arg)
    "Go to the matchig paren if on a paren; otherwise self-insert."
    (interactive "p")
    (cond ((looking-at "\\s(") (forward-list 1) (backward-char 1))
          ((looking-at "\\s)") (forward-char 1) (backward-list 1))
          (t (self-insert-command (or arg 1))))))
1.9.28.2. eglot

LSP stands for Language Something Protocol, developed by M$ for vs-code, but − bear with me, it's weird to say it − it seems a decent idea.

There are two major implementations for emacs: lsp-mode and eglot. lsp-mode is too noisy for me, I prefer eglot as it's less intrusive

(use-package eglot
  :bind (:map eglot-mode-map
              ("<f1>" . eglot-code-actions)
              ("<f2>" . eglot-format))
  :config
  (add-to-list 'eglot-server-programs
               '(c-mode . ("clangd" "--header-insertion=never"))))

clangd has an annoying "feature": it automatically adds include when it thinks they're needed.

Additionally, various LSP backend (at least gopls) like to highlight the symbol at point in the buffer, which gets super annoying, it turns your buffer into some sort of Christmas tree every time you move the point around. Eglot has the concept of "ignored server capabilities" where it would fake to understand some capabilities, but don't actually apply them.

(with-eval-after-load 'eglot
  (add-to-list 'eglot-ignored-server-capabilites
               :documentHighlightProvider))

Protip: when working on a C project, one needs a compile-commands.json file. But, most of the time, a simple compile_flags.txt with the $CFLAGS one per line is enough. See gmid Makefile for instance, but usually this is enough:

compile_commands.txt:
printf "%s\n" ${CFLAGS} > $@
1.9.28.3. prog-mode

Enable auto-fill for comments in prog-mode buffers:

(defun op/auto-fill-comment ()
  "Enable auto-fill for comments."
  (setq-local comment-auto-fill-only-comments t)
  (auto-fill-mode))
(add-hook 'prog-mode-hook #'op/auto-fill-comment)

I finally found a usage for the arrow keys: copy-from-above-command! I've bound that function to the up arrow, so it's easy to copy the previous line. The function bound to the down arrow duplicates the current line.

(define-key prog-mode-map [up] #'copy-from-above-command)

(defun op/dup-line ()
  "Duplicate the current line, using `copy-from-above-command'."
  (interactive)
  (save-excursion
    (forward-line 1)
    (open-line 1)
    (copy-from-above-command))
  (call-interactively #'next-line))
(define-key prog-mode-map [down] #'op/dup-line)
1.9.28.4. text-mode

Enable abbrev-mode in text buffers:

(add-hook 'text-mode-hook #'abbrev-mode)
1.9.28.5. diff

A small usability tweak for diff-mode: I like to have M-SPC scroll down instead then up, just like in telescope!

(with-eval-after-load 'diff-mode
  (define-key diff-mode-map (kbd "M-SPC") #'scroll-down-command))
1.9.28.6. elisp

Enable prettify and checkdock in emacs lisp mode: the former transforms lambda into λ, and the latter enables style warning for elisp packages

(add-hook 'emacs-lisp-mode-hook #'checkdoc-minor-mode)
(add-hook 'emacs-lisp-mode-hook #'prettify-symbols-mode)

Bind a key to run all the tests and to spawn ielm:

(defun op/ert-all ()
  "Run all ert tests."
  (interactive)
  (ert t))

(defun op/ielm-repl (arg)
  "Pop up a ielm buffer."
  (interactive "P")
  (let ((buf (get-buffer-create "*ielm*")))
    (if arg
        (switch-to-buffer buf)
      (pop-to-buffer buf))
    (ielm)))

(let ((map emacs-lisp-mode-map))
  (define-key map (kbd "C-c C-k") #'eval-buffer)
  (define-key map (kbd "C-c k")   #'op/ert-all)
  (define-key map (kbd "C-c C-z") #'op/ielm-repl))

Eros is a nice little package that renders the output of eval-last-sexp in a small overlay right after the cursor, just like CIDER!

(use-package eros
  :config (eros-mode 1))

Emacs-lisp doesn't have namespaces, so usually there's this convention of prefixing every symbol of a package with the package name. Nameless helps with this. It binds _ to insert the name of the package, and it visually replace it with :. It's pretty cool.

(use-package nameless
  :hook (emacs-lisp-mode . nameless-mode)
  :custom ((nameless-private-prefix t)
           (nameless-affect-indentation-and-filling nil))
  :bind (:map emacs-lisp-mode-map
              ("_" . nameless-insert-name-or-self-insert)))

package-lint is kind of cool, if not because it helps you see what's the required emacs version. The main entrypoint is the package-lint-buffer function, which will pop up a buffer with the reported things.

(use-package package-lint)
1.9.28.7. Common LISP

I'm trying to use this convention for repls:

  • C-c C-z opens a repl at the bottom of the window
  • C-u C-c C-z opens the repl in the current buffer
(use-package sly
  :hook ((lisp-mode . prettify-symbols-mode)
         (lisp-mode . op/disable-tabs)
         (lisp-mode . sly-symbol-completion-mode))
  :custom (inferior-lisp-program "sbcl")
  :bind (:map sly-mode-map
              ("C-c C-z" . op/sly-mrepl))
  :config
  (defun op/sly-mrepl (arg)
    "Find or create the first useful REPL for the default connection in a side window."
    (interactive "P")
    (save-excursion
      (sly-mrepl nil))
    (let ((buf (sly-mrepl--find-create (sly-current-connection))))
      (if arg
          (switch-to-buffer buf)
        (pop-to-buffer buf))))

  (use-package sly-mrepl
    :straight nil  ;; it's part of sly!
    :bind (:map sly-mrepl-mode-map
                ("M-r" . comint-history-isearch-backward))))
1.9.28.8. Clojure

Load clojure-mode from MELPA (I guess, or is it ELPA?)

(use-package clojure-mode
  :mode (("\\.clj" . clojure-mode)
         ("\\.cljs" . clojurescript-mode)
         ("\\.cljc" . clojurec-mode)
         ("\\.edn" . clojure-mode))
  :hook ((clojure-mode . subword-mode)
         (clojurec-mode . subword-mode)
         (clojurescript-mode . subword-mode)

         (clojure-mode . op/disable-tabs)
         (clojurec-mode . op/disable-tabs)
         (clojurescript-mode . op/disable-tabs)

         (clojure-mode . abbrev-mode)
         (clojurec-mode . abbrev-mode)
         (clojurescript-mode . abbrev-mode))
  :config
  (put-clojure-indent 'doto-cond '(1 nil nil (1))))

doto-cond is a macro I wrote some time ago, I don't remember where, but anyway.

CIDER is the Clojure Interactive Development Environment that Rocks, aka the best thing for clojure. Just like with ielm and sly, use my convention for C-c C-z behaviour wrt prefix argument, but tweak also the key so the repl behaves more like a comint buffer.

(use-package cider
  :custom (cider-repl-display-help-banner nil)
  :bind (:map cider-repl-mode-map
              ;; more like comint
              ("C-c M-o" . cider-repl-clear-buffer)
              ("C-c C-l" . cider-repl-switch-to-other)
              :map cider-mode-map
              ("C-c C-z" . op/cider-repl))
  :config
  (defun op/cider-repl (arg)
    "Switch to repl buffer in side window.
With non-nil ARG use `display-buffer' ignoring the rules in
`display-buffer-alist'."
    (interactive "P")
    (when-let (buf (cider-current-repl))
      (call-interactively #'cider-repl-set-ns)
      (let ((display-buffer-alist (if arg
                                      ()
                                    display-buffer-alist)))
        (pop-to-buffer buf '(display-buffer-reuse-window))))))
1.9.28.9. Scheme

Geiser works for any scheme IIRC, but needs a tweak to find guile in my system.

(use-package geiser
  :config
  (setq geiser-guile-binary "guile3.0"))
1.9.28.10. Elastic search mode

es-mode let one write kibana-like queries and execute them from Emacs.

(use-package es-mode
  :mode "\\.es\\'"
  :hook (es-mode . op/disable-tabs))
1.9.28.11. SQL

op/visit-new-migration-file prompts for a name and creates an associated migration file, named after $date-$name.sql.

(defun op/visit-new-migration-file (name)
  "Visit a new SQL migration file named after NAME."
  (interactive "Mname: ")
  (let* ((name (replace-regexp-in-string " " "-" (string-trim name)))
         (f (format "%s-%s.sql"
                    (format-time-string "%Y%m%d%H%M")
                    name)))
    (find-file f)))

To please my muscle memory:

(defalias 'psql #'sql-postgres)

Sometimes I need to connect to a PostgreSQL database over a non-standard port, so here's a quick function to do that

(defun op/psql-params (port)
  "Easily connect to a psql on a non-standard PORT."
  (interactive "nPort: ")
  (let ((sql-port port))
    (psql)))

I don't particularly like how the electric-indent behaves in SQL buffers, so try to tame it

(defun op/sql-sane-electric-indent-mode ()
  "Fix function `electric-indent-mode' behaviour locally."
  (interactive)
  (setq-local electric-indent-inhibit nil))

(add-hook 'sql-mode-hook #'op/sql-sane-electric-indent-mode)

The lines in the interactive SQL buffer can get long, and truncation makes them look awful.

(add-hook 'sql-interactive-mode-hook #'toggle-truncate-lines)

Finally, define some handy keys to open a connection

(define-key global-map (kbd "C-z a s") #'psql)
(define-key global-map (kbd "C-z a S") #'op/psql-params)
1.9.28.12. nxml

nxml-mode is the major mode for editing XML buffers. I use it to edit svg files too.

(setq nxml-slash-auto-complete-flag t)

(add-hook 'nxml-mode-hook #'smartparens-strict-mode)
1.9.28.13. web

web-mode provides font-lock, indentation and stuff for various "web-related" file types.

By enabling web-mode-enable-engine-detection it became possible to define web-mode-engines-alist and having web-mode selecting the engine from that alist.

(use-package web-mode
  :mode (("\\.erb\\'" . web-mode)
         ("\\.mustache\\'" . web-mode)
         ("\\.html\\'" . web-mode))
  :custom ((web-mode-markup-indent-offset 2)
           (web-mode-css-indent-offset 2)
           (web-mode-code-indent-offset 2)
           (web-mode-style-padding 0)
           (web-mode-enable-engine-detection t))
  :hook ((web-mode . op/disable-tabs)))

It's useful to use a .dir-locals.el file to customize the engine selection, but that unfortunately doesn't work out-of-the-box. The following hack is needed:

(with-eval-after-load 'web-mode
  (defun op/web-mode-fix-dir-locals ()
    (when (derived-mode-p major-mode 'web-mode)
      (web-mode-guess-engine-and-content-type)))
  (add-hook 'hack-local-variables-hook #'op/web-mode-fix-dir-locals))
1.9.28.14. CSS

I don't use web-mode for CSS, emacs bulit in mode works pretty well. Just disable hard tabs:

(use-package css-mode
  :hook (css-mode . op/disable-tabs))
1.9.28.15. javascript

Just load some useful modes and disable tabs

(use-package js
  :straight nil
  :hook ((js-mode . abbrev-mode)
         (js-mode . subword-mode)
         (js-mode . op/disable-tabs)))
1.9.28.16. C

Usually I follow the OpenBSD KNF style(9) guidelines when writing C.

(setq c-basic-offset 8
      c-default-style "K&R")

(defun op/c-indent ()
  (interactive)
  (c-set-offset 'arglist-intro '+)
  (c-set-offset 'arglist-cont-nonempty '*))

(add-hook 'c-mode-hook #'op/c-indent)

Subword and abbrev mode are particularly useful. With abbrev I can easily fix typos like #inculde#include, and subword is useful for camelCase/PascalCase function name (fortunately enough, they aren't widespread in C)

(dolist (hook '(c-mode-hook c++-mode-hook))
  (add-hook hook #'abbrev-mode)
  (add-hook hook #'subword-mode))

My smartparens configuration automatically adds semicolon when appropriate (well, most of the times). While it's useful, typing a line of code soon becomes a matter of typing the code and then C-e RET to go to the next line. Fortunately we can optimise it:

(defun op/open-line-under ()
  "Like `open-line', but under."
  (interactive)
  (move-end-of-line 1)
  (newline)
  (c-indent-line))

(with-eval-after-load 'cc-mode
  (define-key c-mode-map (kbd "M-RET") #'op/open-line-under))

Use some similar (but slightly different) smartparens key and reserve a key for recompile.

(with-eval-after-load 'cc-mode
  (let ((map c-mode-map))
    (define-key map (kbd "<tab>")   #'indent-for-tab-command)
    (define-key map (kbd "TAB")     #'indent-for-tab-command)
    (define-key map (kbd "C-M-a")   #'sp-beginning-of-sexp)
    (define-key map (kbd "C-M-e")   #'sp-end-of-sexp)
    (define-key map (kbd "C-M-p")   #'beginning-of-defun)
    (define-key map (kbd "C-M-n")   #'end-of-defun)
    (define-key map (kbd "C-c M-c") #'recompile)))

I used to use irony, but now I mostly use eglot if I really need "advanced" support. Just for history sake, here's my old configuration (this is not tangled)

(use-package irony
  :hook ((c++-mode . irony-mode)
         (c-mode   . irony-mode)
         (obj-mode . irony-mode)))

Being able to interactively teach things to emacs is really cool. Sometimes I need to add a header at the top of the buffer. It's not something difficult, I push the point in the mark ring, then jump to the start of the buffer and scroll until I find the block of includes, add the one I want, sort them and pop the mark to continue where I was. But we can do better: op/c-add-include is the answer. It prompts for a header (without completion for the time being) and it inserts it in the right place. With the prefix argument it's possible to require the inclusion of a local header.

There's some space for improvements, but for the time being I'm happy. Things that I'd like to add in the future:

  • I keep the sys/ includes in a separate block, it'd be nice if this would respect that
  • If there aren't includes in the file this raises an error. It'd be nice if it was able to automatically add one after the copyright stuff at the start of the buffer.
(defun op/c-add-include (path &optional localp)
  "Include PATH at the start of the file.
If LOCALP is non-nil, the include will be \"local\"."
  (interactive "Mheader to include: \nP")
  (save-excursion
    (let ((re (if localp
                  "^#[ \t]*include[ \t]*\""
                "^#[ \t]*include[ \t]*<"))
          (ignore-re "^#include \"compat.h\"")
          start)
      (goto-char (point-min))
      (while (not (or (and (looking-at re)
                           (not (looking-at ignore-re)))
                      (eobp)))
        (forward-line))
      (when (eobp)
        (error "Don't know where to insert the header"))
      (open-line 1)
      (insert "#include " (if localp "\"\"" "<>"))
      (backward-char)
      (insert path)
      (move-beginning-of-line 1)
      (setq start (point))
      (forward-line)
      (while (and (looking-at re)
                  (not (eobp)))
        (forward-line))
      (sort-lines nil start (point)))))

(with-eval-after-load 'cc-mode
  (define-key c-mode-map (kbd "C-c C-a") #'op/c-add-include))
1.9.28.17. Go

My go configuration is simple: just load go-mode!

(use-package go-mode
  :mode "\\.go\\'"
  :hook ((go-mode . subword-mode)))
1.9.28.18. Perl

Just require perl-mode and ensure we indent with hard tabs

(use-package perl-mode
  :straight nil
  :custom ((perl-indent-level 8)))
1.9.28.19. Python

Load python-mode and disable hard tabs:

(use-package python
  :hook ((python-mode . op/disable-tabs)))
1.9.28.20. sh-mode

Simple stuff, set the tab width and fix the indentation

(use-package sh-script
  :straight nil
  :custom ((sh-basic-offset 8)
           (sh-indent-after-loop-construct 8)
           (sh-indent-after-continuation nil)))
1.9.28.20.1. TODO fix smartparens and sh' case

In a case statement, we have un-paired closed parethesis that require C-q to be typed because of sp-strict-mode.

1.9.28.21. Lua

Nothing fancy, just load the package

(use-package lua-mode
  :mode "\\.lua\\'"
  :hook ((lua-mode . op/disable-tabs))
  :custom ((lua-default-application "lua53")))

lua-lsp is the LSP server for lua, let's hook it into eglot

(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               '((lua-mode) . ("lua-lsp"))))
1.9.28.22. GDScript

GDScript is the scripting language of the Godot game engine. The gdscript-mode provides also format (via a python program) and integration with Godot:

(use-package gdscript-mode
  :mode "\\.gd\\'"
  :custom (gdscript-gdformat-save-and-format t))

It needs an external program for the formatting:

pip3 install --user gdtoolkit

but see the repository on GitHub for more information!

1.9.28.23. YAML

Yet another simple block for Yet Another Markup Language.

Disable flyspell in yaml. It inherits from text-mode but most of the time grammar check doesn't yield anything useful.

(use-package yaml-mode
  :mode "\\.yml\\'"
  :hook ((yaml-mode . turn-off-flyspell)))
1.9.28.24. TOML
(use-package toml-mode
  :mode "\\.toml\\'")
1.9.28.25. Gemini (text/gemini)

Fetch gemini-mode package. Also, I like to write text/gemini with a nice proportial font and a "bar" as cursor, just like I do with org-mode!

(use-package gemini-mode
  :hook ((gemini-mode . op/gemini-setup))
  :config
  (defun op/gemini-setup ()
    (setq-local cursor-type 'bar)))
(use-package ox-gemini)
1.9.28.26. Markdown

Install markdown-mode and enable auto fill.

(use-package markdown-mode
  :mode "\\.md\\'"
  :hook ((markdown-mode . auto-fill-mode)))

1.10. Applications

Here, configuration for various non text editing related stuff.

(defun op/tigervnc->chiaki ()
  "Connects to chiaki over tigervnc."
  (interactive)
  (async-shell-command "vncviewer.tigervnc 192.168.1.11"))

(define-key global-map (kbd "C-z a c") #'op/tigervnc->chiaki)

1.10.1. bins

It's useful to send a buffer, or part of it, to a bin online and then send the corresponding link to someone. The clbin package does that, in a DWIM manner: send the current region (if any) or the whole buffer, and save the corresponding url in the kill-ring.

(use-package clbin
  :straight nil
  :bind ("C-z w" . clbin-dwim))

1.10.2. sam for the rescue!

I like the design of various plan9 stuff, even if I haven't used the system. sam.el is my ongoing (and slow) attempt at emulating sam

(use-package sam
  :straight nil
  :load-path "~/w/sam/master/")

1.10.3. eshell

Eshell is the Emacs Shell. It's a strange combo, because it isn't a full-blown elisp REPL like ielm, but neither a UNIX shell like shell. It seems fun to use though.

(use-package eshell
  :bind (("C-c e" . op/eshell))
  :hook (eshell-mode . op/setup-eshell)
  :custom ((eshell-compl-dir-ignore
            "\\`\\(\\.\\.?\\|CVS\\|\\.svn\\|\\.git\\|\\.got\\)/\\'")
           (eshell-save-history-on-exit t)
           (eshell-prompt-regexp "[#$] ")
           (eshell-prompt-function (lambda () "$ "))
           (eshell-history-size 1024))
  :config
  <<eshell/aliases>>

  <<eshell/cl>>

  (defun op/eshell-bufname (dir)
    (concat "*eshell " (expand-file-name dir) "*"))

  <<eshell/after-cd>>

  (defun op/eshell (arg)
    "Run or jump to eshell in the current project.
If called with a prefix argument ARG, always create a new eshell
buffer."
    (interactive "P")
    (let* ((proj (project-current))
           (dir (cond (proj (project-root proj))
                      (t default-directory)))
           (default-directory dir)
           (eshell-buffer-name (let ((name (op/eshell-bufname dir)))
                                 (if arg
                                     (generate-new-buffer name)
                                   name))))
      (eshell)))

  <<op/append-to-buffer>>

  <<op/eshell-narrow-to-output>>

  <<op/setup-eshell>>)

I like to sync the $PWD with the buffer name, so an eshell in my home has as buffer name *eshell /home/op*.

<2021-06-16 Wed> Instead of advice-add I should use the eshell-directory-change-hook hook.

(defun op/eshell-after-cd (&rest _)
  (rename-buffer (op/eshell-bufname default-directory) t))

(advice-add #'eshell/cd :after #'op/eshell-after-cd)

To define custom commands in eshell (what would be functions or scripts in other shells), say whatever, one can define a eshell/whatever function. These are some aliases I find useful:

(defun eshell/emacs (&rest args)
  "Open a file in emacs (from the wiki)."
  (if (null args)
      (bury-buffer)
    (mapc #'find-file
          (mapcar #'expand-file-name
                  (eshell-flatten-list (nreverse args))))))

(defalias 'eshell/less #'find-file)

(defun eshell/dired ()
  (dired (eshell/pwd)))

cl is a better clear function, because the built in clear is a joke.

(defun eshell/cl ()
  "Clear the eshell buffer."
  (let ((inhibit-read-only t))
    (erase-buffer)))

Eshell can interact with other Emacs buffers, but the syntax is quite verbose. op/append-to-buffer (bound to C-c C-B) helps with this: it prompts for a buffer and inserts (but not execute) the redirect. This is especially useful when working with the OpenBSD ports tree: I can type cvs -q diff and then C-c C-B to redirect the diff to the mail buffer!

(defun op/append-to-buffer (buf)
  (interactive "Bbuffer: ")
  (insert ">>> " "#<" buf ">"))

Being able to narrow to the output of the command at point seems very useful, so here's a quick implementation:

(defun op/eshell-narrow-to-output ()
  "Narrow to the output of the command at point."
  (interactive)
  (save-excursion
    (let* ((start (progn (eshell-previous-prompt 1)
                         (forward-line +1)
                         (point)))
           (end (progn (eshell-next-prompt 1)
                       (forward-line -1)
                       (point))))
      (narrow-to-region start end))))

Eshell unfortunately doesn't uses from comint, so it lacks some niceties that were added to it. One of these things is the minibuffer completion for history navigation. I like to select an history item using the minibuffer:

(defun op/eshell-select-from-history ()
  (interactive)
  (let ((item (completing-read "Select from history: "
                               (seq-uniq (ring-elements eshell-history-ring)))))
    (when item
      ;; from eshell-previous-matching-input
      (delete-region eshell-last-output-end (point))
      (insert-and-inherit item))))

The weirdest thing about eshell is how it manages its own keys. eshell-mode-map, unlike other *-mode-map variables, is buffer-local, so a

;; just an example!
(define-key eshell-mode-map (kbd "...") #'some-function)

won't work. One needs to define a function and call it during the eshell-mode-hook like this:

(defun op/setup-eshell ()
  (define-key eshell-mode-map (kbd "C-c C-B") #'op/append-to-buffer)
  (define-key eshell-mode-map (kbd "C-c M-l") #'op/eshell-narrow-to-output)
  (message "before binding M-r")
  (define-key eshell-mode-map (kbd "M-r") #'op/eshell-select-from-history))
1.10.3.1. TODO make >>> ignore stderr

1.10.4. vterm ― when eshell isn't enough

vterm is a full-blown terminal emulator for Emacs, powered by the same library that GNOME terminal uses (IIRC)

(use-package vterm
  :bind (:map vterm-mode-map
              ("C-<backspace>" . op/vterm-fix-c-backspace)
              ("C-c M-t" . vterm-copy-mode)
              :map vterm-copy-mode-map
              ("C-c M-t" . vterm-copy-mode))
  :bind-keymap ("C-z v" . op/vterm-map)
  :custom ((vterm-buffer-name-string "*vterm %s*")
           (vterm-shell (concat shell-file-name " -l")))
  :config
  (defun op/vterm-fix-c-backspace ()
    (interactive)
    (vterm-send-key (kbd "C-w")))

  (defvar *op/hostname*
    (with-temp-buffer
      (process-file "hostname" nil (current-buffer))
      (string-trim (buffer-string) "" "\n"))
    "The hostname of this machine.")

  (defun op/project-vterm ()
    "Spawn a vterm in the current project."
    (interactive)
    (let* ((project-current (project-current))
           (default-directory (if project-current
                                  (project-root project-current)
                                default-directory)))
      (vterm)))

  (define-prefix-command 'op/vterm-map)
  (define-key op/vterm-map (kbd "v") #'vterm)
  (define-key op/vterm-map (kbd "v") #'op/project-vterm))

1.10.5. ibuffer

Ibuffer is a package to list, filter and do stuff on the buffers.

(define-key global-map (kbd "C-x C-b") #'ibuffer)

1.10.6. help

Help buffers are, no pun intended, helpful. But they are so more with a bit o' visual lines:

(add-hook 'help-mode-hook #'visual-line-mode)

1.10.7. gdb

I like the "many buffer" view of gdb, so let's enable it

(setq gdb-many-windows t)

And to aid my muscle memory from the shell, define an egdb alias

(defalias 'egdb #'gdb)

1.10.8. Literate calc-mode

This is pretty awesome. It provides a major mode where we can type expression and do math inline. Truly awesome.

(use-package literate-calc-mode)

1.10.9. keycast

Sometimes is nice. It adds the key and the command in the modeline, useful during records.

(use-package keycast
  :custom (keycast-insert-after 'mode-line-misc-info))

1.10.10. email

NB: mu4e is installed from ports, not via straight.

doas pkg_add mu4e
1.10.10.1. TODO split the following into pieces
(use-package emacs
  :straight nil
  :bind ("<f12>" . mu4e)
  :custom ((message-citation-line-format  "On %a, %b %d %Y, %f wrote\n")
           (message-default-charset 'utf8)
           (gnus-treat-display-smileys nil)
           (user-mail-address "op@omarpolo.com")

           (mu4e-maildir (expand-file-name "~/Maildir"))
           (mu4e-get-mail-command "mbsync -a")
           (mu4e-update-interval 180)
           (mu4e-compose-signature-auto-include nil)
           (mu4e-compose-format-flowed nil)
           (mu4e-compose-in-new-frame t)
           (mu4e-change-filenames-when-moving t) ;; don't break mbsync!
           (mu4e-attachment-dir "~/Downloads")
           (mu4e-compose-dont-reply-to-self t)
           (mu4e-confirm-quit nil)
           (mu4e-context-policy 'pick-first)
           (mu4e-compose-context-policy 'always-ask)
           (message-kill-buffer-on-exit t)

           (mu4e~view-beginning-of-url-regexp
            "https?\\://\\|mailto:\\|gemini://")

           ;; use localhost to send mails
           (message-send-mail-function #'smtpmail-send-it)
           (smtpmail-smtp-server "localhost")
           (smtpmail-default-smtp-server "localhost"))
  :hook (mu4e-view-mode . visual-line-mode)
  :config

  (require 'mu4e)

  ;; prefer the old style
  (setq mu4e-headers-thread-child-prefix '("├>" . "┣▶ ")
        mu4e-headers-thread-last-child-prefix '("└>" . "┗▶ ")
        mu4e-headers-thread-connection-prefix '("│" . "┃ ")
        mu4e-headers-thread-orphan-prefix '("┬>" . "┳▶ ")
        mu4e-headers-thread-single-orphan-prefix '("─>" . "━▶ "))

  ;; fix the keys for the view buffer
  (define-key mu4e-view-mode-map (kbd "RET") #'push-button)
  (define-key mu4e-view-mode-map (kbd "SPC") #'scroll-up-command)
  (define-key mu4e-view-mode-map (kbd "S-SPC") #'scroll-down-command)
  ;; just like telescope :P
  (define-key mu4e-view-mode-map (kbd "M-SPC") #'scroll-down-command)

  (add-to-list 'mu4e-view-actions
               '("ViewInBrowser" . mu4e-action-view-in-browser) t)

  (defun op/mu4e-change-headers ()
    (interactive)
    (setq mu4e-headers-fields
          `((:human-date . 10)
            (:flags . 4)
            (:mailing-list . 8)
            (:from . 20)
            (:to . 20)
            (:thread-subject))))
  (add-hook 'mu4e-headers-mode-hook #'op/mu4e-change-headers)

  (defun op/mu4e-do-compose-stuff ()
    (interactive)
    (set-fill-column 72)
    (auto-fill-mode)
    (flyspell-mode))

  (add-hook 'mu4e-compose-mode-hook #'op/mu4e-do-compose-stuff)

  (defun op/mu4e-make-bookmarks (mdir)
    (list (make-mu4e-bookmark :name "Global Unread Messages"
                              :query "flag:unread and not flag:trashed"
                              :key ?u)
          (make-mu4e-bookmark :name "Local Unread Messages"
                              :query (concat "maildir:" mdir "/* and flag:unread and not flag:trashed")
                              :key ?l)
          (make-mu4e-bookmark :name "Today's Messages"
                              :query (concat "maildir:" mdir "/* and date:today..now")
                              :key ?t)
          (make-mu4e-bookmark :name "Big Messages"
                              :query (concat "maildir:" mdir "/* and size:1M..500")
                              :key ?b)
          (make-mu4e-bookmark :name "Sent"
                              :query (concat "maildir:" mdir "/Sent")
                              :key ?s)
          (make-mu4e-bookmark :name "Drafts"
                              :query (concat "maildir:" mdir "/Drafts")
                              :key ?d)))

  (defun op/mu4e-match-fn (mdir)
    (lambda (msg)
      (when msg
        (string-prefix-p mdir (mu4e-message-field msg :maildir)))))

  ;; loading the mu4e configuration without halting emacs if its not found
  ;;
  ;; the my-mu4e-config.el file contains:
  ;;   (setq mu4e-contexts
  ;;         (list
  ;;          (make-mu4e-context
  ;;           :name "foobar"
  ;;           :match-func (my/mu4e-match-fn "/maildir-name")
  ;;           :vars `((user-mail.address . "")
  ;;                   (user-full-name . "")
  ;;                   (mu4e-sent-folder . "")
  ;;                   (mu4e-drafts-folder . "")
  ;;                   (mu4e-trash-folder . "")
  ;;                   (mu4e-compose-format-flowed . t)
  ;;                   (mu4e-maildir-shortcuts . (("/foo/bar" . ?f)
  ;;                                              ...))
  ;;                   (mu4e-bookmarks . ,(my/mu4e-make-bookmars "/maildir-name"))))))
  ;;
  (condition-case nil
      (progn
        (message "before load")
        (load "my-mu4e-config")
        (message "after load"))
    (error (message "NOT loading mu4e configuration.  It's missing!"))))
1.10.10.2. attach multiple files

mu4e uses mml-attach-file which, sadly, doesn't accept multiple files. Here's an attempt that uses dired to attach multiple file:

(defun op/composing-buffers ()
  "Return a list of composing email buffers."
  (cl-loop for buf being the buffers
           when (with-current-buffer buf
                  (eq major-mode 'mu4e-compose-mode))
           collect buf))

;; XXX: broken when multiple mu4e-compose-mode buffers are present!
(defun op/mml-attach-multiple-files ()
  "Attach the file marked from a dired buffer.
Uses `mml-attach-file' to attach the single files."
  (interactive)
  (let ((files (dired-get-marked-files))
        (bufs  (op/composing-buffers)))
    (with-current-buffer (if (cdr bufs) ; if more than one
                             (completing-read "Select email: " bufs)
                           (car bufs))
      (dolist (file files)
        (if (file-regular-p file)
            (mml-attach-file file
                             (mm-default-file-encoding file)
                             nil
                             "inline")
          (message "Skipping non-regular file %s" file))))))

(define-key dired-mode-map (kbd "C-c C-a") #'op/mml-attach-multiple-files)

1.10.11. news

I'm trying to use GNUS to read usenet. I made an account over at eternal-september.org, added

machine news.eternal-september.org login mynick force yes password ****

in ~/.authinfo. Then, to tell GNUS to connect to the server:

(setq gnus-select-method '(nntp "news.eternal-september.org"))

This bit could also be added to ~/.gnus eventually.

At this point M-x gnus RET and I'm ready to read the news!

Gnus has some strange keys:

A A
to list all the groups known to the server
u
to toggle the subscription to the group at point
M M u u
(yes, really) to mark an entry as unread

etc…

1.10.12. pass

I've recently switched to the pass password manager. It stores every password in a gpg-encrypted file inside a git repository, it's quite neat. My personal naming scheme is something along the lines of

<category/service>/<optional sub-specifier>/<account-name>
(use-package pass
  :custom ((pass-show-keybindings nil))
  :bind (("C-z P" . pass)))

1.10.13. vmd

I wrote a small package to interact with vmd(8) on OpenBSD. It's kinda nice, if I may say so.

(use-package vmd
  :straight nil
  :load-path "~/.emacs.d/lisp/vmd/"
  :custom (vmd-console-function #'op/vmd-vterm)
  :bind ("C-z a v" . vmd)
  :config
  (defun op/vmd-vterm (name cmd)
    (let ((vterm-shell (mapconcat #'shell-quote-argument cmd " "))
          (vterm-buffer-name (concat "*" name "*")))
      (vterm))))

1.10.14. sndio

I wrote also a package to interact with sndio(8). Yep, I like these small package.

(use-package sndio
  :straight nil
  :load-path "~/.emacs.d/lisp/sndio.el/"
  :bind (("C-z a A" . sndio)
         ("C-z a a" . sndio-win-open)))

1.10.15. EMMS

Emacs Multi Media System is cool. It only needs a decent UI, which I provide via hydra.

A good companion for EMMS is versuri: a package that fetches lyrics from various websites and saves them locally.

(use-package versuri
  :config
  (defun op/versuri-select ()
    (interactive)
    (when-let (match (call-interactively #'versuri-search))
      (apply #'versuri-display match))))

There's a bit of hackery to make it see and play opus files, it's probably not needed anymore but who knows.

Also, I should split into pieces

(use-package emms
  :commands (emms)
  :bind ("C-z e" . hydra-emms/body)
  :config
  (setq emms-source-file-default-directory "~/music/"
        emms-mode-line-format "「%s」"
        emms-browser-covers 'emms-browser-cache-thumbnail-async)

  (require 'emms-setup)
  (emms-all)
  (emms-default-players)
  (emms-playing-time-disable-display)

  (add-to-list 'emms-player-base-format-list "opus")
  ;; re-compute the regxp for mpv
  (emms-player-set emms-player-mpv 'regex
                   (apply #'emms-player-simple-regexp emms-player-base-format-list))

  ;; save on quit and recover on startup
  (require 'emms-history)
  (emms-history-load)

  ;; use libtag to extract tracks info.
  ;;
  ;; XXX: this needs to be compiled from sources
  ;; (~/.emacs.d/straight/repos/emms/) and cp emms-print-metadata
  ;; ~/bin.
  (require 'emms-info)
  (require 'emms-info-libtag)
  (setq emms-info-functions '(emms-info-libtag))
  (setq emms-info-libtag-known-extensions
        (regexp-opt '("opus" "mp3" "mp4" "m4a" "ogg" "flac" "spx" "wma")))

  (defun my/tick-symbol (x)
    "Return a tick if X is true-ish."
    (if x "x" " "))

  (defun my/emms-player-status ()
    "Return the state of the EMMS player: `not-active', `playing', `paused' or `dunno'.

Modeled after `emms-player-pause'."
    (cond ((not emms-player-playing-p)
           ;; here we should return 'not-active.  The fact is that
           ;; when i change song, there is a short amount of time
           ;; where we are ``not active'', and the hydra is rendered
           ;; always during that short amount of time.  So we cheat a
           ;; little.
           'playing)

          (emms-player-paused-p
           (let ((resume (emms-player-get emms-player-playing-p 'resume))
                 (pause (emms-player-get emms-player-playing-p 'pause)))
             (cond (resume 'paused)
                   (pause  'playing)
                   (t      'dunno))))
          (t (let ((pause (emms-player-get emms-player-playing-p 'pause)))
               (if pause 'playing 'dunno)))))

  (defun my/emms-toggle-time-display ()
    "Toggle the display of time information in the modeline"
    (interactive)
    (if emms-playing-time-display-p
        (emms-playing-time-disable-display)
      (emms-playing-time-enable-display)))

  (defun my/emms-select-song ()
    "Select and play a song from the current EMMS playlist."
    (interactive)
    (with-current-emms-playlist
      (emms-playlist-mode-center-current)
      (let* ((current-line-number (line-number-at-pos))
             (lines (cl-loop
                     with min-line-number = (line-number-at-pos (point-min))
                     with buffer-text-lines = (split-string (buffer-string) "\n")
                     with lines = nil
                     for l in buffer-text-lines
                     for n = min-line-number then (1+ n)
                     do (push (cons l n)
                              lines)
                     finally return (nreverse lines)))
             (selected-line (completing-read "Song: " lines)))
        (when selected-line
          (let ((line (cdr (assoc selected-line lines))))
            (goto-line line)
            (emms-playlist-mode-play-smart)
            (emms-playlist-mode-center-current))))))

  (defun op/emms-current-lyrics ()
    "Find the lyrics for the current song."
    (interactive)
    (let* ((track  (cdr (emms-playlist-current-selected-track)))
           (artist (cdr (assoc 'info-artist (cdr (emms-playlist-current-selected-track)))))
           (title  (cdr (assoc 'info-title (cdr (emms-playlist-current-selected-track))))))
      (versuri-display artist title)))

  (defhydra hydra-emms (:hint nil)
    "
%(my/emms-player-status) %(emms-track-description (emms-playlist-current-selected-track))

^Volume^     ^Controls^       ^Playback^              ^Misc^
^^^^^^^^----------------------------------------------------------------
_+_: inc     _n_: next        _r_: repeat one [% s(my/tick-symbol emms-repeat-track)]     _t_oggle modeline
_-_: dec     _p_: prev        _R_: repeat all [% s(my/tick-symbol emms-repeat-playlist)]     _T_oggle only time
_v_: vol     _<_: seek bw     _#_: shuffle            _s_elect
^ ^          _>_: seek fw     _%_: sort               _g_oto EMMS buffer
^ ^        _SPC_: play/pause                        _l_yrics
^ ^        _DEL_: restart                           _L_yrics select
  "
    ("+" emms-volume-raise)
    ("-" emms-volume-lower)
    ("v" sndio-win-open :exit t)
    ("n" emms-next)
    ("p" emms-previous)
    ("<" emms-seek-backward)
    (">" emms-seek-forward)
    ("SPC" emms-pause)
    ("DEL" (emms-player-seek-to 0))
    ("<backspace>" (emms-player-seek-to 0))
    ("r" emms-toggle-repeat-track)
    ("R" emms-toggle-repeat-playlist)
    ("#" emms-shuffle)
    ("%" emms-sort)
    ("t" (progn (my/emms-toggle-time-display)
                (emms-mode-line-toggle)))
    ("T" my/emms-toggle-time-display)
    ("s" my/emms-select-song)
    ("g" (progn (emms)
                (with-current-emms-playlist
                  (emms-playlist-mode-center-current))))
    ("l" op/emms-current-lyrics :exit t)
    ("L" op/versuri-select :exit t)

    ("q" nil :exit t)))

1.10.16. eww

The infamous Emacs Web Wrowser. Nothing fancy, just don't store cookie please!

(use-package eww
  :straight nil
  :custom ((url-cookie-trusted-urls nil)
           (url-cookie-untrusted-urls '(".*"))))

1.10.17. exwm

not tangled!

(use-package exwm
  :hook ((exwm-update-class . op/exwm-update-class)
         (exwm-update-title . op/exwm-update-title))
  :custom ((exwm-input-simulation-keys
            '(
              ;; movement
              ([?\C-b] . [left])
              ([?\M-b] . [C-left])
              ([?\C-f] . [right])
              ([?\M-f] . [C-right])
              ([?\C-p] . [up])
              ([?\C-n] . [down])
              ([?\C-a] . [home])
              ([?\C-e] . [end])
              ([?\M-v] . [prior])
              ([?\C-v] . [next])
              ([?\C-d] . [delete])
              ([?\C-k] . [S-end delete])
              ;; cut/paste.
              ([?\C-w] . [?\C-x])
              ([?\M-w] . [?\C-c])
              ([?\C-y] . [?\C-v])
              ;; search
              ([?\C-s] . [?\C-f]))))
  :bind (("s-r" . exwm-reset)
         ("s-w" . exwm-workspace-switch)
         ("M-&" . op/run-command)

         :map exwm-mode-map
         ("C-q" . exwm-input-send-next-key))
  :config
  (setenv "EDITOR" "emacsclient")
  (setenv "VISUAL" "emacsclient")

  (cl-loop for k in '(?& ?\{ ?\[ ?\( ?= ?+ ?\) ?\] ?\} ?!)
           for i from 1
           do (define-key global-map (kbd (format "s-%c" k))
                (lambda ()
                  (interactive)
                  (exwm-workspace-switch-create i))))

  (defun op/exwm-update-class ()
    (unless (or (string-prefix-p "sun-awt-X11-" exwm-instance-name)
                (string= "gimp" exwm-instance-name))
      (exwm-workspace-rename-buffer exwm-class-name)))

  (defun op/exwm-update-title ()
    (when (or (not exwm-instance-name)
              (string-prefix-p "sun-awt-X11-" exwm-instance-name)
              (string= "gimp" exwm-instance-name))
      (exwm-workspace-rename-buffer exwm-title)))

  (defun op/run-command (cmd)
    (interactive (list (read-shell-command "$ ")))
    (start-process-shell-command cmd nil cmd)))

1.10.18. elpher

Elpher is a Gemini/Gopher browser for Emacs.

(use-package elpher
  :custom ((elpher-ipv4-always nil)
           (elpher-default-url-type "gemini"))
  :commands (elpher elpher-go elpher-jump))

1.10.19. pq

pq is a postgres library module for elisp. Sound crazy, hu?

(use-package pq
  :straight nil
  :load-path "/home/op/build/emacs-libpq/")

1.10.20. nov.el

nov is a package to read epub from Emacs. It's really cool, and integrates with both bookmarks and org-mode.

(use-package nov
  :mode ("\\.epub\\'" . nov-mode)
  :hook (nov-mode . op/novel-setup)
  :config
  (defun op/novel-setup ()
    (interactive)
    (variable-pitch-mode +1)
    (olivetti-mode +1)
    (setq-local cursor-type 'bar)))

1.10.21. Toxe

Toxe is my try at writing a tox client in elisp. One day will be functional, I promise!

(use-package toxe
  :straight nil
  :load-path "~/w/toxe/")

1.10.22. rcirc

rcirc is the first (and only) Emacs IRC client I tried, and the only IRC client I use. I really like it out-of-the-box!

(use-package rcirc
  :straight nil
  :bind (("C-z i i" . rcirc))
  :custom ((rcirc-buffer-maximum-lines 1000)
           (rcirc-log-flag t)
           (rcirc-omit-responses '("JOIN" "PART" "QUIT"
                                   "NICK" "AWAY"))
           (rcirc-fill-column 72)
           (rcirc-keywords '("godot" "poedit" "mu4e")))
  :config
  (rcirc-track-minor-mode)
  (add-hook 'rcirc-mode-hook
            (lambda ()
              (flyspell-mode 1)
              (rcirc-omit-mode)))

  (setq rcirc-default-nick "op2"
        rcirc-default-user-name "op"
        rcirc-default-full-name "op"
        rcirc-server-alist '(("irc.libera.chat"
                              :channels ("#clojure"
                                         "#emacs"
                                         "#gemini-it"
                                         "#matrix"
                                         "#openbsd"
                                         "#openbsd-gaming"
                                         "#gameoftrees"
                                         "#postgresql"
                                         "#postgresql-it")
                              :port 6697 :encryption tls))
        rcirc-authinfo '(("libera"
                          certfp
                          "/home/op/.emacs.d/irc/key.pem"
                          "/home/op/.emacs.d/irc/cert.pem"))))

1.10.23. circe

<2021-11-11 Thu> I briefly tried circe before going back to rcirc and adding support for certfp to it. Just for reference, here's the configuration I used for a day.

(use-package circe
  :bind (("C-z i i" . circe))
  :config
  (tracking-mode +1)
  (setq tracking-position 'end
        tracking-most-recent-first t)

  (setq circe-reduce-lurker-spam t
        circe-format-say "{nick:-16s} {body}"
        circe-format-self-say "{nick}              {body}"
        lui-time-stamp-position 'right-margin
        lui-time-stamp-format "%H:%M"
        lui-fill-type nil)

  ;; it's using a certificate... :D
  (setq circe-network-options
        '(("Libera"
           :host "irc.libera.chat"
           :port 6697
           :tls t
           :tls-keylist (("/home/op/.emacs.d/irc/key.pem"
                          "/home/op/.emacs.d/irc/cert.pem"))
           :sasl-external t
           :nick "op2"
           :user "op2"
           :realname "op"
           :channels ("#clojure"
                      "#emacs"
                      "#gemini-it"
                      "#matrix"
                      "#openbsd"
                      "#openbsd-gaming"
                      "#gameoftrees"
                      "#postgresql"
                      "#postgresql-it"))))

  (defun op/circe-set-margin ()
    (setq fringes-outside-margins t
          right-margin-width 5
          word-wrap t
          ;; yep, really...
          wrap-prefix "                 ")
    (setf (cdr (assoc 'continuation fringe-indicator-alist)) nil))
  (add-hook 'lui-mode-hook #'op/circe-set-margin)

  ;; provides some command that automatically uses /msg ServOp behind
  ;; the scene, like /getop, /dropop, /mode, /bans, /kick
  (require 'circe-chanop)

  (require 'lui-logging)
  (enable-lui-logging-globally)

  (enable-lui-track))

1.10.24. telega

Telega is the best telegram client. period.

I'm using a non-standard recipe because the standard one doesn't include the contrib/* stuff (in particular I'm interested in the org integration).

(use-package telega
  :straight (:type git
                   :flavor melpa
                   :files (:defaults "etc" "server" "Makefile" "telega-pkg.el"
                                     "contrib/ol-telega.el"
                                     "contrib/telega-url-shorten.el")
                   :branch "master"
                   :host github
                   :repo "zevlg/telega.el")
  :custom ((telega-chat-input-markups '(nil "markdown1" "markdown2"))
           (telega-use-images t)
           (telega-emoji-font-family "Noto Color Emoji"))
  :hook ((telega-root-mode . telega-notifications-mode)
         ;; (telega-chat-mode . op/telega-enable-company)
         (telega-load-hook . global-telega-url-shorten-mode))
  :bind-keymap ("C-c t" . telega-prefix-map)
  :config
  ;; if put in the :custom will fail, and now I have other things to
  ;; do.
  (setq telega-completing-read-function #'completing-read)

  (comment
   (defun op/telega-enable-company ()
     (interactive)
     (company-mode +1)
     (set (make-local-variable 'company-backends)
          (append '(telega-company-emoji
                    telega-company-username
                    telega-company-hashtag)
                  (when (telega-chat-bot-p telega-chatbuf--chat)
                    '(telega-company-botcmd))))))

  ;; for telega-url-shorten
  (use-package all-the-icons))

1.10.25. elfeed

Elfeed is a RSS reader for Emacs that rocks!

(use-package elfeed
  :bind (("C-x w" . elfeed)
         :map elfeed-show-mode-map
         ("q" . delete-window)
         ("S-SPC" . scroll-down-command)
         ("M-SPC" . scroll-down-command))
  :custom (elfeed-feeds
           '("https://undeadly.org/cgi?action=rss&full=yes&items=10"
             "http://www.tedunangst.com/flak/rss"
             "https://www.dragonflydigest.com/feed"
             "https://www.mirbsd.org/news.rss"
             "https://www.mirbsd.org/announce.rss"
             "https://bentsukun.ch/index.xml"
             "https://drewdevault.com/feed.xml"
             "https://www.cambus.net/atom.xml"
             "https://dataswamp.org/~solene/rss.xml"
             "https://briancallahan.net/blog/feed.xml"
             "https://www.poolp.org/index.xml"
             "https://jcs.org/rss"
             "https://sanctum.geek.nz/arabesque/feed/"
             "https://tech.toryanderson.com/"
             "https://alexschroeder.ch/wiki?action=journal;search=-tag:rpg -tag:rsp;lang=en;title=English Diary without RPG Pages"
             "http://boston.conman.org/bostondiaries.rss"
             "https://emacsninja.com/feed.atom"
             "https://bsdly.blogspot.com/feeds/posts/default"
             "https://crawshaw.io/atom.xml"
             "https://nullprogram.com/feed/"
             "http://pragmaticemacs.com/feed/"
             "https://emacsnotes.wordpress.com/feed/"
             "https://metaredux.com/feed.xml"
             "https://emacsredux.com/atom.xml"
             "https://endlessparentheses.com/atom.xml"
             "https://www.masteringemacs.org/feed"
             "https://cestlaz.github.io/rss.xml"
             "https://utcc.utoronto.ca/~cks/space/blog/?atom"
             "https://irreal.org/blog/?feed=rss2"
             "https://jao.io/blog/rss.xml"
             "https://planet.lisp.org/rss20.xml"
             "https://insideclojure.org/feed.xml"
             "https://tech.toryanderson.com/index.xml"
             "https://vermaden.wordpress.com/feed/"
             "https://www.arp242.net/feed.xml"
             "https://tymoon.eu/api/reader/atom"
             "https://venam.nixers.net/blog/feed.xml"
             "https://www.omarpolo.com/rss.xml"
             "https://owarisubs.lacumpa.biz/feed/"
             "https://asenshi.moe/feed/"
             "https://godotengine.org/rss.xml"

             "https://github.com/go-gitea/gitea/releases.atom"

             "https://github.com/yshui/picom/releases.atom"
             "https://github.com/vslavik/poedit/releases.atom"
             "https://github.com/TokTok/c-toxcore/releases.atom"
             "https://github.com/alexander-akhmetov/python-telegram/releases.atom"
             "https://github.com/paul-nameless/tg/releases.atom"
             "https://github.com/YACReader/yacreader/releases.atom"
             "https://github.com/luarocks/luarocks/releases.atom"
             "https://github.com/okbob/pspg/releases.atom"

             "https://www.crimsonmagic.me/feed/"
             "https://fullybookedtls.wordpress.com/feed/"))
  :config
  (setq elfeed-show-entry-switch #'pop-to-buffer))
1.10.25.1. TODO find a way to integrate elfeed-org with this document

it would be sick!

1.10.26. firefox integration

There is a package, edit-server, that let me edit textareas from Emacs. I'm still trying to decide if it's worth or not.

(use-package edit-server
  :init (if after-init-time
            (edit-server-start)
          (add-hook 'after-init-hook #'edit-server-start))
  :custom (edit-server-new-frame-alist '((name . "Edit with Emacs")
                                         (minibuffer . t))))

1.11. "smol" packages

These are stuff that I keep in emacs-user-directory/lisp, but that isn't useful enough to submit a package for it.

1.11.1. Scratchpads

Creates a new *scratchpad* buffer on demand. When called with a prefix argument, choose the mode!

;;; scratchpads.el --- create scratchpads            -*- lexical-binding: t; -*-

;; Copyright (C) 2021  Omar Polo

;; Author: Omar Polo <op@omarpolo.com>
;; Keywords: convenience

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Quickly create temp scratch buffer

;;; Code:

(require 'cl-lib)

(defun scratchpads--list-major-modes ()
  "List all the major modes.
Inspired from ieure/scratch-el.  Naïve probably."
  (cl-loop for sym the symbols of obarray
           for name = (symbol-name sym)
           when (and (functionp sym)
                     (not (member sym minor-mode-alist))
                     (string-match "-mode$" name)
                     (not (string-match "--" name)))
           collect name))

(defun scratchpads--select-mode ()
  "Select an appropriate major mode."
  (if current-prefix-arg
      (intern (concat (completing-read
                       "Major Mode: "
                       (scratchpads--list-major-modes)
                       nil t nil nil)))
    major-mode))

;;;###autoload
(defun scratchpads-new-scratchpad (mode)
  "Create a new *scratch* buffer for the MODE."
  (interactive (list (scratchpads--select-mode)))
  (let ((buf (generate-new-buffer "*scratch*")))
    (pop-to-buffer buf)
    (funcall mode)))

(provide 'scratchpads)
;;; scratchpads.el ends here

1.11.2. clbin

Do you know what NIH syndrome is? It's the definition of this package!

;;; clbin.el --- post stuff to clbin                 -*- lexical-binding: t; -*-

;; Copyright (C) 2021  Omar Polo

;; Author: Omar Polo <op@omarpolo.com>
;; Keywords: comm

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; clbin.el posts something to clbin and saves the corresponding link
;; to the kill ring.

;;; Code:

(defun clbin--do (start end)
  "Send the region from START to END to clbin, add the link to the `kill-ring'."
  (let ((temp-buffer (generate-new-buffer " *clbin*" t)))
    (unwind-protect
        (let ((res (call-process-region start end
                                        "curl" nil temp-buffer nil
                                        "-Fclbin=<-" "https://clbin.com")))
          (cond ((zerop res)
                 (with-current-buffer temp-buffer
                   (goto-char (point-max))
                   (forward-line -1)
                   (let ((start (line-beginning-position))
                         (end   (line-end-position)))
                     (kill-ring-save start end)
                     (message "%s" (buffer-substring start end)))))
                (t
                 (error "Subprocess (curl) failed"))))
      (and (buffer-name temp-buffer)
           (kill-buffer temp-buffer)))))

;;;###autoload
(defun clbin-dwim ()
  "Send to clbin the current (narrowed) buffer or the active region."
  (interactive)
  (if mark-active
      (clbin--do (point) (mark))
    (clbin--do (point-min) (point-max))))

(provide 'clbin)
;;; clbin.el ends here

1.11.3. my-abbrev

Where I store the abbreviation I need.

;;; my-abbrev --- Abbrev stuff  -*- lexical-binding: t; -*-
;;; Commentary:

;; This adds various abbrevs for various modes.  Abbrevs are useful to
;; avoid typos, for instance.  To prevent the expansion, type ``word
;; C-q SPC'' instead of ``word SPC''.

;;; Code:

(clear-abbrev-table global-abbrev-table)

(define-abbrev-table 'global-abbrev-table
  '(("prots@" "ports@")

    ("supprots" "supports")

    ("het" "the")
    ("teh" "the")

    ("wehn" "when")

    ("perchè" "perché")
    ("perche" "perché")
    ("nonchè" "nonché")
    ("nonche" "nonché")
    ("quetse" "queste")
    ("sovlgimento" "svolgimento")
    ("sovlgere" "svolgere")
    ("sbagilo" "sbaglio")
    ("caffe" "caffè")))

(when (boundp 'text-mode-abbrev-table)
  (clear-abbrev-table text-mode-abbrev-table))

(define-abbrev-table 'text-mode-abbrev-table
  '(("hw" "hardware")
    ("sw" "software")))

(when (boundp 'clojure-mode-abbrev-table)
  (clear-abbrev-table clojure-mode-abbrev-table))

(define-abbrev-table 'clojure-mode-abbrev-table
  '(("erq" "req")))

(when (boundp 'c-mode-abbrev-table)
  (clear-abbrev-table c-mode-abbrev-table))

(define-abbrev-table 'c-mode-abbrev-table
  '(("inculde" "include")
    ("inlcude" "include")))

;; turn on abbrev mode globally
(setq-default abbrev-mode t)

(provide 'my-abbrev)
;;; my-abbrev.el ends here

1.11.4. my-modeline

This is my customized modeline. It hides informations that I don't find particularly interesting. It looks like this

modeline.png

Figure 1: screenshot of the modeline

;; -*- lexical-binding: t; -*-

(defvar op/mode-line-format-bk mode-line-format
  "Backup of the default `mode-line-format'.")

(setq-default mode-line-format
              '("%e"
                mode-line-front-space
                mode-line-mule-info
                mode-line-client
                mode-line-modified
                mode-line-remote
                mode-line-frame-identification
                mode-line-buffer-identification
                " "
                mode-line-position
                (vc-mode vc-mode)
                " "
                ;; mode-line-modes
                mode-line-misc-info
                mode-line-end-spaces))

(provide 'my-modeline)
;;; my-modeline.el ends here

1.11.5. minimal-light-theme

minimal-light-theme is my personal fork of anler/minimal-theme. Since I change it pretty often, I don't see how it could be useful to others.

;;; minimal-light-theme.el --- A light/dark minimalistic Emacs 27 theme.  -*- lexical-binding: t; -*-

;; Copyright (C) 2020 Omar Polo
;; Copyright (C) 2014 Anler Hp

;; Author: Anler Hp <anler86 [at] gmail.com>
;; Keywords: color, theme, minimal
;; X-URL: http://github.com/ikame/minimal-theme
;; URL: http://github.com/ikame/minimal-theme

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; A minimalistic color theme to avoid distraction with
;; colors.  Based on monochrome theme.

;;; Code:
(deftheme minimal-light "minimal light theme.")

(let* ((class '((class color) (min-colors 89)))
       (foreground "#586e75")
       (background "#ffffff") ;; "#fdf6e3"
       (cursor "#333")
       (border "grey90")
       (minibuffer cursor)
       (region "grey85")
       (comment-delimiter "grey55")
       (comment "grey30")
       (constant foreground)
       (string "grey40")
       (modeline-foreground foreground)
       (modeline-background "#e1dbca")
       (modeline-foreground-inactive comment)
       (modeline-background-inactive background)
       (modeline-border-color "white")
       (isearch-background modeline-background)
       (hl-background "grey94")
       (hl-face-background nil)
       (failure "red")
       (org-background "grey94"))
  (custom-theme-set-faces
   'minimal-light

   ;; basic stuff
   `(default ((,class (:background ,background :foreground ,foreground))))
   `(fringe ((,class (:inherit default))))
   `(cursor ((,class (:background ,cursor :inverse-video t))))
   `(vertical-border ((,class (:foreground ,border))))

   ;; minibuffer
   `(minibuffer-prompt ((,class (:foreground ,minibuffer :weight bold))))

   ;; region
   `(region ((,class (:background ,region))))
   `(secondary-selection ((,class (:background ,region))))

   ;; faces
   `(fl:builtin-face ((,class (:foreground ,foreground :weight bold))))
   `(fl:constant-face ((,class (:foreground ,foreground :weight bold))))
   `(fl:keyword-face ((,class (:foreground ,foreground :weight bold))))
   `(fl:type-face ((,class (:foreground ,foreground :slant italic))))
   `(fl:function-name-face ((,class (:foreground ,foreground :weight bold))))
   `(fl:variable-name-face ((,class (:foreground ,foreground))))

   `(fl:comment-delimiter-face ((,class (:foreground ,comment-delimiter))))
   `(fl:comment-face ((,class (:foreground ,comment
                                                  :slant italic))))
   `(fl:doc-face ((,class (:inherit (fl:comment-face)))))
   `(fl:string-face ((,class (:foreground ,foreground :foreground ,string))))

   ;; faces used by isearch
   `(isearch ((,class (:foreground ,foreground :background ,isearch-background :weight normal))))
   `(isearch-fail ((,class (:foreground ,failure :bold t))))
   `(lazy-highlight
     ((,class (:foreground ,foreground :background ,region))))

   ;; flymake-error
   `(flymake-error ((,class :underline (:style line :color "Red1"))))

   ;; ido-mode
   `(ido-subdir ((,class (:foreground ,foreground :weight bold))))
   `(ido-only-match ((,class (:foreground ,foreground :weight bold))))

   ;; show-paren
   `(show-paren-match
     ((,class (:inherit highlight
                        :underline (:color ,foreground :style line)))))
   `(show-paren-mismatch
     ((,class (:foreground ,failure :weight bold))))
   `(show-paren-match-expression
     ((,class (:inherit default :background "#eee"))))

   ;; highlight-sexp
   `(hl-sexp-face
     ((,class (:inherit show-paren-match-expression))))

   ;; tab-bar
   `(tab-bar ((,class :background ,modeline-background)))
   `(tab-bar-tab ((,class :background ,modeline-background-inactive
                          :box (:line-width 4 :color ,modeline-background-inactive))))
   `(tab-bar-tab-inactive ((,class :background ,modeline-background
                                   :box (:line-width 4 :color ,modeline-background))))

   ;; help.  Don't print funny boxes around keybindings
   `(help-key-binding ((,class)))

   ;; modeline
   `(mode-line
     ((,class (:inverse-video unspecified
                              :overline ,border
                              :underline nil
                              :foreground ,modeline-foreground
                              :background ,modeline-background
                              :overline ,border
                              :box (:line-width 3 :color ,modeline-background)))))
   `(mode-line-buffer-id ((,class (:weight bold))))
   `(mode-line-inactive
     ((,class (:inverse-video unspecified
                              :overline ,border
                              :underline nil
                              :foreground ,modeline-foreground-inactive
                              :background ,modeline-background-inactive
                              :box (:line-width 3 :color ,background)))))

   ;; hl-line-mode
   `(hl-line ((,class (:background ,hl-background))))
   `(hl-line-face ((,class (:background ,hl-face-background))))

   ;; highlight-stages-mode
   `(highlight-stages-negative-level-face ((,class (:foreground ,failure))))
   `(highlight-stages-level-1-face ((,class (:background ,org-background))))
   `(highlight-stages-level-2-face ((,class (:background ,region))))
   `(highlight-stages-level-3-face ((,class (:background ,region))))
   `(highlight-stages-higher-level-face ((,class (:background ,region))))

   ;; org-mode
   `(org-level-1 ((,class (:foreground ,foreground :height 1.6))))
   `(org-level-2 ((,class (:foreground ,foreground :height 1.5))))
   `(org-level-3 ((,class (:foreground ,foreground :height 1.4))))
   `(org-level-4 ((,class (:foreground ,foreground :height 1.3))))
   `(org-level-5 ((,class (:foreground ,foreground :height 1.2))))
   `(org-level-6 ((,class (:foreground ,foreground :height 1.1))))
   `(org-level-7 ((,class (:foreground ,foreground))))
   `(org-level-8 ((,class (:foreground ,foreground))))

   `(org-ellipsis ((,class (:inherit org-ellipsis :underline nil))))

   `(org-table ((,class (:inherit fixed-pitch))))
   `(org-meta-line ((,class (:inherit (fl:comment-face fixed-pitch)))))
   `(org-property-value ((,class (:inherit fixed-pitch))) t)
   `(org-verbatim ((,class (:inherit (shadow fixed-pitch)))))
   `(org-quote ((,class (:slant italic))))

   `(org-document-title ((,class (:foreground ,foreground))))

   `(org-link ((,class (:foreground ,foreground :underline t))))
   `(org-tag ((,class (:background ,org-background :foreground ,foreground))))
   `(org-warning ((,class (:background ,region :foreground ,foreground :weight bold))))
   `(org-todo ((,class (:weight bold))))
   `(org-done ((,class (:weight bold))))
   `(org-headline-done ((,class (:foreground ,foreground))))

   `(org-table ((,class (:background ,org-background))))
   `(org-code ((,class (:background ,org-background))))
   `(org-date ((,class (:background ,org-background :underline t))))
   `(org-block ((,class (:background ,org-background))))
   `(org-block-background ((,class (:background ,org-background :foreground ,foreground))))
   `(org-block-begin-line
     ((,class (:background ,org-background :foreground ,comment-delimiter :weight bold))))
   `(org-block-end-line
     ((,class (:background ,org-background :foreground ,comment-delimiter :weight bold))))

   ;; outline
   `(outline-1 ((,class (:inherit org-level-1))))
   `(outline-2 ((,class (:inherit org-level-2))))
   `(outline-3 ((,class (:inherit org-level-3))))
   `(outline-4 ((,class (:inherit org-level-4))))
   `(outline-5 ((,class (:inherit org-level-5))))
   `(outline-6 ((,class (:inherit org-level-6))))
   `(outline-7 ((,class (:inherit org-level-7))))
   `(outline-8 ((,class (:inherit org-level-8))))

   ;; js2-mode
   `(js2-external-variable ((,class (:inherit base-faces :weight bold))))
   `(js2-function-param ((,class (:inherit base-faces))))
   `(js2-instance-member ((,class (:inherit base-faces))))
   `(js2-jsdoc-html-tag-delimiter ((,class (:inherit base-faces))))
   `(js2-jsdoc-html-tag-name ((,class (:inherit base-faces))))
   `(js2-jsdoc-tag ((,class (:inherit base-faces))))
   `(js2-jsdoc-type ((,class (:inherit base-faces :weight bold))))
   `(js2-jsdoc-value ((,class (:inherit base-faces))))
   `(js2-magic-paren ((,class (:underline t))))
   `(js2-private-function-call ((,class (:inherit base-faces))))
   `(js2-private-member ((,class (:inherit base-faces))))

   ;; sh-mode
   `(sh-heredoc ((,class (:inherit base-faces :slant italic))))

   ;; telega
   `(telega-msg-heading ((,class (:inherit base-faces :underline ,comment-delimiter
                                           :foreground ,comment))))
   `(telega-msg-user-title ((,class (:inherit telega-msg-heading))))
   `(telega-msg-inline-reply ((,class (:inherit telega-msg-heading
                                                :slant italic))))

   ;; objed
   `(objed-hl ((,class (:background ,region))))

   ;; circe
   `(circe-prompt-face ((,class (:inherit default))))))

;;;###autoload
(when (and (boundp 'custom-theme-load-path) load-file-name)
  (add-to-list 'custom-theme-load-path
               (file-name-as-directory (file-name-directory load-file-name))))

(provide-theme 'minimal-light)
;;; minimal-light-theme.el ends here

1.11.6. even-more-cua

Even more cua is something I wrote for someone who wanted to try Emacs but needed "CUA" keys. It extends a bit cua-mode, and it's a bit opinionated. I don't have a use for it anymore, but anyway.

;; -*- lexical-binding: t; -*-

(cua-mode +1)

(setq mouse-drag-and-drop-region t)

(define-key global-map (kbd "C-s") 'save-buffer)
(define-key global-map (kbd "C-f") 'isearch-forward)
(define-key global-map (kbd "C-q") 'save-buffers-kill-terminal)

(define-key global-map (kbd "S-<mouse-1>")
  (lambda (arg e)
    (interactive "P\ne")
    (unless (region-active-p)
      (set-mark-command arg))
    (mouse-set-point e)))

(defun emc--move-text-impl (arg)
  "Move text up or down by ARG lines.

If ARG is negative, the text will be moved up.  ``text'' means the
current line or the current region if its active.  The region is
automatically extended to the beginning of the first line and to the
end of the last line."
  (atomic-change-group
    (cond
     ((and mark-active transient-mark-mode)
      (if (> (point) (mark))
          (exchange-point-and-mark))

      ;; extend the region to the beginning of the first line/end of
      ;; the last
      (when (not (bolp))
        (move-beginning-of-line nil))
      (exchange-point-and-mark)
      (when (not (bolp))
        (next-line)
        (move-beginning-of-line nil))
      (exchange-point-and-mark)

      (let ((column (current-column))
            (text (delete-and-extract-region (point) (mark))))
        (forward-line arg)
        (move-to-column column t)
        (set-mark (point))
        (insert text)
        (exchange-point-and-mark)
        (setq deactivate-mark nil)))
     (t
      (let ((column (current-column)))
        (beginning-of-line)
        (when (or (> arg 0) (not (bobp)))
          (forward-line)
          (when (or (< arg 0) (not (eobp)))
            (transpose-lines arg))
          (forward-line -1))
        (move-to-column column t))))))

(define-key global-map (kbd "C-S-<up>")
  (lambda (arg)
    (interactive "*p")
    (emc--move-text-impl (- arg))))

(define-key global-map (kbd "C-S-<down>")
  (lambda (arg)
    (interactive "*p")
    (emc--move-text-impl arg)))

(provide 'even-more-cua)
;;; even-more-cua.el ends here

Author: Omar Polo

Created: 2021-11-11 Thu 14:49

Validate