The Emacs Operating System (EOS)

Table of Contents

Author Lee Hinman (lee@writequit.org)
Date 2017-08-21 14:30:13

Emacs Operating System

This is my attempt at a complete workflow inside of Emacs.

There are many like it, but this one is mine. So let's start with a story.

I used to be a hardcore Vim guy (hence the website named after a Vim command), in 2010 I joined a small company called Sonian doing Clojure as a full-time gig. As part of the job, we all pair-programmed together on Clojure code in Emacs using TMUX. This meant that I needed to learn Emacs having never used it before. I dived in head first and found Emacs to be a more customizable and elegant editor.

Around the same time, I switched to typing in Dvorak, so if you are reading this configuration and wondering why some of the bindings are the way they are, remember that my keys might be different than someone on Qwerty :)

People often say about Emacs "sure it's a great operating system, but it lacks a good editor" so I decided to call my configuration "EOS" for the Emacs Operating System. This is my tribute to the Complete Computing Environment, which heavily inspires this and from which I continue to find interesting tidbits to copy.

If you want to look at specific parts, jump down to the module list to see links to different parts of the configuration.

How to use these files

So usually you would check out this repository and then run make in the directory. That will tangle a bunch of .el files. If you want to install this, run make install (or just make init if you only want to run initialization things).

Once it's installed, make changes to the files directly and then just run make to re-tangle the files, no re-installation necessary, since symlinks are set up the first time you ran make install.

If you get lost at any point, checking the org source is probably the best way to go, or send me an email (lee at writequit dot org) or a message on twitter.

Because the installation symlinks things into whatever directory EOS is checked out it, it allows me to type make and generate new .el files without any file copying.

Initial Preparation

There is some initial prep that needs to happen before someone could run this, basically creating a bunch of directories so the tangled files and symlinks can go in the right place.

This will tangle an initialization script and invoke it. This is handled by the Makefile which special-cases this one file. Basically the following goes into initialize.sh:

# Directory for user-installed scripts
mkdir -p ~/bin

# GnuPG
mkdir -p ~/.gnupg
chmod 700 ~/.gnupg

# SSH
mkdir -p ~/.ssh
chmod 700 ~/.ssh

# Emacs configuration folders
mkdir -p ~/.emacs.d
mkdir -p ~/.emacs.d/snippets
mkdir -p ~/.emacs.d/eshell

This initialization file is then run first thing when running make install (or you can run make initialize manually to run this part).

Installation script

In addition to the prep script, there needs to be a script used for installation that actually links up the appropriate parts. It ends up in install.sh

Warning! This will overwrite your current configuration if you run install.sh!

