Improving Dired in Emacs Solo: Git Status and File Icons

Cover Image for Improving Dired in Emacs Solo: Git Status and File Icons
Rahul M. Juliato
Rahul M. Juliato
#emacs#dired# git

⚠️ WARNING: EMOJI HEAVY CONTENT AHEAD ⚠️

🚨📢 Pro Tip: Need more emoji in your life? Emacs C-x 8 RET is magic 🪄✨


There are many ways to enhance Dired in Emacs. From full-featured packages like diff-hl, all-the-icons-dired and nerd-icons-dired to built-in solutions. There's absolutely nothing wrong with using well-maintained packages (I do for many things!), but sometimes you might want something more minimal or tailored to your workflow.

These are my personal implementations from Emacs Solo that follow a specific philosophy:

🛠️ No dependencies: beyond Emacs itself

📋 Copy/paste installable: into any config

🧰 Hacky but practical: they solve my needs first and small imperfections are ok

🔧 Easily modifiable: if you know basic Elisp

Some screenshots of what were are talking about:

Enhancements demo 01

Enhancements demo 02

Enhancements demo 03

Enhancements demo 04

What These Do

1️⃣ Git Status Gutter: Left-margin indicators for file status (modified/added/deleted)

2️⃣ File Type Icons: Unicode symbols instead of heavy icon fonts

Note: These aren't trying to replace full packages. They're alternatives for those who prefer lightweight solutions.


1. File Icons Using Unicode

A font-free alternative to icon packages:

