Org-roam on Android
I’ve been using the note-taking Zettelkasten-ish Org-roam system for a few months and it’s been very useful to me, just as a low-friction way of making more notes and easily finding and/or (re)discovering notes that I’ve made.
It’s pretty useful to be able to have access to these notes, and be able to quickly add notes, on mobile as well. I thought it might be useful to include here some notes on how to do, since (especially since v2 of Org-roam) there are some hurdles.
On Android/LineageOS install the F-Droid app store, and then from
there install Termux. Open Termux and install four things we’ll need
(strictly speaking you don’t need curl and ripgrep, but they’ll
be useful): Emacs, sqlite, curl, and ripgrep via pkg install emacs sqlite curl ripgrep
.
You can then open up Emacs via emacs
and get started.
I include here a commented partial version of my ~/.emacs.d/init.el
configuration file for my Termux/Emacs Org-roam setup:
;; BASIC SETUP:
;; package setup - bootstrap the package system
(require 'package)
(setq package-enable-at-startup nil)
(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")
(setq package-archives
'(("GNU ELPA" . "https://elpa.gnu.org/packages/")
("ORG" . "https://orgmode.org/elpa/")
("MELPA Stable" . "https://stable.melpa.org/packages/")
("MELPA" . "https://melpa.org/packages/"))
package-archive-priorities
'(("ORG" . 20)
("MELPA" . 15)
("MELPA Stable" . 10)
("GNU ELPA" . 5)))
(package-initialize)
;; Bootstrap `use-package'
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
(eval-when-compile
(require 'use-package))
;; for Termux-specific things; useful if you want to share
;; configs across platforms
(defvar termux-p
(not (null (getenv "ANDROID_ROOT")))
"If non-nil, GNU Emacs is running on Termux.")
(when termux-p
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package)))
;; This makes Emacs in Termux use your Android browser for opening urls
(setq browse-url-browser-function 'browse-url-xdg-open)
;; mouse
;; enable mouse reporting for terminal emulators
;; this lets you scroll around by swiping
(unless window-system
(xterm-mouse-mode 1)
(global-set-key [mouse-4] (lambda ()
(interactive)
(scroll-down 1)))
(global-set-key [mouse-5] (lambda ()
(interactive)
(scroll-up 1))))
;; ORG
(use-package org
:ensure t
:ensure org-plus-contrib
:init
(setq org-src-fontify-natively t)
:config
;; (add-to-list 'auto-mode-alist '("\\.org\\'" . org-mode))
(define-key org-mode-map (kbd "M-p") 'org-metaup)
(define-key org-mode-map (kbd "M-n") 'org-metadown)
(setq org-catch-invisible-edits 'show-and-error)
(setq org-cycle-separator-lines -1)
(setq org-return-follows-link t)
(setq org-export-with-toc 'nil)
(setq org-startup-folded 'content)
(setq org-ellipsis "⇣")
;; **** use regular android apps to view pdfs & images *****
(when termux-p
(add-to-list 'org-file-apps '("\\.pdf\\'" . "termux-open %s"))
(add-to-list 'org-file-apps '("\\.png\\'" . "termux-open %s"))
(add-to-list 'org-file-apps '("\\.jpg\\'" . "termux-open %s"))
(add-to-list 'org-file-apps '("\\.jpeg\\'" . "termux-open %s")))
;; needed for <s etc. expansion of code-blocks
(require 'org-tempo))
;; define our Org-roam user and their email (set to your desired name/email)
(defvar roam-user "Some User"
"The name of the Org-roam note author.")
(defvar roam-email "roman@mode.org"
"The public email of that author.")
(setq org-roam-v2-ack t)
;; we need this package for v2 of Org-oram
(use-package emacsql-sqlite3
:ensure t)
;; If you've replicated my setup; otherwise change to the Termux
;; local path.
(setq org-roam-directory (file-truename "~/Documents/Org/org-roam/"))
;; org-roam
(use-package org-roam
:ensure t
:custom
(setq org-roam-db-location (file-truename "~"))
(org-roam-directory (file-truename "~/Documents/Org/org-roam/"))
:bind (("C-c n l" . org-roam-buffer-toggle)
("C-c n f" . org-roam-node-find)
("C-c n r" . org-roam-graph)
("C-c n i" . org-roam-node-insert)
("C-c n c" . org-roam-capture)
("C-c n g" . org-id-get-create)
;; Dailies
("C-c n n" . org-roam-dailies-capture-today)
("C-c n d" . org-roam-dailies-goto-today) ; find toDay
("C-c n v" . org-roam-dailies-goto-date)
("C-c n f" . org-roam-dailies-goto-next-note)
("C-c n b" . org-roam-dailies-goto-previous-note))
:config
;; this is a chunglak's hack to get sqlite to work on Android with org-roam v2:
;; from: https://github.com/org-roam/org-roam/issues/1605#issuecomment-885997237
(defun org-roam-db ()
"Entrypoint to the Org-roam sqlite database.
Initializes and stores the database, and the database connection.
Performs a database upgrade when required."
(unless (and (org-roam-db--get-connection)
(emacsql-live-p (org-roam-db--get-connection)))
(let ((init-db (not (file-exists-p org-roam-db-location))))
(make-directory (file-name-directory org-roam-db-location) t)
(let ((conn (emacsql-sqlite3 org-roam-db-location)))
(emacsql conn [:pragma (= foreign_keys ON)])
(set-process-query-on-exit-flag (emacsql-process conn) nil)
(puthash (expand-file-name org-roam-directory)
conn
org-roam-db--connection)
(when init-db
(org-roam-db--init conn))
(let* ((version (caar (emacsql conn "PRAGMA user_version")))
(version (org-roam-db--upgrade-maybe conn version)))
(cond
((> version org-roam-db-version)
(emacsql-close conn)
(user-error
"The Org-roam database was created with a newer Org-roam version. "
"You need to update the Org-roam package"))
((< version org-roam-db-version)
(emacsql-close conn)
(error "BUG: The Org-roam database scheme changed %s"
"and there is no upgrade path")))))))
(org-roam-db--get-connection))
(defun org-roam-db--init (db)
"Initialize database DB with the correct schema and user version."
(emacsql-with-transaction db
(emacsql db "PRAGMA foreign_keys = ON") ;; added
(emacsql db [:pragma (= foreign_keys ON)])
(pcase-dolist (`(,table ,schema) org-roam-db--table-schemata)
(emacsql db [:create-table $i1 $S2] table schema))
(pcase-dolist (`(,index-name ,table ,columns) org-roam-db--table-indices)
(emacsql db [:create-index $i1 :on $i2 $S3] index-name table columns))
(emacsql db (format "PRAGMA user_version = %s" org-roam-db-version))))
;; end chunglak hack
(org-roam-setup)
;; If using org-roam-protocol
(require 'org-roam-protocol))
;; These are my capture templates:
(setq org-roam-capture-templates
`(("d" "default" plain "%?" :if-new
(file+head "%<%Y%m%d%H%M%S>-${slug}.org"
,(concat "#+title: ${title}\n"
"#+date: %U\n\n"))
:unnarrowed t)))
(setq org-roam-dailies-directory "~/Documents/Org/org-roam/daily")
(setq org-roam-dailies-capture-templates
`(("d" "default" entry "* %?" :if-new
(file+head "%(concat org-roam-dailies-directory \"/%<%Y-%m-%d>.org\")"
,(concat "#+title: %<%Y-%m-%d>" "\n"
"#+filetags: :daily_journal:\n\n")))))
;; deft - one way to search Org-roam notes, but not the fastest (see below)
(use-package deft
:ensure t
:config
:after org
:bind
("C-c r d" . deft)
:custom
(deft-recursive t)
(deft-use-filter-string-for-filename t)
(deft-default-extension "org")
(deft-directory "~/Documents/Org/org-roam/"))
;; Here end the basic setup, but....
;; SOME OTHER THINGS YOU MIGHT ADD
;; bars seems pointless here, but if you like, don't do this
(menu-bar-mode -1)
(tool-bar-mode -1)
;; You could use a different theme
(use-package cyberpunk-theme
:ensure t
:config
(load-theme 'cyberpunk))
;;;;;;;;;;;;;;;;;;;;;
;; Spell-checking ;;;
;;;;;;;;;;;;;;;;;;;;;
(require 'flymake)
(setq ispell-program-name "hunspell") ; could be ispell as well, depending on your preferences
(setq ispell-dictionary "en_GB") ; this can obviously be set to any language your spell-checking program supports
;; I installed the en_GB ones, but these don't come in Termux by default. To add arbitrary hunspell languages, see:
;; https://www.reddit.com/r/termux/comments/k5o6mp/new_hunspell_dictionaries/?
;; in summary:
;; - how to add new: copy .aff and .dic files in /data/data/com.termux/files/usr/share/hunspell/
;; - where to get new: https://www.freeoffice.com/en/download/dictionaries
(dolist (hook '(org-mode-hook))
(add-hook hook (lambda () (flyspell-mode 1))))
(add-hook 'org-mode-hook (lambda () (setq ispell-parser 'tex))) ; make orgmode recognise LaTeX syntax [from http://stackoverflow.com/questions/11646880/flyspell-in-org-mode-recognize-latex-syntax-like-auctex ]
(add-hook 'text-mode-hook #'flyspell-mode)
;;;;;;;;;;;;;;;
;; Undo-Tree ;; - a new undo package
;;;;;;;;;;;;;;;
(use-package undo-tree
:ensure t
:config
;; (setq undo-tree-auto-save-history 1)
;; Each node in the undo tree should have a timestamp.
(setq undo-tree-visualizer-timestamps t)
;; Show a diff window displaying changes between undo nodes.
(setq undo-tree-visualizer-diff t)
(global-undo-tree-mode))
;; display time and date in modeline, if you like
(setq display-time-day-and-date t)
(display-time-mode 1)
;; prettier bullets
(use-package org-bullets
:ensure t
:config
(add-hook 'org-mode-hook (lambda () (org-bullets-mode 1)))
(setq org-bullets-bullet-list '("⋇" "∴" "∵" "∷" "∺")))
;; A nice way of quickly adding links.
;; (Though in Termux, you first must paste from your
;; Android clipboard and then copy/kill via Emacs before
;; it'll work.)
(use-package org-cliplink
:ensure t
:config
(define-key org-mode-map (kbd "C-c o c") #'org-cliplink))
;; This is also not needed, but adds some (dubiously) useful properties
;; to the Org-roam file's property drawer.
;; First, set up a system for getting location
;; (we could also try to leverage termux's built-in
;; GPS location abilities via `termux-location`, but
;; it seems a bit slow and doesn't even always work if
;; your device can't get a good satellite connection.)
(setq calendar-latitude 0)
(setq calendar-longitude 0)
(defun bms/get-lat-long-from-ipinfo ()
(let*
((latlong (substring
(shell-command-to-string "curl -s 'ipinfo.io/loc'") 0 -1))
(latlong-list (split-string latlong ",")))
(setq calendar-latitude (string-to-number (car latlong-list)))
(setq calendar-longitude (string-to-number (cadr latlong-list)))))
(defun bms/add-other-auto-props-to-org-roam-properties ()
(unless (file-exists-p (buffer-file-name))
(unless (org-find-property "CREATION_TIME")
(org-roam-add-property (format-time-string "%s"
(nth 5
(file-attributes (buffer-file-name))))
"CREATION_TIME"))
(unless (org-find-property "AUTHOR")
(org-roam-add-property roam-user "AUTHOR"))
(unless (org-find-property "MAIL")
(org-roam-add-property roam-email "MAIL"))
(unless (org-find-property "LAT_LONG")
(bms/get-lat-long-from-ipinfo)
(org-roam-add-property (concat (number-to-string calendar-latitude) "," (number-to-string calendar-longitude)) "LAT-LONG"))))
(add-hook 'org-roam-capture-new-node-hook #'bms/add-other-auto-props-to-org-roam-properties)
;; You could use Ivy or Helm or the default, but I
;; like Selectrum, Consult & friends. Plus we can leverage
;; Consult for a nice alternative to deft for note-searching.
;; You'll need this to use my ripgrep note searching feature below.
;; selectrum
(use-package selectrum
:ensure t
:config
(selectrum-mode +1))
;; ;; prescient - T9
(use-package prescient
:ensure t
:config
(setq prescient-persist-mode t)
(setq prescient-filter-method '(literal regexp initialism fuzzy))) ;; added fuzzy
(use-package orderless
:ensure t
:init (icomplete-mode) ; optional but recommended!
:custom (completion-styles '(orderless))
:config
(setq orderless-matching-styles '(orderless-flex))
;; This means that the company-capf backend will automatically use orderless, but following issue exists:
;; Pressing SPC takes you out of completion, so with the default separator you are limited to one component,
;; which is no fun. To fix this add a separator that is allowed to occur in identifiers, for example, for
;; Emacs Lisp code you could use an ampersand:
(setq orderless-component-separator "[ &]")
;; The matching portions of candidates aren’t highlighted. But while you can’t get different faces for
;; different components, you can at least get the matches highlighted in the sole available face with this configuration
(defun just-one-face (fn &rest args)
(let ((orderless-match-faces [completions-common-part]))
(apply fn args)))
(advice-add 'company-capf--candidates :around #'just-one-face))
(use-package selectrum-prescient
:ensure t
:config
;; to make sorting and filtering more intelligent
(selectrum-prescient-mode +1)
;; Filtering with orderless
(setq selectrum-refine-candidates-function #'orderless-filter)
(setq selectrum-highlight-candidates-function #'orderless-highlight-matches)
;; If you also configure `completion-styles` for orderless you might want to use the
;; following advice because orderless isn't well suited for initial gathering of
;; candidates by completion in region.
(advice-add #'completion--category-override :filter-return
(defun completion-in-region-style-setup+ (res)
"Fallback to default styles for region completions with orderless."
(or res
;; Don't use orderless for initial candidate gathering.
(and completion-in-region-mode-predicate
(not (minibufferp))
(equal '(orderless) completion-styles)
'(basic partial-completion emacs22)))))
;; Minibuffer-actions with embark
;; You should bind embark commands like embark-act, embark-act-noexit
;; and embark-export in minibuffer-local-map (as embark commands are not selectrum specific).
;; For available commands and other embark configurations see the embark documentation and its wiki.
(defun current-candidate+category ()
(when selectrum-is-active
(cons (selectrum--get-meta 'category)
(selectrum-get-current-candidate))))
(add-hook 'embark-target-finders #'current-candidate+category)
(defun current-candidates+category ()
(when selectrum-is-active
(cons (selectrum--get-meta 'category)
(selectrum-get-current-candidates
;; Pass relative file names for dired.
minibuffer-completing-file-name))))
(add-hook 'embark-candidate-collectors #'current-candidates+category)
;; No unnecessary computation delay after injection.
(add-hook 'embark-setup-hook 'selectrum-set-selected-candidate)
;; The following is not selectrum specific but included here for convenience.
;; If you don't want to use which-key as a key prompter skip the following code.
(setq embark-action-indicator
(lambda (map) (which-key--show-keymap "Embark" map nil nil 'no-paging)
#'which-key--hide-popup-ignore-command)
embark-become-indicator embark-action-indicator)
;; to save your command history on disk, so the sorting gets more
;; intelligent over time
(prescient-persist-mode +1))
;; Example configuration for Consult
(use-package consult
;; Replace bindings. Lazily loaded due by `use-package'.
:bind (("C-x M-:" . consult-complex-command)
("C-c h" . consult-history)
("C-c m" . consult-mode-command)
("C-x b" . consult-buffer)
("C-x 4 b" . consult-buffer-other-window)
("C-x 5 b" . consult-buffer-other-frame)
("C-x r x" . consult-register)
("C-x r b" . consult-bookmark)
("M-g g" . consult-goto-line)
("M-g M-g" . consult-goto-line)
("M-g o" . consult-outline) ;; "M-s o" is a good alternative.
("M-g l" . consult-line) ;; "M-s l" is a good alternative.
("M-g m" . consult-mark) ;; I recommend to bind Consult navigation
("M-g k" . consult-global-mark) ;; commands under the "M-g" prefix.
("M-g r" . consult-ripgrep) ;; or consult-grep, consult-ripgrep
("M-g f" . consult-find) ;; or consult-locate, my-fdfind
("M-g i" . consult-project-imenu) ;; or consult-imenu
("M-g e" . consult-error)
("M-s m" . consult-multi-occur)
("M-y" . consult-yank-pop)
("<help> a" . consult-apropos))
;; The :init configuration is always executed (Not lazy!)
:init
;; Custom command wrappers. It is generally encouraged to write your own
;; commands based on the Consult commands. Some commands have arguments which
;; allow tweaking. Furthermore global configuration variables can be set
;; locally in a let-binding.
(defun my-fdfind (&optional dir)
(interactive "P")
(let ((consult-find-command '("fdfind" "--color=never" "--full-path")))
(consult-find dir)))
;; Replace `multi-occur' with `consult-multi-occur', which is a drop-in replacement.
(fset 'multi-occur #'consult-multi-occur)
;; Configure other variables and modes in the :config section, after lazily loading the package
:config
;; Configure preview. Note that the preview-key can also be configured on a
;; per-command basis via `consult-config'.
;; (setq consult-preview-key 'any) ;; any key triggers preview, the default
;; Optionally configure narrowing key.
;; Both < and C-+ work reasonably well.
(setq consult-narrow-key "<") ;; (kbd "C-+")
;; Optionally make narrowing help available in the minibuffer.
;; Probably not needed if you are using which-key.
;; (define-key consult-narrow-map (vconcat consult-narrow-key "?") #'consult-narrow-help)
;; Optional configure a view library to be used by `consult-buffer'.
;; The view library must provide two functions, one to open the view by name,
;; and one function which must return a list of views as strings.
;; Example: https://github.com/minad/bookmark-view/
;; (setq consult-view-open-function #'bookmark-jump
;; consult-view-list-function #'bookmark-view-names)
;; Optionally configure a function which returns the project root directory
;; (autoload 'projectile-project-root "projectile")
;; (setq consult-project-root-function #'projectile-project-root)
)
;; Optionally add the `consult-flycheck' command.
(use-package consult-flycheck
:bind (:map flycheck-command-map
("!" . consult-flycheck)))
;; Optionally enable richer annotations using the Marginalia package
(use-package marginalia
:ensure t
;; The :init configuration is always executed (Not lazy!)
:init
;; Must be in the :init section of use-package such that the mode gets
;; enabled right away. Note that this forces loading the package.
(marginalia-mode))
(use-package embark
:ensure t
:bind
("C-S-a" . embark-act) ; pick some comfortable binding
:config
;; For Selectrum users:
(defun current-candidate+category ()
(when selectrum-is-active
(cons (selectrum--get-meta 'category)
(selectrum-get-current-candidate))))
(add-hook 'embark-target-finders #'current-candidate+category)
(defun current-candidates+category ()
(when selectrum-is-active
(cons (selectrum--get-meta 'category)
(selectrum-get-current-candidates
;; Pass relative file names for dired.
minibuffer-completing-file-name))))
(add-hook 'embark-candidate-collectors #'current-candidates+category)
;; No unnecessary computation delay after injection.
(add-hook 'embark-setup-hook 'selectrum-set-selected-candidate))
;; org-roam-rg-search - this is a much faster way to search Org-roam notes:
;; requires the Selectrum+Consult setup immediately preceding.
;; Use C-c r r to search notes via consult's ripgrep interface
(defun bms/org-roam-rg-search ()
"Search org-roam directory using consult-ripgrep. With live-preview."
(interactive)
(let ((consult-ripgrep "rg --null --ignore-case --type org --line-buffered --color=always --max-columns=500 --no-heading --line-number . -e ARG OPTS"))
(consult-ripgrep org-roam-directory)))
(global-set-key (kbd "C-c rr") 'bms/org-roam-rg-search)
;; speed-keys - see https://github.com/alhassy/emacs.d#manipulating-sections
(setq org-use-speed-commands t)
;; On an org-heading, C-a goes to after the star, heading markers. To use speed keys, run C-a C-a to get to the star markers.
;; C-e goes to the end of the heading, not including the tags.
(setq org-special-ctrl-a/e t)
;;drag images into orgmode
(use-package org-download
:ensure t
:config
(add-hook 'dired-mode-hook 'org-download-enable)
(global-set-key (kbd "C-c o i") #'org-download-yank)
(setq org-download-method 'attach))
(defun bms/org-attach-insert-link (&optional in-emacs)
"Insert attachment from list."
(interactive "P")
(let ((attach-dir (org-attach-dir)))
(if attach-dir
(let* ((file (pcase (org-attach-file-list attach-dir)
(`(,file) file)
(files (completing-read "Insert attachment: "
(mapcar #'list files) nil t))))
(path (expand-file-name file attach-dir))
(desc (file-name-nondirectory path)))
(let ((initial-input
(cond
((not org-link-make-description-function) desc)
(t (condition-case nil
(funcall org-link-make-description-function link desc)
(error
(message "Can't get link description from %S"
(symbol-name org-link-make-description-function))
(sit-for 2)
nil))))))
(setq desc (if (called-interactively-p 'any)
(read-string "Description: " initial-input)
initial-input))
(org-insert-link nil path (concat "attachment:" desc))))
(error "No attachment directory exist"))))
(define-key org-mode-map (kbd "C-c o l") #'bms/org-attach-insert-link)
;; in case you want some things not in melpa
;; you'll need it for the remaining things below
(use-package quelpa
:ensure t)
(use-package quelpa-use-package
:ensure t)
;; A bit of sugar for the visual appearance of Org syntax
;; Use if you like.
(use-package org-appear
:ensure t
:quelpa (org-appear :fetcher github :repo "awth13/org-appear")
:config
(setq org-hide-emphasis-markers t)
(add-hook 'org-mode-hook 'org-appear-mode))
;; You don't need this, but it's cool and it does work on Android:
;; see https://github.com/org-roam/org-roam-ui for features
(use-package org-roam-ui
:ensure t
:quelpa (org-roam-ui :fetcher github :repo "org-roam/org-roam-ui" :branch "main" :files ("*.el" "out"))
:after org-roam
:hook (org-roam . org-roam-ui-mode))
An excellent way of keeping Org notes (and files more generally) in sync between desktop, laptop, and mobile devices is Syncthing. On Android I recommend using the Syncthing-Fork app (via F-Droid), which has various improvements over the default Syncthing app on Android, including better file-access features. (On iOS there is now a third-party solution for syncing via Syncthing: Möbius-Sync. I have no idea how to use Emacs/Org-mode on iOS though, but I recall hearing about some ways of running a Linux shell on iOS like iSH, so possibly there’s some way.)
I have Syncthing sync my Org files to a directory in my main “home”
directory on Android Documents/Org
and then in Termux created a
Documents
directory and inside of that directory created a symlink to
my actual Org directory via ln -s storage/shared/Documents/Org Org
. I’ve found that is easier for allowing Syncthing to have access
to the files in order to keep them in sync. (And having my Org files
live at ~/Documents/Org
in Termux mimics the directory structure on my
Linux boxes, which makes lots of things easier in terms of sharing
configurations.)