Using Emacs Org-mode As My Package Manager

3 min read Original article ↗

Actually, let’s add a block for searching for packages first. We’ll read all packages from pacman, and use completing-reading to prompt the user (that is, me) for the package name:

(let* ((upstream (split-string (shell-command-to-string "pacman -Slq")))
       (pkg (completing-read "Package: " upstream)))
  (term (concat "pacman -Si " pkg)))

It’s a bit slow as it runs pacman -Slq to get the package list. But personally, the auto-complete experience is actually better than my personal Zsh config, where every tab-completion also seems to fetch the package list afresh.

Also, if we are to always use Org-mode for package management, we can very easily add a cache for this. But I’m just too lazy for that.

Similarly, when installing a package from Org-mode, we prompt for the package name (or package names, with completing-read-multiple), use org-link-open-from-string to jump to our installed table, update it, jump back, and emit an extra babel block for the user to execute, which actually installs things with pacman.

…Confused? That means, when you C-c C-c on the following babel block:

#+begin_src elisp :var packages=installed :wrap src bash
  (setq upstream (fetch-all-packages))
  (setq package (completing-read "What to install: " upstream))
  (update-our-installed-table package (read-string "Reason: "))
  (prin1-to-string (list 'term (format "sudo pacman -S %s" package)))
#+end_src

It will update things in the note file, and then generate a new babel block (thanks to the :wrap src elisp header argument), like this:

#+RESULTS:
#+begin_src elisp
  (term "sudo pacman -S linux")
#+end_src

…which you can use C-c C-c again to confirm and execute, to actually install that package (or use C-/ to undo the table change).

The actual code I use for installing things

Note that this is likely garbage code. And I only use it because I’m convinced that I will be able to recover from any problem this might cause me. Use with caution or code your own! Note that you need :var packages=installed :wrap src bash on the code block header.

(let* ((upstream (split-string (shell-command-to-string "pacman -Slq")))
       (pkg-string (completing-read-multiple "Package(s) to install: " upstream))
       (pkgs (read (concat "(" (string-join pkg-string " ") ")")))
       (reasons (seq-filter
                 (lambda (s) (not (string-match-p "#" s)))
                 (seq-uniq (mapcar #'cadr (cdr packages)))))
       (reason (completing-read "Category: " reasons))
       (reason (if (yes-or-no-p "Temporary? ")
                   (concat (string-trim-right reason ")") "#)")
                 reason))
       (note (read-string "Notes for the package(s): ")))
  (save-excursion
    (org-link-open-from-string "[[installed]]")
    (forward-line)
    (goto-char (org-table-end))
    (insert
     (mapconcat
      (lambda (pkg) (format "| %s | %s | %s | |\n" pkg reason note))
      pkgs))
    (org-table-align))
  (prin1-to-string (list 'term (format "sudo pacman -S %s" (mapconcat #'symbol-name pkgs " ")))))

Of course we also need another babel block for uninstalling things, which will prompt the user to select a package from the installed table, optionally move the info to an uninstalled table for track record, and then emit a different uninstalling babel block. But otherwise it is quite similar to installing packages.

The actual code I used for uninstalling things

Use with caution.

(let* ((packages (if (yes-or-no-p "Temporary? ")
                     (seq-filter (lambda (pkg) (string-match-p "#" (cadr pkg))) packages)
                   packages))
       (pkgs (mapcar #'car packages))
       (pkg (completing-read "Package to remove: " pkgs)))
  (save-excursion
    (org-link-open-from-string "[[installed]]")
    (re-search-forward (format "^|[ \t]*%s[ \t]*|" (regexp-quote pkg)))
    (let ((line (buffer-substring (pos-bol) (pos-bol 2))))
      (save-excursion
        (org-link-open-from-string "[[uninstalled]]")
        (forward-line)
        (goto-char (org-table-end))
        (insert line)
        (org-table-align))
      (delete-line)))
  (prin1-to-string (list 'term (format "sudo pacman -Rs %s" pkg))))