;;; EMACS-SOLO-DIRED-ICONS
;;
(use-package emacs-solo-dired-icons
  :ensure nil
  :no-require t
  :defer t
  :init
  (defvar emacs-solo/dired-icons-file-icons
	'(("el" . "📜")      ("rb" . "💎")      ("js" . "⚙️")      ("ts" . "⚙️")
	  ("json" . "🗂️")    ("md" . "📝")      ("txt" . "📝")     ("html" . "🌐")
	  ("css" . "🎨")     ("scss" . "🎨")    ("png" . "🖼️")    ("jpg" . "🖼️")
	  ("jpeg" . "🖼️")   ("gif" . "🖼️")    ("svg" . "🖼️")    ("pdf" . "📄")
	  ("zip" . "📦")     ("tar" . "📦")     ("gz" . "📦")      ("bz2" . "📦")
	  ("7z" . "📦")      ("org" . "📝")    ("sh" . "💻")      ("c" . "🔧")
	  ("h" . "📘")       ("cpp" . "➕")     ("hpp" . "📘")     ("py" . "🐍")
	  ("java" . "☕")    ("go" . "🌍")      ("rs" . "💨")      ("php" . "🐘")
	  ("pl" . "🐍")      ("lua" . "🎮")     ("ps1" . "🔧")     ("exe" . "⚡")
	  ("dll" . "🔌")     ("bat" . "⚡")      ("yaml" . "⚙️")    ("toml" . "⚙️")
	  ("ini" . "⚙️")     ("csv" . "📊")     ("xls" . "📊")     ("xlsx" . "📊")
	  ("sql" . "🗄️")    ("log" . "📝")     ("apk" . "📱")     ("dmg" . "💻")
	  ("iso" . "💿")     ("torrent" . "⏳") ("bak" . "🗃️")    ("tmp" . "⚠️")
	  ("desktop" . "🖥️") ("md5" . "🔐")     ("sha256" . "🔐")  ("pem" . "🔐")
	  ("sqlite" . "🗄️")  ("db" . "🗄️")
	  ("mp3" . "🎶")     ("wav" . "🎶")     ("flac" . "🎶")
	  ("ogg" . "🎶")     ("m4a" . "🎶")     ("mp4" . "🎬")     ("avi" . "🎬")
	  ("mov" . "🎬")     ("mkv" . "🎬")     ("webm" . "🎬")    ("flv" . "🎬")
	  ("ico" . "🖼️")     ("ttf" . "🔠")     ("otf" . "🔠")     ("eot" . "🔠")
	  ("woff" . "🔠")    ("woff2" . "🔠")   ("epub" . "📚")    ("mobi" . "📚")
	  ("azw3" . "📚")    ("fb2" . "📚")     ("chm" . "📚")     ("tex" . "📚")
	  ("bib" . "📚")     ("apk" . "📱")     ("rar" . "📦")     ("xz" . "📦")
	  ("zst" . "📦")     ("tar.xz" . "📦")  ("tar.zst" . "📦") ("tar.gz" . "📦")
	  ("tgz" . "📦")     ("bz2" . "📦")     ("mpg" . "🎬")     ("webp" . "🖼️")
	  ("flv" . "🎬")     ("3gp" . "🎬")     ("ogv" . "🎬")     ("srt" . "🔠")
	  ("vtt" . "🔠")     ("cue" . "📀"))
	"Icons for specific file extensions in Dired.")

  (defun emacs-solo/dired-icons-icon-for-file (file)
	(if (file-directory-p file)
		"📁"
	  (let* ((ext (file-name-extension file))
			 (icon (and ext (assoc-default (downcase ext) emacs-solo/dired-icons-file-icons))))
		(or icon "📄"))))

  (defun emacs-solo/dired-icons-icons-regexp ()
	"Return a regexp that matches any icon we use."
	(let ((icons (mapcar #'cdr emacs-solo/dired-icons-file-icons)))
	  (concat "^\\(" (regexp-opt (cons "📁" icons)) "\\) ")))

  (defun emacs-solo/dired-icons-add-icons ()
	"Add icons to filenames in Dired buffer."
	(when (derived-mode-p 'dired-mode)
	  (let ((inhibit-read-only t)
			(icon-regex (emacs-solo/dired-icons-icons-regexp)))
		(save-excursion
		  (goto-char (point-min))
		  (while (not (eobp))
			(condition-case nil
				(when-let ((file (dired-get-filename nil t)))
				  (dired-move-to-filename)
				  (unless (looking-at-p icon-regex)
					(insert (concat (emacs-solo/dired-icons-icon-for-file file) " "))))
			  (error nil))  ;; gracefully skip invalid lines
			(forward-line 1))))))

  (add-hook 'dired-after-readin-hook #'emacs-solo/dired-icons-add-icons))

Tradeoffs:

✔️ Works everywhere (no special fonts)

✔️ Adds negligible overhead

❌ Less pretty than proper icons

❌ Manual extension mapping


2. Git Status Gutter for Dired

A "good enough" implementation showing Git status without external packages:

;;; EMACS-SOLO-DIRED-GUTTER
;;
(use-package emacs-solo-dired-gutter
  :ensure nil
  :no-require t
  :defer t
  :init
  (setq emacs-solo-dired-gutter-enabled t)

  (defvar emacs-solo/dired-git-status-overlays nil
	"List of active overlays in Dired for Git status.")

  (defun emacs-solo/dired--git-status-face (code)
	"Return a cons cell (STATUS . FACE) for a given Git porcelain CODE."
	(let* ((git-status-untracked "??")
		   (git-status-modified " M")
		   (git-status-modified-alt "M ")
		   (git-status-deleted "D ")
		   (git-status-added "A ")
		   (git-status-renamed "R ")
		   (git-status-copied "C ")
		   (git-status-ignored "!!")
		   (status (cond
					((string-match-p "\\?\\?" code) git-status-untracked)
					((string-match-p "^ M" code) git-status-modified)
					((string-match-p "^M " code) git-status-modified-alt)
					((string-match-p "^D" code) git-status-deleted)
					((string-match-p "^A" code) git-status-added)
					((string-match-p "^R" code) git-status-renamed)
					((string-match-p "^C" code) git-status-copied)
					((string-match-p "\\!\\!" code) git-status-ignored)
					(t "  ")))
		   (face (cond
				  ((string= status git-status-ignored) 'shadow)
				  ((string= status git-status-untracked) 'warning)
				  ((string= status git-status-modified) 'font-lock-function-name-face)
				  ((string= status git-status-modified-alt) 'font-lock-function-name-face)
				  ((string= status git-status-deleted) 'error)
				  ((string= status git-status-added) 'success)
				  (t 'font-lock-keyword-face))))
	  (cons status face)))

  (defun emacs-solo/dired-git-status-overlay ()
	"Overlay Git status indicators on the first column in Dired."
	(interactive)
	(require 'vc-git)
	(let ((git-root (ignore-errors (vc-git-root default-directory))))
	  (when (and git-root
				 (not (file-remote-p default-directory))
				 emacs-solo-dired-gutter-enabled)
		(setq git-root (expand-file-name git-root))
		(let* ((git-status (vc-git--run-command-string nil "status" "--porcelain" "--ignored" "--untracked-files=normal"))
			   (status-map (make-hash-table :test 'equal)))
		  (mapc #'delete-overlay emacs-solo/dired-git-status-overlays)
		  (setq emacs-solo/dired-git-status-overlays nil)

		  (dolist (line (split-string git-status "\n" t))
			(when (string-match "^\\(..\\) \\(.+\\)$" line)
			  (let* ((code (match-string 1 line))
					 (file (match-string 2 line))
					 (fullpath (expand-file-name file git-root))
					 (status-face (emacs-solo/dired--git-status-face code)))
				(puthash fullpath status-face status-map))))

		  (save-excursion
			(goto-char (point-min))
			(while (not (eobp))
			  (let* ((file (ignore-errors (expand-file-name (dired-get-filename nil t)))))
				(when file
				  (setq file (if (file-directory-p file) (concat file "/") file))
				  (let* ((status-face (gethash file status-map (cons "  " 'font-lock-keyword-face)))
						 (status (car status-face))
						 (face (cdr status-face))
						 (status-str (propertize (format " %s " status) 'face face))
						 (ov (make-overlay (line-beginning-position) (1+ (line-beginning-position)))))
					(overlay-put ov 'before-string status-str)
					(push ov emacs-solo/dired-git-status-overlays))))
			  (forward-line 1)))))))

  (add-hook 'dired-after-readin-hook #'emacs-solo/dired-git-status-overlay))

Tradeoffs:

✔️ Native Emacs speed

✔️ Visual feedback without clutter

✔️ No extra packages

❌ Less detailed than diff-hl-dired-mode

❌ Local files only

❌ Requires manual refresh (with g)


Philosophy Behind These Hacks

🎯 Solve immediate needs: These do exactly what I need. Your mileage may vary, and that's ok!

🔍 Stay editable: No abstractions hiding the core logic.

Embrace imperfection: The icons don't cover every file type, and that's okay.

And AGAIN: These aren't polished packages. They're starter code for your own tweaks. 😊


When To Use These vs Packages

| Topic         | My Version    | Full Packages       |
| ------------- | ------------- | ------------------- |
| Installation  | Copy/paste    | Package-install     |
| Customization | Edit directly | Read package docs   |
| Appearance    | Functional    | Polished            |
| Maintenance   | You own it    | Community-supported |

Choose these if: You value simplicity over features. You want to deal with things on your own. You want to learn while creating your own tools.

Choose packages if: You want battle-tested solutions


🚀 Try It Out

  1. 📋 Copy either/both snippets to your config
  2. 🎨 Tweak the icons or status symbols
  3. ⚙️ Adapt to your workflow

Or grab the latest from: Emacs Solo.

Happy hacking! 💻