The Arcology Garden

Yeah, I Run Emacs on my Steam Deck, Ever Heard of It?


Look, listen. Any of my friends are gonna think I'm a terrible goblin when I say, and everyone else will be so so confused, but: I spent some time setting up Emacs on my SteamDeck so that very narrow parts of my org-mode's functionality works with the game pad inputs.

Wow a handheld Linux device from a Big Company!! ATTACH

The SteamDeck is Valve's first attempt at a handheld game console. It's basically a really chonky Nintendo Switch which does not switch, and runs an operating system you may have heard of called Arch Linux.

Now, the default experience is much closer to Steam's "big picture" mode – it's called Deck Mode or Gaming Mode and it's a nice UI built around the controller and touch screen that lets you launch many of the games in your Steam library.

You can peel back a few layers, though, and unlock some neat functionality like, well, emulating almost every system up to the PS3 and Switch. You can run arbitrary Windows applications through Valve's Proton fork of WINE. You can jump to a full KDE Plasma desktop and run any application available on FlatHub.

Me? I installed Nix and Emacs on it

Okay, so I did all of that, too, actually and have a really cool little gaming machine that has, well, nearly every game I have ever wanted to play on it. Things which need a lot of screen real estate or keyboard don't work obviously, so games like Cogmind and Sid Meier's Alpha Centauri are out, but I can comfortably run both of those on my laptop.

But I was missing something… I was missing Emacs! Now, look here, kids. Emacs isn't just a fancy programming tool for old farts and hipsters. It also is a text editor! And it's the only gosh darn text editor that works for the weird outlining format I have built my life around, ye olde org-mode. Without a keyboard, this is not so useful in general, but there are weird little Hypermedia apps on top of it like the one I use for Spaced Repetition Study – i can annotate any of my notes and call up flashcards on them, and it only needs like 10 buttons. I would like to do those flash cards without all my laptop and chat windows and distractions, and this means the Steam Deck could be perfect for this. It sure beats trying to extract Anki decks from my notes. I really do recommend using org-fc to stop forgetting things in your notes.

My existing Emacs is a version referred to as GccEmacs – it generates C code for Emacs Lisp libraries and then uses a GCC just in time compiler to generate native byte code which is noticeably faster than previous versions. I use nix-community/emacs-overlay to enable this, a set of helper functions that lay on top of nixpkgs's Emacs package infrastructure to provide pre-compiling of libraries, dependency management, etc.

use steamdeck-nix-multiuser to install Nix

This is a neat little set of scripts that an acquaintance of mine from Fediverse is working on. There is a script in the repository which sets up overlay file systems and bind-mounts to have a persistent installation of Nix which will survive Valve's OS updates. Check out the repository, but also here's what I did to get all the way to a working home-manager installation.

