diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/ai-term.el | 103 | ||||
| -rw-r--r-- | modules/calendar-sync.el | 10 | ||||
| -rw-r--r-- | modules/dirvish-config.el | 9 | ||||
| -rw-r--r-- | modules/eww-config.el | 7 | ||||
| -rw-r--r-- | modules/google-keep-config.el | 210 | ||||
| -rw-r--r-- | modules/ledger-config.el | 40 | ||||
| -rw-r--r-- | modules/prog-general.el | 10 | ||||
| -rw-r--r-- | modules/ui-navigation.el | 10 | ||||
| -rw-r--r-- | modules/user-constants.el | 6 |
9 files changed, 349 insertions, 56 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el index 04ff9f6c7..b463da90b 100644 --- a/modules/ai-term.el +++ b/modules/ai-term.el @@ -52,12 +52,14 @@ ;; picker, even when an agent buffer is currently displayed. ;; Used when the user wants to start a new project session ;; instead of toggling the current one. -;; - s-F9 `cj/ai-term-next' -- step to the next open agent in the -;; queue. The queue is the live agent buffers in buffer-name -;; order (a stable rotation). When an agent window is on -;; screen, swap it to the next agent and focus it, wrapping -;; after the last; when none is shown but agents exist, show -;; the first. This is the "switch among existing agents" +;; - s-F9 `cj/ai-term-next' -- step to the next active agent in the +;; queue. The queue is every active agent in buffer-name order +;; (a stable rotation): attached agents (a live buffer) and +;; detached ones (a live tmux session with no Emacs buffer). +;; Stepping onto a detached agent attaches it. When an agent +;; window is on screen, swap it to the next agent and focus it, +;; wrapping after the last; when none is shown but agents exist, +;; show the first. This is the "switch among existing agents" ;; surface F9 deliberately doesn't provide. ;; - M-F9 `cj/ai-term-close' -- gracefully close an agent: kill its ;; tmux session (stopping the agent process), then its terminal @@ -186,20 +188,39 @@ recently-selected first. Non-AI-term buffers are filtered out via `cj/--ai-term-buffer-p'." (seq-filter #'cj/--ai-term-buffer-p (buffer-list))) -(defun cj/--ai-term-next-agent-buffer (current buffers) - "Return the agent buffer after CURRENT in BUFFERS, wrapping to the first. +(defun cj/--ai-term-next-agent-dir (current dirs) + "Return the project dir after CURRENT in DIRS, wrapping to the first. -BUFFERS is an ordered list of live agent buffers. When CURRENT is the -last element, wrap to the first. When CURRENT is nil or not a member of -BUFFERS, return the first buffer. Returns nil when BUFFERS is empty. +DIRS is an ordered list of active-agent project dirs. When CURRENT is +the last element, wrap to the first. When CURRENT is nil or not a member +of DIRS, return the first dir. Returns nil when DIRS is empty. Matches +with `member' (string equality) since dirs are paths. Pure decision helper (no buffer or window side effects) so the cycle -order driving `cj/ai-term-next' (s-F9) is exercisable in tests." - (when buffers - (if (memq current buffers) - (or (cadr (memq current buffers)) - (car buffers)) - (car buffers)))) +order driving `cj/ai-term-next' is exercisable in tests." + (when dirs + (if (member current dirs) + (or (cadr (member current dirs)) + (car dirs)) + (car dirs)))) + +(defun cj/--ai-term-active-agent-dirs () + "Return project dirs that have a live agent buffer or a live tmux session. + +Sorted by the agent buffer name, so the rotation is stable and matches +what the picker shows. This is the queue `cj/ai-term-next' steps through: +it includes detached sessions (alive in tmux but with no Emacs buffer), +which the step materializes by attaching." + (let* ((sessions (cj/--ai-term-live-tmux-sessions)) + (live-names (mapcar #'buffer-name (cj/--ai-term-agent-buffers)))) + (sort + (seq-filter + (lambda (dir) + (or (member (cj/--ai-term-buffer-name dir) live-names) + (cj/--ai-term-session-active-p dir sessions))) + (cj/--ai-term-candidates)) + (lambda (a b) + (string< (cj/--ai-term-buffer-name a) (cj/--ai-term-buffer-name b)))))) (defun cj/--ai-term-most-recent-non-agent-buffer () "Return the most-recently-selected live non-agent buffer, or nil. @@ -988,35 +1009,43 @@ interrupt work in progress. Bound to M-<f9>." (defun cj/ai-term-next () "Step to the next open AI-term agent in the queue. -The queue is the live agent buffers ordered by buffer name -- a stable -rotation, unaffected by which agent was most recently selected. When an -agent window is on screen, swap it to the next agent in the queue -\(wrapping after the last) and select it. When no agent is displayed but -agents exist, show the first. When none are open, open the project picker -to launch the first agent rather than erroring. +The queue is every active agent ordered by buffer name -- a stable +rotation, unaffected by which agent was most recently selected. Active +means a live agent buffer (attached) OR a live tmux session with no Emacs +buffer (detached); stepping onto a detached agent attaches it (recreates +its terminal, which reattaches the session). When an agent window is on +screen, swap it to the next agent (wrapping after the last) and select it. +When no agent is displayed but agents exist, show the first. When none +are open, open the project picker to launch the first agent rather than +erroring. Bound to M-SPC. Unlike C-; a a (toggle the most-recent agent on/off), this is the \"switch among existing agents\" surface; C-; a s opens the project picker and C-; a k closes an agent." (interactive) - (let* ((buffers (sort (cj/--ai-term-agent-buffers) - (lambda (a b) - (string< (buffer-name a) (buffer-name b))))) + (let* ((dirs (cj/--ai-term-active-agent-dirs)) (win (cj/--ai-term-displayed-agent-window)) - (current (and win (window-buffer win))) - (next (cj/--ai-term-next-agent-buffer current buffers))) - (if (not next) + (current-name (and win (buffer-name (window-buffer win)))) + (current-dir (and current-name + (seq-find (lambda (d) + (equal (cj/--ai-term-buffer-name d) current-name)) + dirs))) + (next-dir (cj/--ai-term-next-agent-dir current-dir dirs))) + (if (not next-dir) ;; No agents open: launch the first via the project picker instead of ;; erroring, so the swap key doubles as a "start an agent" key. (cj/ai-term-pick-project) - (if win - (progn - (set-window-buffer win next) - (select-window win)) - (display-buffer next) - (let ((w (get-buffer-window next))) - (when w (select-window w)))) - (message "Agent: %s" (buffer-name next))))) + (let* ((name (cj/--ai-term-buffer-name next-dir)) + (existing (get-buffer name))) + ;; Live agent and an agent window is up: swap it into that window in + ;; place (faithful to the prior buffer-only behavior). Detached, or no + ;; window yet: show-or-create attaches the tmux session / displays it. + (if (and win existing (cj/--ai-term-process-live-p existing)) + (progn (set-window-buffer win existing) (select-window win)) + (cj/--ai-term-show-or-create next-dir name) + (let ((w (get-buffer-window name))) + (when w (select-window w)))) + (message "Agent: %s" name))))) ;; ai-term lives under the C-; a prefix (vacated when gptel was archived). ;; The frequent "swap to the next agent" also gets M-SPC for a fast chord. diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index 8d7552d3e..7ed14921f 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -540,7 +540,15 @@ Compares year, month, day, hour, minute." (plist-put result :location (plist-get exception :location))) ;; Pass through new fields if exception overrides them (when (plist-get exception :attendees) - (plist-put result :attendees (plist-get exception :attendees))) + (plist-put result :attendees (plist-get exception :attendees)) + ;; Re-derive the user's status from the overridden attendees so a + ;; singly-declined occurrence drops its inherited series "accepted" + ;; (otherwise `calendar-sync--filter-declined' can't drop it). Leave the + ;; inherited status when the override doesn't name the user. + (let ((status (calendar-sync--find-user-status + (plist-get exception :attendees) calendar-sync-user-emails))) + (when status + (plist-put result :status status)))) (when (plist-get exception :organizer) (plist-put result :organizer (plist-get exception :organizer))) (when (plist-get exception :url) diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el index f33e8cf74..c4c5f1aae 100644 --- a/modules/dirvish-config.el +++ b/modules/dirvish-config.el @@ -400,18 +400,19 @@ regardless of what file or subdirectory the point is on." "Return the (PROGRAM PRE-FILE-ARG...) list for setting wallpaper under ENV. ENV is a display-server symbol: `x11' picks feh with --bg-fill, `wayland' -picks swww with the img subcommand. Any other value returns nil so the -caller can surface an \"unknown display server\" error. +picks the `set-wallpaper' script (on PATH from dotfiles; it wraps the awww +backend and persists the choice to waypaper's config). Any other value +returns nil so the caller can surface an \"unknown display server\" error. Pure helper used by `cj/set-wallpaper'." (pcase env ('x11 '("feh" "--bg-fill")) - ('wayland '("swww" "img")) + ('wayland '("set-wallpaper")) (_ nil))) (defun cj/set-wallpaper () "Set the image at point as the desktop wallpaper. -Uses feh on X11, swww on Wayland." +Uses feh on X11, the `set-wallpaper' script on Wayland." (interactive) (let* ((raw (dired-file-name-at-point)) (file (and raw (expand-file-name raw))) diff --git a/modules/eww-config.el b/modules/eww-config.el index a5271f6bc..ff7ddc211 100644 --- a/modules/eww-config.el +++ b/modules/eww-config.el @@ -44,6 +44,13 @@ :type 'string :group 'my-eww-user-agent) +;; This file is lexical-binding, so `let'-binding url.el's special var below +;; needs it declared special at compile time. Without this the byte-compiled +;; advice binds `url-request-extra-headers' lexically and the injected +;; User-Agent never reaches `url-retrieve' (it reads the dynamic value) -- the +;; UA injection silently no-ops in compiled production, and the test sees nil. +(defvar url-request-extra-headers) + (defun my-eww--inject-user-agent (orig-fun &rest args) "Set a User-Agent only when making requests from an EWW buffer." (if (derived-mode-p 'eww-mode) diff --git a/modules/google-keep-config.el b/modules/google-keep-config.el new file mode 100644 index 000000000..1738fa6e0 --- /dev/null +++ b/modules/google-keep-config.el @@ -0,0 +1,210 @@ +;;; google-keep-config.el --- Google Keep -> org integration -*- lexical-binding: t; coding: utf-8; -*- +;; author Craig Jennings <c@cjennings.net> + +;;; Commentary: +;; A read-only view of Google Keep notes as an org page. `cj/keep-refresh' +;; runs a Python gkeepapi bridge (scripts/google-keep/keep-bridge.py), parses +;; its JSON, and regenerates `keep-file' with one org header per note. Editing +;; the file does NOT sync back to Keep -- that is v2. +;; +;; The pure JSON-to-org core (the cj/keep--render* / --note-* helpers) is kept +;; free of .emacs.d specifics so it can later extract to a standalone package; +;; the IO layer and this module supply paths, auth, and keys. +;; +;; One-time setup: install the client (pip install gkeepapi), obtain a Google +;; master token, set `cj/keep-email', and store the token in authinfo.gpg as +;; machine google-keep login <you@gmail.com> password <master-token> +;; See docs/specs/google-keep-emacs-integration-spec.org. + +;;; Code: + +(require 'json) +(require 'subr-x) +(require 'system-lib) ;; cj/auth-source-secret-value, cj/executable-find-or-warn +(require 'user-constants) ;; keep-file + +;; ------------------------------ Configuration -------------------------------- + +(defgroup cj/keep nil + "Google Keep to org integration." + :group 'applications + :prefix "cj/keep-") + +(defcustom cj/keep-email nil + "Google account email for the Keep bridge, also the authinfo login. +Unset until the one-time setup is done; `cj/keep-refresh' warns when nil." + :type '(choice (const :tag "Unset" nil) string) + :group 'cj/keep) + +(defcustom cj/keep-auth-host "google-keep" + "The authinfo.gpg machine entry holding the Keep master token." + :type 'string + :group 'cj/keep) + +(defcustom cj/keep-python "python3" + "Python interpreter used to run the Keep bridge." + :type 'string + :group 'cj/keep) + +(defvar cj/keep--bridge-script + (expand-file-name "scripts/google-keep/keep-bridge.py" user-emacs-directory) + "Path to the gkeepapi bridge script.") + +(defconst cj/keep--web-base "https://keep.google.com/#NOTE/" + "Base URL for a Keep note back-link.") + +;; --------------------------- Pure core: JSON -> org -------------------------- +;; These take plain data and return strings -- no IO, no .emacs.d paths -- so +;; they unit-test directly and lift out to a package unchanged. + +(defun cj/keep--parse-json (json-string) + "Parse the bridge JSON-STRING into a list of note alists." + (json-parse-string json-string + :object-type 'alist :array-type 'list + :false-object nil :null-object nil)) + +(defun cj/keep--label-to-tag (label) + "Sanitize LABEL into a valid org tag (alphanumerics / _ / @ / # / %)." + (replace-regexp-in-string "[^[:alnum:]_@#%]" "_" label)) + +(defun cj/keep--note-tags (note) + "Return the trailing org-tag string for NOTE (labels + archived), or \"\"." + (let ((tags (append (mapcar #'cj/keep--label-to-tag (alist-get 'labels note)) + (and (alist-get 'archived note) '("archived"))))) + (if tags (concat " :" (string-join tags ":") ":") ""))) + +(defun cj/keep--note-heading (note) + "Render NOTE (an alist) as one org subtree string." + (let* ((id (alist-get 'id note)) + (title (alist-get 'title note)) + (text (alist-get 'text note)) + (heading (if (and title (> (length title) 0)) title "(untitled)"))) + (concat + "* " heading (cj/keep--note-tags note) "\n" + ":PROPERTIES:\n" + ":KEEP_ID: " (or id "") "\n" + ":PINNED: " (if (alist-get 'pinned note) "t" "nil") "\n" + ":COLOR: " (or (alist-get 'color note) "") "\n" + ":ARCHIVED: " (if (alist-get 'archived note) "t" "nil") "\n" + ":UPDATED: " (or (alist-get 'updated note) "") "\n" + ":END:\n" + (if (and id (> (length id) 0)) + (concat "[[" cj/keep--web-base id "][open in Keep]]\n") + "") + "\n" + (if (and text (> (length text) 0)) (concat text "\n") "")))) + +(defun cj/keep--sort-pinned-first (notes) + "Return NOTES with pinned ones first, original order otherwise preserved." + (let (pinned rest) + (dolist (n notes) + (if (alist-get 'pinned n) (push n pinned) (push n rest))) + (append (nreverse pinned) (nreverse rest)))) + +(defun cj/keep--render (notes &optional generated-at) + "Render NOTES (a list of alists) into the full org page string. +GENERATED-AT is an optional last-refresh timestamp string for the header." + (concat + "# Generated by cj/keep-refresh -- read-only view; edits here do NOT sync to Keep.\n" + "#+TITLE: Google Keep\n" + (if generated-at (concat "# Last refresh: " generated-at "\n") "") + "\n" + (mapconcat #'cj/keep--note-heading (cj/keep--sort-pinned-first notes) ""))) + +;; ------------------------------- IO: run + write ----------------------------- + +(defun cj/keep--write-atomically (content file) + "Write CONTENT to FILE via a temp file in FILE's directory + atomic rename." + (let ((tmp (make-temp-file + (expand-file-name (concat "." (file-name-nondirectory file) ".") + (file-name-directory file)) + nil nil content))) + (rename-file tmp file t))) + +(defun cj/keep--warn (token) + "Surface a Keep bridge failure TOKEN as a `display-warning'." + (display-warning + 'cj/keep + (pcase token + ("no-gkeepapi" "Keep bridge: gkeepapi is not installed (pip install gkeepapi).") + ("no-token" "Keep bridge: no master token in authinfo.gpg, or `cj/keep-email' is unset.") + ("auth-failed" "Keep bridge: Google rejected the credentials (token expired or revoked?).") + ("network" "Keep bridge: network error reaching Google Keep.") + (_ (format "Keep bridge failed: %s" (if (string-empty-p token) "unknown error" token)))) + :error)) + +(defun cj/keep--write-notes (json) + "Parse bridge JSON, render, and write `keep-file' atomically. +Returns the note count." + (let* ((notes (cj/keep--parse-json json)) + (org (cj/keep--render notes (format-time-string "%Y-%m-%d %H:%M")))) + (cj/keep--write-atomically org keep-file) + (length notes))) + +;;;###autoload +(defun cj/keep-refresh () + "Fetch Google Keep notes and regenerate `keep-file' (a read-only view)." + (interactive) + (let ((token (and cj/keep-email + (cj/auth-source-secret-value cj/keep-auth-host cj/keep-email)))) + (cond + ((not (file-exists-p cj/keep--bridge-script)) + (user-error "Keep bridge script not found: %s" cj/keep--bridge-script)) + ((or (not cj/keep-email) (not token)) + (cj/keep--warn "no-token")) + (t + (let* ((out (generate-new-buffer " *keep-bridge-out*")) + (err (generate-new-buffer " *keep-bridge-err*")) + (process-environment + (append (list (concat "KEEP_EMAIL=" cj/keep-email) + (concat "KEEP_MASTER_TOKEN=" token)) + process-environment))) + (message "Keep: fetching...") + (make-process + :name "keep-bridge" + :buffer out + :stderr err + :command (list cj/keep-python cj/keep--bridge-script) + :sentinel + (lambda (proc _event) + (when (memq (process-status proc) '(exit signal)) + (unwind-protect + (if (and (eq (process-status proc) 'exit) + (= (process-exit-status proc) 0)) + (let ((n (cj/keep--write-notes + (with-current-buffer out (buffer-string))))) + (message "Keep: wrote %d notes to %s" n keep-file)) + (cj/keep--warn + (string-trim (if (buffer-live-p err) + (with-current-buffer err (buffer-string)) + "")))) + (when (buffer-live-p out) (kill-buffer out)) + (when (buffer-live-p err) (kill-buffer err))))))))))) + +;;;###autoload +(defun cj/keep-open () + "Open the generated Keep org file, offering to refresh when it's absent." + (interactive) + (if (file-exists-p keep-file) + (find-file keep-file) + (if (y-or-n-p "Keep file doesn't exist yet. Refresh now? ") + (cj/keep-refresh) + (message "Run M-x cj/keep-refresh to generate it")))) + +;; --------------------------------- Glue / keys ------------------------------- + +(defvar cj/keep-prefix-map + (let ((map (make-sparse-keymap))) + (define-key map "r" #'cj/keep-refresh) + (define-key map "o" #'cj/keep-open) + map) + "Prefix keymap for Google Keep commands (bound to \\=`C-c k').") + +(keymap-global-set "C-c k" cj/keep-prefix-map) + +;; Warn at load if the interpreter is missing; gkeepapi/token failures surface +;; at refresh time via the bridge's stderr reason token. +(cj/executable-find-or-warn cj/keep-python "Google Keep bridge" 'google-keep-config) + +(provide 'google-keep-config) +;;; google-keep-config.el ends here diff --git a/modules/ledger-config.el b/modules/ledger-config.el index 5b2712b57..018601043 100644 --- a/modules/ledger-config.el +++ b/modules/ledger-config.el @@ -2,15 +2,25 @@ ;; author Craig Jennings <c@cjennings.net> ;;; Commentary: +;; Editing support for ledger-format plain-text accounting files: ledger-mode, +;; flycheck linting, company completion, clean-on-save, and a small report set. +;; The reports and reconcile shell out to the `ledger' CLI; a load-time check +;; warns when it is missing rather than letting a report fail cryptically. ;;; Code: ;; ------------------------------- Declarations -------------------------------- (declare-function ledger-mode-clean-buffer "ledger-mode") +(declare-function cj/executable-find-or-warn "system-lib") (defvar ledger-mode-map) (defvar company-backends) +(defcustom cj/ledger-clean-on-save t + "When non-nil, tidy a ledger buffer with `ledger-mode-clean-buffer' before save." + :type 'boolean + :group 'ledger) + ;; -------------------------------- Ledger Mode -------------------------------- ;; edit files in ledger format @@ -19,36 +29,38 @@ "\\.ledger\\'" "\\.journal\\'") :preface - (defun cj/ledger-save () - "Automatically clean the ledger buffer at each save." - (interactive) - (save-excursion - (when (buffer-modified-p) - (with-demoted-errors "Error cleaning ledger buffer: %S" - (ledger-mode-clean-buffer)) - (save-buffer)))) - :bind - (:map ledger-mode-map - ("C-x C-s" . cj/ledger-save)) + (defun cj/ledger--clean-before-save () + "Tidy the ledger buffer before save when `cj/ledger-clean-on-save' is set. +Errors are demoted so a malformed buffer still saves." + (when cj/ledger-clean-on-save + (with-demoted-errors "Error cleaning ledger buffer: %S" + (ledger-mode-clean-buffer)))) + (defun cj/ledger--enable-clean-on-save () + "Install the clean-on-save hook buffer-locally so it fires on every save path." + (add-hook 'before-save-hook #'cj/ledger--clean-before-save nil t)) + :hook (ledger-mode . cj/ledger--enable-clean-on-save) :custom (ledger-clear-whole-transactions t) (ledger-reconcile-default-commodity "$") (ledger-report-use-header-line nil) + (ledger-highlight-xact-under-point t) (ledger-reports '(("bal" "%(binary) --strict -f %(ledger-file) bal") ("bal this month" "%(binary) --strict -f %(ledger-file) bal -p %(month) -S amount") ("bal this year" "%(binary) --strict -f %(ledger-file) bal -p 'this year'") ("net worth" "%(binary) --strict -f %(ledger-file) bal Assets Liabilities") - ("account" "%(binary) --strict -f %(ledger-file) reg %(account)")))) + ("account" "%(binary) --strict -f %(ledger-file) reg %(account)"))) + :config + (cj/executable-find-or-warn "ledger" 'ledger-mode)) ;; ------------------------------ Flycheck Ledger ------------------------------ -;; syntax and unbalanced transaction linting +;; syntax and unbalanced-transaction linting (use-package flycheck-ledger :after ledger-mode) ;; ------------------------------- Company Ledger ------------------------------ -;; autocompletion for ledger +;; account/payee autocompletion for ledger (use-package company-ledger :after (company ledger-mode) diff --git a/modules/prog-general.el b/modules/prog-general.el index 99b3cbfab..8e317413c 100644 --- a/modules/prog-general.el +++ b/modules/prog-general.el @@ -290,6 +290,16 @@ seeded by `cj/deadgrep--initial-term'. Shared tail of the deadgrep commands." (with-eval-after-load 'dired (keymap-set dired-mode-map "G" #'cj/deadgrep-here)) +;; ------------------------------------ wgrep ---------------------------------- +;; Make a grep buffer editable, then write the edits back across files -- turns +;; a consult-grep / embark-export result into a project-wide find-and-replace. +;; In a grep buffer: C-c C-p to start editing, C-c C-c to apply. + +(use-package wgrep + :custom + (wgrep-auto-save-buffer t) ;; save the touched files when applying + (wgrep-change-readonly-file t)) ;; let edits flow into read-only buffers + ;; ---------------------------------- Snippets --------------------------------- ;; reusable code and text diff --git a/modules/ui-navigation.el b/modules/ui-navigation.el index c099e0834..cb0fc5697 100644 --- a/modules/ui-navigation.el +++ b/modules/ui-navigation.el @@ -283,5 +283,15 @@ With numeric prefix ARG, re-open the ARGth most-recently-killed file :config (winner-mode 1)) +;; ------------------------------- Cursor Jump (avy) --------------------------- +;; Jump anywhere visible by typing a few of the target's characters, then the +;; decision-tree key avy overlays. Fills the in-buffer motion gap that windmove +;; (windows) and isearch (text) leave. + +(use-package avy + :bind (("C-:" . avy-goto-char-timer) ;; type chars, pause, jump to a match + ("M-g w" . avy-goto-word-1) + ("M-g l" . avy-goto-line))) + (provide 'ui-navigation) ;;; ui-navigation.el ends here diff --git a/modules/user-constants.el b/modules/user-constants.el index b392212ed..570b142fb 100644 --- a/modules/user-constants.el +++ b/modules/user-constants.el @@ -167,6 +167,12 @@ Proton Calendar.") Stored in .emacs.d/data/ so each machine syncs independently from Google Calendar.") +(defvar keep-file (expand-file-name "data/keep.org" user-emacs-directory) + "The location of the generated org file containing Google Keep notes. +A read-only view regenerated by `cj/keep-refresh'; edits here do not +sync back to Keep. Stored in .emacs.d/data/ so each machine syncs +independently.") + (defvar reference-file (expand-file-name "reference.org" org-dir) "The location of the org file containing reference information.") |
