Skip to content

My Emacs configuration. Pretty minimal, tailored primarily for Ruby/C/Rust programming. Uses use-package extensively. Tested on Linux/macOS only

Notifications You must be signed in to change notification settings

eightbitraptor/dotemacs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 

Repository files navigation

#+TITLE Main Emacs Configuration File #+AUTHOR Matthew Valentine-House #+STARTUP overview

Emacs config - eightbitraptor

https://user-images.githubusercontent.com/31869/201213452-ad555d25-565d-4550-bffc-c64b00170de4.png

This is the latest incarnation of my emacs config. It’s been built up, torn down, refactored and rebuilt many times since I started using Emacs (the earliest reference I can find is a really ugly screenshot of my work Mac circa 2011 - a long time before I knew remotely what I was doing :|).

Introduction

This configuration uses straight.el as a package manager and org babel to document the configuration. Alongside this file there is also an early-init.el which Emacs will run before setting up the GUI and before loading any other init files.

The early-init in this repo does a couple of things:

  1. Uses straight.el for packaging. That is:
    • install the latest version of straight.el if it’s not already installed
    • disable the built in package.el
    • install use-package and configure it to use straight.el by default
  2. Install the latest version of org-mode, with no config, just to be able to tangle this file.

I install the latest version of org-mode in the early init, to avoid any compatability problems later on when using straight.el to properly configure org and some dependencies that I like to use.

Setup Instructions

You should be able to clone this repo straight into ~/.emacs.d and start Emacs and everything will be setup and configured on first start. Although note that due to a lot of work being done in early-init.el (including loading this file) the first startup will be very slow as all the packages are git cloned from their various repositories, but also that the UI will not be running

This may make it look like it didn’t work, but check in htop/Task Manager/whatever and you should see the process active and doing stuff and eventually Emacs will appear. This will only happen on first boot.

I appreciate the UX for this is shit. But it only happens once per machine, and well; It doesn’t really bother me enough to fix it.

General Editor Settings

Setting up the PATH

I like to make sure that Emacs is using the same PATH and Ruby paths as my shell for consistency. This is important on macOS where processes are started with a different environment.

(use-package exec-path-from-shell
  :if (memq system-type '(darwin gnu/linux))
  :init (setq exec-path-from-shell-variables '("PATH"
                                               "MANPATH"
                                               "GEM_HOME"
                                               "GEM_PATH"))
        (setq exec-path-from-shell-check-startup-files nil)
        (exec-path-from-shell-initialize))

(setq frame-resize-pixelwise t)

No littering

No littering makes etc and var directories inside .emacs.d and organises all the files that Emacs and other packages would otherwise just dump directly into .emacs.d

(use-package no-littering)

Fish mode

I use the Fish shell. So I install the major mode for editing config files.

(use-package fish-mode)

Dirvish

Is a more fully featured dired mode. Let’s try it out for a bit

(use-package dirvish
  :init (dirvish-override-dired-mode))

Organise backup files

This block turns off auto save, turns off automatic backups, and sets a backup directory for any manually created backups to be inside the user-emacs-directory which is .emacs.d. This avoids littering projects with lots of ~~~ files, that aren’t often gitignored.

(let ((backup-dir (expand-file-name "backup/" user-emacs-directory))
      (autosave-dir (expand-file-name "autosave/" user-emacs-directory)))
  (when (not (file-directory-p autosave-dir))
    (make-directory autosave-dir))
  (when (not (file-directory-p backup-dir))
    (make-directory backup-dir))
  (setq auto-save-file-name-transforms `((".*" ,autosave-dir t)))
  (setq backup-directory-alist `(("." . ,backup-dir)))
  (setq backup-by-copying t))

Theming and Aesthetics

First install solaire-mode, this changes the background colour of “unreal” buffers - ie. buffers that are not backed by a file. In general use this has the effect of making files being edited immediately and obviously distinct from other types of buffers, like dired, treemacs, magit, or the find-file buffer.

This feature is one of the things I really enjoyed, visually, about VSCode

(use-package solaire-mode
  :demand t
  :init (solaire-global-mode +1))

Now set up a theme. We need to choose a theme here that explicitly supports solaire-mode.