visudo -f /etc/sudoers.d/wheel 
usermod -aG wheel deck
sh <(curl -L --daemon
nix-channel --add nixpkgs
nix-channel --add home-manager
nix-channel --update

add to .bash_profile:

. ~/.nix-profile/etc/profile.d/
export NIX_PATH=$HOME/.nix-defexpr/channels:/nix/var/nix/profiles/per-user/root/channels${NIX_PATH:+:$NIX_PATH}

log out and back in, then:

nix-shell '<home-manager>' -A install

In theory I could get cached GccEmacs builds, but in practice it builds from source some times:

nix-env -iA cachix -f
cachix use nix-community

From Home Manager to Running Emacs

So now that I have home-manager in my path, I can make a Nix file repository or clone my existing Arroyo Home Manager stuff on to it. For simplicity's sake, I made a new one:

{ config, pkgs, lib, ... }:

  my-emacs = import ./pkgs/emacs.nix { inherit pkgs; };
  programs.home-manager.enable = true;

  home.username = "deck";
  home.homeDirectory = "/home/deck";
  fonts.fontconfig.enable = true;

This is important to have the applications' .desktop files show up in the launcher: = [

I installed Syncthing and some other simple tools which org-fc needs:

home.packages = with pkgs; [
  # python3Packages.jisho-api

# syncthing
services.syncthing.enable = true;
services.syncthing.tray.enable = true; = lib.mkForce "${pkgs.syncthingtray}/bin/syncthingtray --wait"; = lib.mkForce [""]; = lib.mkForce [];

and of course my init.el and emacs-overlay itself:

  home.file.".emacs.d/init.el".source = ./init.el;

  # emacs-overlay
  nixpkgs.overlays = [
    (import (builtins.fetchTarball {
      url =;


My Emacs package embeds a build of org-fc in it since it's not packaged in an ELPA:

{ pkgs }:

pkgs.emacsWithPackagesFromUsePackage {
  config = ../init.el;
  package = pkgs.emacsPgtkNativeComp;
  alwaysEnsure = true;
  override = epkgs: epkgs // {
    org-fc = epkgs.melpaBuild {
      pname = "org-fc";
      version = "20220823.2107";
      commit = "f64b5336485a42be91cfe77850c02a41575f5984";

      src = pkgs.fetchFromGitHub {
        owner = "l3kn";
        repo = "org-fc";
        rev = "f64b5336485a42be91cfe77850c02a41575f5984";
        sha256 = "sha256-pb1UWhqim88uWVj/8UYEEGTT3HL3sespo3f+BPIeCrQ=";

      recipe = pkgs.writeText "recipe" ''
           :files (:defaults "awk")
           :repo "l3kn/org-fc"
           :fetcher github)

      packageRequires = [ pkgs.gawk epkgs.hydra ];

      meta = with pkgs; {
        homepage = "";
        license = lib.licenses.gpl3Plus;

My init.el is verbose, I'll only include some relevant parts of it below:

Relevant init.el functionality

Start by setting up use-package:

(add-to-list 'package-archives
             '("gnu" . "") t)
(add-to-list 'package-archives
             '("melpa" . "") t)

(setq cce/did-refresh-packages nil)
(defun install-pkg (pkg)
  (unless (package-installed-p pkg)
    (unless cce/did-refresh-packages
      (setq cce/did-refresh-packages t))
    (package-install pkg)))

(install-pkg 'use-package)

(require 'use-package)
(setq use-package-always-ensure t
      use-package-compute-statistics t)

Minimal-ish org-mode configuration for this:

(use-package org-bullets
  :after org
  :hook (org-mode . org-bullets-mode))
(use-package org-indent
  :after org
  :ensure nil
  (setq org-startup-indented t
        org-hide-leading-stars nil)
  :hook (org-mode . org-indent-mode))

(setq org-export-coding-system 'utf-8)
(prefer-coding-system 'utf-8)
(set-charset-priority 'unicode)
(setq default-process-coding-system '(utf-8-unix . utf-8-unix))

(use-package org-id
  :after org
  :ensure nil
  (setq org-id-method 'ts)
  (setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)
  (setq org-clone-delete-id t))

(use-package org
  ("C-c l" . org-store-link)
  ("C-c c" . org-capture)
  (require 'org-compat)
  (setq org-startup-folded t
        org-catch-invisible-edits 'error
        org-src-fontify-natively t
        org-return-follows-link t
        org-edit-src-content-indentation 0)
  ;; use org-mode in org files
  (add-to-list 'auto-mode-alist
               '("\\.\\(org\\|org_archive\\)$" . org-mode))
  ;; org-modules defines a bunch of "plugins" to load
  (setq org-modules '(org-id))
  (setq org-file-apps '((auto-mode . emacs)
                        (t . system)))
  (setq org-image-actual-width nil))

I use Evil Mode, but these keybindings basically map to the defaults; be sure to uncomment the (evil prefixed lines if you do, too.

(use-package org-fc
  (org-fc-directories '("~/org/"))
  (org-fc-review-history-file (expand-file-name "~/org/org-fc-reviews.tsv"))
  (setq org-fc-custom-contexts
        '((japanese . (:filter (and (tag "japanese")
                                    (tag "vocabulary"))))
          (buddhism . (:filter (tag "Buddhism")))
          (trivia . (:filter (tag "trivia")))
          (kanji . (:filter (or (tag "jokugo") (tag "kanji"))))
          (poetry . (:filter (tag "poem")))
          (tokipona . (:filter (tag "tokipona")))
          (row . (:filter (not (tag "vocabulary"))))))
  (require 'org-fc-hydra)
  (defalias 'srs #'org-fc-dashboard)
  ;; (evil-set-initial-state #'org-fc-dashboard-mode 'motion)
  ;; (evil-define-minor-mode-key '(normal insert emacs) 'org-fc-review-flip-mode
  ;;   (kbd "RET") 'org-fc-review-flip
  ;;   (kbd "n") 'org-fc-review-flip
  ;;   (kbd "s") 'org-fc-review-suspend-card
  ;;   (kbd "q") 'org-fc-review-quit)

  ;; (evil-define-minor-mode-key '(normal insert emacs) 'org-fc-review-rate-mode
  ;;   (kbd "a") 'org-fc-review-rate-again
  ;;   (kbd "h") 'org-fc-review-rate-hard
  ;;   (kbd "g") 'org-fc-review-rate-good
  ;;   (kbd "e") 'org-fc-review-rate-easy
  ;;   (kbd "s") 'org-fc-review-suspend-card
  ;;   (kbd "q") 'org-fc-review-quit)

Make it look nice and legible:

(use-package ef-themes
  :ensure t
  (load-theme 'ef-autumn t)
  (load-theme 'ef-spring t t))

(defun cce/set-font-scale (size)
  (interactive "nWhat font size do you want? ")
  (require 'cl)
  (set-face-attribute 'mode-line nil :inherit 'fixed-pitch :height (+ 10 size))
  (lexical-let ((size size))
    (with-eval-after-load 'org (set-face-attribute 'org-block nil :inherit 'fixed-pitch))
    (with-eval-after-load 'linum (set-face-attribute 'linum nil :inherit 'default :height size)))
  (set-face-attribute 'default nil        :height size)
  (set-face-attribute 'fixed-pitch nil    :inherit 'default)
  (set-face-attribute 'variable-pitch nil :slant 'oblique :height size))
(cce/set-font-scale 140)

Some helpers from my Japanese Study configuration:

(with-eval-after-load 'org
  (defun cce/eldoc-jhk (&rest args)
    (when (looking-at "[ぁ-ヿ]")
      (get-char-code-property (string-to-char (thing-at-point 'char)) 'name)))

  (defun cce/eldoc-jhk-setup ()
    (add-hook 'eldoc-documentation-functions
              #'cce/eldoc-jhk nil t))

  (add-hook 'org-mode-hook #'cce/eldoc-jhk-setup))

(defalias 'jisho-this 'cce/jisho-at-point)

(defun cce/jisho-at-point ()
  (let ((chr (thing-at-point 'word)))
    (browse-url (format "" chr))))

These keybindings will come in handy when setting up the gamepad mapping:

(global-set-key (kbd "<f7>") #'execute-extended-command)
(global-set-key (kbd "<f9>") #'execute-extended-command-for-buffer)
(global-set-key (kbd "<f8>") #'srs)
(global-set-key (kbd "<f12>") #'org-fc-review)

Briefly: x11docker

So now that Emacs is running Desktop Mode, things are moving along nicely, but we're still falling short: Desktop Mode does not have access to all the controller rebinding stuff that is present in Gaming Mode. They're "just" gamepad devices as far as linux is concerned and Emacs doesn't support gamepad devices, so you'd have to make a goofy little cyberdeck for this to be usable. This is, of course, a shortcoming which would/should/could be rectified with enough parentheses but there are other options.

Viv pointed me to this gnarly fucking shell script called x11docker and it does seem useful in general. Ultimately it can be used to run a full desktop inside of the Gaming Mode interface. It seems like a good tool to have in the belt in general. In theory i could take the x11docker/xfce image or so and extend it with nixpkgs's dockerTools to inject my emacs-overlay package or even a full home-manager derivation but augh augh augh. i spent a few hours on this and got frustrated with it.

Emacs inside of Gaming Mode

So instead, I found this neat little hack to run Desktop Mode within Gaming Mode. You end up with a little inside of your Steam Deck launcher, and when you click it the desktop mode opens. I have no idea what happens if you launch Steam in there! don't do it! Just launch Emacs.

exec startplasma-wayland --xwayland --x11-display $DISPLAY --no-lockscreen --width 1280 --height 800 -- plasma_session

Set Emacs to Auto Start in the KDE settings, you could even have Home Manager set all this up for you. I also recommend making a Window Management Rule in System Settings to maximize the Emacs window when it opens, or invoke M-x toggle-frame-fullscreen. Someone with slightly more patience than me could probably just get this running with a home-manager managed Weston or Sway but this is fine enough and lets me run other things without thinking too hard about it.

Now, without a keyboard, there's not much you can do with Emacs, but the controller settings in the Steam Deck actually give you a lot of levers to fix this. Any of the hardware buttons, touch pads, etc, can be set up to do things like show command menus, send keyboard and mouse events, change layers. Imagine QMK but you don't have to write any fucking code to do it.

I made some absolutely cursed shit. Naturally, I can't figure out how to share the layout directly.

Two layers: one for navigating to the SRS mode, one for navigating and rating the cards. Pressing UP on the D-PAD changes between the two. (i know these don't rotate, i'm sorry, it's not my fault.)

With that, I can hit "R4" to open my SRS dashboard, then "A" to start reviewing, then UP to enter review mode. I can mash B to send C-g.

In review mode, right bumper will flip the card for rating, left bumper will quit. A is "good" rating, B is "hard" rating, X is "easy", A is "again, please".

This is a hilariously stupid system and I'm not sure I'll keep using it. It will, however, work, and allow me to study my flashcards in bed without getting distracted by my laptop and whatnot. It will not keep me from getting distracted by art of rally though.

enjoy your gaming.