Minimal Setup

This layer holds some same standards which should be enabled across most machines.

Emacs Server

Start the server first, but only if emacs is not currently running.

(load "server")
(unless (server-running-p) (server-start))

When connected to server using emacsclient, we want to kill the client using C-x k, the way it seems natural.

(add-hook 'server-switch-hook
          (lambda ()
            (when (current-local-map)
              (use-local-map (copy-keymap (current-local-map))))
            (local-set-key (kbd "C-x k") 'server-edit)))

Theme

Load and/or enable my theme of choice

(cond
 ;; "dark" theme is based on wombat
 ((eq do.theme/enabled-theme 'dark)
  (load-theme 'wombat))
 ;; "light theme is based on plan9-theme
 ((eq do.theme/enabled-theme 'light)
  (use-package plan9-theme
    :ensure t
    :demand)))

Popup menu colors

Check Completion and Snippets. Those two require some colors for the popup tooltips. We set them here and refer to them in those files.

(cond

 ((eq do.theme/enabled-theme 'dark)
  ;; main face
  (setq do.theme.popup/popup-face-attrs
        `(:foreground ,(face-attribute 'font-lock-comment-face :foreground) :background "#2d2d2d"))
  ;; selected face
  (setq do.theme.popup/selected-face-attrs
        `(:foreground ,(face-attribute 'font-lock-builtin-face :foreground) :background "gray30" :slant italic :weight semibold))
  ;; summary
  (setq do.theme.popup/summary-face-attrs
        `(:foreground ,(face-attribute 'font-lock-builtin-face :foreground)))
  ;; summary (selected)
  (setq do.theme.popup/summary-selected-face-attrs
        `(:foreground ,(face-attribute 'font-lock-builtin-face :foreground) :slant italic :weight semibold))
  ;; scroll bar
  (setq do.theme.popup/scrollbar-fg-attrs ; foreground
        `(:background ,(face-attribute 'font-lock-comment-face :foreground)))
  (setq do.theme.popup/scrollbar-bg-attrs ; background
        `(:background "#2d2d2d"))
  ;; annotation
  (setq do.theme.popup/annotation-attrs
        `(:background ,(face-attribute 'font-lock-comment-face :foreground)))
  ;; preview
  (setq do.theme.popup/preview-attrs
        `(:foreground ,(face-attribute 'font-lock-builtin-face :foreground) :background "SlateBlue4"))
  (setq do.theme.popup/preview-search-attrs
        '(:background "SlateBlue1")))

 ((eq do.theme/enabled-theme 'light)
  (plan9/with-color-variables
    (setq do.theme.popup/popup-face-attrs
          `(:foreground ,fg :background ,bg-dark))
    ;; selected face
    (setq do.theme.popup/selected-face-attrs
          `(:foreground ,fg :background ,cyan :slant italic :weight semibold))
    ;; summary
    (setq do.theme.popup/summary-face-attrs
          `(:foreground ,red :background ,bg-dark))
    ;; summary (selected)
    (setq do.theme.popup/summary-selected-face-attrs
          `(:foreground ,red :background ,cyan :slant italic :weight semibold))
    ;; scroll bar
    (setq do.theme.popup/scrollbar-fg-attrs ; foreground
          `(:background ,cyan-light))
    (setq do.theme.popup/scrollbar-bg-attrs ; background
          `(:background ,bg-dark))
    ;; annotation
    (setq do.theme.popup/annotation-attrs
          `(:foreground ,green :background ,bg-dark))
    ;; preview
    (setq do.theme.popup/preview-attrs
          `(:foreground ,fg :background ,green))
    (setq do.theme.popup/preview-search-attrs
          `(:background ,blue))))
 (t
  (setq do.theme.popup/popup-face-attrs '())
  (setq do.theme.popup/selected-face-attrs '())
  (setq do.theme.popup/summary-face-attrs '())
  (setq do.theme.popup/summary-selected-face-attrs '())
  (setq do.theme.popup/scrollbar-fg-attrs '())
  (setq do.theme.popup/scrollbar-bg-attrs '())
  (setq do.theme.popup/preview-attrs '())
  (setq do.theme.popup/preview-search-attrs '())))

We define a small convenience function to set these attributes.

(defun do.theme/set-face-attr-from-list (face attrs)
  "Apply the attributes in ATTRS to the face FACE."
  (when (and face attrs)
    (set-face-attribute face nil (car attrs) (cadr attrs))
    (do.theme/set-face-attr-from-list face (cddr attrs))))

UI Elements

To reduce some visual noise, I get rid of most UI elements. The toolbar can be useful however, so I show it on the press of a button.

(unless on-windows
  (menu-bar-mode -1))
(when (display-graphic-p)
  (global-set-key [f9] 'toggle-menu-bar-mode-from-frame)
  (tool-bar-mode -1)
  (scroll-bar-mode -1))

Fringe

Set the width of the fringe

(defvar do.minimal.ui/fringe-width 6
  "The width of both fringes.")

(when (display-graphic-p)
  (fringe-mode (cons do.minimal.ui/fringe-width do.minimal.ui/fringe-width)))

For the dark theme, I use the fringe as divider between buffers, so I have to make sure that its backround differs from the default background. For the light theme, I want the fringe to be the same color as the background.

<2020-07-26 Sun> changed this a little bit.

(cond
 ((eq do.theme/enabled-theme 'dark)
  ;; (set-face-attribute 'fringe nil :background "#2d2d2d")
  (set-face-attribute 'fringe nil :background (face-attribute 'default :background)))
 ((eq do.theme/enabled-theme 'light)
  (set-face-attribute 'fringe nil :background (face-attribute 'default :background))))

For the dark theme, I want to remove the vertical border.

(cond
 ((eq do.theme/enabled-theme 'dark)
  ;; (set-face-attribute 'vertical-border nil :foreground (face-attribute 'fringe :background))
  (set-face-attribute 'vertical-border nil :foreground "#2d2d2d"))
 ((eq do.theme/enabled-theme 'light)))

Welcome Screen

We also want to get rid of the splash screen and start viewing my notes file, if it exists.

(defvar do.minimal/notes-file ""
  "Path to my notes file.")

(if (file-exists-p do.minimal/notes-file)
    (progn
      (setq initial-buffer-choice do.minimal/notes-file)
      ;; pop to the notes file with =C-c n=
      (global-set-key (kbd "C-c n") (lambda () (interactive) (find-file do.minimal/notes-file))))
  (setq initial-buffer-choice "~/"))

(setq inhibit-startup-message t)
(setq inhibit-splash-screen t)
(setq initial-scratch-message nil)

Window Geometry

(defvar do.minimal.ui/initial-height 80
  "Initial height of a frame")
(defvar do.minimal.ui/initial-width 120
  "Initial width of a frame")

(when (display-graphic-p)
  (add-to-list 'default-frame-alist (cons 'height do.minimal.ui/initial-height))
  (add-to-list 'default-frame-alist (cons 'width do.minimal.ui/initial-width)))

Scrolling

More natural scrolling. I'm not a fan of the default paged scrolling.

(setq scroll-step            1
      scroll-conservatively  10000)

Autosave and Backup files

It's annoying when autosave files are in the same directory. Too much clutter. Instead, let's save them somewhere in ~/.emacs.d/.

;; save backups and autosaves in .emacs.d/
(defvar do.minimal/backup-dir nil)
(defvar do.minimal/autosave-dir nil)
(let ((backup-dir (or do.minimal/backup-dir
                      (expand-file-name (concat initel-directory "emacs_backup/"))))
      (autosave-dir (or do.minimal/autosave-dir
                        (expand-file-name (concat initel-directory "autosave/")))))
  ;; emacs internals
  (setq backup-directory-alist (list (cons ".*" backup-dir)))
  (setq auto-save-list-file-prefix autosave-dir)
  (setq auto-save-file-name-transforms (list (list ".*" autosave-dir t)))
  ;; TRAMP
  (setq tramp-backup-directory-alist backup-directory-alist)
  (setq tramp-auto-save-directory autosave-dir))

Window movement

Navigate through windows and frames using Shift-<Arrow>

(use-package framemove
  :ensure t
  :demand
  :init
  (require 'cl-lib)
  (windmove-default-keybindings)
  :config
  (setq framemove-hook-into-windmove t))

Trailing whitespace

Show, identify, and nuke the devil which calls itself "trailing whitespace".

(use-package whitespace
  :demand
  :init
  ;; delete trailing whitespace before saving a file
  (add-hook 'before-save-hook 'delete-trailing-whitespace)
  :config
  (set-face-attribute 'trailing-whitespace nil :background "indian red")
  (setq-default show-trailing-whitespace t))

Don't show trailing whitespace in some modes

…but sometimes (especially in read-only buffers that I don't control), this gets annoying. Which is why we can add this small function to any hook that we want:

(defun no-trailing-whitespace ()
  (setq show-trailing-whitespace nil))

We already know two places to add it: the minibuffer and eww.

(add-hook 'minibuffer-setup-hook
          'no-trailing-whitespace)
(add-hook 'eww-mode-hook
          'no-trailing-whitespace)
(add-hook 'ielm-mode-hook
          'no-trailing-whitespace)
(add-hook 'help-mode-hook
          'no-trailing-whitespace)

Cursor

Highlight current line

Globally enable this, turn off when not needed.

(use-package hl-line
  :config
  (global-hl-line-mode 1)
  (make-variable-buffer-local 'global-hl-line-mode))

Blink forever

(setq blink-cursor-blinks 0)

Change cursor based on reading or writing

Always reset the cursor color and the faces of the highlight line. Also, use vertical bar if we're in edit mode, and use box if in read-only mode, (almost like in gvim…)

But first, define the colors for the different themes.

(cond
 ;; dark theme colors
 ((eq do.theme/enabled-theme 'dark)
  (setq do.minimal.cursor/color "gold")
  (setq do.minimal.cursor/region-attrs '(:background "gold" :foreground "black"))
  (setq do.minimal.cursor/hl-line-color "gray30")
  (setq do.minimal.cursor/overwrite-color "red"))
 ;; light theme colors
 ((eq do.theme/enabled-theme 'light)
  (plan9/with-color-variables
   (setq do.minimal.cursor/color "black")
   (setq do.minimal.cursor/region-attrs `(:background ,cyan-light :foreground nil))
   (setq do.minimal.cursor/hl-line-color purple-light)
   (setq do.minimal.cursor/overwrite-color red)))
 (t
  (setq do.minimal.cursor/color "green")
  (setq do.minimal.cursor/region-attrs '(:background "red" :foreground nil))
  (setq do.minimal.cursor/hl-line-color "SlateBlue1")
  (setq do.minimal.cursor/overwrite-color "red")))

Now, define our cursor-update function

(defun do.minimal.cursor/update ()
  ;; update the colors
  (set-cursor-color do.minimal.cursor/color)
  (apply #'set-face-attribute (append '(region nil) do.minimal.cursor/region-attrs))
  (set-face-background 'hl-line do.minimal.cursor/hl-line-color)
  ;; make sure the hl-line does not get screwed up
  (set-face-foreground 'highlight nil)
  (set-face-underline 'hl-line nil)
  ;; decide bar/box
  (cond
   (buffer-read-only (setq cursor-type 'box))
   (t (setq cursor-type 'bar)))
  ;; red cursor for overwrite mode
  (when overwrite-mode
    (set-cursor-color do.minimal.cursor/overwrite-color)))

This needs to run after every command, since some modes screw with the cursor.

(when (bound-and-true-p do.theme/enabled-theme)
  (do.minimal.cursor/update)
  (add-hook 'post-command-hook 'do.minimal.cursor/update))

Line numbers

A long time ago I used linum-mode for this. Since Emacs 26 there is a nicer built-in way of doing this.

Configure display-line-numbers-mode

;; in narrowed buffers we still want the "true" line number'
(setq display-line-numbers-widen t)

(cond
 ;; dark theme colors
 ((eq do.theme/enabled-theme 'dark)
  (set-face-attribute 'line-number nil
                      :background (face-attribute 'default :background)
                      :foreground (face-attribute 'font-lock-comment-face :foreground))
  (set-face-attribute 'line-number-current-line nil
                      :foreground (face-attribute 'default :foreground)
                      :background (face-attribute 'hl-line :background)))
 ;; light theme colors
 ((eq do.theme/enabled-theme 'light)
  (plan9/with-color-variables
   (set-face-attribute 'line-number nil
                       :background (face-attribute 'default :background)
                       :foreground fg)
   ;; also change the current-line-face
   (set-face-attribute 'line-number-current-line nil
                       :background purple-light
                       :foreground red))))

Turn line numbers on and off

Somehow, calling display-line-numbers-mode by itself does not quite work for me…

(defun do.minimal.line-numbers/toggle-line-numbers (&optional relative)
  "Toggle the display of line numbers in the buffer. I am not sure why I need this."
  (interactive)
  (cond (display-line-numbers (display-line-numbers-mode nil)
                              (setq display-line-numbers nil))
        (t (display-line-numbers-mode t)
           (setq display-line-numbers (or relative t)))))

(defun do.minimal.line-numbers/toggle-relative-line-numbers ()
  "Toggle the display of relative line numbers."
  (interactive)
  (do.minimal.line-numbers/toggle-line-numbers 'relative))

(defalias 'num #'do.minimal.line-numbers/toggle-line-numbers)
(defalias 'rnum #'do.minimal.line-numbers/toggle-relative-line-numbers)

Show line numbers while using goto-line

Shamelessly stolen from here.

(defun do.minimal.line-numbers/goto-line-with-feedback ()
  "Show line numbers temporarily, while prompting for the line number input"
  (interactive)
  (let ((line-numbers-off-p (not display-line-numbers)))
    (unwind-protect
        (progn (when line-numbers-off-p
                 (do.minimal.line-numbers/toggle-line-numbers))
               (call-interactively 'goto-line))
      (when line-numbers-off-p
        (do.minimal.line-numbers/toggle-line-numbers)))))
(global-set-key [remap goto-line] #'do.minimal.line-numbers/goto-line-with-feedback)

Keys

Change a key for current buffer only

(defun do.minimal.keys/buffer-local-set-key (key func)
      (interactive "KSet key on this buffer: \naCommand: ")
      (let ((name (format "%s-magic" (buffer-name))))
        (eval
         `(define-minor-mode ,(intern name)
            "Automagically built minor mode to define buffer-local keys."))
        (let* ((mapname (format "%s-map" name))
               (map (intern mapname)))
          (unless (boundp (intern mapname))
            (set map (make-sparse-keymap)))
          (eval
           `(define-key ,map ,key func)))
        (funcall (intern name) t)))

To undo, use

(buffer-local-set-key key nil)

Spaces and tabs

Spaces instead of tabs

Tabs are evil! I want spaces instead of tabs, and want exactly 2 spaces instead of a tab. In Python modes, we revert back to 4 spaces, but we will do that there.

(setq-default indent-tabs-mode nil)
(setq-default tab-width 2)
(setq-default tab-stop-list (number-sequence 2 120 2))
(setq sh-basic-offset 2)

Exceptions

Makefiles

Makefiles are not so friendly when it comes to spaces.

(defun do.minimal.spaces/makefile-tabs-exception ()
  (setq indent-tabs-mode t))
(add-hook 'makefile-mode-hook 'do.minimal.spaces/makefile-tabs-exception)

Parentheses

Show parens

This mode highlights the mathing parenthesis on point.

(use-package paren
  :config
  (show-paren-mode 1)
  (setq show-paren-delay 0))

Highlight Parens

This mode highlights the parentheses surrounding the point in shades of red. It works quite well for c-based languages and bash and other things, which is why I enable it globally.

(use-package highlight-parentheses
  :ensure t
  :demand
  :diminish highlight-parentheses-mode
  :config
  ;; define a global minor mode for this
  (define-globalized-minor-mode global-highlight-parentheses-mode
    highlight-parentheses-mode
    (lambda ()
      (highlight-parentheses-mode t)))
  ;; enable it
  (global-highlight-parentheses-mode t))

Rainbow delimiters for LISP

For lispy languages, I want to witness the full power of colorful rainbow-delimiters!

(use-package rainbow-delimiters
  :ensure t
  :init
  <<do.minimal.parens.rainbow/hooks>>
  :config
  <<do.minimal.parens.rainbow/theme-1>>
  <<do.minimal.parens.rainbow/theme-2>>)

Color theme

I stole some pastel rainbow colors from this wallpaper.

(set-face-attribute 'rainbow-delimiters-depth-1-face nil
                    :foreground "#78c5d6")
(set-face-attribute 'rainbow-delimiters-depth-2-face nil
                    :foreground "#bf62a6")
(set-face-attribute 'rainbow-delimiters-depth-3-face nil
                    :foreground "#459ba8")
(set-face-attribute 'rainbow-delimiters-depth-4-face nil
                    :foreground "#e868a2")
(set-face-attribute 'rainbow-delimiters-depth-5-face nil
                    :foreground "#79c267")
(set-face-attribute 'rainbow-delimiters-depth-6-face nil
                    :foreground "#f28c33")
(set-face-attribute 'rainbow-delimiters-depth-7-face nil
                    :foreground "#c5d647")
(set-face-attribute 'rainbow-delimiters-depth-8-face nil
                    :foreground "#f5d63d")
(set-face-attribute 'rainbow-delimiters-depth-9-face nil
                    :foreground "#78c5d6")

We also want to make unmatched parens stand out more with a strikethrough.

(set-face-attribute 'rainbow-delimiters-unmatched-face nil
                    :foreground 'unspecified
                    :inherit 'show-paren-mismatch
                    :strike-through t)

Hooks

Now we just need to adjust the hook for lispy languages.

(add-hook 'emacs-lisp-mode-hook 'rainbow-delimiters-mode)
(add-hook 'lisp-mode-hook 'rainbow-delimiters-mode)
(add-hook 'scheme-mode-hook 'rainbow-delimiters-mode)
(add-hook 'c-mode-common-hook 'rainbow-delimiters-mode)

Insert closing parens automagically

(use-package elec-pair
  :demand
  :config
  (electric-pair-mode 1))

Paredit

No editing lispy languages without paredit!

(use-package paredit
  :ensure t
  :bind
  <<do.minimal.parens.paredit/keys>>
  :init
  <<do.minimal.parens.paredit/hooks>>)

Enable this for all lispy modes.

(add-hook 'lisp-interaction-mode-hook #'paredit-mode)
(add-hook 'emacs-lisp-mode-hook #'paredit-mode)
(add-hook 'ielm-mode-hook #'paredit-mode)
(add-hook 'eval-expression-minibuffer-setup-hook #'paredit-mode)
(add-hook 'lisp-mode-hook #'paredit-mode)

Paredit overwrites the C-j binding to eval-last-sexp. This needs to be undone.

(:map paredit-mode-map ("C-j" . nil))

Ibuffer

Combining some Ibuffer tips from here and here. Ibuffer lets me filter the list of all currently open buffers.

(use-package ibuffer
  :demand
  :config
  <<do.minimal.ibuffer/filters>>
  <<do.minimal.ibuffer/filters-adv>>
  <<do.minimal.ibuffer/misc-1>>
  <<do.minimal.ibuffer/misc-2>>
  <<do.minimal.ibuffer/misc-3>>
  <<do.minimal.ibuffer/format>>
  <<do.minimal.ibuffer/hooks>>
  :bind
  <<do.minimal.ibuffer/keys>>)

Filter groups

My filter groups are defined below.

(setq ibuffer-saved-filter-groups
      (list
       (nreverse
        '(("Directories" (mode . dired-mode))
          ("Magit" (name . "^\\*magit.*$"))
          ("Org" (mode . org-mode))
          ("IRC" (mode . erc-mode))
          ("Interactive" (or (mode . matlab-shell-mode)
                             (mode . inferior-julia-shell-mode)
                             (mode . inferior-python-mode)
                             (mode . inferior-octave-mode)
                             (mode . inferior-lisp-mode)
                             (mode . inferior-scheme-mode)
                             (mode . ielm-mode)))
          ("Global" (name . "^\\*.*\\*$"))
          ("Shell" (or (mode . term-mode)
                       (mode . eshell-mode)
                       (mode . shell-mode)
                       (name . "^\\*ansi-term.*$")))
          ("Jabber" (name . "^\\*-jabber-.*$"))
          "do.minimal.ibuffer.filters/default"))
       ))

Reverse the order of the filter groups. Kind of confusing: Since I'm reversing the order of the groups above, this snippet ensures that the groups are ordered in the way they are written above, with the "Default" group on top. This advice might need to be ported to the new advice system soon.

(defadvice do.minimal.ibuffer/generate-filter-groups
    (after reverse-ibuffer-groups () activate)
  (setq ad-return-value (nreverse ad-return-value)))

Other settings

Only show groups that have active buffers

(setq ibuffer-show-empty-filter-groups nil)

Don't show the summary or headline

(setq ibuffer-display-summary nil)

Enable instead of list-buffers

(defalias 'list-buffers 'ibuffer)

Format

Display more characters in the buffer name column, also convert sizes to a human-readable format. Stolen from the wiki.

;; Use human readable Size column instead of original one
(define-ibuffer-column size-h
  (:name "Size" :inline t)
  (cond
   ((> (buffer-size) 1000000) (format "%7.1fM" (/ (buffer-size) 1000000.0)))
   ((> (buffer-size) 100000) (format "%7.0fk" (/ (buffer-size) 1000.0)))
   ((> (buffer-size) 1000) (format "%7.1fk" (/ (buffer-size) 1000.0)))
   (t (format "%8d" (buffer-size)))))

;; Modify the default ibuffer-formats
(setq ibuffer-formats
      '((mark modified read-only " "
              (name 40 60 :left :elide)
              " "
              (size-h 9 -1 :right)
              " "
              (mode 16 16 :left :elide)
              " "
              filename-and-process)))

Hooks

Automagically keep the buffer list up to date, enable the filter groups defined above, disable trailing whitespace

(defun do.minimal.ibuffer/hooks ()
  (ibuffer-auto-mode 1)
  (ibuffer-switch-to-saved-filter-groups "do.minimal.ibuffer.filters/default")
  (no-trailing-whitespace))
(add-hook 'ibuffer-mode-hook #'do.minimal.ibuffer/hooks)

Keys

Bind to the key that normally is bound to list-buffers

(("C-x C-b" . ibuffer))

Dired

General Settings

The first two lines of this tell dired to stop asking me whether I want to recursively delete or copy, since I never respond to that question with "No".

The last line enables "Do What I Mean" mode for dired: If I'm in a split frame with two dired buffers, the default target to copy (and rename) will be the other window.

(use-package dired
  :config
  (setq dired-recursive-copies 'always)
  (setq dired-recursive-deletes 'always)
  (setq dired-dwim-target t)
  (setq dired-listing-switches "-alh")
  ;; enable ALL THE FEATURES
  (require 'dired-x)
  (require 'dired-aux))

Automatically revert dired buffers

I also want dired to automatically revert, but to be quiet about it. The first line actually enables auto-revert for any buffers.

(use-package autorevert
  :config
  (global-auto-revert-mode 1)
  (setq global-auto-revert-non-file-buffers t)
  (setq auto-revert-verbose nil))

Less verbosity

We can show file details using the ( and ) keys.

(use-package dired-details
  :ensure t
  :config
  (setq dired-details-hidden-string "")
  (dired-details-install))

We want to omit the . and .. files, which are shown in dired by default.

(setq-default dired-omit-mode t) ;; in dired-x
(setq-default dired-omit-verbose nil)
(setq-default dired-omit-files "^.dropbox$\\|^.dropbox.cache$\\|^\\.$\\|^\\.\\.$")
(defadvice dired-omit-startup (after diminish-dired-omit activate)
  "Make sure to remove \"Omit\" from the modeline."
  (diminish 'dired-omit-mode) dired-mode-map)

Some files we need to show regardless

(setq dired-omit-extensions (remove ".bin" dired-omit-extensions))

Opening files

This is mostly stolen from here. Uses nohup to spawn child processes without annoying new buffers. First, we define a list of default programs.

;; some reasonable defaults. but use site file for this. (defaults to nil)
(unless dired-guess-shell-alist-user
  (setq dired-guess-shell-alist-user
        '(("\\.\\(?:djvu\\|eps\\|pdf\\)\\'" "evince")
          ("\\.\\(?:ipe\\)\\'" "ipe")
          ("\\.\\(?:jpg\\|jpeg\\|png\\|gif\\|xpm\\)\\'" "eog")
          ("\\.\\(?:xcf\\)\\'" "gimp")
          ("\\.\\(?:csv\\|odt\\|ods\\)\\'" "libreoffice")
          ("\\.\\(?:mp4\\|mp3\\|mkv\\|avi\\|flv\\|ogv\\)\\(?:\\.part\\)?\\'" "vlc")
          ("\\.html?\\'" "firefox"))))

Now, define a new function to start a process in the background.

(defvar do.minimal.dired/filelist-cmd
  '(("vlc" "-L")))

(defun do.minimal.dired/start-process (cmd &optional file-list)
  (interactive
   (let ((files (dired-get-marked-files
                 t current-prefix-arg)))
     (list
      (dired-read-shell-command "Open with: "
                                current-prefix-arg files)
      files)))
  (let (list-switch)
    (start-process
     cmd nil shell-file-name
     shell-command-switch
     (format
      "nohup 1>/dev/null 2>/dev/null %s \"%s\""
      (if (and (> (length file-list) 1)
               (setq list-switch
                     (cadr (assoc cmd do.minimal.dired/filelist-cmd))))
          (format "%s %s" cmd list-switch)
        cmd)
      (mapconcat #'expand-file-name file-list "\" \"")))))

At last, we remap two standard keys: We want !, which defaults to dired-do-shell-command, to run the old dired-do-aync-shell-command and use it for things like a quick unzip or unrar x or something like that. The old & shall be remapped to dired-start-process.

(define-key dired-mode-map "!" 'dired-do-async-shell-command)
(define-key dired-mode-map "&" 'do.minimal.dired/start-process)

Async

We can use async to copy files asynchronously.

(use-package async
  :ensure t
  :demand)

TRAMP

(setq tramp-default-method "scp")

Keybindings in tmux

Update <2020-04-23 Thu>: I don't know when I used this last. This might not be necessary anymore.

When launching terminal emacs in tmux, some keys do not work. This hint from the archwiki is supposed to help. To make this smarter, we add some logic to toggle the key-translation-map on a frame-by-frame basis.

First, we define the translation map.

(setq do.minimal.tmux/tmux-translation-map
      (let ((x 2)
            (tkey "")
            (km (make-sparse-keymap)))
        (while (<= x 8)
          (when (= x 2) (setq tkey "S-")) ;; shift
          (when (= x 3) (setq tkey "M-")) ;; alt
          (when (= x 4) (setq tkey "M-S-")) ;; alt + shift
          (when (= x 5) (setq tkey "C-")) ;; ctrl
          (when (= x 6) (setq tkey "C-S-")) ;; ctrl + shift
          (when (= x 7) (setq tkey "C-M-")) ;; ctrl + alt
          (when (= x 8) (setq tkey "C-M-S-")) ;; ctrl + alt + shift
          ;; arrows
          (define-key km (kbd (format "M-[ 1 ; %d A" x)) (kbd (format "%s<up>" tkey))) ;; uparrow
          (define-key km (kbd (format "M-[ 1 ; %d B" x)) (kbd (format "%s<down>" tkey))) ;; downarrow
          (define-key km (kbd (format "M-[ 1 ; %d C" x)) (kbd (format "%s<right>" tkey))) ;; rightarrow
          (define-key km (kbd (format "M-[ 1 ; %d D" x)) (kbd (format "%s<left>" tkey))) ;; leftarrow
          ;; special keys
          (define-key km (kbd (format "M-[ 1 ; %d H" x)) (kbd (format "%s<home>" tkey))) ;; home
          (define-key km (kbd (format "M-[ 1 ; %d F" x)) (kbd (format "%s<end>" tkey))) ;; end
          (define-key km (kbd (format "M-[ 5 ; %d ~" x)) (kbd (format "%s<prior>" tkey))) ;; pgup
          (define-key km (kbd (format "M-[ 6 ; %d ~" x)) (kbd (format "%s<next>" tkey))) ;; pgdown
          (define-key km (kbd (format "M-[ 2 ; %d ~" x)) (kbd (format "%s<delete>" tkey))) ;; insert
          (define-key km (kbd (format "M-[ 3 ; %d ~" x)) (kbd (format "%s<delete>" tkey))) ;; delete
          ;; function keys
          (define-key km (kbd (format "M-[ 1 ; %d P" x)) (kbd (format "%s<f1>" tkey)))
          (define-key km (kbd (format "M-[ 1 ; %d Q" x)) (kbd (format "%s<f2>" tkey)))
          (define-key km (kbd (format "M-[ 1 ; %d R" x)) (kbd (format "%s<f3>" tkey)))
          (define-key km (kbd (format "M-[ 1 ; %d S" x)) (kbd (format "%s<f4>" tkey)))
          (define-key km (kbd (format "M-[ 15 ; %d ~" x)) (kbd (format "%s<f5>" tkey)))
          (define-key km (kbd (format "M-[ 17 ; %d ~" x)) (kbd (format "%s<f6>" tkey)))
          (define-key km (kbd (format "M-[ 18 ; %d ~" x)) (kbd (format "%s<f7>" tkey)))
          (define-key km (kbd (format "M-[ 19 ; %d ~" x)) (kbd (format "%s<f8>" tkey)))
          (define-key km (kbd (format "M-[ 20 ; %d ~" x)) (kbd (format "%s<f9>" tkey)))
          (define-key km (kbd (format "M-[ 21 ; %d ~" x)) (kbd (format "%s<f10>" tkey)))
          (define-key km (kbd (format "M-[ 23 ; %d ~" x)) (kbd (format "%s<f11>" tkey)))
          (define-key km (kbd (format "M-[ 24 ; %d ~" x)) (kbd (format "%s<f12>" tkey)))
          (define-key km (kbd (format "M-[ 25 ; %d ~" x)) (kbd (format "%s<f13>" tkey)))
          (define-key km (kbd (format "M-[ 26 ; %d ~" x)) (kbd (format "%s<f14>" tkey)))
          (define-key km (kbd (format "M-[ 28 ; %d ~" x)) (kbd (format "%s<f15>" tkey)))
          (define-key km (kbd (format "M-[ 29 ; %d ~" x)) (kbd (format "%s<f16>" tkey)))
          (define-key km (kbd (format "M-[ 31 ; %d ~" x)) (kbd (format "%s<f17>" tkey)))
          (define-key km (kbd (format "M-[ 32 ; %d ~" x)) (kbd (format "%s<f18>" tkey)))
          (define-key km (kbd (format "M-[ 33 ; %d ~" x)) (kbd (format "%s<f19>" tkey)))
          (define-key km (kbd (format "M-[ 34 ; %d ~" x)) (kbd (format "%s<f20>" tkey)))
          ;; continue loop
          (setq x (+ x 1)))
        ;; return the keymap
        km))

Save the default translation map in a variable

(setq do.minimal.tmux/default-translation-map (copy-keymap key-translation-map))

Define a function to toggle between both keymaps. TODO: figure out a hook that we can attach this to. It seems like focus-in-hook does not work for this.

(defun do.minimal.tmux/toggle-maps ()
  "Toggle the `key-translation-map' on frame focus."
  (interactive)
  (if (getenv "TMUX" (selected-frame)) ;; check env of current frame!
      (setq key-translation-map do.minimal.tmux/tmux-translation-map)
    (setq key-translation-map do.minimal.tmux/default-translation-map)))

Note that in order for this to work, we need to have the following snippet in .tmux.conf:

setw -g xterm-keys on

Eshell

Update <2020-04-23 Thu>: Another one of these things that I haven't touched in years.

Eshell is interesting. It could be a great replacement for bash if I'm ever forced to use a windows machine. Also, it's just kind of cool to have sort of a lisp/shell hybrid thingy.

(use-package eshell
  :config
  <<do.minimal.eshell/general>>
  <<do.minimal.eshell/visual-commands>>
  <<do.minimal.eshell/clear>>
  <<do.minimal.eshell/prompt-1>>
  <<do.minimal.eshell/prompt-2>>
  <<do.minimal.eshell/prompt-3>>
  <<do.minimal.eshell/hooks>>)

General settings

Disable the banner message.

(setq eshell-banner-message "")

Visual commands

Inspired by Howard's eshell config, this next block defines some programs for which eshell launches a comint buffer. These are mostly curses programs.

(defvar do.minimal.eshell/visual-commands '("ssh" "htop" "ncmpcpp" "tail")
  "List of eshell commands for which to launch a seperate comint buffer.")

Clear the screen

Clear the screen using the (regular?) clear command. Stolen from this old list post.

(defun eshell/clear ()
  "Deletes the contents of eshell buffer, except the last prompt"
  (save-excursion
    (goto-char eshell-last-output-end)
    (let ((lines (count-lines 1 (point)))
          (inhibit-read-only t))
      (beginning-of-line)
      (let ((pos (point)))
        (if (bobp)
            (if (interactive-p)
                (error "Buffer too short to truncate"))
          (delete-region (point-min) (point)))))))

Prompt

This is mimicking my zsh prompt. Without the git indicator for now, but that should be easy to code up.

First, we want to make sure that the home directory is always replaced

(defun do.minimal.eshell.prompt/pwd-replace-home (pwd)
  "Replace home in PWD with tilde (~) character."
  (interactive)
  (let* ((home (expand-file-name (getenv "HOME")))
         (home-len (length home)))
    (if (and
         (>= (length pwd) home-len)
         (equal home (substring pwd 0 home-len)))
        (concat "~" (substring pwd home-len))
      pwd)))

Now the prompt function itself. Note that this setup should be modified somehow to play nice with tramp. I should decide based on (file-remote-p) whether I'm editing a remote file. If that's the case, take the default-directory string and either split it into "real" hostname and directory or truncate the /ssh:<hostname> part out of it if I can get the "real" hostname from somewhere else. Emacs will always have a small project for you…

;; a macro to propertize, stolen from emacswiki.
(defmacro do.minimal.eshell.prompt/with-face (str &rest properties)
  `(propertize ,str 'face (list ,@properties)))
;; the prompt function
(defun do.minimal.eshell.prompt/prompt ()
  (let*
      ;; need some string lengths to fill the screen with spaces
      ((spc-cnt (length hostname))
       (dir (do.minimal.eshell.prompt/pwd-replace-home (eshell/pwd)))
       (offset (if on-windows 3 2)) ;; need extra offset in terminal
       (fill-size (- (window-body-width) (+ spc-cnt (length dir) offset))))
    ;; put together the prompt
    (concat
     "\n"
     (do.minimal.eshell.prompt/with-face hostname :foreground "cornflower blue" :weight "bold")
     (do.minimal.eshell.prompt/with-face "|" :foreground "red" :weight "bold")
     (do.minimal.eshell.prompt/with-face dir :foreground "gold" :weight "extra-bold")
     (make-string fill-size 32) ;; go to the right side
     ;; the return status indicator
     (if (equal eshell-last-command-status 0)
         (do.minimal.eshell.prompt/with-face (char-to-string #x2713) :foreground "green" :weight "bold")
       (do.minimal.eshell.prompt/with-face (char-to-string #x2717) :foreground "red" :weight "bold"))
     "\n"
     (make-string spc-cnt 32)
     (do.minimal.eshell.prompt/with-face "|" :foreground "red" :weight "bold")
     (do.minimal.eshell.prompt/with-face (char-to-string #x25b8) :foreground (face-attribute 'default :foreground))
     " ")))

Enable the prompt.

(setq
 eshell-prompt-function #'do.minimal.eshell.prompt/prompt
 eshell-prompt-regexp (concat "\s*|" (char-to-string #x25b8) " ")) ;; \s* captures whitespace

Hooks

(defun do.minimal.eshell/hooks ()
  (eshell-read-aliases-list)
  (setq global-hl-line-mode nil)
  (setq show-trailing-whitespace nil)
  (setq-local ml-interactive? t) ;; for mode line
  (eshell/addpath "~/bin")
  (dolist (command do.minimal.eshell/visual-commands)
    add-to-list 'eshell-visual-commands command))
(add-hook 'eshell-mode-hook #'do.minimal.eshell/hooks)

Magit

(use-package magit
  :ensure t
  :bind
  (("C-x g" . magit-status))
  :config
  (setq magit-push-always-verify nil)
  ;; fix color issue
  (set-face-attribute 'magit-section-highlight nil
                      :background "#3a3a3a")
  (set-face-attribute 'magit-branch-current nil
                      :foreground "black"
                      :background "gold"))

ledger

Started doing my personal accouting in plain text. This works very well.

(use-package ledger-mode
  :ensure t
  :hook
  ((ledger-mode . outline-minor-mode)
   (ledger-mode . (lambda () (outline-hide-sublevels 1))))
  :init
  (defvar do.minimal.ledger/current-ledger ""
    "Path to this year's ledger file")
  (defun do.minimal.ledger/find-current-ledger ()
    "Visit the current ledger"
    (interactive)
    (find-file do.minimal.ledger/current-ledger))
  :config
  (font-lock-add-keywords 'ledger-mode outline-font-lock-keywords)
  (setq ledger-default-date-format ledger-iso-date-format)
  :bind
  (("C-c L" . #'do.minimal.ledger/find-current-ledger)
   :map ledger-mode-map
   ("TAB" . org-cycle)))

Normally, I wouldn't use something like this, but for ledger files, automatically comitting every change to git seems like a good idea. I enable this by setting a file-local variable like this: ; -*- mode: Ledger; eval: (git-auto-commit-mode 1) -*-

(use-package git-auto-commit-mode
  :ensure t
  :after ledger-mode
  :diminish git-auto-commit-mode)

Projectile

(use-package projectile
  :ensure t
  :diminish projectile-mode
  :config
  (define-key projectile-mode-map (kbd "s-p") 'projectile-command-map)
  (define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)
  (setq projectile-completion-system 'ivy)
  (projectile-mode +1))

HiDPI

This is a very low-tech solution to the HiDPI problem on the linux desktop. Simply scale the default face and fringe.

(defvar do.minimal.hidpi/scale-factor 2
  "The scale factor for my global HiDPI minor mode")

(define-minor-mode do.minimal.hidpi-mode
  "Toggle HiDPI minor mode."
  :lighter " HD"
  :global t
  (if (bound-and-true-p do.minimal.hidpi-mode)
      ;; on toggle, scale the fonts and change powerline
      (let ((do.minimal.hidpi/height (* 100 do.minimal.hidpi/scale-factor)))
        (setq do.minimal.hidpi/prev-height (face-attribute 'default :height)
              do.minimal.hidpi/prev-powerline-height powerline-height
              do.minimal.hidpi/prev-powerline-separator powerline-default-separator
              do.minimal.hidpi/prev-powerline-separator-dir powerline-default-separator-dir)
        (set-face-attribute 'default nil :height do.minimal.hidpi/height)
        (setq powerline-height  (+ 5 (frame-char-height))
              powerline-default-separator 'contour
              powerline-default-separator-dir '(right . left))
        (fringe-mode '(12 . 12)))
    ;; else undo the changes
    (set-face-attribute 'default nil :height do.minimal.hidpi/prev-height)
    (setq powerline-height do.minimal.hidpi/prev-powerline-height
          powerline-default-separator do.minimal.hidpi/prev-powerline-separator
          powerline-default-separator-dir do.minimal.hidpi/prev-powerline-separator-dir)
    (fringe-mode '(4 . 4))))

;; set a quick alias
(defalias 'hd 'do.minimal.hidpi-mode)

Highlighting symbols

Use highlight-symbol with a custom face and a low timeout value.

(use-package highlight-symbol
  :ensure t
  :diminish highlight-symbol-mode
  :config
  (setq highlight-symbol-idle-delay 0.5)
  ;; use the same background as hl-line
  (set-face-attribute 'highlight-symbol-face nil
                      :background (face-attribute 'hl-line :background))
  ;; add to prog-mode-hooks
  (add-hook 'prog-mode-hook 'highlight-symbol-mode))

Misc. settings

Some one-liners and other settings go in here. Update <2020-04-23 Thu>: There is some ancient stuff in here.

Only y/n questions, not yes/no

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

Disable ad-handle-definition warnings

(setq ad-redefinition-action 'accept)

Unique buffer names

(setq uniquify-buffer-name-style 'forward)

Stop pasting at the mouse click point

Emacs' default setting first moves point to the click location and then yanks. This is annoying, and we rather want it to yank at the current position of point.

(setq mouse-yank-at-point t)

Open file as root

Inspired by this, but using the regular find-file instead of ido-find-file.

(defun do.minimal.misc/sudo-open ()
  "Like `find-file', but with root rights using TRAMP"
  (interactive)
  (let ((file (read-file-name "Open as root: ")))
    (unless (file-writable-p file)
      (find-file (concat "/sudo:[email protected]:" file)))))
;; bind to a key
(global-set-key (kbd "C-x F") #'do.minimal.misc/sudo-open)

Auto-fill-mode

(setq-default fill-column 79)
(setq default-fill-column 79)

DocView

Make DocView suck a little less. I still prefer evince.

(use-package doc-view
  :config
  (defun doc-view-buffer-message () "rendering pdf...")
  (setq doc-view-continuous t)
  (add-hook 'doc-view-mode-hook 'auto-revert-mode)  )

Moving to beginning of line

Spotted here. Using C-a to toggle between going to the true beginning of the line and the beginning of the indented line. Goes to the true beginning of line first.

(defun do.minimal.misc/beginning-of-line-or-indentation ()
  "Move to beginning of line, or indentation"
  (interactive)
  (if (bolp)
      (back-to-indentation)
    (beginning-of-line)))

Bind this to the correct key.

(global-set-key [remap move-beginning-of-line]
                #'do.minimal.misc/beginning-of-line-or-indentation)

Shell-mode

Use bash when invoking M-x shell.

(setq explicit-shell-file-name "/bin/bash")
(add-hook 'shell-mode-hook 'no-trailing-whitespace)
(add-hook 'shell-mode-hook '(lambda () (setq-local ml-interactive? t)))

Google this!

Take the current region and send it as search query to google.

(defun google ()
  "Googles a query or region if any."
  (interactive)
  (browse-url
   (concat
    "http://www.google.com/search?ie=utf-8&oe=utf-8&q="
    (if mark-active
        (buffer-substring (region-beginning) (region-end))
      (read-string "Google: ")))))

Fill line from point to end with some character

(defvar do.minimal.misc/fill-to-end-with-char-col fill-column
  "The value that `fill-to-end' fills the column until.")

(defun fill-to-end-with-char (char)
  "Fill the current line with CHAR from point until the column
  including `do.minimal.misc/fill-to-end-with-char-col'.

When called with a prefix argument, show a prompt asking for the
character to fill with. The default character to fill is '-'."
  (interactive
   (if current-prefix-arg
       (list (let ((input))
               (while (not (= (length input) 1))
                 (setq input (read-string "Fill with character: ")))
               input))
     (list "-")))
  (save-excursion
    (let* ((cur-point (point))
           (cur-point-on-line (- cur-point (point-at-bol)))
           (str-len (- do.minimal.misc/fill-to-end-with-char-col cur-point-on-line))
           (str (make-string str-len (string-to-char char))))
      (goto-char cur-point)
      (insert str))))

Reuse frames when popping up

(add-to-list 'display-buffer-alist
             '("." nil (reusable-frames . t)))

Cycle when popping mark ring

(setq set-mark-command-repeat-pop t)

Recursive grep by default

(setq grep-command "grep --color -nH --null -r -e ")

Copy strings to OS clipboard

(defun do.minimal/string-to-clipboard (str)
  "Insert the string STR into the OS clipboard by way of the kill ring."
  (with-temp-buffer
    (insert str)
    (kill-region (point-min) (point-max))))

PDF reader

(defvar do.minimal/pdf-reader "xdg-open"
  "The default PDF reader")

Flash evaluated regions

(use-package eval-sexp-fu
  :ensure t
  :commands (eval-sexp-fu-flash-mode)
  :init (eval-sexp-fu-flash-mode))

Insert ISO 8601 date

(defun do.misc/insert-date ()
  "Insert an ISO 8601 formatted date string at point."
    (interactive)
    (insert (format-time-string "%Y-%m-%d")))

FlyCheck

This is only in this file because I'm only using one line of configuration.

(unless on-windows
  (use-package flycheck
  :ensure t
  :init
  (global-flycheck-mode)
  (setq flycheck-checker-error-threshold 2000)))

Which-key

(unless on-windows
  (use-package which-key
    :ensure t
    :diminish which-key-mode
    :init
    (which-key-setup-side-window-bottom)
    (setq which-key-side-window-max-height 0.25)
    (setq which-key-idle-delay 0.4)
    (which-key-mode)))

ivy/counsel

(use-package ivy
  :ensure t
  :demand
  :diminish ivy-mode
  :bind (("C-c C-r" . ivy-resume)
   ("C-x B" . ivy-switch-buffer-other-window)
   ("C-x b" . ivy-switch-buffer))
  :custom
  (ivy-count-format "(%d/%d) ")
  (ivy-use-virtual-buffers t)
  (ivy-use-selectable-prompt t)
  ;; :hook
  ;; (ivy-mode . ivy-posframe-mode) ;; see the posframe block below
  :config
  (ivy-mode)
  (setq enable-recursive-minibuffers t))

(use-package counsel
  :after ivy
  :ensure t
  :demand
  :diminish counsel-mode
  :config
  (counsel-mode 1)
  ;; configure ripgrep for fast and easy searching.
  <<do.minimal/counsel-ripgrep>>
  ;; change default regexes
  (setq ivy-initial-inputs-alist
  '((counsel-minor . "^+")
    (counsel-package . "^+")
    (counsel-org-capture . "")
    (counsel-M-x . "")
    (counsel-describe-function . "")
    (counsel-describe-variable . "")
    (org-refile . "^")
    (org-agenda-refile . "^")
    (org-capture-refile . "^")
    (Man-completion-table . "^")
    (woman . "^"))))

(use-package ivy-rich
  :after ivy
  :ensure t
  :demand
  :custom
  (ivy-virtual-abbreviate 'full
        ivy-rich-switch-buffer-align-virtual-buffer t
        ivy-rich-path-style 'abbrev)
  :config
  (setq ivy-rich-parse-remote-buffer nil)
  (setq ivy-rich-parse-remote-file-path nil)
  (ivy-set-display-transformer 'ivy-switch-buffer
       'ivy-rich-switch-buffer-transformer)
  (ivy-rich-mode 1))

Ripgrep checks a lot of boxes: fast, compatible with ivy, and it is distributed as a small static binary that I can include in my Emacs "distribution". So let's configure it

;; this needs to go in the config
(setq counsel-rg-base-command (cons (concat initel-directory "random/"
                                            (car counsel-rg-base-command))
                                    (cdr counsel-rg-base-command)))

;; we want the search results to be displayed at the bottom of the window with a half split
(defun do.minimal.rg/get-window-height (caller)
  "Return the height of the current window divided by 2"
  (/ (window-total-height) 2))

(add-to-list 'ivy-height-alist '(counsel-rg . do.minimal.rg/get-window-height))

;; always default to the current directory and display a smaller prompt
(defun do.minimal.rg/call-counsel-rg ()
  (interactive)
  (let ((ivy-fixed-height-minibuffer t)
        (ivy-count-format ""))
    (counsel-rg nil default-directory nil "rg: ")))

;; set an alias for easier search
(defalias 'rg #'do.minimal.rg/call-counsel-rg)

I dabbled with using ivy-posframe, but I found that it is not responsive enough for my tastes. So I'll disable it for now and get back to it later.

(use-package ivy-posframe
  :ensure t
  :after ivy
  :diminish ivy-posframe-mode
  :custom-face
  (ivy-posframe ((t (list :background (face-attribute 'default :background)))))
  (ivy-posframe-border ((t (:background "gold"))))
  (ivy-posframe-cursor ((t (:background "gold"))))
  :config
  ;; custom define height of post frame per function
  (setq ivy-posframe-height-alist '((find-file . 13)
            (t         . 13)))
  ;; display at `ivy-posframe-style'
  (setq ivy-posframe-display-functions-alist
  '((complete-symbol   . ivy-posframe-display-at-point)
    (counsel-M-x       . ivy-posframe-display-at-point)
    (counsel-find-file . ivy-posframe-display-at-window-bottom-left)
    (ivy-switch-buffer . ivy-posframe-display-at-window-bottom-left)
    (t                 . ivy-posframe-display-at-window-bottom-left)))
  ;; other customizations
  (setq ivy-posframe-hide-minibuffer t)
  ;; (setq ivy-posframe-min-width 120)
  ;; (setq ivy-posframe-width 120)
  (setq ivy-posframe-border-width 1)
  (ivy-posframe-mode 1))

Windows

(when on-windows
  (setq visible-bell t))