ln -sfv $PWD/eos.el ~/.emacs.d/init.el
ln -sfv $PWD ~/.emacs.d/eos
ln -sfv $PWD/site-lisp ~/.emacs.d/site-lisp
cp -vf bin/* ~/bin

The EOS Module Set

There is of course the prerequisite, which is this file, and then EOS contains a number of different configurations and modules. This is a basic overview of the modules, which you should visit should you desire more information about a particular module.

Ideally, each module except for the "Core" module is optional and can be skipped if not desired. In practice though, I load all of them, because this is my config. I haven't really tried loading them all individually to make sure I don't have links between them.

init.el

init.el is the file that Emacs reads when it starts up, so here we do most of the bootstrapping before the EOS modules are loaded, then load the modules, then some cleanup at the end. It's worth noticing that even though this would tangle to eos.el by default, it gets symlinked to ~/.emacs.d/init.el.

Since an error may occur in loading any EOS files, I set some debugging things so a debugger is entered if there's a problem. These get unset after everything loads successfully.

(setq debug-on-error t)
(setq debug-on-quit t)

I load a couple of custom versions of libraries that are included in Emacs. This is so I can run a newer version than what's bundled, in particular this checks for the existence and loads them if there are there, otherwise it uses the bundled version.

A custom version of CEDET:

;; Load a custom version of cedet, if available
(when (file-exists-p "~/src/elisp/cedet/cedet-devel-load.el")
  (load "~/src/elisp/cedet/cedet-devel-load.el"))

I hardcode a version of of Org-mode:

;; Load a custom version of org-mode, if available
(when (file-exists-p "~/src/elisp/org-mode/lisp")
  (add-to-list 'load-path "~/src/elisp/org-mode/lisp")
  (add-to-list 'load-path "~/src/elisp/org-mode/contrib/lisp")
  (require 'org))

Also, let's make cl things available right from the start

(require 'cl)

I can't live without this, "x" on Dvorak is where "b" is on Qwerty, and it's just too hard for all the C-x things I have to hit. Maybe one day I'll just switch to evil (or god-mode) and be done with it.

For now, 't' is much more convenient so I switch C-x and C-t on the keyboard. I don't transpose things nearly as often as I C-x things

(define-key key-translation-map "\C-t" "\C-x")
(define-key key-translation-map "\C-x" "\C-t")

package.el Setup

My strategy with regard to packaging is simple, I make heavy use of use-package which does most of the installing with the :ensure keyword, but I need to set up the sources at least

(require 'package)
(package-initialize)

;; orgmode.org unfortunately does not support https
(add-to-list 'package-archives
             '("org" . "http://orgmode.org/elpa/") t)
(add-to-list 'package-archives
             '("gnu" . "https://elpa.gnu.org/packages/") t)
(add-to-list 'package-archives
             '("melpa-stable" . "https://stable.melpa.org/packages/") t)
(add-to-list 'package-archives
             '("melpa" . "https://melpa.org/packages/") t)

Let's also set up a custom file and load it before we do anything too fancy, we want to make sure to keep customize settings in their own file instead of init.el.

(setq custom-file "~/.emacs.d/custom.el")
(when (file-exists-p custom-file)
  (load custom-file))

I define eos/did-refresh-packages, which is used as a signal in install-pkgs that we need to refresh the package archives.

(defvar eos/did-refresh-packages nil
  "Flag for whether packages have been refreshed yet")

install-pkgs is a simple elisp function that will iterate over a list, and install each package in it, if it is not installed. If eos/did-refresh-packages is set to nil, it'll also refresh the package manager.

(defun install-pkgs (list)
  (dolist (pkg list)
    (progn
      (if (not (package-installed-p pkg))
          (progn
            (if (not eos/did-refresh-packages)
                (progn (package-refresh-contents)
                       (setq eos/did-refresh-packages t)))
            (package-install pkg))))))

Pin some of the packages that go wonky if I use the bleeding edge.

(when (boundp 'package-pinned-packages)
  (setq package-pinned-packages
        '((org-plus-contrib                  . "org")
          (cider                             . "melpa-stable")
          (ac-cider                          . "melpa-stable")
          (clojure-mode                      . "melpa-stable")
          (clojure-mode-extra-font-locking   . "melpa-stable")
          (company-cider                     . "melpa-stable"))))

Now, install the things we need in the future for all other package installation/configuration, in particular, use-package needs to be installed because we require it everywhere else.

(install-pkgs '(use-package))
;; Load use-package, used for loading packages everywhere else
(require 'use-package nil t)
;; Set to t to debug package loading or nil to disable
(setq use-package-verbose nil)

el-get setup

I install el-get, but so far I haven't really used it for much, because everything I want is on MELPA, and I don't really mind bleeding edge, regardless, it's there if I want it.

(add-to-list 'load-path "~/.emacs.d/el-get/el-get")

(unless (require 'el-get nil 'noerror)
  (with-current-buffer
      (url-retrieve-synchronously
       "https://raw.githubusercontent.com/dimitri/el-get/master/el-get-install.el")
    (goto-char (point-max))
    (eval-print-last-sexp)))

(add-to-list 'el-get-recipe-path "~/.emacs.d/el-get-user/recipes")
;;(el-get 'sync)

Module setup

And now, let's start things up by loading all of the modules. I'd eventually like to keep the module list in an org table and reference it here, but I'm not quite sure how that would work for tangling, so for now it's hard-coded.

(defvar after-eos-hook nil
  "Hooks to run after all of the EOS has been loaded")

(defvar emacs-start-time (current-time)
  "Time Emacs was started.")

;; Installed by `make install`
(add-to-list 'load-path "~/.emacs.d/eos/")

(defmacro try-load (module)
  "Try to load the given module, logging an error if unable to load"
  `(condition-case ex
       (require ,module)
     ('error
      (message "EOS: Unable to load [%s] module: %s" ,module ex))))

;; The EOS modules
(try-load 'eos-core)
;; (try-load 'eos-ido)
(try-load 'eos-helm)
;; (try-load 'eos-ivy)
(try-load 'eos-appearance)
(try-load 'eos-navigation)
(try-load 'eos-notify)
(try-load 'eos-completion)
(try-load 'eos-develop)
(try-load 'eos-git)
(try-load 'eos-es)
(try-load 'eos-org)
(try-load 'eos-writing)
(try-load 'eos-dired)
(try-load 'eos-remote)
(try-load 'eos-java)
(try-load 'eos-clojure)
(try-load 'eos-web)
(try-load 'eos-shell)
(try-load 'eos-mail)
(try-load 'eos-irc)
(try-load 'eos-distributed)
(try-load 'eos-rss)
(try-load 'eos-twitter)
(try-load 'eos-leisure)
(try-load 'eos-music)

;; Hooks
(add-hook 'after-eos-hook
          (lambda ()
            (message "The Emacs Operating System has been loaded")))

(defun eos/time-since-start ()
  (float-time (time-subtract (current-time)
                             emacs-start-time)))

(add-hook 'after-eos-hook
          `(lambda ()
             (let ((elapsed (eos/time-since-start)))
               (message "Loading %s...done (%.3fs)"
                        ,load-file-name elapsed))) t)
(add-hook 'after-init-hook
          `(lambda ()
             (let ((elapsed (eos/time-since-start)))
               (message "Loading %s...done (%.3fs) [after-init]"
                        ,load-file-name elapsed))) t)
(run-hooks 'after-eos-hook)

(setq initial-scratch-message ";; ╔═╗┌─┐┬─┐┌─┐┌┬┐┌─┐┬ ┬\n;; ╚═╗│  ├┬┘├─┤ │ │  ├─┤\n;; ╚═╝└─┘┴└─┴ ┴ ┴ └─┘┴ ┴\n")

Turn debugging back off, if there were no errors then things successfully got loaded.

(setq debug-on-error nil)
(setq debug-on-quit nil)

If you've checked this out so far, head back up and check out the Module Set!

License

Copyright (C) 2015-2017 Lee Hinman <lee@writequit.org>

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.

Code in this document 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 code 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.

This document https://writequit.org/eos/ (either in its HTML format or in its Org format is licensed under the GNU Free Documentation License version 1.3 or later (http://www.gnu.org/copyleft/fdl.html).

The code examples and CSS stylesheets are licensed under the GNU General Public License v3 or later (http://www.gnu.org/licenses/gpl.html).

Author: Lee Hinman

Created: 2017-08-21 Mon 14:30