(push '(menu-bar-lines . 0)   default-frame-alist)
(push '(tool-bar-lines . 0)   default-frame-alist)
(push '(vertical-scroll-bars) default-frame-alist)

(use-package doom-themes
  :config (load-theme 'doom-tokyo-night :no-confirm))

Then nyan cat as a progress bar in my modeline

(use-package nyan-mode
  :init (nyan-mode))

Silence the audible bell, and make the visual bell quite a bit more subtle - just flash the text on the modeline white briefly, rather than flashing the whole window.

(setq ring-bell-function
    (lambda ()
      (let ((orig-fg (face-foreground 'mode-line)))
        (set-face-foreground 'mode-line "#FFFFFF")
        (run-with-idle-timer 0.1 nil
                             (lambda (fg) (set-face-foreground 'mode-line fg))
                             orig-fg))))

I like the cursor to be an I-beam, and I like to disable all the toolbars and scrollbars for a pretty clean looking frame.

(setq-default cursor-type 'bar)
(toggle-scroll-bar -1)
(menu-bar-mode 0)
(tool-bar-mode 0)

Don’t display text in the startup buffer or the scratch buffer by default

(setq initial-scratch-message "")
(setq inhibit-startup-message t)

Increase the line spacing a little from the default and turn on line numbers globally.

(setq-default line-spacing 5)
(global-display-line-numbers-mode t)

Now we’ll configure the default fonts. We have to do this per OS because everything renders fonts differently and at different sizes.

(cond ((eq system-type 'gnu/linux)
       (set-face-attribute 'default nil :font "JetBrains Mono 12"))
      ((eq system-type 'darwin)
       (setq mac-frame-tabbing nil)
       (set-face-attribute 'default nil :font "Jetbrains Mono 16"))
      ((eq system-type 'windows-nt)
       (set-face-attribute 'default nil :font "Consolas 16")))

I spend time manually resizing my Emacs frame ususally when it starts, I usually want it to just take up roughly 70% of the screen, and be centered, so let’s make it do that. I used this code almost verbatim from here, but had to change the width calculation to consider multiple monitors used horizontally. This will always open the initial frame on the Primary monitor.

https://www.reddit.com/r/emacs/comments/9c0a4d/tip_setting_initial_frame_size_and_position/

;; Set initial frame size and position

(nth 3 (assoc 'geometry (car (display-monitor-attributes-list))))
(defun mvh/set-initial-frame ()
  (let* ((base-factor 0.85)
         (primary-monitor-actual-width
          (nth 3 (assoc 'geometry (car (display-monitor-attributes-list)))))
	 (a-width (* primary-monitor-actual-width base-factor))
         (a-height (* (display-pixel-height) base-factor))
         (a-left (truncate (/ (- primary-monitor-actual-width a-width) 2)))
	 (a-top (truncate (/ (- (display-pixel-height) a-height) 2))))
    (set-frame-position (selected-frame) a-left a-top)
    (set-frame-size (selected-frame) (truncate a-width)  (truncate a-height) t)))
(setq frame-resize-pixelwise t)
(mvh/set-initial-frame)

Line Lengths

Break all long lines automatically at fill-column so I don’t have to press M-q all the time. And then set fill-column to 80 chars, because 70 is a bit short.

Because I enable auto-fill-mode I also provide a convenience function, bound to C-c u to unfill any paragrahs that get broken when I don’t want them to be.

(setq-default fill-column 80)
(add-hook 'text-mode-hook 'turn-on-auto-fill)

(defun unfill-paragraph ()
  (interactive)
  (let ((fill-column (point-max)))
    (fill-paragraph nil)))
(global-set-key (kbd "C-u") 'unfill-paragraph)

Also enable a vertical ruler at 80 columns using display-fill-column-indicator-mode everywhere.

(global-display-fill-column-indicator-mode)

Mouse Scrolling

This controls how many lines the mouse wheel scrolls by.

(setq mouse-wheel-scroll-amount '(1 ((shift) . 1) ((control) . nil)))

Answer prompt questions faster

Use y/n in prompts instead of having to explicitly type yes or no

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

Ergonomics

I always used to smash these keys accidentally and they’d do random stuff. I was bad at typing!

I don’t think I’ve had this problem since I changed to using split ergo keyboards, so I’m not sure whether it’s still relevant or not. At some point I’ll get around to testing that…

(when window-system
  ((lambda ()
     (global-unset-key "\C-z")
     (global-unset-key "\C-x\C-z"))))

Also enable Auto-revert mode globally. I do a lot of stuff on the command line and in other tools, it’s nice to not have to get hit with the prompt when I switch back to Emacs and try to edit something.

(setq global-auto-revert-mode 1)

Default Indentation

Default to 4 spaces as an indent everywhere. Obviously other modes are going to override this as necessary, but I like a 4 space indent generally.

(setq-default indent-tabs-mode nil)
(setq-default c-basic-offset 4)

Default Selection behaviour

And turn on delete-selection-mode. This makes emacs visual selection behave much more like “modern” editors. ie. when you select stuff and start typing your text will replace the selected text, and you can highlight text and then hit backspace to delete it.

Without this minor mode enabled Emacs will start inserting text wherever the point is located (often at the end of the selection), and not actually remove the seletion.

(delete-selection-mode t)

M-q should toggle Fill Paragraph

(use-package unfill
  :bind ([remap fill-paragraph] . unfill-toggle))

Configuring the GC

Here we increase the number of bytes that are required to be allocated before starting a GC. This is mostly to appease lsp-mode later on this file, which does a lot of async json communication and generates a lot of garbage

(setq gc-cons-threshold 100000000)
(setq read-process-output-max (* 1024 1024))

Undo Tree.

This package is magical, it lets you see the entire edit history of your file as a tree instead of a linear series of changes. It also provides a way of visualising the tree, so you can get back basically any change you make while editing a file.

(use-package undo-tree
  :init (setq undo-tree-auto-save-history nil
              undo-tree-history-directory-alist '("~/.emacs.d/autosave/"))

  :config (global-undo-tree-mode))

Crux

Crux really is a collection of really useful extensions! The ones I like are:

  • crux-move-beginning-of-line bounces between the first non whitespace char in the line and the actual beginning of the line
  • crux-smart-open-line-above Inserts a new line above the point and indents it according to the context. Basically the same as O in Vim.
  • crux-smart-kill-line kills from the point to the end of the line, then when pressed again, kills the rest of the line. Just means I can usually hit C-k twice instead of C-a C-k which is quicker.
(use-package crux
  :bind (("C-a" . crux-move-beginning-of-line)
         ("C-o" . crux-smart-open-line-above)
         ("C-k" . crux-smart-kill-line)))

Whitespace Butler

Makes sure I don’t accidentally commit loads of bad whitespace.

(use-package ws-butler
  :init (setq ws-butler-keep-whitespace-before-point nil)
  (ws-butler-global-mode))

Helper Functions

A few things that I’ve found useful over the years. They should already be pretty well documented - just checkout the function documentation.

Kill other buffers and focus the window

(defun kill-other-buffers ()
  "Make the current buffer the only focus, and kill other buffers
that are associated with files."
  (interactive)
  (delete-other-windows)
  (mapc 'kill-buffer
        (delq (current-buffer)
              (remove nil (mapcar #'(lambda (b) (when (buffer-file-name b) b))
                                  (buffer-list))))))

Duplicate lines

(defun duplicate-line (arg)
  "Duplicate current line, leaving point in lower line."
  (interactive "*p")
  ;; save the point for undo
  (setq buffer-undo-list (cons (point) buffer-undo-list))
  ;; local variables for start and end of line
  (let ((bol (save-excursion (beginning-of-line) (point)))
        eol)
    (save-excursion
      ;; don't use forward-line for this, because you would have
      ;; to check whether you are at the end of the buffer
      (end-of-line)
      (setq eol (point))

      ;; store the line and disable the recording of undo information
      (let ((line (buffer-substring bol eol))
            (buffer-undo-list t)
            (count arg))
        ;; insert the line arg times
        (while (> count 0)
          (newline)         ;; because there is no newline in 'line'
          (insert line)
          (setq count (1- count)))
        )

      ;; create the undo information
      (setq buffer-undo-list (cons (cons eol (point)) buffer-undo-list)))
    ) ; end-of-let

  ;; put the point in the lowest line and return
  (next-line arg))

Quickly change frame font sizes

Useful when sharing my screen on a vidoe call so the font can be legible on the recording.

(defun my-alter-frame-font-size (fn)
  (let* ((current-font-name (frame-parameter nil 'font))
         (decomposed-font-name (x-decompose-font-name current-font-name))
         (font-size (string-to-number (aref decomposed-font-name 5))))
    (aset decomposed-font-name 5 (number-to-string (funcall fn font-size)))
    (set-frame-font (x-compose-font-name decomposed-font-name))))

(defun my-inc-frame-font-size ()
  (interactive)
  (my-alter-frame-font-size '1+))

(defun my-dec-frame-font-size ()
  (interactive)
  (my-alter-frame-font-size '1-))

Keybindings

Custom functions

First map some of the aformentioned custom functions.

(global-set-key (kbd "C-d") 'duplicate-line)
(global-set-key (kbd "C-+") 'my-inc-frame-font-size)
(global-set-key (kbd "C-=") 'my-inc-frame-font-size)
(global-set-key (kbd "C--") 'my-dec-frame-font-size)

Remap M-x

This is something I learned from Steve Yegge’s excellent blog post about effective Emacs - Use a key combo for M-x that doesn’t involve the Alt key as it’s non standard across environments and requires some weird hand scrunching to type properly.

I also like C-x C-m as it has as kind of tempo to it (command sequences having tempo is a really nice idea I learned about in Mickey Peterson’s Mastering Emacs book back in the day.

(global-set-key "\C-x\C-m" 'execute-extended-command)
(global-set-key "\C-c\C-m" 'execute-extended-command)

Resizing windows

Some keybindings for resizing Windows. I can’t remember when I last used these but you know I’d need them if I ever got rid of them so here they are.

(global-set-key (kbd "s-<left>")  'shrink-window-horizontally)
(global-set-key (kbd "s-<right>") 'enlarge-window-horizontally)
(global-set-key (kbd "s-<up>")    'enlarge-window)
(global-set-key (kbd "s-<down>")  'shrink-window)

Maximise frames

(global-set-key (kbd "s-<return>") 'toggle-frame-fullscreen)

Delete frames

With Emacs server running

(global-set-key (kbd "M-∑") 'delete-frame)

Move between windows

Enable windmove keybingings. This slightly arcanely named setting means you can move between windows with shift-u/d/l/r rather than cycling through with C-x o or the mouse.

(when (fboundp 'windmove-default-keybindings)
  (windmove-default-keybindings))

Which key tooltips

This package pops up a buffer containing all possible key combinations if you hit the start of a chord. I hope to one day not need this, but it’s stupidly useful when using stuff I don’t normally use everyday.

I’ve set the delay to be quite long at 3s, just to make sure it doesn’t get in my way when I’m doing normal things.

(use-package which-key
  :config (which-key-mode)
          (setq which-key-idle-delay 3))

Navigation, Search & Project Management

Ripgrep

for fast project searches, relies on the rg binary being somewhere on your path.

(use-package ripgrep)

Flx

Not actually sure why this is here. It does fuzzy matching, but I think it’s either pulled in as a dep of something or I don’t use it anymore.

(use-package flx)

Ivy

Ivy is a completion framework. So when you search for stuff it’ll help you narrow down onto the result that you’re looking for.

A nice write-up about it lives here. And the manual is here.

I use it in conjunction with ivy-rich which makes the UI column based to show more information.

I also use it in conjunction with xref so that when I do things like jumping between functions or searching for functions in source code, I get a nice looking list of functions and meta data about them and can narrow down on the one I want.

(use-package ivy-rich)
(use-package counsel)

(use-package ivy
  :init (setq ivy-use-virtual-buffers t
              ivy-sort-matches-functions-alist '((t . nil)
                                                 (ivy-completion-in-region . ivy--shorter-matches-first)
                                                 (execute-extended-command . ivy--shorter-matches-first)
                                                 (ivy-switch-buffer . ivy-sort-function-buffer)))
  (ivy-mode 1)
  (ivy-rich-mode 1))

I cargo-culted this init section from somewhere and I can’t remember what it fixes anymore.

(use-package ivy-xref
  :init (when (>= emacs-major-version 27)
          (setq xref-show-definitions-function #'ivy-xref-show-defs))
  (setq xref-show-xrefs-function #'ivy-xref-show-xrefs))

Imenu-list

This is the most lightweight equivalent of Vim’s Tagbar plugin that I could find. It uses imenu-mode to breakdown a source file and show you a list of Classes, structures functions and whatnot in a vertical bar on the right of the frame.

Fair warning though. It seems to crap itself in org-mode.

While we’re here let’s configure M-t to use trigger imenu.

(use-package imenu-list
  :bind ("C-c C-t" . imenu-list-smart-toggle))
(global-set-key (kbd "M-t") 'imenu)

Projectile

;; Projectile spins trying to calculate what to write in the modeline when using TRAMP.
;; forcing a static modeline causes tramp mode to get fast again
(use-package projectile
  :config (setq projectile-dynamic-mode-line nil)
  (projectile-global-mode)
  :bind-keymap ("C-c p" . projectile-command-map)
  :bind (("M-o" . projectile-find-file)
         ("C-S-g" . projectile-grep))
  :init (setq projectile-completion-system 'ivy))

(use-package ag)

(use-package projectile-rails
  :config (projectile-rails-global-mode t))
(use-package projectile-ripgrep)

Direnv

Direnv is heckin useful, I use it everywhere! You need the binary installed and set up in your shell, but then you can create a .envrc file in a directory, export shell variables in it, and they’re only applied when you’re in that directory.

I use it mainly for setting cflags on various projects.

(use-package direnv
  :init (direnv-mode))

Editorconfig

Support the ubiquitous .editorconfig files that keep cropping up all over the place.

Personally I’m a little uncomfortable about other people having control over my editor settings, but there’s no doubt they are useful. And we use them at work, so I guess I’ll just deal with it.

(use-package editorconfig
  :init (editorconfig-mode 1))

Programming Utilities

Company mode

Company mode handles tab completion for me. Not much extra config here, mostly just the reduction of some delays, so it appears quicker, and the addition of company box, which is analagous to ivy-rich for ivy. It pretties up the UI, and provides icons and stuff depending on what’s being completed, and also can link out to docs.

(use-package company
  :init (setq company-dabbrev-downcase nil
              company-idle-delay 0
              company-dabbrev-ignore-case 1)
  :hook (prog-mode . company-mode))

(use-package company-box
  :hook (company-mode . company-box-mode))

Magit

Magit mode is, imo, Emacs killer feature. Or at least on of the top ones. It’s an amazing way of interacting with Git.

No config to really note - I’ve set the magit status window to take up the whole frame, because when I context switch into Git mode I like to focus fully on it.

I’ve also enabled vc-follow-symlinks which helps out if you ever try and edit a symlink pointing to a file under source control, by following the link and opening the original file in Emacs.

(setq vc-follow-symlinks t)
(use-package magit
  :init (setq magit-display-buffer-function #'magit-display-buffer-fullframe-status-v1)
  (setq magit-push-current-set-remote-if-missing nil)
  :bind ("C-c s" . magit-status))

LSP

LSP configuration is slightly frustrating, because due to the way existence of a single lsp-client mode that talks to multiple lsp-servers I need to configure the servers here.

This means that this lsp-mode config block contains config that’s relevant to multiple different languages. Predominantly C, Ruby and Rust.

As a companion to using M-t to trigger imenu earlier in my config. We’ll mape M-T here to lsp-ivy-workspace-symbol so we can use M-t when we want to fuzzy jump to a symbol in the buffer, and M-T when we want to do the same but somwhere else in the project. This pretty closely my Vim config which uses <Space> t and <Space> T for the same.

(use-package flycheck)

(use-package lsp-mode
  :config (setq lsp-idle-delay 0.1
                lsp-headerline-breadcrumb-enable nil
                lsp-enable-on-type-formatting nil
                lsp-enable-indentation nil
                lsp-solargraph-formatting nil
                lsp-eldoc-enable-hover nil
                lsp-enable-snippets nil
                lsp-rust-analyzer-cargo-watch-command "clippy"
                lsp-rust-analyzer-server-display-inlay-hints t
                lsp-enabled-clients '(clangd
                                      ruby-lsp-ls
                                      rust-analyzer)
                lsp-clients-clangd-args '("--header-insertion=never"
                                          "--all-scopes-completion"
                                          "--background-index"))
  :hook ((c-mode . lsp)
         (c++-mode . lsp)
         (enh-ruby-mode . lsp)
         (rustic-mode . lsp)
         (lsp-mode . lsp-enable-which-key-integration))
  :bind (("<mouse-4>" . lsp-find-definition)
         ("S-<down-mouse-1>" . lsp-find-definition)
         ("S-<down-mouse-2>" . lsp-find-references)
         ("<mouse-5>" . xref-pop-marker-stack)
         ("<f12>" . lsp-find-references)
         ("s-'" . xref-pop-marker-stack))
  :bind-keymap ("C-c l" . lsp-command-map))

(use-package lsp-ivy
  :bind ("M-T" . 'lsp-ivy-workspace-symbol)
  :config (advice-add 'lsp-ivy--goto-symbol :before
                      (lambda (arg)
                        (xref-push-marker-stack)))
  (advice-add 'lsp-find-definition :before (lambda () (xref-push-marker-stack))))

(use-package ivy-xref
  :init
  ;; xref initialization is different in Emacs 27 - there are two different
  ;; variables which can be set rather than just one
  (when (>= emacs-major-version 27)
    (setq xref-show-definitions-function #'ivy-xref-show-defs))
  ;; Necessary in Emacs <27. In Emacs 27 it will affect all xref-based
  ;; commands other than xref-find-definitions (e.g. project-find-regexp)
  ;; as well
  (setq xref-show-xrefs-function #'ivy-xref-show-xrefs))


(use-package lsp-ui
  :config (setq lsp-ui-sideline-mode nil
                lsp-ui-flycheck-live-reporting nil
                lsp-ui-sideline-enable nil
                lsp-ui-sideline-show-diagnostics nil)
  :bind (:map
         lsp-mode-map
         ([remap xref-find-definitions] . #'lsp-ui-peek-find-definitions)
         ([remap xref-find-references] . #'lsp-ui-peek-find-references)))

While we’re on xref and stuff. Let’s also make is so that searching adds the point we’re left on to the xref stack.

(advice-add 'isearch-forward :before (lambda (arg1 arg2) (xref-push-marker-stack)))
(advice-add 'isearch-backward :before (lambda (arg1 arg2) (xref-push-marker-stack)))

Language Modes

Ruby

Some of the codebases I use often require frozen strings. So this convenience function will add the magic header if it’s not already there.

(use-package chruby)

(defun ruby-frozen-string-literal ()
  "Check the current buffer for the magic comment # frozen_string_literal: true.
If the comment doesn't exist, offer to insert it."
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (unless (string= (thing-at-point 'line)
                     "# frozen_string_literal: true\n")
      (insert "# frozen_string_literal: true\n\n"))))

I prefer to use rbenv to manage my Ruby versions, in conjunction with ruby-build. rbenv is pretty easy to understand, and whilst I’m not wild about the shims, it is working well for me. Work however, are all in on chruby and some of the bits of software I need to integrate with require it.

So I’ll use chruby mode when I have to, on my Shopify machine, and rbenv everywhere else

I also use minitest for testing.

(if (string-match "Shopify" (system-name))
    (use-package chruby
      :init (chruby "3.3.0"))
  (use-package rbenv
      :init (global-rbenv-mode)
      (rbenv-use-global)))

  (use-package minitest :ensure t)

I use enh-ruby-mode instead of the built in ruby-mode. This is entirely due to the existence of enh-ruby-bounce-deep-indent

ruby-mode’s default behaviour is to do this:

test_var = if condition
           "yes"
         else
           "no"
         end

Whereas enh-ruby-mode will let you toggle between that, and my preferred format with another press of the tab key

test_var = if condition
  "yes"
else
  "no"
end

There’s a small amount of customisation happening here

  • Make sure that we don’t add encoding comments to our files. Generally I don’t want anything in the git diff, other than what I’m explicitly changing.
  • The magic enh-ruby-bounce-deep-indent as well as clearing out the list of deep indent constructs so that we default to my preferred way. Normally if, def, class and module are deep indented by default
  • Turn on case-fold-search, this means that searching is basically case insensitive.
  • Makes sure that Ruby mode is activated for things that might not look like Ruby files: rack configs, Rakefiles, Gemfiles etc.

And some other things that I need to look into

  • [ ] Do I really want case-fold-search to be turned on?
  • [ ] What does enh-ruby-hanging-brace-indent-level do?
(use-package enh-ruby-mode
  :mode "\\.rb"
        "\\Gemfile"
        "\\.ru"
        "\\Rakefile"
        "\\.rake"
  :hook (enh-ruby-mode . subword-mode)
  :config (setq ruby-insert-encoding-magic-comment nil
                enh-ruby-add-encoding-comment-on-save nil
                enh-ruby-bounce-deep-indent t
                enh-ruby-deep-indent-construct nil
                enh-ruby-hanging-brace-indent-level 2
                case-fold-search t))

C

The Ruby core team maintain an emacs style mode inside the main CRuby source tree to help format the Ruby codebase according to their programming style (which as far as I can tell is a mix of K&R and GNU).

If I have a Ruby checkout in the standard place I keep my source files, then we should require the ruby-style file.

(let ((ruby-misc-dir "~/git/ruby/misc"))
  (if (file-directory-p ruby-misc-dir)
      (progn
        (add-to-list 'load-path ruby-misc-dir)
        (require 'ruby-style))))

Also add a hook to cc-mode that sets up the comment/uncomment toggle shortcut key, because by default it’s bound to comment region only and there’s no shortcut for uncommenting a region, which is bonkers.

(defun mvh/prog-mode-hook ()
  (define-key c-mode-map (kbd "C-c C-c") 'comment-or-uncomment-region))
(add-hook 'c-initialization-hook 'mvh/prog-mode-hook)
  (defun cruby/compile-command ()
    "Returns a String representing the compile command to run for the given context"
    "make miniruby")

  (defun cruby/test-command ()
    "Returns a String representing the test command to run for the given context"
    (cond
     ((eq major-mode 'c-mode) "make btest")
     ((eq major-mode 'enh-ruby-mode)
      (format "make test-all TESTS=\"%s\"" (buffer-file-name)))
     ))

  (projectile-register-project-type 'cruby '("ruby.c" "kernel.rb" "yjit.c" )
                                    :compile 'cruby/compile-command
                                    :test 'cruby/test-command)

(use-package meson-mode)

Rust

  • [ ] Remove or explain the cargo-culted rustic-mode-hook

This section sets up defaults for programming in Rust. I’m using rustic-mode. With a few keybindings to tie in to specific functions in lsp-mode (defined further up).

Most of the useful stuff is actually defined in the lsp-mode section.

(use-package rustic
  :bind (:map rustic-mode-map
              ("C-c C-c a" . lsp-execute-code-action)
              ("C-c C-c r" . lsp-rename)
              ("C-c C-c s" . lsp-rust-analyzer-status))
  :config (setq lsp-eldoc-hook nil
                lsp-enable-symbol-highlighting nil
                lsp-signature-auto-activate nil
                rustic-format-on-save nil)
          (add-hook 'rustic-mode-hook 'mvh/rustic-mode-hook))

(defun mvh/rustic-mode-hook ()
  ;; so that run C-c C-c C-r works without having to confirm, but
  ;; don't try to save rust buffers that are not file visiting. Once
  ;; https://github.com/brotzeit/rustic/issues/253 has been resolved
  ;; this should no longer be necessary.
  (when buffer-file-name
    (setq-local buffer-save-without-query t)))

R

I use R generally for a bit of light stats munging. These days its mostly taking benchmark results of perf numbers of the Ruby VM and visualising them.

(use-package ess)

Web (js/html/css)

I don’t do much web stuff anymore so this may not be the most up to date way of handling this. I basically just bring in web-mode by default for a bunch of files that look a lot like they could be web adjacent and configure a consistent 4 space indent.

(use-package web-mode
  :mode "\\.tsx"
        "\\.erb"
        "\\.jsx"
        "\\.html"
        "\\.css"
        "\\.scss"
        "\\.sass"
  :init (setq web-mode-markup-indent-offset 2)
        (setq web-mode-css-indent-offset 2)
        (setq web-mode-code-indent-offset 2)
        (setq web-mode-content-types-alist '(("jsx" . "\\.js[x]?\\'")))
        (setq web-mode-enable-auto-indentation 1))

Zig

An experiment with Zig.

(use-package zig-mode)

Markup (Markdown/toml/yaml)

Import the packages and associate the right file types required to write content in Markdown, Toml and Yaml.

I also configure a default stylesheet here for previewing Markdown documents in HTML. Leaving everything up to the browser really doesn’t do our documents any favours.

(setq markdown-preview-stylesheets
      (list "http://thomasf.github.io/solarized-css/solarized-light.min.css"))

(use-package toml-mode
  :mode "\\.toml")

(use-package yaml-mode
  :mode "\\.yml"
        "\\.yaml")

(use-package markdown-mode
  :mode "\\.md"
        "\\.markdown")

Org Mode

The org mode package has already been installed from the package repos in the early-init file, so that we could use the latest version to tangle this file into the standard init file init.el.

Org mode is then specified again here, so that we can define some more thorough initialisation on the package and set some custom variables.

The main ones defined here are the shift hooks. Setting these to the windmove functions, means that org-mode window switching behaviour is much more consistent with the rest of my emacs, which also has windmove enabled.

Now I can move windows with shift+arrows no matter the buffer type

(use-package org-make-toc
  :hook org-mode)
(use-package org
  :config (setq org-startup-truncated 1
                org-log-done 1)
          (add-to-list 'org-modules 'org-tempo t)

          (add-hook 'org-shiftup-final-hook 'windmove-up)
          (add-hook 'org-shiftleft-final-hook 'windmove-left)
          (add-hook 'org-shiftdown-final-hook 'windmove-down)
          (add-hook 'org-shiftright-final-hook 'windmove-right)

          (org-babel-do-load-languages 'org-babel-load-languages '((ruby . t)
                                                                   (emacs-lisp . t)
                                                                   (C . t)))
  :mode ("\\.org" . org-mode))

Quick notes (with Denote)

I often use the Apple Notes app to take super fast notes on stuff, that I can look back on later, snippets of debug commands and whatnot.

I’d like to use Emacs to do this so I don’t have to leave the editor so I’m testing out denote.

(use-package denote
  :custom (denote-directory "~/git/notes/misc")
  :bind ("C-x C-n" . denote))

Journal

I also use org-journal to document my days. It’s configured to start a new journal file per day in a folder in my home directory.

Each new entry in the same day gets a new timestamped org mode heading in that file.

(use-package org-journal
  :init (setq org-journal-prefix-key "C-c j ")
  :custom (org-journal-dir "~/git/notes/log_book/")
          (org-journal-file-format "%Y/%m/%d")
          (org-journal-date-format "%A %d %b %Y")
          (org-agenda-files "~/Documents/org/"))

Blogging

The following section is an experiment to see whether I can configure and live with a staticly generated blog/website entirely done within Emacs.

Currently my homepage uses Hugo and the process required to push a new post has a high enough barrier to entry that I forget it every time, and it makes me want to post less.

This is still **in progress**

(use-package org-static-blog
  :init
  (setq org-static-blog-use-preview t
        org-static-blog-preview-convert-titles t
        org-static-blog-preview-ellipsis "..."
        org-static-blog-enable-tags t
        org-static-blog-publish-url "http://localhost:9090/"
        org-static-blog-publish-title "eightbitraptor.com"
        org-static-blog-posts-directory "~/src/org-blog/org/posts"
        org-static-blog-drafts-directory "~/src/org-blog/org/drafts/"
        org-static-blog-publish-directory "~/src/org-blog/")

  (setq org-static-blog-page-header
        (concat
         "<meta name=\"author\" content=\"eightbitraptor\">"
         "<meta name=\"referrer\" content=\"no-referrer\">"
         "<link href= \"/static/style.css\" rel=\"stylesheet\"
                type=\"text/css\" />"
         "<link rel=\"icon\" href=\"static/favicon.ico\">")

        org-static-blog-page-preamble
        (concat
         "<div class=\"header\">"
         "  <a href=\"https://www.eightbitraptor.com\">eightbitraptor.com</a>"
         "  <div class=\"sitelinks\">"
         "    <a href=\"/blog/about.html\">about</a>"
         "    | <a href=\"/blog/software.html\">software</a>"
         "    | <a href=\"/blog/archive.html\">archive</a>"
         "    | <a href=\"/blog/rss.xml\">rss</a>"
         "  </div>"
         "</div>")))

;; Customize the HTML output
(setq org-html-validation-link nil
      org-html-head-include-scripts nil
      org-html-head-include-default-style nil
      org-html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"https://cdn.simplecss.org/simple.min.css\" />")

(setq org-publish-project-alist
      '(("orgfiles"
         :base-directory "~/org/"
         :base-extension "org"
         :publishing-directory "~/org/html"
         :publishing-function org-html-publish-to-html
         :headline-levels 3
         :section-numbers t
         :with-toc t
         :html-preamble t)

        ("images"
         :base-directory "~/org/images/"
         :base-extension "jpg\\|gif\\|png"
         :publishing-directory "~/org/html/images/"
         :publishing-function org-publish-attachment)

        ("other"
         :base-directory "~/org/other/"
         :base-extension "css\\|el"
         :publishing-directory "~/org/html/other/"
         :publishing-function org-publish-attachment)
        ("eightbitraptor" :components ("orgfiles" "images" "other"))))

Audio

MPD client

Here we configure the built-in mpc-mode to connect to a running Mopidy server on my home network desktop machine “senjougahara”.

This relies on the following things:

  • Mopidy is running with the MPD plugin on a host, using the default Mopidy port
  • There is some way of mapping the hostname “senjougahara” to an IP. My network is small so I just use an entry in /etc/hosts for this.

MPC mode has a really weird UI. It looks like it should behave like a “normal” music player, it has selection windows for genre, artist, album etc. But there doesn’t seem to be any built in ways to manipulate the main playlist in MPD beyond the standard mpc-add.

So there are a few helper functions in here that help to add groups of stuff to the playlist, as well as remove things and clear down the playlist. All features I use from ncmpcpp all the time.

I usually listen to Albums, so my workflow looks a bit like this:

  • browse for the album I want
  • press a to append it to the playlist
  • press p to start playing (this toggles play/pause states)
  • continue to add more albums as and when I feel like it.
  • when I want a change, hit S to stop playing and clear the current playlist

    Soon I’ll discover a simple way of selectively removing stuff from the playlist but I’m not quite there yet.

(use-package mpc
  :init
  (defun ebr/mpc-unselect-all (&optional event)
    "Unselect all selected songs in the current mpc buffer."
    (interactive)
    (save-excursion
      (goto-char (point-min))
      (while (not (eobp))
        (cond
         ((get-char-property (point) 'mpc-select)
          (let ((ols nil))
            (dolist (ol mpc-select)
              (if (and (<= (overlay-start ol) (point))
                       (> (overlay-end ol) (point)))
                  (delete-overlay ol)
                (push ol ols)))
            (cl-assert (= (1+ (length ols)) (length mpc-select)))
            (setq mpc-select ols)))
         ((mpc-tagbrowser-all-p) nil)
         (t nil))
        (forward-line 1))))
  (defun ebr/mpc-add-selected ()
    "Append to playlist, then unmark the song."
    (interactive)
    (mpc-playlist-add)
    (ebr/mpc-unselect-all))
  (defun ebr/mpc-add-at-point-and-unmark ()
    "Mark, append to playlist, then unmark the song."
    (interactive)
    (mpc-select-toggle)
    (mpc-playlist-add)
    (ebr/mpc-unselect-all))
  :custom
  (mpc-host "senjougahara")
  (mpc-songs-format "%2{Disc--}%3{Track} %28{Title} %18{Album} %18{Artist}")
  (mpc-browser-tags '(Artist Album))
  (mpc-cover-image-re "[Ff]older.jpg")
  :bind (:map mpc-mode-map
              ("a" . ebr/mpc-add-at-point-and-unmark)
              ("A" . ebr/mpc-add-selected)
              ("c" . ebr/mpc-unselect-all)
              ("d" . mpc-playlist-delete)
              ("p" . mpc-toggle-play)
              ("P" . mpc-playlist)
              ("s" . mpc-select)
              ("S" . mpc-stop)))

Emacs server

Finally we’ll start a server attached to this GUI instance so that I can use emacsclient in the terminal to open stuff in this instance.

(use-package server
  :init (server-start))

About

My Emacs configuration. Pretty minimal, tailored primarily for Ruby/C/Rust programming. Uses use-package extensively. Tested on Linux/macOS only

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published