Commit af90ba62e992

Vincent Demeester <vincent@sbr.pm>
2026-06-19 10:50:30
chore(emacs): remove ~35 unused packages
Removed unused packages from Nix package list and deleted orphaned site-lisp files (goose, journelly, org-kanban, whisper, ext-whisper).
1 parent c26d719
dots/config/emacs/site-lisp/ext-whisper.el
@@ -1,636 +0,0 @@
-;;; whisper.el --- Speech-to-Text interface using OpenAI's whisper model -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2022 Imran Khan.
-
-;; Author: Imran Khan <imran@khan.ovh>
-;; URL: https://github.com/natrys/whisper.el
-;; Version: 0.3.3
-;; Package-Requires: ((emacs "27.1"))
-
-;; This file is NOT part of GNU Emacs.
-
-;; This program is free software; you can redistribute it and/or modify
-;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or (at
-;; your option) any later version.
-;;
-;; This program is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-;; GNU General Public License for more details.
-;;
-;; You should have received a copy of the GNU General Public License
-;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-;;; Commentary:
-;;
-;; Speech-to-Text interface for Emacs using OpenAI's whisper model
-;; Uses the awesome C/C++ port that runs on CPU.
-;; See: https://github.com/ggerganov/whisper.cpp
-;;
-;;; Code:
-
-(require 'cl-lib)
-
-;;; User facing options
-
-(defgroup whisper ()
-  "Speech-to-text interface using OpenAI's whisper model."
-  :group 'external)
-
-(defcustom whisper-enable-speed-up nil
-  "Whether to sacrifices some accuracy to speed up transcribing.
-
-Basically whether to use \"-su\" flag in whisper.cpp.  You should experiment
-enabling it to see if it works well enough for you.
-
-It's currently disabled by upstream because of bugs, so does nothing."
-  :type 'boolean
-  :group 'whisper)
-
-(defcustom whisper-use-threads nil
-  "How many threads to use for transcribing.
-
-Default is whisper.cpp default (which is number of cores but maxed at 4)."
-  :type 'integer
-  :group 'whisper)
-
-(defcustom whisper-install-directory (locate-user-emacs-file ".cache/")
-  "Location of where whisper.cpp is installed."
-  :type 'directory
-  :group 'whisper)
-
-(defcustom whisper-recording-timeout 300
-  "Number of seconds after which recording will be automatically stopped."
-  :type '(choice integer (const nil))
-  :group 'whisper)
-
-(defcustom whisper-model "base"
-  "Which whisper model to use (default is base).
-
-Choose between: tiny, base, small, medium, large.
-
-The first four comes with .en variant that only works with English, but
-might speed up transcribing."
-  :type 'string
-  :group 'whisper)
-
-(defcustom whisper-language "en"
-  "Set spoken language for audio.
-
-When dealing with unknown language, set this to `auto'.
-
-Be sure to use generic model (without .en suffix) when language is not English."
-  :type 'string
-  :group 'whisper)
-
-(defcustom whisper-translate nil
-  "Whether to translate to English first, before transcribing."
-  :type 'boolean
-  :group 'whisper)
-
-(defcustom whisper-quantize nil
-  "Whether to use quantized version of the model in whisper.cpp.
-
-Quantization is a technique to reduce the computational and memory costs of
-running inference by representing the weights and activations with low-precision
-data types.  This sacrifices precision for resource efficiency.  The idea is
-that quantized version of bigger model may afford you to use it (if you are RAM
-constrained e.g.) with some penalty, while still being better than the smaller
-model you would be using otherwise.
-
-Valid values are (from lowest to highest quality):
-- q4_0
-- q4_1
-- q4_k
-- q5_0
-- q5_1
-- q5_k
-- q6_k
-- q8_0"
-  :type '(choice string (const nil))
-  :group 'whisper)
-
-(defcustom whisper-install-whispercpp t
-  "Specify whether to install whisper.cpp automatically.
-
-By default whisper.el compiles whisper.cpp automatically.   But if you are on a
-platform where our automatic whisper.cpp install doesn't work but you are able
-to do so manually, you can set this to `manual' to skip our try (and failure)
-to install it automatically.  Note that in case a functional install is found
-at `whisper-install-directory', we can still do model download, quantization
-automatically.
-
-But if you are planning to use something other than whisper.cpp entirely, as
-such don't want to install it nor run checks for it, you may opt out of
-whisper.cpp as a whole by setting this to nil.  In that case it's your
-responsibility to override `whisper-command' with appropriate function."
-  :type '(choice boolean (const manual))
-  :group 'whisper)
-
-(defcustom whisper-insert-text-at-point t
-  "Whether to put whisper output under point in current buffer.
-
-When nil, instead of inserting text under current point, a temporary buffer
-containing whisper output text is displayed.  The buffer name is distinguised
-with current timestamp and it's the user's responsibility to kill the buffer if
-they want to."
-  :type 'boolean
-  :group 'whisper)
-
-(defcustom whisper-return-cursor-to-start t
-  "Whether to re-position the cursor after transcription.
-
-When non-nil, the cursor is returned to the original invocation point.
-Otherwise, the cursor remains at the end of the inserted transcription."
-  :type 'boolean
-  :group 'whisper)
-
-(defcustom whisper-show-progress-in-mode-line t
-  "Whether to show transcription progress in mode line."
-  :type 'boolean
-  :group 'whisper)
-
-(define-obsolete-variable-alias 'whisper-pre-process-hook 'whisper-before-transcription-hook "0.3.0")
-(defcustom whisper-before-transcription-hook '(whisper--check-buffer-read-only-p)
-  "Hook run before whisper.el does anything."
-  :type 'hook
-  :group 'whisper)
-
-(define-obsolete-variable-alias 'whisper-post-process-hook 'whisper-after-transcription-hook "0.3.0")
-(defcustom whisper-after-transcription-hook nil
-  "Hook run after whisper command finishes producing output.
-
-If you want to transform the command output text in some way before they are
-inserted into the original buffer, add your function here.  Each function in
-the hook will be run in a buffer containing the whisper command output text
-as its current buffer, and with point set to beginning of that buffer."
-  :type 'hook
-  :group 'whisper)
-
-(defcustom whisper-after-insert-hook nil
-  "Hook run after whisper command has inserted the transcription.
-
-This hook will be run from the buffer in which the transcription was inserted."
-  :type 'hook
-  :group 'whisper)
-
-;;; Internal variables
-
-(defvar whisper--stdout-buffer-name "*whisper-stdout*")
-(defvar whisper--stderr-buffer-name "*whisper-stderr*")
-(defvar whisper--compilation-buffer-name "*whisper-compilation*")
-
-(defvar whisper--point-buffer nil)
-(defvar whisper--compilation-buffer nil)
-
-(defvar whisper--recording-process nil)
-(defvar whisper--transcribing-process nil)
-(defvar whisper--marker (make-marker))
-
-(defvar whisper--install-path "/tmp")
-
-(defvar whisper--temp-file
-  (concat (temporary-file-directory) "emacs-whisper.wav")
-  "Location of the temporary audio file.")
-
-(defvar whisper--ffmpeg-input-format
-  (pcase system-type
-    ('gnu/linux (if (or (executable-find "pulseaudio")
-                        (executable-find "pipewire-pulse"))
-                    "pulse"
-                  "alsa"))
-    ('darwin "avfoundation")
-    ('windows-nt "dshow")
-    (_ nil)))
-
-(defvar whisper--ffmpeg-input-device
-  (pcase whisper--ffmpeg-input-format
-    ("pulse" "default")
-    (_ nil)))
-
-(defvar whisper--ffmpeg-input-file nil)
-
-(defvar whisper--using-whispercpp nil)
-
-(defvar whisper--progress-level "0")
-
-(defvar whisper--mode-line-recording-indicator
-  (propertize "" 'face font-lock-warning-face))
-
-(defvar whisper--mode-line-transcribing-indicator
-  (propertize "" 'face font-lock-warning-face))
-
-(defun whisper--check-buffer-read-only-p ()
-  "Error out if current buffer is read-only."
-  (when (and whisper-insert-text-at-point buffer-read-only)
-    (error "Buffer is read-only, can't insert text here")))
-
-;; Maybe sox would be a lighter choice for something this simple?
-(defun whisper--record-command (output-file)
-  "Produces FFmpeg command to be run given location of OUTPUT-FILE."
-  (unless (executable-find "ffmpeg")
-    (error "Needs FFmpeg to record audio"))
-
-  (unless (or whisper--ffmpeg-input-file
-              whisper--ffmpeg-input-format)
-    (error "Set a suitable value for whisper--ffmpeg-input-format"))
-
-  (unless (or whisper--ffmpeg-input-file
-              whisper--ffmpeg-input-device)
-    (error "Set a suitable value for whisper--ffmpeg-input-device"))
-
-  `("ffmpeg"
-    ,@(unless whisper--ffmpeg-input-file
-        (list "-f" whisper--ffmpeg-input-format))
-    "-i" ,(or whisper--ffmpeg-input-file whisper--ffmpeg-input-device)
-    ,@(when (and (not whisper--ffmpeg-input-file) whisper-recording-timeout)
-        (list "-t" (number-to-string whisper-recording-timeout)))
-    "-ar" "16000"
-    "-y" ,output-file))
-
-(defun whisper-command (input-file)
-  "Produces whisper.cpp command to be run on the INPUT-FILE.
-
-If you want to use something other than whisper.cpp, you should override this
-function to produce the command for the inference engine of your choice."
-  `(,(whisper--find-whispercpp-main)
-    ,@(when whisper-use-threads (list "--threads" (number-to-string whisper-use-threads)))
-    ;; ,@(when whisper-enable-speed-up '("--speed-up"))
-    ,@(when whisper-translate '("--translate"))
-    ,@(when whisper-show-progress-in-mode-line '("--print-progress"))
-    "--language" ,whisper-language
-    "--model" ,(whisper--model-file whisper-quantize)
-    "--no-timestamps"
-    "--file" ,input-file))
-
-(defalias 'whisper--transcribe-command 'whisper-command)
-(make-obsolete 'whisper--transcribe-command 'whisper-command "0.1.6")
-
-(defun whisper--mode-line-indicator (phase)
-  "Determine what to show in mode line depending on PHASE."
-  (if (eq phase 'recording)
-      whisper--mode-line-recording-indicator
-    (if whisper--using-whispercpp
-        '(:eval (concat whisper--mode-line-transcribing-indicator whisper--progress-level "%%"))
-      whisper--mode-line-transcribing-indicator)))
-
-(defun whisper--setup-mode-line (command phase)
-  "Set up PHASE appropriate indicator in the mode line.
-
-Depending on the COMMAND we either show the indicator or hide it."
-  (when whisper-show-progress-in-mode-line
-    (let ((indicator `(t ,(whisper--mode-line-indicator phase))))
-      (if (eq command :show)
-          (cl-pushnew indicator global-mode-string :test #'equal)
-        (setf global-mode-string (remove indicator global-mode-string))
-        (setq whisper--progress-level "0")))
-    (force-mode-line-update t)))
-
-(defun whisper--get-whispercpp-progress (_process output)
-  "Notify user of transcription progress by parsing whisper.cpp OUTPUT."
-  (let ((marker "whisper_print_progress_callback: progress ="))
-    (when (string-match (rx-to-string `(seq bol ,marker (* blank) (group (+ digit)) "%")) output)
-      (setq whisper--progress-level (match-string 1 output))
-      (force-mode-line-update))))
-
-(defun whisper--using-whispercpp-p ()
-  "Crude way to check we are in fact using whisper.cpp."
-  (let ((command (car (whisper-command whisper--temp-file)))
-        (pattern '(seq (or bol (any "/\\"))
-                       (or "main" "whisper-cli")
-                       (? ".exe")
-                       eol)))
-    (or (string-match-p (rx-to-string pattern) command)
-        ;; for the staunch Nix user
-        (string-equal command "whisper-cpp"))))
-
-(defun whisper--find-whispercpp-main ()
-  "Find whisper.cpp main binary in a backward compatible way."
-  (executable-find "whisper-cli"))
-
-(defun whisper--record-audio ()
-  "Start audio recording process in the background."
-  (when whisper-insert-text-at-point
-    (with-current-buffer whisper--point-buffer
-      (setq whisper--marker (point-marker))))
-  (if whisper--ffmpeg-input-file
-      (message "[*] Pre-processing media file")
-    (message "[*] Recording audio")
-    (whisper--setup-mode-line :show 'recording))
-  (if (string-equal whisper--ffmpeg-input-file whisper--temp-file)
-      (whisper--transcribe-audio)
-    (setq whisper--recording-process
-          (make-process
-           :name "whisper-recording"
-           :command (whisper--record-command whisper--temp-file)
-           :connection-type nil
-           :buffer nil
-           :sentinel (lambda (_process event)
-                       (whisper--setup-mode-line :hide 'recording)
-                       (cond ((or (string-equal "finished\n" event)
-                                  ;; this is would be sane
-                                  (string-equal "terminated\n" event)
-                                  ;; but this is reality
-                                  (string-equal "exited abnormally with code 255\n" event))
-                              (whisper--transcribe-audio))
-                             ((string-match-p "exited abnormally with code [0-9]+\n" event)
-                              (if whisper--ffmpeg-input-file
-                                  (error "FFmpeg failed to convert given file")
-                                (error "FFmpeg failed to record audio")))))))))
-
-(defun whisper--transcribe-audio ()
-  "Start audio transcribing process in the background."
-  (message "[-] Transcribing/Translating audio")
-  (setq whisper--using-whispercpp (whisper--using-whispercpp-p))
-  (whisper--setup-mode-line :show 'transcribing)
-  (setq whisper--transcribing-process
-        (make-process
-         :name "whisper-transcribing"
-         :command (whisper-command whisper--temp-file)
-         :connection-type nil
-         :buffer (get-buffer-create whisper--stdout-buffer-name)
-         :stderr (if (and whisper-show-progress-in-mode-line whisper--using-whispercpp)
-                     (make-pipe-process
-                      :name "whisper-stderr"
-                      :filter #'whisper--get-whispercpp-progress)
-                   (get-buffer-create whisper--stderr-buffer-name))
-         :coding 'utf-8
-         :sentinel (lambda (_process event)
-                     (unwind-protect
-                         (when-let* ((whisper--stdout-buffer (get-buffer whisper--stdout-buffer-name))
-                                     (finished (and (buffer-live-p whisper--stdout-buffer)
-                                                    (string-equal "finished\n" event))))
-                           (with-current-buffer whisper--stdout-buffer
-                             (goto-char (point-min))
-                             (skip-chars-forward " \n")
-                             (when (> (point) (point-min))
-                               (delete-region (point-min) (point)))
-                             (goto-char (point-max))
-                             (skip-chars-backward " \n")
-                             (when (> (point-max) (point))
-                               (delete-region (point) (point-max)))
-                             (when (= (buffer-size) 0)
-                               (error "Whisper command produced no output"))
-                             (goto-char (point-min))
-                             (run-hook-wrapped 'whisper-after-transcription-hook
-                                               (lambda (f)
-                                                 (with-current-buffer whisper--stdout-buffer
-                                                   (save-excursion
-                                                     (funcall f)))
-                                                 nil))
-                             (when (> (buffer-size) 0)
-                               (if whisper-insert-text-at-point
-                                   (with-current-buffer (marker-buffer whisper--marker)
-                                     (goto-char whisper--marker)
-                                     (insert-buffer-substring whisper--stdout-buffer)
-                                     (when whisper-return-cursor-to-start
-                                       (goto-char whisper--marker)))
-                                 (with-current-buffer
-                                     (get-buffer-create
-                                      (format "*whisper-%s*" (format-time-string "%+4Y%m%d%H%M%S")))
-                                   (insert-buffer-substring whisper--stdout-buffer)
-                                   (display-buffer (current-buffer)))))))
-                       (set-marker whisper--marker nil)
-                       (setq whisper--point-buffer nil)
-                       (kill-buffer whisper--stdout-buffer-name)
-                       (unless whisper-show-progress-in-mode-line (kill-buffer whisper--stderr-buffer-name))
-                       (whisper--setup-mode-line :hide 'transcribing)
-                       (message nil)
-                       (run-hooks 'whisper-after-insert-hook))))))
-
-(defun whisper--check-model-consistency ()
-  "Check if chosen language and model are consistent."
-  (when (and (not (string-equal "en" whisper-language))
-             (string-suffix-p ".en" whisper-model))
-    (error "Use generic model (non .en version) for non-English languages"))
-
-  (unless (or (= 2 (length whisper-language))
-              (string-equal "auto" whisper-language))
-    (error (concat "Unknown language shortcode. If unsure use 'auto'. For full list, see: "
-                   "https://github.com/ggerganov/whisper.cpp/blob/master/whisper.cpp")))
-
-  (let ((model-pattern (rx (seq bol
-                                (or "tiny" "base" "small" "medium"
-                                    (seq "large" (opt (seq "-v" (any "1-3") (opt "-turbo")))))
-                                (opt (seq "." (= 2 (any "a-z"))))
-                                eol)))
-        (quantization-pattern (rx (or "q4_0" "q4_1" "q4_k" "q5_0" "q5_1" "q5_k" "q6_k" "q8_0"))))
-    (unless (string-match-p model-pattern whisper-model)
-      (error (concat "Speech recognition model " whisper-model " not recognised. For the list, see: "
-                     "https://github.com/ggerganov/whisper.cpp/tree/master/models")))
-    (when whisper-quantize
-      (unless (string-match-p quantization-pattern whisper-quantize)
-        (error "Quantization format not recognized")))))
-
-(defun whisper--model-file (quantized)
-  "Return path of QUANTIZED model file relative to `whisper-install-directory'."
-  (let ((base (concat
-               (expand-file-name (file-name-as-directory whisper-install-directory))
-               "whisper.cpp/"))
-        (name (if quantized (concat whisper-model "-" whisper-quantize) whisper-model)))
-    (concat base "models/ggml-" name ".bin")))
-
-(defun whisper--check-install-and-run (buffer status)
-  "Run whisper after ensuring installation correctness.
-
-This is a horrible function, and in time due a rewrite. But for now I find this
-amusing and a little bit instructive as to how it became a mess.
-
-To conduct and display installation progress, `compilation-mode' is used because
-it's asynchronous and most importantly capable of handling progress output
-of programs like wget.  However the asynchronicity comes with some complexity
-cost when more than one tasks are run (but only one after another), as there is
-no built-in async/await support, so need to use callbacks instead.
-
-That is done by adding the callback to `compilation-finish-functions'.  Arguably
-it would be simpler to use one function per task and then chain these callbacks.
-However personally I preferred to logically group these together and handle
-synchronisation and cleaning up in one place, hence this big function.
-
-Conventionally, these callbacks are going to be called by passing current
-compilation-buffer in BUFFER and what event triggered the callback in STATUS so
-that's the function signature here.  Checking if BUFFER is indeed originating
-from this particular compilation buffer and not something the user have running
-elsewhere is necessary.  It's possible to make the hook buffer local, but
-compilation command starts before the hook could be added so I have some
-theoretical concern about possible race condition in that approach.
-
-Small Downside of re-using same function is that we need to differentiate
-whether this run is a callback or first normal call, that's what the made up
-status \"whisper-start\" does.
-
-The unfortunate price of asynchronicity is that this breaks dynamic binding
-because call stack is disrupted, callbacks are executed at a later time outside
-of the original dynamic context.  The fault not only lies here, but ultimately
-`make-process' itself is async, so it will likely take some insane hacks that
-escapes me right now, to get let bindings work like synchronous code."
-  (catch 'early-return
-    (unless (string-equal "whisper-start" status)
-      ;; shouldn't do anything when triggered by compilation buffers from elsewhere
-      (unless (eq buffer whisper--compilation-buffer)
-        (throw 'early-return nil))
-
-      ;; being here means this compilation job either finished or was interrupted
-      (remove-hook 'compilation-finish-functions #'whisper--check-install-and-run)
-      (when (string-equal "finished\n" status)
-        (kill-buffer whisper--compilation-buffer)))
-
-    (let ((base (concat
-                 (expand-file-name (file-name-as-directory whisper-install-directory))
-                 "whisper.cpp/"))
-          (old-bin-name (if (eq system-type 'windows-nt) "main.exe" "main"))
-          (bin-name (if (eq system-type 'windows-nt) "whisper-cli.exe" "whisper-cli"))
-          (compilation-buffer-name-function '(lambda (_) whisper--compilation-buffer-name)))
-
-      (setq whisper--install-path base)
-
-      (when (and (not (or (string-equal "interrupt\n" status)
-                          (string-prefix-p "exited abnormally with code" status)))
-                 (not (or (file-exists-p (concat base old-bin-name)) ;; old location
-                          (file-exists-p (concat base "build/bin/" bin-name)))))
-
-        (when (eq whisper-install-whispercpp 'manual)
-          (error (format "Couldn't find whisper.cpp install at: %s" base)))
-
-        (if (yes-or-no-p (format "Couldn't find whisper.cpp, install it at: %s ?" base))
-            (let ((make-commands
-                   (concat
-                    "mkdir -p " whisper-install-directory " && "
-                    "cd " whisper-install-directory " && "
-                    "git clone https://github.com/ggerganov/whisper.cpp && "
-                    "cd whisper.cpp && "
-                    "CLICOLOR=0 make")))
-              (setq whisper--compilation-buffer (get-buffer-create whisper--compilation-buffer-name))
-              (add-hook 'compilation-finish-functions #'whisper--check-install-and-run)
-              (compile make-commands)
-              (throw 'early-return nil))
-          (error "Needs whisper.cpp to be installed")))
-
-      (when (and (not (file-exists-p (whisper--model-file nil)))
-                 (not (or (string-equal "interrupt\n" status)
-                          (string-prefix-p "exited abnormally with code" status))))
-        (if (yes-or-no-p (format "Speech recognition model \"%s\" isn't available, download now?" whisper-model))
-            (let ((make-commands
-                   (concat
-                    "cd " base " && "
-                    "models/download-ggml-model.sh " whisper-model)))
-              (setq whisper--compilation-buffer (get-buffer-create whisper--compilation-buffer-name))
-              (add-hook 'compilation-finish-functions #'whisper--check-install-and-run)
-              (compile make-commands)
-              (throw 'early-return nil))
-          (error "Needs speech recognition model to run whisper")))
-
-      (when (and whisper-quantize
-                 (not (file-exists-p (whisper--model-file t)))
-                 (not (or (string-equal "interrupt\n" status)
-                          (string-prefix-p "exited abnormally with code" status))))
-        (if (not (file-exists-p (concat base "build/bin/quantize")))
-            (let ((make-commands
-                   (concat
-                    "cd " base " && "
-                    "make quantize" " && "
-                    "echo 'Quantizing the model....'" " && "
-                    "./build/bin/quantize " (whisper--model-file nil) " " (whisper--model-file t) " " whisper-quantize)))
-              (setq whisper--compilation-buffer (get-buffer-create whisper--compilation-buffer-name))
-              (add-hook 'compilation-finish-functions #'whisper--check-install-and-run)
-              (compile make-commands)
-              (throw 'early-return nil))
-          (let ((quantize-command
-                 (concat (concat base "build/bin/quantize")
-                         " " (whisper--model-file nil) " " (whisper--model-file t) " " whisper-quantize)))
-            (message "Running quantize binary...")
-            (shell-command quantize-command)
-            (throw 'early-return nil))))
-
-      (when (string-equal "interrupt\n" status)
-        ;; double check to be sure before cleaning up
-        (when (and (file-directory-p base) (string-suffix-p "/whisper.cpp/" base))
-          (if (file-exists-p (concat base "build/bin/" bin-name))
-              ;; model download interrupted probably, should delete partial file
-              (progn
-                (message "Download interrupted, cleaning up.")
-                (delete-file (concat base "models/" "ggml-" whisper-model ".bin")))
-            ;; otherwise whisper.cpp compilation got interrupted
-            (message "Installation interrupted, cleaning up.")
-            (unless (eq whisper-install-whispercpp 'manual)
-              (delete-directory whisper--install-path t))))
-        (throw 'early-return nil))
-
-      (when (string-prefix-p "exited abnormally with code" status)
-        (if (eq whisper-install-whispercpp 'manual)
-            (message "Compilation exited abnormally, but not deleting directory because installation is manual.")
-          (progn
-            (delete-directory whisper--install-path t)
-            (message "Couldn't compile whisper.cpp. Check that you have Git, a C++ compiler and CMake installed.")))
-        (display-buffer whisper--compilation-buffer)
-        (throw 'early-return nil))
-
-      (when (string-equal "finished\n" status)
-        (unless (or whisper--ffmpeg-input-file
-                    (yes-or-no-p "Speech recognition model download completed, want to record audio now?"))
-          (throw 'early-return nil)))
-
-      ;; finally
-      (whisper--record-audio))))
-
-;;;###autoload
-(defun whisper-run (&optional arg)
-  "Transcribe/translate audio using whisper.
-
-When ARG is given, uses a local file as input. Otherwise records the audio.
-
-This is a dwim function that does different things depending on current state:
-
-- When inference engine (whisper.cpp) isn't installed, installs it first.
-- When speech recognition model isn't available, downloads it.
-- When installation/download is already in progress, cancels those.
-- When installation is valid, starts recording audio.
-- When recording is in progress, stops it and starts transcribing.
-- When transcribing is in progress, cancels it."
-  (interactive "P")
-  (if (process-live-p whisper--transcribing-process)
-      (when (yes-or-no-p "A transcribing is already in progress, kill it?")
-        (kill-process whisper--transcribing-process))
-
-    (cond
-     ((process-live-p whisper--recording-process)
-      (interrupt-process whisper--recording-process))
-     ((and (buffer-live-p whisper--compilation-buffer)
-           (process-live-p (get-buffer-process whisper--compilation-buffer)))
-      (when-let* ((proc (get-buffer-process whisper--compilation-buffer)))
-	(interrupt-process proc)))
-     (t
-      (setq whisper--point-buffer (current-buffer))
-      (run-hooks 'whisper-before-transcription-hook)
-      (when whisper-install-whispercpp
-        (whisper--check-model-consistency))
-      (setq-default
-       whisper--ffmpeg-input-file
-       (pcase arg
-        ('nil nil)
-        ('(4)
-         (when-let* ((file (expand-file-name (read-file-name "Media file: " nil nil t))))
-          (unless (file-readable-p file)
-            (error "Media file doesn't exist or isn't readable"))
-          file))
-        ((and (pred file-readable-p) file) file)))
-      (setq whisper--using-whispercpp nil)
-      (if whisper-install-whispercpp
-          (whisper--check-install-and-run nil "whisper-start")
-        ;; if user is bringing their own inference engine, we at least check the command exists
-        (let ((command (car (whisper-command whisper--temp-file))))
-          (if (or (file-exists-p command)
-                  (executable-find command))
-              (whisper--record-audio)
-            (error (format "Couldn't find %s in PATH, nor is it a file" command)))))))))
-
-;;;###autoload
-(defun whisper-file ()
-  "Transcribe/translate local file using whisper."
-  (interactive)
-  (let ((current-prefix-arg '(4)))
-    (call-interactively #'whisper-run)))
-
-(provide 'whisper)
-;;; whisper.el ends here
dots/config/emacs/site-lisp/goose.el
@@ -1,247 +0,0 @@
-;;; goose.el --- Integrate Goose CLI via vterm -*- lexical-binding: t; -*-
-
-;; Author: Daisuke Terada <pememo@gmail.com>
-;; Package-Requires: ((emacs "29") (vterm "0.0.2") (transient "0.9.1") (consult "2.5"))
-;; Version: 0.1.0
-;; Keywords: tools, convenience, ai
-;; URL: https://github.com/aq2bq/goose.el
-
-;;; Commentary:
-;; Seamless integration of the Goose CLI within Emacs using the `vterm` terminal emulator.
-;;
-;; Provides:
-;; - Intuitive session management: start and restart Goose CLI sessions with easy labeling (name or timestamp)
-;; - Immediate context injection (file path, buffer, region, template, text) into the Goose prompt, sent directly (no internal queuing)
-;; - Prompt templates (consult-based), auto-detected from ~/.config/goose/prompts/
-;; - Customizable context formatting, prompt directory, and keybinding (transient menu)
-;; - Designed for rapid AI prompt iteration and CLI-interactive workflows from Emacs
-;;
-;; Usage example:
-;;   M-x goose-start-session
-;;   M-x goose-add-context-buffer ; send current buffer to the Goose session
-;;
-
-;; The MIT License (MIT)
-;;
-;; Copyright (c) 2025 Daisuke Terada
-;;
-;; Permission is hereby granted, free of charge, to any person obtaining a copy
-;; of this software and associated documentation files (the "Software"), to deal
-;; in the Software without restriction, including without limitation the rights
-;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-;; copies of the Software, and to permit persons to whom the Software is
-;; furnished to do so, subject to the following conditions:
-;;
-;; The above copyright notice and this permission notice shall be included in all
-;; copies or substantial portions of the Software.
-;;
-;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
-;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-;; SOFTWARE.
-
-;;; Code:
-(require 'vterm)
-(require 'transient)
-(require 'consult)
-
-(define-derived-mode goose-mode vterm-mode "Goose"
-  "Major mode for Goose terminal (inherits `vterm-mode').
-This mode provides `goose-mode-hook` for customizations.
-Do not call this mode interactively.  Use `goose-start-session` instead."
-  (when (called-interactively-p 'interactive)
-    (user-error "`goose-mode` is an internal mode.  Use `M-x goose-start-session` instead")))
-
-;; Prevent interactive use from misapplying to the current buffer
-(put 'goose-mode 'function-documentation
-     "Goose terminal mode. Do not call directly. Use `goose-start-session` instead.")
-(put 'goose-mode 'interactive-form
-     '(progn (user-error "`goose-mode` is not meant to be called interactively.  Use `M-x goose-start-session` instead")))
-
-
-(defgroup goose nil
-  "Goose CLI integration using vterm."
-  :group 'tools)
-
-(defcustom goose-program-name "goose"
-  "Name or path of the Goose CLI executable."
-  :type 'string
-  :group 'goose)
-
-(defcustom goose-default-buffer-name "*goose*"
-  "Default buffer name prefix for Goose sessions."
-  :type 'string
-  :group 'goose)
-
-(defcustom goose-prompt-directory (expand-file-name "~/.config/goose/prompts/")
-  "Directory containing prompt templates for Goose integration."
-  :type 'directory
-  :group 'goose)
-
-(defcustom goose-context-format "%s"
-  "Format string applied to CONTEXT text before sending to Goose.
-Use %s as placeholder for the raw text."
-  :type 'string
-  :group 'goose)
-
-(defcustom goose-context-file-path-prefix "File from path: %s"
-  "Prefix format for inserting file path into Goose.  %s will be replaced by the file path."
-  :type 'string
-  :group 'goose)
-
-(defcustom goose-context-buffer-prefix "File: %s\n%s"
-  "Prefix format for inserting buffer content into Goose.  %s will be replaced by file path and buffer content."
-  :type 'string
-  :group 'goose)
-
-(defcustom goose-context-region-prefix "File: %s\nRegion:\n%s"
-  "Prefix format for inserting region content into Goose.  %s will be replaced by file path and region."
-  :type 'string
-  :group 'goose)
-
-(defvar goose--last-args nil
-  "Last Goose CLI argument list for restart.")
-
-(defvar goose--last-label nil
-  "Last session label for restart.")
-
-(defun goose--session-label (name)
-  "Return session label for NAME, or timestamp string if NAME is empty."
-  (if (and name (not (string-empty-p name)))
-      (shell-quote-argument name)
-    (format-time-string "%Y%m%d-%H%M%S")))
-
-(defun goose--build-args (name)
-  "Construct Goose CLI argument list for 'session --name NAME'."
-  (let* ((label (goose--session-label name))
-         (base-args (list "session" "--name" label)))
-    base-args))
-
-(defun goose--run-session (label args)
-  "Start or restart a Goose session buffer labeled LABEL with ARGS list."
-  (let ((bufname (format "%s<%s>" goose-default-buffer-name label)))
-    (when (get-buffer bufname)
-      (kill-buffer bufname))
-    (let* ((proj (when (fboundp 'project-current) (project-current)))
-           (root (when proj (project-root proj)))
-           (default-directory (or root default-directory))
-           (vterm-buffer (generate-new-buffer bufname)))
-      (with-current-buffer vterm-buffer
-        (let ((vterm-shell "/bin/bash"))
-          (goose-mode)
-          (rename-buffer bufname t)
-          (vterm-send-key "l" nil nil :ctrl) ;; suppress any previous output
-          (vterm-send-string
-           (mapconcat #'identity (cons goose-program-name args) " "))
-          (vterm-send-return)))
-      (setq goose--last-label label
-            goose--last-args  args)
-      (pop-to-buffer vterm-buffer)
-      (message "Goose session started in buffer %s (dir: %s)" bufname default-directory))))
-
-;;;###autoload
-(defun goose-start-session (&optional name)
-  "Start a new Goose session with optional NAME, or switch if exists."
-  (interactive "sSession name (optional): ")
-  (let ((label (goose--session-label name))
-        (args  (goose--build-args   name)))
-    (goose--run-session label args)))
-
-;;;###autoload
-(defun goose-restart-session ()
-  "Restart the last Goose session using previous NAME and ARGS, with confirmation."
-  (interactive)
-  (unless (and goose--last-label goose--last-args)
-    (error "No Goose session to restart"))
-  (when (yes-or-no-p (format "Restart Goose session <%s>? " goose--last-label))
-    (goose--run-session goose--last-label goose--last-args)))
-
-(defun goose--session-buffer-name ()
-  "Return the current Goose session buffer name."
-  (format "%s<%s>" goose-default-buffer-name goose--last-label))
-
-(defun goose--insert-context (text)
-  "Send TEXT as input to the current Goose session, deferring execution until RET.
-Applies `goose-context-format` to TEXT before sending.
-If the session is not started, starts it automatically."
-  (let* ((bufname (goose--session-buffer-name))
-         (buf     (get-buffer bufname)))
-    (unless buf
-      (goose-start-session)
-      (setq buf (get-buffer (goose--session-buffer-name))))
-    (with-current-buffer buf
-      (vterm-send-string (format goose-context-format text))
-      (vterm-send-key "j" nil nil :ctrl))))
-
-;;;###autoload
-(defun goose-add-context-file-path ()
-  "Insert the current buffer's file path into the Goose prompt."
-  (interactive)
-  (unless (buffer-file-name) (error "Buffer is not visiting a file"))
-  (goose--insert-context (format goose-context-file-path-prefix (buffer-file-name)))
-  (message "Inserted file path into prompt"))
-
-;;;###autoload
-(defun goose-add-context-buffer ()
-  "Insert the current buffer's content and file path into the Goose prompt."
-  (interactive)
-  (goose--insert-context
-   (format goose-context-buffer-prefix
-           (or (buffer-file-name) "<no file>")
-           (buffer-string)))
-  (message "Inserted buffer content into prompt"))
-
-;;;###autoload
-(defun goose-add-context-region ()
-  "Insert the active region's content and file path into the Goose prompt."
-  (interactive)
-  (unless (use-region-p) (error "No region selected"))
-  (goose--insert-context
-   (format goose-context-region-prefix
-           (or (buffer-file-name) "<no file>")
-           (buffer-substring-no-properties
-            (region-beginning)
-            (region-end))))
-  (message "Inserted region into prompt"))
-
-;;;###autoload
-(defun goose-add-context-template ()
-  "Insert a prompt template from `goose-prompt-directory' into the Goose prompt."
-  (interactive)
-  (unless (file-directory-p goose-prompt-directory)
-    (error "Prompt directory %s does not exist" goose-prompt-directory))
-  (let* ((files (directory-files goose-prompt-directory nil "^[^.].*"))
-         (choice (consult--read files :prompt "Choose template: "))
-         (content (with-temp-buffer
-                    (insert-file-contents
-                     (expand-file-name choice goose-prompt-directory))
-                    (buffer-string))))
-    (goose--insert-context content)
-    (message "Inserted template %s into prompt" choice)))
-
-;;;###autoload
-(defun goose-add-context-text (text)
-  "Prompt for and insert arbitrary TEXT into the Goose prompt."
-  (interactive "sText to insert: ")
-  (goose--insert-context text)
-  (message "Inserted text into prompt"))
-
-;;;###autoload
-(transient-define-prefix goose-transient ()
-  "Transient interface for Goose commands."
-  ["Goose Session"
-   ("s" "Start session" goose-start-session)
-   ("r" "Restart session" goose-restart-session)]
-  ["Insert Context"
-   ("f" "File path" goose-add-context-file-path)
-   ("b" "Buffer" goose-add-context-buffer)
-   ("e" "Region" goose-add-context-region)
-   ("t" "Template" goose-add-context-template)
-   ("x" "Text" goose-add-context-text)])
-
-
-(provide 'goose)
-;;; goose.el ends here
dots/config/emacs/site-lisp/journelly-batch-functions.el
@@ -1,474 +0,0 @@
-;;; journelly-batch-functions.el --- Batch functions for Journelly journal entries -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2025 Vincent Demeester
-
-;; Author: Vincent Demeester <vincent@demeester.fr>
-;; Keywords: org-mode, journelly, batch
-;; Version: 1.0.0
-
-;;; Commentary:
-
-;; Emacs batch mode functions for manipulating Journelly.org journal files.
-;; Journelly is an iOS app that stores journal entries in org-mode format.
-;;
-;; Format:
-;; - Single file with entries in reverse chronological order (newest first)
-;; - Each entry is a top-level heading: * [YYYY-MM-DD Day HH:MM] @ Location
-;; - Optional PROPERTIES drawer with GPS/weather metadata
-;; - Free-form org-mode content
-;;
-;; Functions:
-;; - journelly-batch-create-entry: Create new journal entry
-;; - journelly-batch-create-entry-auto: Create entry with automatic location/weather
-;; - journelly-batch-append-to-today: Append to today's entry
-;; - journelly-batch-list-entries: List recent entries
-;; - journelly-batch-search: Search entry content
-;; - journelly-batch-get-entry: Get specific entry by date/time
-;;
-;; Usage:
-;;   emacs --batch \
-;;     --load journelly-batch-functions.el \
-;;     --eval "(journelly-batch-create-entry \
-;;               \"~/desktop/org/Journelly.org\" \
-;;               \"Home\" \
-;;               \"Entry content\")"
-
-;;; Code:
-
-(require 'org)
-(require 'org-element)
-(require 'json)
-
-;; Load location/weather functions if available
-(let ((location-weather-file
-       (expand-file-name "journelly-location-weather.el"
-                        (file-name-directory (or load-file-name buffer-file-name)))))
-  (when (file-exists-p location-weather-file)
-    (load location-weather-file)))
-
-;; Declare functions from journelly-location-weather.el (loaded conditionally above)
-(declare-function journelly-get-location "journelly-location-weather")
-(declare-function journelly-get-weather "journelly-location-weather")
-
-;;; Utility functions
-
-(defun journelly--format-timestamp ()
-  "Generate org-mode timestamp for current time: [YYYY-MM-DD Day HH:MM]."
-  (format-time-string "[%Y-%m-%d %a %H:%M]"))
-
-(defun journelly--format-date-only ()
-  "Generate date only: YYYY-MM-DD."
-  (format-time-string "%Y-%m-%d"))
-
-(defun journelly--parse-timestamp (heading)
-  "Extract timestamp from HEADING.
-Expected format: * [YYYY-MM-DD Day HH:MM] @ Location
-Returns the timestamp string or nil."
-  (when (string-match "\\[\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) \\([A-Z][a-z][a-z]\\) \\([0-9]\\{2\\}:[0-9]\\{2\\}\\)\\]" heading)
-    (match-string 0 heading)))
-
-(defun journelly--parse-location (heading)
-  "Extract location from HEADING.
-Expected format: * [YYYY-MM-DD Day HH:MM] @ Location
-Returns the location string or nil."
-  (when (string-match "@ \\(.+\\)$" heading)
-    (match-string 1 heading)))
-
-(defun journelly--make-heading (location)
-  "Create journal entry heading with current timestamp and LOCATION."
-  (format "* %s @ %s" (journelly--format-timestamp) location))
-
-(defun journelly--make-properties (latitude longitude temperature condition symbol)
-  "Create PROPERTIES drawer with GPS and weather data.
-LATITUDE, LONGITUDE, TEMPERATURE, CONDITION, SYMBOL are optional strings.
-Returns nil if no properties provided."
-  (let ((props '()))
-    (when latitude
-      (push (format ":LATITUDE: %s" latitude) props))
-    (when longitude
-      (push (format ":LONGITUDE: %s" longitude) props))
-    (when temperature
-      (push (format ":WEATHER_TEMPERATURE: %s" temperature) props))
-    (when condition
-      (push (format ":WEATHER_CONDITION: %s" condition) props))
-    (when symbol
-      (push (format ":WEATHER_SYMBOL: %s" symbol) props))
-    (when props
-      (concat ":PROPERTIES:\n"
-              (mapconcat 'identity (nreverse props) "\n")
-              "\n:END:\n"))))
-
-(defun journelly--find-header-end (buffer)
-  "Find the end of the Journelly header in BUFFER.
-Returns the position after the :end: line, or nil if not found."
-  (with-current-buffer buffer
-    (goto-char (point-min))
-    (when (re-search-forward "^:end:$" nil t)
-      (forward-line 1)
-      (point))))
-
-(defun journelly--json-response (success data &optional message)
-  "Create JSON response object.
-SUCCESS is boolean, DATA is any JSON-serializable value.
-MESSAGE is optional error/success message."
-  (let ((response `((success . ,success)
-                    (data . ,data))))
-    (when message
-      (push `(message . ,message) response))
-    (json-encode response)))
-
-(defun journelly--output-json (success data &optional message)
-  "Output JSON response to stdout.
-SUCCESS is boolean, DATA is the response data, MESSAGE is optional."
-  (princ (journelly--json-response success data message))
-  (terpri))
-
-;;; Main functions
-
-(defun journelly-batch-create-entry (file location content &optional latitude longitude temperature condition symbol content-file)
-  "Create new journal entry in FILE.
-
-Arguments:
-  FILE: Path to Journelly.org file
-  LOCATION: Location string (e.g., \"Home\", \"Kyushu\")
-  CONTENT: Entry content (can be empty string)
-  LATITUDE: Optional GPS latitude
-  LONGITUDE: Optional GPS longitude
-  TEMPERATURE: Optional temperature (e.g., \"15,2°C\")
-  CONDITION: Optional weather condition (e.g., \"Cloudy\")
-  SYMBOL: Optional weather symbol (e.g., \"cloud\")
-  CONTENT-FILE: Optional path to file containing content
-
-If CONTENT-FILE is provided, reads content from file instead of CONTENT arg.
-
-Returns JSON with success status and entry details."
-  (condition-case err
-      (let ((actual-content (if content-file
-                                (with-temp-buffer
-                                  (insert-file-contents content-file)
-                                  (buffer-string))
-                              content)))
-        (with-temp-buffer
-          (insert-file-contents file)
-
-          ;; Find where to insert (after header)
-          (let ((insert-pos (journelly--find-header-end (current-buffer))))
-            (unless insert-pos
-              (error "Could not find Journelly header end marker (:end:)"))
-
-            (goto-char insert-pos)
-
-            ;; Build entry
-            (let ((heading (journelly--make-heading location))
-                  (properties (journelly--make-properties
-                              latitude longitude temperature condition symbol))
-                  (timestamp (journelly--format-timestamp)))
-
-              ;; Insert entry
-              (insert heading "\n")
-              (when properties
-                (insert properties))
-              (when (and actual-content (not (string-empty-p actual-content)))
-                (insert actual-content)
-                (unless (string-suffix-p "\n" actual-content)
-                  (insert "\n")))
-              (insert "\n")  ;; Blank line after entry
-
-              ;; Write back to file
-              (write-region (point-min) (point-max) file)
-
-              ;; Return success
-              (journelly--output-json
-               t
-               `((timestamp . ,timestamp)
-                 (location . ,location)
-                 (has-properties . ,(if properties t :json-false))
-                 (file . ,file))
-               "Journal entry created successfully")))))
-    (error
-     (journelly--output-json nil nil (error-message-string err)))))
-
-(defun journelly-batch-append-to-today (file content &optional content-file)
-  "Append CONTENT to today's journal entry in FILE.
-
-Arguments:
-  FILE: Path to Journelly.org file
-  CONTENT: Content to append
-  CONTENT-FILE: Optional path to file containing content
-
-If no entry exists for today, returns error.
-Returns JSON with success status."
-  (condition-case err
-      (let ((actual-content (if content-file
-                                (with-temp-buffer
-                                  (insert-file-contents content-file)
-                                  (buffer-string))
-                              content))
-            (today-date (journelly--format-date-only)))
-        (with-temp-buffer
-          (insert-file-contents file)
-          (goto-char (point-min))
-
-          ;; Find today's entry
-          (let ((found nil)
-                (search-pattern (format "^\\* \\[%s " today-date)))
-            (while (and (not found)
-                       (re-search-forward search-pattern nil t))
-              (setq found t))
-
-            (unless found
-              (error "No journal entry found for today (%s)" today-date))
-
-            ;; Move to end of this entry (before next heading or end of file)
-            (forward-line 1)
-            (if (re-search-forward "^\\* \\[" nil t)
-                (progn
-                  (beginning-of-line)
-                  (backward-char 1))  ;; Before the newline
-              (goto-char (point-max)))
-
-            ;; Insert content
-            (insert "\n" actual-content)
-            (unless (string-suffix-p "\n" actual-content)
-              (insert "\n"))
-
-            ;; Write back
-            (write-region (point-min) (point-max) file)
-
-            ;; Return success
-            (journelly--output-json
-             t
-             `((date . ,today-date)
-               (file . ,file))
-             "Content appended to today's entry"))))
-    (error
-     (journelly--output-json nil nil (error-message-string err)))))
-
-(defun journelly-batch-list-entries (file &optional limit)
-  "List recent journal entries from FILE.
-
-Arguments:
-  FILE: Path to Journelly.org file
-  LIMIT: Optional number of entries to return (default 10)
-
-Returns JSON with list of entries."
-  (condition-case err
-      (let ((max-entries (or (and limit (string-to-number limit)) 10))
-            (entries '()))
-        (with-temp-buffer
-          (insert-file-contents file)
-          (goto-char (point-min))
-
-          ;; Skip header
-          (journelly--find-header-end (current-buffer))
-
-          ;; Parse entries
-          (while (and (< (length entries) max-entries)
-                     (re-search-forward "^\\* \\(\\[.*?\\]\\) @ \\(.+\\)$" nil t))
-            (let ((timestamp (match-string 1))
-                  (location (match-string 2))
-                  (has-properties nil)
-                  (content-preview ""))
-
-              ;; Check for properties
-              (save-excursion
-                (forward-line 1)
-                (when (looking-at "^:PROPERTIES:")
-                  (setq has-properties t)))
-
-              ;; Get content preview (first 100 chars)
-              (save-excursion
-                (forward-line 1)
-                (when has-properties
-                  (re-search-forward "^:END:$" nil t)
-                  (forward-line 1))
-                (let ((content-start (point)))
-                  (if (re-search-forward "^\\* \\[" nil t)
-                      (beginning-of-line)
-                    (goto-char (point-max)))
-                  (setq content-preview
-                        (string-trim
-                         (buffer-substring-no-properties content-start (point))))
-                  (when (> (length content-preview) 100)
-                    (setq content-preview
-                          (concat (substring content-preview 0 100) "...")))))
-
-              (push `((timestamp . ,timestamp)
-                     (location . ,location)
-                     (has-properties . ,(if has-properties t :json-false))
-                     (preview . ,content-preview))
-                    entries)))
-
-          ;; Return results (already in reverse chronological from file)
-          (journelly--output-json t (nreverse entries))))
-    (error
-     (journelly--output-json nil nil (error-message-string err)))))
-
-(defun journelly-batch-search (file query)
-  "Search journal entries in FILE for QUERY.
-
-Arguments:
-  FILE: Path to Journelly.org file
-  QUERY: Search string (case-insensitive)
-
-Returns JSON with matching entries."
-  (condition-case err
-      (let ((matches '())
-            (query-lower (downcase query)))
-        (with-temp-buffer
-          (insert-file-contents file)
-          (goto-char (point-min))
-
-          ;; Skip header
-          (journelly--find-header-end (current-buffer))
-
-          ;; Search entries
-          (while (re-search-forward "^\\* \\(\\[.*?\\]\\) @ \\(.+\\)$" nil t)
-            (let ((timestamp (match-string 1))
-                  (location (match-string 2))
-                  (entry-start (point))
-                  (entry-end nil)
-                  (entry-content ""))
-
-              ;; Find entry end
-              (save-excursion
-                (if (re-search-forward "^\\* \\[" nil t)
-                    (setq entry-end (match-beginning 0))
-                  (setq entry-end (point-max))))
-
-              ;; Get entry content
-              (setq entry-content
-                    (buffer-substring-no-properties entry-start entry-end))
-
-              ;; Check if query matches
-              (when (string-match-p query-lower (downcase entry-content))
-                (push `((timestamp . ,timestamp)
-                       (location . ,location)
-                       (content . ,(string-trim entry-content)))
-                      matches))))
-
-          ;; Return results
-          (journelly--output-json
-           t
-           (nreverse matches)
-           (format "Found %d matching entries" (length matches)))))
-    (error
-     (journelly--output-json nil nil (error-message-string err)))))
-
-(defun journelly-batch-get-entry (file date &optional time)
-  "Get specific journal entry from FILE by DATE and optional TIME.
-
-Arguments:
-  FILE: Path to Journelly.org file
-  DATE: Date string (YYYY-MM-DD)
-  TIME: Optional time string (HH:MM)
-
-Returns JSON with entry details or error if not found."
-  (condition-case err
-      (let ((search-pattern (if time
-                               (format "^\\* \\[%s .* %s\\]" date time)
-                             (format "^\\* \\[%s " date)))
-            (found nil))
-        (with-temp-buffer
-          (insert-file-contents file)
-          (goto-char (point-min))
-
-          ;; Skip header
-          (journelly--find-header-end (current-buffer))
-
-          ;; Search for entry
-          (when (re-search-forward search-pattern nil t)
-            (beginning-of-line)
-            (when (looking-at "^\\* \\(\\[.*?\\]\\) @ \\(.+\\)$")
-              (let ((timestamp (match-string 1))
-                    (location (match-string 2))
-                    (entry-end nil)
-                    (has-properties nil)
-                    (properties nil)
-                    (content ""))
-
-                (forward-line 1)
-
-                ;; Check for properties
-                (when (looking-at "^:PROPERTIES:")
-                  (setq has-properties t)
-                  (let ((props-start (point)))
-                    (re-search-forward "^:END:$" nil t)
-                    (setq properties
-                          (buffer-substring-no-properties props-start (point)))
-                    (forward-line 1)))
-
-                ;; Get content
-                (let ((content-start (point)))
-                  (if (re-search-forward "^\\* \\[" nil t)
-                      (setq entry-end (match-beginning 0))
-                    (setq entry-end (point-max)))
-                  (setq content
-                        (string-trim
-                         (buffer-substring-no-properties content-start entry-end))))
-
-                (setq found `((timestamp . ,timestamp)
-                            (location . ,location)
-                            (has-properties . ,(if has-properties t :json-false))
-                            (properties . ,(or properties ""))
-                            (content . ,content))))))
-
-          (if found
-              (journelly--output-json t found)
-            (journelly--output-json
-             nil
-             nil
-             (format "No entry found for %s%s"
-                    date
-                    (if time (format " at %s" time) ""))))))
-    (error
-     (journelly--output-json nil nil (error-message-string err)))))
-
-(defun journelly-batch-create-entry-auto (file content &optional content-file use-location use-weather)
-  "Create journal entry with automatic location and/or weather detection.
-
-Arguments:
-  FILE: Path to Journelly.org file
-  CONTENT: Entry content
-  CONTENT-FILE: Optional path to file containing content
-  USE-LOCATION: If non-nil, automatically detect location
-  USE-WEATHER: If non-nil, automatically detect weather
-
-Requires journelly-location-weather.el to be loaded.
-
-Returns JSON with success status and entry details."
-  (condition-case err
-      (progn
-        (unless (fboundp 'journelly-get-location)
-          (error "Location/weather functions not available. Load journelly-location-weather.el"))
-
-        (let ((location-data (when use-location (journelly-get-location)))
-              (weather-data (when use-weather (journelly-get-weather)))
-              (actual-content (if content-file
-                                 (with-temp-buffer
-                                   (insert-file-contents content-file)
-                                   (buffer-string))
-                               content)))
-
-          ;; Extract data
-          (let ((city (when location-data (cdr (assoc 'city location-data))))
-                (lat (when location-data (cdr (assoc 'lat location-data))))
-                (lon (when location-data (cdr (assoc 'lon location-data))))
-                (temp (when weather-data (cdr (assoc 'temperature weather-data))))
-                (cond (when weather-data (cdr (assoc 'condition weather-data))))
-                (symbol (when weather-data (cdr (assoc 'symbol weather-data)))))
-
-            ;; Create entry
-            (journelly-batch-create-entry
-             file
-             (or city "Unknown")
-             actual-content
-             lat lon temp cond symbol nil))))
-    (error
-     (journelly--output-json nil nil (error-message-string err)))))
-
-;;; Provide
-
-(provide 'journelly-batch-functions)
-
-;;; journelly-batch-functions.el ends here
dots/config/emacs/site-lisp/journelly-location-weather.el
@@ -1,257 +0,0 @@
-;;; journelly-location-weather.el --- Location and weather helpers for Journelly -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2025 Vincent Demeester
-
-;; Author: Vincent Demeester <vincent@demeester.fr>
-;; Keywords: org-mode, journelly, location, weather
-;; Version: 1.0.0
-
-;;; Commentary:
-
-;; Emacs Lisp functions to get location and weather data for Journelly journal entries.
-;;
-;; Location:
-;; - Uses IP-based geolocation (ipinfo.io)
-;; - Returns city name and GPS coordinates
-;; - Caches results for 1 hour
-;;
-;; Weather:
-;; - Uses wttr.in weather service
-;; - Returns temperature, condition, and iOS SF Symbol
-;; - Caches results for 30 minutes
-;; - Intelligent day/night symbol mapping
-;;
-;; Functions:
-;; - journelly-get-location: Get current location via IP geolocation
-;; - journelly-get-weather: Get current weather
-;; - journelly-batch-get-location: Batch mode wrapper for location
-;; - journelly-batch-get-weather: Batch mode wrapper for weather
-;;
-;; Usage (batch mode):
-;;   emacs --batch \
-;;     --load journelly-location-weather.el \
-;;     --eval "(journelly-batch-get-location)"
-;;
-;;   emacs --batch \
-;;     --load journelly-location-weather.el \
-;;     --eval "(journelly-batch-get-weather)"
-
-;;; Code:
-
-(require 'url)
-(require 'json)
-
-;;; Configuration
-
-(defvar journelly-cache-dir
-  (expand-file-name "journal" (or (getenv "XDG_CACHE_HOME")
-                                   (expand-file-name ".cache" "~")))
-  "Directory for caching location and weather data.")
-
-(defvar journelly-location-cache-timeout 3600
-  "Location cache timeout in seconds (default: 1 hour).")
-
-(defvar journelly-weather-cache-timeout 1800
-  "Weather cache timeout in seconds (default: 30 minutes).")
-
-;;; Utility functions
-
-(defun journelly--ensure-cache-dir ()
-  "Ensure cache directory exists."
-  (unless (file-exists-p journelly-cache-dir)
-    (make-directory journelly-cache-dir t)))
-
-(defun journelly--cache-file (key)
-  "Get cache file path for KEY."
-  (expand-file-name (format "%s.json" key) journelly-cache-dir))
-
-(defun journelly--cache-valid-p (cache-file timeout)
-  "Check if CACHE-FILE is valid within TIMEOUT seconds."
-  (when (file-exists-p cache-file)
-    (let* ((file-time (nth 5 (file-attributes cache-file)))
-           (current-time (current-time))
-           (age (float-time (time-subtract current-time file-time))))
-      (< age timeout))))
-
-(defun journelly--read-cache (cache-file)
-  "Read JSON data from CACHE-FILE."
-  (when (file-exists-p cache-file)
-    (with-temp-buffer
-      (insert-file-contents cache-file)
-      (goto-char (point-min))
-      (json-read))))
-
-(defun journelly--write-cache (cache-file data)
-  "Write DATA as JSON to CACHE-FILE."
-  (journelly--ensure-cache-dir)
-  (with-temp-file cache-file
-    (insert (json-encode data))))
-
-(defun journelly--fetch-url (url)
-  "Fetch URL and return parsed JSON response."
-  (let ((url-request-method "GET")
-        (url-request-extra-headers '(("User-Agent" . "Emacs/journelly"))))
-    (with-current-buffer (url-retrieve-synchronously url t nil 10)
-      (goto-char (point-min))
-      ;; Skip HTTP headers
-      (re-search-forward "^$")
-      (forward-line)
-      (let ((json-data (json-read)))
-        (kill-buffer)
-        json-data))))
-
-(defun journelly--is-night-p ()
-  "Return t if current time is night (20:00-06:00)."
-  (let ((hour (string-to-number (format-time-string "%H"))))
-    (or (>= hour 20) (< hour 6))))
-
-;;; Location functions
-
-(defun journelly--map-weather-symbol (description &optional is-night)
-  "Map weather DESCRIPTION to iOS SF Symbol name.
-If IS-NIGHT is non-nil, return night-appropriate symbols."
-  (let ((desc (downcase description)))
-    (if is-night
-        ;; Night conditions
-        (cond
-         ((string-match-p "\\(clear\\|sunny\\)" desc) "moon.stars")
-         ((string-match-p "partly.*cloud" desc) "cloud.moon")
-         ((string-match-p "\\(rain\\|drizzle\\|shower\\)" desc) "cloud.moon.rain")
-         (t "cloud.moon"))
-      ;; Day conditions
-      (cond
-       ((string-match-p "\\(clear\\|sunny\\)" desc) "sun.max")
-       ((string-match-p "partly.*cloud" desc) "cloud.sun")
-       ((string-match-p "\\(cloudy\\|overcast\\)" desc) "cloud")
-       ((string-match-p "heavy.*rain" desc) "cloud.heavyrain")
-       ((string-match-p "\\(rain\\|shower\\)" desc) "cloud.rain")
-       ((string-match-p "\\(drizzle\\|light.*rain\\)" desc) "cloud.drizzle")
-       ((string-match-p "snow" desc) "cloud.snow")
-       ((string-match-p "sleet" desc) "cloud.sleet")
-       ((string-match-p "\\(fog\\|mist\\)" desc) "cloud.fog")
-       ((string-match-p "\\(haze\\|smoke\\)" desc) "smoke")
-       ((string-match-p "wind" desc) "wind")
-       ((string-match-p "\\(thunder\\|storm\\)" desc) "cloud.bolt")
-       (t "cloud")))))
-
-(defun journelly-get-location (&optional no-cache)
-  "Get current location via IP geolocation.
-Returns alist with city, latitude, and longitude.
-If NO-CACHE is non-nil, fetch fresh data ignoring cache."
-  (let ((cache-file (journelly--cache-file "location")))
-    (if (and (not no-cache)
-             (journelly--cache-valid-p cache-file journelly-location-cache-timeout))
-        ;; Return cached data
-        (journelly--read-cache cache-file)
-      ;; Fetch fresh data
-      (let* ((response (journelly--fetch-url "https://ipinfo.io/json"))
-             (city (cdr (assoc 'city response)))
-             (loc (cdr (assoc 'loc response)))
-             (coords (when loc (split-string loc ",")))
-             (lat (when coords (car coords)))
-             (lon (when coords (cadr coords)))
-             (data `((city . ,(or city "Unknown"))
-                    (lat . ,(or lat "0"))
-                    (lon . ,(or lon "0")))))
-        ;; Cache the result
-        (journelly--write-cache cache-file data)
-        data))))
-
-(defun journelly-get-weather (&optional location no-cache)
-  "Get current weather for LOCATION (city name or coordinates).
-If LOCATION is nil, uses current location via IP.
-Returns alist with temperature, condition, and symbol.
-If NO-CACHE is non-nil, fetch fresh data ignoring cache."
-  (let* ((loc (or location ""))
-         (cache-key (if (string-empty-p loc) "weather-auto" (format "weather-%s" loc)))
-         (cache-file (journelly--cache-file cache-key)))
-    (if (and (not no-cache)
-             (journelly--cache-valid-p cache-file journelly-weather-cache-timeout))
-        ;; Return cached data
-        (journelly--read-cache cache-file)
-      ;; Fetch fresh data
-      (let* ((url (if (string-empty-p loc)
-                     "https://wttr.in/?format=j1"
-                   (format "https://wttr.in/%s?format=j1" (url-hexify-string loc))))
-             (response (journelly--fetch-url url))
-             (current (aref (cdr (assoc 'current_condition response)) 0))
-             (temp-c (cdr (assoc 'temp_C current)))
-             (weather-desc-array (cdr (assoc 'weatherDesc current)))
-             (weather-desc (cdr (assoc 'value (aref weather-desc-array 0))))
-             (temperature (format "%s°C" temp-c))
-             (is-night (journelly--is-night-p))
-             (symbol (journelly--map-weather-symbol weather-desc is-night))
-             (data `((temperature . ,temperature)
-                    (condition . ,weather-desc)
-                    (symbol . ,symbol))))
-        ;; Cache the result
-        (journelly--write-cache cache-file data)
-        data))))
-
-;;; Batch mode functions
-
-(defun journelly-batch-get-location (&optional format no-cache)
-  "Batch mode: Get location and print to stdout.
-FORMAT can be: json (default), city, coords, lat, lon, or all.
-If NO-CACHE is non-nil, ignore cache."
-  (let* ((format-type (or format "json"))
-         (data (journelly-get-location no-cache))
-         (city (cdr (assoc 'city data)))
-         (lat (cdr (assoc 'lat data)))
-         (lon (cdr (assoc 'lon data))))
-    (cond
-     ((string= format-type "json")
-      (princ (json-encode data))
-      (terpri))
-     ((string= format-type "city")
-      (princ city)
-      (terpri))
-     ((string= format-type "coords")
-      (princ (format "%s,%s" lat lon))
-      (terpri))
-     ((string= format-type "lat")
-      (princ lat)
-      (terpri))
-     ((string= format-type "lon")
-      (princ lon)
-      (terpri))
-     ((string= format-type "all")
-      (princ (format "%s (%s,%s)" city lat lon))
-      (terpri))
-     (t
-      (error "Unknown format: %s" format-type)))))
-
-(defun journelly-batch-get-weather (&optional location format no-cache)
-  "Batch mode: Get weather and print to stdout.
-LOCATION is optional city name or coordinates.
-FORMAT can be: json (default), temperature, condition, symbol, or all.
-If NO-CACHE is non-nil, ignore cache."
-  (let* ((format-type (or format "json"))
-         (data (journelly-get-weather location no-cache))
-         (temperature (cdr (assoc 'temperature data)))
-         (condition (cdr (assoc 'condition data)))
-         (symbol (cdr (assoc 'symbol data))))
-    (cond
-     ((string= format-type "json")
-      (princ (json-encode data))
-      (terpri))
-     ((string= format-type "temperature")
-      (princ temperature)
-      (terpri))
-     ((string= format-type "condition")
-      (princ condition)
-      (terpri))
-     ((string= format-type "symbol")
-      (princ symbol)
-      (terpri))
-     ((string= format-type "all")
-      (princ (format "%s %s (%s)" temperature condition symbol))
-      (terpri))
-     (t
-      (error "Unknown format: %s" format-type)))))
-
-;;; Provide
-
-(provide 'journelly-location-weather)
-
-;;; journelly-location-weather.el ends here
dots/config/emacs/site-lisp/journelly-manager
@@ -1,326 +0,0 @@
-#!/usr/bin/env bash
-# journelly-manager - CLI tool for Journelly journal file manipulation via Emacs batch mode
-# Copyright (C) 2026 Vincent Demeester
-# Loads elisp from site-lisp for consistency with interactive Emacs
-
-set -euo pipefail
-
-# Configuration
-EMACS="${EMACS:-emacs}"
-EMACS_DIR="${EMACS_DIR:-$HOME/.config/emacs}"
-SITE_LISP="$EMACS_DIR/site-lisp"
-
-# Debug mode
-DEBUG="${DEBUG:-0}"
-
-# Colors for output (if not outputting JSON)
-if [[ -t 1 ]] && [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
-    RED='\033[0;31m'
-    GREEN='\033[0;32m'
-    YELLOW='\033[1;33m'
-    BLUE='\033[0;34m'
-    NC='\033[0m' # No Color
-else
-    RED=''
-    GREEN=''
-    YELLOW=''
-    BLUE=''
-    NC=''
-fi
-
-# Error handling
-error() {
-    echo -e "${RED}Error: $*${NC}" >&2
-    exit 1
-}
-
-debug() {
-    if [[ "$DEBUG" == "1" ]]; then
-        echo -e "${YELLOW}Debug: $*${NC}" >&2
-    fi
-}
-
-info() {
-    if [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
-        echo -e "${BLUE}$*${NC}" >&2
-    fi
-}
-
-success() {
-    if [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
-        echo -e "${GREEN}$*${NC}" >&2
-    fi
-}
-
-# Check dependencies
-check_deps() {
-    if ! command -v "$EMACS" &> /dev/null; then
-        error "Emacs not found. Set EMACS environment variable or install emacs."
-    fi
-
-    if [[ ! -d "$SITE_LISP" ]]; then
-        error "Emacs site-lisp directory not found at: $SITE_LISP"
-    fi
-
-    if [[ ! -f "$SITE_LISP/journelly-batch-functions.el" ]]; then
-        error "journelly-batch-functions.el not found in site-lisp"
-    fi
-}
-
-# Run Emacs batch command
-run_batch() {
-    local function_call="$1"
-    debug "Running: $EMACS --batch --directory \"$SITE_LISP\" --load journelly-batch-functions.el --eval \"$function_call\""
-
-    "$EMACS" --batch \
-        --directory "$SITE_LISP" \
-        --load journelly-batch-functions.el \
-        --eval "$function_call" 2>&1
-}
-
-# Usage information
-usage() {
-    cat <<EOF
-journelly-manager - CLI tool for managing Journelly journal files
-
-USAGE:
-    journelly-manager <command> [arguments]
-
-COMMANDS:
-    create FILE LOCATION CONTENT [options]
-        Create new journal entry
-
-        Options:
-            --latitude=LAT         GPS latitude
-            --longitude=LON        GPS longitude
-            --temperature=TEMP     Temperature (e.g., "15,2°C")
-            --condition=COND       Weather condition (e.g., "Cloudy")
-            --symbol=SYM           Weather symbol (e.g., "cloud")
-
-        Examples:
-            journelly-manager create ~/desktop/org/Journelly.org "Home" "Today was great"
-
-            journelly-manager create ~/desktop/org/Journelly.org "Kyushu" \\
-                "Work session notes" \\
-                --latitude=48.8534 --longitude=2.3488 \\
-                --temperature="15°C" --condition="Cloudy" --symbol="cloud"
-
-    append FILE DATE CONTENT
-        Append to existing journal entry by date (YYYY-MM-DD)
-
-        Examples:
-            journelly-manager append ~/desktop/org/Journelly.org \\
-                "2026-01-16" "Additional thoughts"
-
-    list FILE [--limit=N]
-        List recent journal entries
-
-        Options:
-            --limit=N    Number of entries to show (default: 10)
-
-        Examples:
-            journelly-manager list ~/desktop/org/Journelly.org
-            journelly-manager list ~/desktop/org/Journelly.org --limit=20
-
-    search FILE QUERY
-        Search journal entries for keyword
-
-        Examples:
-            journelly-manager search ~/desktop/org/Journelly.org "wireguard"
-
-    get FILE DATE
-        Get specific entry by date (YYYY-MM-DD)
-
-        Examples:
-            journelly-manager get ~/desktop/org/Journelly.org "2026-01-16"
-
-ENVIRONMENT:
-    EMACS           Emacs executable (default: emacs)
-    EMACS_DIR       Emacs config directory (default: ~/.config/emacs)
-    DEBUG           Set to 1 for debug output
-    JSON_OUTPUT     Set to 1 for JSON output (no colors)
-
-EXAMPLES:
-    # Create entry with auto location/weather (use get-location/get-weather)
-    LOC=\$(get-location --json)
-    WEATHER=\$(get-weather --json)
-    journelly-manager create ~/desktop/org/Journelly.org \\
-        "\$(echo \$LOC | jq -r .city)" "Entry content" \\
-        --latitude="\$(echo \$LOC | jq -r .lat)" \\
-        --longitude="\$(echo \$LOC | jq -r .lon)" \\
-        --temperature="\$(echo \$WEATHER | jq -r .temperature)" \\
-        --condition="\$(echo \$WEATHER | jq -r .condition)" \\
-        --symbol="\$(echo \$WEATHER | jq -r .symbol)"
-
-    # Append to today's entry
-    journelly-manager append ~/desktop/org/Journelly.org \\
-        "\$(date +%Y-%m-%d)" "More thoughts"
-
-    # Search entries
-    journelly-manager search ~/desktop/org/Journelly.org "claude"
-
-EOF
-    exit 0
-}
-
-# Parse create command
-cmd_create() {
-    local file="$1"
-    local location="$2"
-    local content="$3"
-    shift 3
-
-    local latitude="" longitude="" temperature="" condition="" symbol=""
-
-    # Parse options
-    while [[ $# -gt 0 ]]; do
-        case "$1" in
-            --latitude=*)
-                latitude="${1#*=}"
-                shift
-                ;;
-            --longitude=*)
-                longitude="${1#*=}"
-                shift
-                ;;
-            --temperature=*)
-                temperature="${1#*=}"
-                shift
-                ;;
-            --condition=*)
-                condition="${1#*=}"
-                shift
-                ;;
-            --symbol=*)
-                symbol="${1#*=}"
-                shift
-                ;;
-            *)
-                error "Unknown option: $1"
-                ;;
-        esac
-    done
-
-    # Build Emacs Lisp call
-    local elisp_call="(journelly-batch-create-entry \"$file\" \"$location\" \"$content\""
-
-    if [[ -n "$latitude" ]]; then elisp_call="$elisp_call :latitude \"$latitude\""; fi
-    if [[ -n "$longitude" ]]; then elisp_call="$elisp_call :longitude \"$longitude\""; fi
-    if [[ -n "$temperature" ]]; then elisp_call="$elisp_call :temperature \"$temperature\""; fi
-    if [[ -n "$condition" ]]; then elisp_call="$elisp_call :condition \"$condition\""; fi
-    if [[ -n "$symbol" ]]; then elisp_call="$elisp_call :symbol \"$symbol\""; fi
-
-    elisp_call="$elisp_call)"
-
-    run_batch "$elisp_call"
-    success "Journal entry created"
-}
-
-# Parse append command
-cmd_append() {
-    local file="$1"
-    local date="$2"
-    local content="$3"
-
-    local elisp_call="(journelly-batch-append-to-date \"$file\" \"$date\" \"$content\")"
-    run_batch "$elisp_call"
-    success "Content appended to entry"
-}
-
-# Parse list command
-cmd_list() {
-    local file="$1"
-    shift
-
-    local limit="10"
-
-    # Parse options
-    while [[ $# -gt 0 ]]; do
-        case "$1" in
-            --limit=*)
-                limit="${1#*=}"
-                shift
-                ;;
-            *)
-                error "Unknown option: $1"
-                ;;
-        esac
-    done
-
-    local elisp_call="(journelly-batch-list-entries \"$file\" $limit)"
-    run_batch "$elisp_call"
-}
-
-# Parse search command
-cmd_search() {
-    local file="$1"
-    local query="$2"
-
-    local elisp_call="(journelly-batch-search \"$file\" \"$query\")"
-    run_batch "$elisp_call"
-}
-
-# Parse get command
-cmd_get() {
-    local file="$1"
-    local date="$2"
-
-    local elisp_call="(journelly-batch-get-entry \"$file\" \"$date\")"
-    run_batch "$elisp_call"
-}
-
-# Main command dispatcher
-main() {
-    if [[ $# -eq 0 ]]; then
-        usage
-    fi
-
-    local command="$1"
-    shift
-
-    case "$command" in
-        -h|--help|help)
-            usage
-            ;;
-        create)
-            check_deps
-            if [[ $# -lt 3 ]]; then
-                error "create requires: FILE LOCATION CONTENT"
-            fi
-            cmd_create "$@"
-            ;;
-        append)
-            check_deps
-            if [[ $# -lt 3 ]]; then
-                error "append requires: FILE DATE CONTENT"
-            fi
-            cmd_append "$@"
-            ;;
-        list)
-            check_deps
-            if [[ $# -lt 1 ]]; then
-                error "list requires: FILE"
-            fi
-            cmd_list "$@"
-            ;;
-        search)
-            check_deps
-            if [[ $# -lt 2 ]]; then
-                error "search requires: FILE QUERY"
-            fi
-            cmd_search "$@"
-            ;;
-        get)
-            check_deps
-            if [[ $# -lt 2 ]]; then
-                error "get requires: FILE DATE"
-            fi
-            cmd_get "$@"
-            ;;
-        *)
-            error "Unknown command: $command (try --help)"
-            ;;
-    esac
-}
-
-main "$@"
dots/config/emacs/site-lisp/journelly.el
@@ -1,459 +0,0 @@
-;;; journelly.el --- Smart Journelly capture with location/weather -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2026 Vincent Demeester
-
-;; Author: Vincent Demeester <vincent@demeester.fr>
-;; Keywords: org-mode, journelly, journal, capture
-;; Version: 1.0.0
-
-;;; Commentary:
-
-;; Smart capture system for Journelly.org journal entries.
-;;
-;; Features:
-;; - Create-or-append behavior: first entry creates, subsequent append with timestamps
-;; - Automatic location and weather via IP geolocation
-;; - Separate entries for regular journal and Claude sessions
-;; - Full org-capture integration
-;;
-;; Entry formats:
-;; - Regular: * [YYYY-MM-DD Day HH:MM] @ Location (hostname)
-;;            - HH:MM :: entry content #tags (appended entries, tags optional)
-;; - Claude:  * [YYYY-MM-DD Day HH:MM] @ Claude session
-;;            - HH:MM :: session summary #auto-tagged (automated only)
-;;
-;; Usage:
-;;   (require 'journelly)
-;;   ;; Use org-capture: C-c o c then 'j' for journal
-;;   ;; Or quick functions: M-x journelly-quick-entry
-;;   ;; Claude sessions: programmatic only via journelly-claude-session
-
-;;; Code:
-
-(require 'org)
-(require 'org-capture)
-
-;; Load location/weather helpers from site-lisp
-(require 'journelly-location-weather)
-
-;; Declare functions from journelly-location-weather.el
-(declare-function journelly-get-location "journelly-location-weather")
-(declare-function journelly-get-weather "journelly-location-weather")
-
-;;; Helper Functions
-
-(defun journelly--find-todays-entry ()
-  "Find today's journal entry in Journelly.org.
-Returns the position of the entry if found, nil otherwise."
-  (let ((today (format-time-string "%Y-%m-%d")))
-    (save-excursion
-      (goto-char (point-min))
-      ;; Skip the file header
-      (when (re-search-forward "^:END:" nil t)
-        (forward-line))
-      ;; Search for today's regular entry (not Claude session)
-      (when (re-search-forward
-             (format "^\\* \\[%s[^]]+\\] @ \\([^C]\\|C[^l]\\)" today) nil t)
-        (line-beginning-position)))))
-
-(defun journelly--find-todays-claude-entry ()
-  "Find today's Claude session entry in Journelly.org.
-Returns the position of the entry if found, nil otherwise."
-  (let ((today (format-time-string "%Y-%m-%d")))
-    (save-excursion
-      (goto-char (point-min))
-      ;; Skip the file header
-      (when (re-search-forward "^:END:" nil t)
-        (forward-line))
-      ;; Search for today's Claude session entry
-      (when (re-search-forward
-             (format "^\\* \\[%s.*@ Claude session" today) nil t)
-        (line-beginning-position)))))
-
-(defun journelly--goto-insert-position ()
-  "Navigate to the correct insert position for new journal entries.
-Goes after the file header but before existing entries."
-  (goto-char (point-min))
-  (if (re-search-forward "^:END:" nil t)
-      (progn
-        (forward-line)
-        (point))
-    ;; No header found, go to beginning
-    (goto-char (point-min))
-    (point)))
-
-(defun journelly--create-entry-heading (&optional custom-location)
-  "Create journal entry heading with timestamp, location, and hostname.
-Format: * [YYYY-MM-DD Day HH:MM] @ Location (hostname)
-If CUSTOM-LOCATION is provided, uses it instead of IP geolocation.
-Returns the heading string."
-  (let* ((location-data (when (not custom-location) (journelly-get-location)))
-         (city (or custom-location (cdr (assoc 'city location-data))))
-         (hostname (system-name))
-         (timestamp (format-time-string "[%Y-%m-%d %a %H:%M]")))
-    (if (string= custom-location "Claude session")
-        ;; Claude session - no location
-        (format "* %s @ Claude session" timestamp)
-      ;; Regular entry - location (hostname)
-      (format "* %s @ %s (%s)" timestamp city hostname))))
-
-(defun journelly--create-entry-properties (&optional skip-weather)
-  "Create properties drawer with location and weather metadata.
-If SKIP-WEATHER is non-nil, only includes location data.
-Returns the properties string."
-  (let* ((location-data (journelly-get-location))
-         (lat (cdr (assoc 'lat location-data)))
-         (lon (cdr (assoc 'lon location-data)))
-         (weather-data (unless skip-weather (journelly-get-weather)))
-         (temp (when weather-data (cdr (assoc 'temperature weather-data))))
-         (condition (when weather-data (cdr (assoc 'condition weather-data))))
-         (symbol (when weather-data (cdr (assoc 'symbol weather-data)))))
-    (concat ":PROPERTIES:\n"
-            (format ":LATITUDE: %s\n" lat)
-            (format ":LONGITUDE: %s\n" lon)
-            (when temp (format ":WEATHER_TEMPERATURE: %s\n" temp))
-            (when condition (format ":WEATHER_CONDITION: %s\n" condition))
-            (when symbol (format ":WEATHER_SYMBOL: %s\n" symbol))
-            ":END:")))
-
-;;; Capture Target Functions
-
-(defun journelly-capture-target ()
-  "Org capture target function for smart Journelly entries.
-Creates new entry if today's doesn't exist, or appends to existing."
-  (let ((entry-pos (journelly--find-todays-entry)))
-    (if entry-pos
-        ;; Entry exists - go to end to append
-        (progn
-          (goto-char entry-pos)
-          (org-end-of-subtree t)
-          ;; Skip back over any trailing blank lines
-          (while (and (not (bobp))
-                      (looking-back "^[ \t]*\n" (line-beginning-position 0)))
-            (forward-line -1))
-          ;; Now at the last non-blank line of content
-          (end-of-line)
-          (insert "\n")
-          (point))
-      ;; Entry doesn't exist - create new one
-      (journelly--goto-insert-position)
-      (insert (journelly--create-entry-heading) "\n")
-      (insert (journelly--create-entry-properties) "\n")
-      ;; Leave point on blank line inside the new entry, not at the
-      ;; start of the next heading.  org-capture checks org-at-heading-p
-      ;; to set :target-entry-p; if point lands on the old first entry's
-      ;; heading, the template text gets appended to that entry instead.
-      (open-line 1)
-      (point))))
-
-(defun journelly-claude-capture-target ()
-  "Org capture target function for Claude session entries.
-Creates new entry if today's Claude session doesn't exist, or appends."
-  (let ((entry-pos (journelly--find-todays-claude-entry)))
-    (if entry-pos
-        ;; Entry exists - go to end to append
-        (progn
-          (goto-char entry-pos)
-          (org-end-of-subtree t)
-          ;; Skip back over any trailing blank lines
-          (while (and (not (bobp))
-                      (looking-back "^[ \t]*\n" (line-beginning-position 0)))
-            (forward-line -1))
-          ;; Now at the last non-blank line of content
-          (end-of-line)
-          (insert "\n")
-          (point))
-      ;; Entry doesn't exist - create new one
-      (journelly--goto-insert-position)
-      (insert (journelly--create-entry-heading "Claude session") "\n")
-      (insert (journelly--create-entry-properties t) "\n")  ;; Skip weather for Claude
-      ;; Leave point on blank line inside the new entry (see
-      ;; journelly-capture-target for detailed explanation).
-      (open-line 1)
-      (point))))
-
-;;; Interactive Functions
-
-(defun journelly-quick-entry (content)
-  "Quick journal entry with CONTENT.
-Location/weather added automatically.
-Creates today's entry if it doesn't exist, or appends to existing entry."
-  (interactive "sJournal: ")
-  (with-current-buffer (find-file-noselect org-journelly-file)
-    (save-excursion
-      (journelly-capture-target)
-      (insert content))
-    (save-buffer))
-  (message "Journal entry added"))
-
-(defun journelly--detect-tags (summary)
-  "Auto-detect tags for Claude session based on context and SUMMARY.
-Returns a list of tag strings (without # prefix)."
-  (let ((tags '()))
-
-    ;; Detect from file extensions (git status)
-    (when (file-exists-p ".git")
-      (let ((changed-files (shell-command-to-string "git status --short 2>/dev/null")))
-        (when (string-match-p "\\.nix" changed-files)
-          (push "nixos" tags))
-        (when (string-match-p "\\.go" changed-files)
-          (push "golang" tags))
-        (when (string-match-p "\\.el" changed-files)
-          (push "emacs" tags))
-        (when (string-match-p "\\.py" changed-files)
-          (push "python" tags))
-        (when (string-match-p "\\.rs" changed-files)
-          (push "rust" tags))
-        (when (string-match-p "Dockerfile\\|docker-compose" changed-files)
-          (push "docker" tags))
-        (when (string-match-p "\\.ya?ml" changed-files)
-          (push "kubernetes" tags))
-        (when (string-match-p "skills/" changed-files)
-          (push "claude-skills" tags))))
-
-    ;; Detect from git repository name
-    (when (file-exists-p ".git")
-      (let* ((remote-url (shell-command-to-string "git remote get-url origin 2>/dev/null"))
-             (repo-name (when (string-match "/\\([^/]+\\)\\.git" remote-url)
-                         (match-string 1 remote-url))))
-        (cond
-         ((string= repo-name "home") (push "homelab" tags))
-         ((string-match-p "tekton" repo-name) (push "tekton" tags))
-         ((string-match-p "pipeline" repo-name) (push "tekton" tags)))))
-
-    ;; Detect from keywords in summary
-    (let ((summary-lower (downcase summary)))
-      ;; Development activities
-      (when (string-match-p "\\(bug\\|fix\\|debug\\)" summary-lower)
-        (push "debugging" tags))
-      (when (string-match-p "\\(feature\\|implement\\)" summary-lower)
-        (push "development" tags))
-      (when (string-match-p "refactor" summary-lower)
-        (push "refactoring" tags))
-      (when (string-match-p "\\(test\\|testing\\)" summary-lower)
-        (push "testing" tags))
-      (when (string-match-p "\\(doc\\|documentation\\)" summary-lower)
-        (push "documentation" tags))
-
-      ;; Infrastructure & tools
-      (when (string-match-p "\\(kubernetes\\|k8s\\)" summary-lower)
-        (push "kubernetes" tags))
-      (when (string-match-p "docker" summary-lower)
-        (push "docker" tags))
-      (when (string-match-p "\\(commit\\|push\\|git\\)" summary-lower)
-        (push "git" tags))
-      (when (string-match-p "\\(capture\\|journal\\)" summary-lower)
-        (push "journelly" tags))
-
-      ;; AI/LLM
-      (when (string-match-p "\\(llm\\|language model\\)" summary-lower)
-        (push "llm" tags))
-      (when (string-match-p "\\(ai\\|artificial intelligence\\)" summary-lower)
-        (push "ai" tags))
-      (when (string-match-p "claude" summary-lower)
-        (push "claude" tags))
-      (when (string-match-p "anthropic" summary-lower)
-        (push "anthropic" tags))
-      (when (string-match-p "gemini" summary-lower)
-        (push "gemini" tags))
-      (when (string-match-p "ollama" summary-lower)
-        (push "ollama" tags))
-      (when (string-match-p "\\(openai\\|chatgpt\\|gpt\\)" summary-lower)
-        (push "openai" tags))
-
-      ;; Cloud providers
-      (when (string-match-p "\\(cloud\\|infrastructure\\)" summary-lower)
-        (push "cloud" tags))
-      (when (string-match-p "\\(digitalocean\\|\\bdo\\b\\)" summary-lower)
-        (push "digitalocean" tags))
-      (when (string-match-p "\\(gcp\\|google cloud\\)" summary-lower)
-        (push "gcp" tags))
-      (when (string-match-p "oracle.*cloud" summary-lower)
-        (push "oracle-cloud" tags))
-      (when (string-match-p "\\(aws\\|amazon web services\\)" summary-lower)
-        (push "aws" tags))
-      (when (string-match-p "azure" summary-lower)
-        (push "azure" tags))
-
-      ;; Architecture
-      (when (string-match-p "\\(arm\\|aarch64\\)" summary-lower)
-        (push "arm" tags))
-      (when (string-match-p "x86.64" summary-lower)
-        (push "x86_64" tags))
-      (when (string-match-p "\\(cross.compile\\|cross compile\\)" summary-lower)
-        (push "cross-compile" tags))
-      (when (string-match-p "riscv" summary-lower)
-        (push "riscv" tags))
-
-      ;; Monitoring/Observability
-      (when (string-match-p "\\(monitoring\\|observability\\)" summary-lower)
-        (push "monitoring" tags))
-      (when (string-match-p "prometheus" summary-lower)
-        (push "prometheus" tags))
-      (when (string-match-p "grafana" summary-lower)
-        (push "grafana" tags))
-      (when (string-match-p "\\(alert\\|alerting\\)" summary-lower)
-        (push "alerting" tags))
-
-      ;; Media
-      (when (string-match-p "media" summary-lower)
-        (push "media" tags))
-      (when (string-match-p "jellyfin" summary-lower)
-        (push "jellyfin" tags))
-      (when (string-match-p "plex" summary-lower)
-        (push "plex" tags))
-
-      ;; Desktop/Window managers
-      (when (string-match-p "niri" summary-lower)
-        (push "niri" tags))
-      (when (string-match-p "sway" summary-lower)
-        (push "sway" tags))
-      (when (string-match-p "wayland" summary-lower)
-        (push "wayland" tags))
-      (when (string-match-p "x11" summary-lower)
-        (push "x11" tags))
-
-      ;; Networking
-      (when (string-match-p "\\(network\\|networking\\)" summary-lower)
-        (push "networking" tags))
-      (when (string-match-p "wireguard" summary-lower)
-        (push "wireguard" tags))
-      (when (string-match-p "\\(vpn\\|virtual private network\\)" summary-lower)
-        (push "vpn" tags))
-      (when (string-match-p "\\(dns\\|domain name\\)" summary-lower)
-        (push "dns" tags))
-
-      ;; Security
-      (when (string-match-p "security" summary-lower)
-        (push "security" tags))
-      (when (string-match-p "\\(auth\\|authentication\\)" summary-lower)
-        (push "auth" tags))
-      (when (string-match-p "\\(encryption\\|encrypt\\)" summary-lower)
-        (push "encryption" tags))
-      (when (string-match-p "\\(secret\\|agenix\\)" summary-lower)
-        (push "secrets" tags))
-      (when (string-match-p "yubikey" summary-lower)
-        (push "yubikey" tags))
-
-      ;; Backup/Storage
-      (when (string-match-p "backup" summary-lower)
-        (push "backup" tags))
-      (when (string-match-p "storage" summary-lower)
-        (push "storage" tags))
-      (when (string-match-p "syncthing" summary-lower)
-        (push "syncthing" tags))
-      (when (string-match-p "restic" summary-lower)
-        (push "restic" tags))
-
-      ;; Communication
-      (when (string-match-p "\\(email\\|mail\\)" summary-lower)
-        (push "email" tags))
-      (when (string-match-p "\\(mu4e\\|notmuch\\)" summary-lower)
-        (push "email" tags))
-      (when (string-match-p "xmpp" summary-lower)
-        (push "xmpp" tags))
-
-      ;; Databases
-      (when (string-match-p "\\(database\\|\\bdb\\b\\)" summary-lower)
-        (push "database" tags))
-      (when (string-match-p "\\(postgres\\|postgresql\\)" summary-lower)
-        (push "postgres" tags))
-      (when (string-match-p "sqlite" summary-lower)
-        (push "sqlite" tags))
-
-      ;; Web/HTTP
-      (when (string-match-p "\\(web\\|http\\|https\\)" summary-lower)
-        (push "web" tags))
-      (when (string-match-p "nginx" summary-lower)
-        (push "nginx" tags))
-      (when (string-match-p "caddy" summary-lower)
-        (push "caddy" tags))
-
-      ;; Hardware
-      (when (string-match-p "hardware" summary-lower)
-        (push "hardware" tags))
-      (when (string-match-p "\\(raspberry.pi\\|rpi\\)" summary-lower)
-        (push "raspberry-pi" tags))
-      (when (string-match-p "keyboard" summary-lower)
-        (push "keyboard" tags))
-
-      ;; Configuration
-      (when (string-match-p "home.manager" summary-lower)
-        (push "home-manager" tags))
-      (when (string-match-p "flake" summary-lower)
-        (push "flakes" tags))
-      (when (string-match-p "\\(deploy\\|deployment\\)" summary-lower)
-        (push "deployment" tags))
-      (when (string-match-p "\\(ci\\|cd\\|pipeline\\)" summary-lower)
-        (push "ci-cd" tags)))
-
-    ;; Remove duplicates and return
-    (delete-dups tags)))
-
-(defun journelly-claude-session (summary)
-  "Add Claude session SUMMARY to today's Claude session entry.
-Auto-detects and appends tags based on context and content.
-Creates entry if it doesn't exist, or appends to existing entry."
-  (interactive "sClaude session: ")
-  (let* ((timestamp (format-time-string "%H:%M"))
-         (tags (journelly--detect-tags summary))
-         (tags-string (if tags
-                          (concat " " (mapconcat (lambda (tag) (concat "#" tag)) tags " "))
-                        ""))
-         (entry (format "- %s :: %s%s\n" timestamp summary tags-string)))
-    (with-current-buffer (find-file-noselect org-journelly-file)
-      (save-excursion
-        (journelly-claude-capture-target)
-        (insert entry))
-      (save-buffer))
-    (message "Claude session logged%s" (if tags (format " with tags: %s" tags-string) ""))))
-
-(defun journelly-open ()
-  "Open Journelly.org file and jump to today's entry or top."
-  (interactive)
-  (find-file org-journelly-file)
-  (let ((entry-pos (journelly--find-todays-entry)))
-    (if entry-pos
-        (goto-char entry-pos)
-      ;; No entry today, go to insert position
-      (journelly--goto-insert-position)))
-  (recenter-top-bottom 0))
-
-;;; Capture Templates Setup
-
-(defun journelly-setup-capture-templates ()
-  "Setup org-capture templates for Journelly.
-Call this after org-capture is loaded and org-journelly-file is defined."
-
-  ;; Remove old journelly templates if they exist
-  (setq org-capture-templates
-        (seq-remove (lambda (x) (member (car x) '("j" "J")))
-                    org-capture-templates))
-
-  ;; Smart default journal entry (creates or appends with timestamp)
-  (add-to-list 'org-capture-templates
-               `("j" "📝 Journal entry" plain
-                 (file+function ,org-journelly-file journelly-capture-target)
-                 "- %(format-time-string \"%H:%M\") :: %?"
-                 :empty-lines 0)
-               t))
-
-;;; Keybindings
-
-(defun journelly-setup-keybindings ()
-  "Setup keybindings for Journelly functions."
-  (global-set-key (kbd "C-c j j") 'journelly-quick-entry)
-  (global-set-key (kbd "C-c j o") 'journelly-open))
-
-;;; Auto-setup
-
-;; Setup keybindings when loaded
-(journelly-setup-keybindings)
-
-;; Setup capture templates after org-capture is loaded
-(with-eval-after-load 'org-capture
-  (journelly-setup-capture-templates))
-
-(provide 'journelly)
-
-;;; journelly.el ends here
dots/config/emacs/site-lisp/org-kanban.el
@@ -1,894 +0,0 @@
-;;; org-kanban.el --- Kanban board view for org-mode TODOs -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2026 Vincent Demeester
-
-;; Author: Vincent Demeester <vincent@sbr.pm>
-;; Keywords: org, kanban, productivity
-;; Version: 0.1.0
-;; Package-Requires: ((emacs "29.1") (org-ql "0.8"))
-
-;;; Commentary:
-
-;; Interactive kanban board that renders org-mode TODOs as cards in
-;; columns based on their TODO state.  Cards can be moved between
-;; columns (changing their state) with simple keybindings.
-;;
-;; Uses org-ql for querying and org-mode API for state changes.
-;; Data source is your existing todos.org file.
-;;
-;; Keybindings in org-kanban buffers:
-;;   j / n / ↓ - next card down in column
-;;   k / p / ↑ - previous card up in column
-;;   l / →     - next column
-;;   h / ←     - previous column
-;;   > / L     - move card to next state
-;;   < / H     - move card to previous state
-;;   RET       - jump to heading in org file
-;;   v / SPC   - preview card in side window (toggle)
-;;   f         - filter by section
-;;   F         - clear filters
-;;   D         - toggle DONE/CANX visibility
-;;   g         - refresh board
-;;   q         - quit
-;;   TAB       - cycle card detail
-;;
-;; Interactive commands:
-;;   M-x org-kanban       - open kanban board
-;;   M-x org-kanban-work  - board filtered to Work section
-
-;;; Code:
-
-(require 'org)
-(require 'org-ql)
-(require 'cl-lib)
-
-;;; Customization
-
-(defgroup org-kanban nil
-  "Kanban board view for org-mode TODOs."
-  :group 'org
-  :prefix "org-kanban-")
-
-(defcustom org-kanban-file (expand-file-name "~/desktop/org/todos.org")
-  "Path to the org file to display as a kanban board."
-  :type 'file
-  :group 'org-kanban)
-
-(defcustom org-kanban-columns '("TODO" "NEXT" "STRT" "WAIT")
-  "TODO states to display as columns (left to right).
-DONE and CANX are toggled with `D'."
-  :type '(repeat string)
-  :group 'org-kanban)
-
-(defcustom org-kanban-done-columns '("DONE" "CANX")
-  "Completed states, hidden by default.  Toggle with `D'."
-  :type '(repeat string)
-  :group 'org-kanban)
-
-(defcustom org-kanban-state-order '("TODO" "NEXT" "STRT" "WAIT" "DONE" "CANX")
-  "Order of states for moving cards left/right."
-  :type '(repeat string)
-  :group 'org-kanban)
-
-(defcustom org-kanban-column-width 30
-  "Width of each column in characters."
-  :type 'integer
-  :group 'org-kanban)
-
-(defcustom org-kanban-max-cards nil
-  "Maximum number of cards per column.  Nil means no limit."
-  :type '(choice (const :tag "No limit" nil) integer)
-  :group 'org-kanban)
-
-(defcustom org-kanban-level 2
-  "Org heading level to display as cards.
-2 means direct children of top-level sections."
-  :type 'integer
-  :group 'org-kanban)
-
-(defcustom org-kanban-open-in-tab t
-  "If non-nil, RET opens the heading in a new tab.
-If nil, opens in a new window via `pop-to-buffer'."
-  :type 'boolean
-  :group 'org-kanban)
-
-(defcustom org-kanban-sort 'priority
-  "Default sort order for cards within each column."
-  :type '(choice (const :tag "Priority (highest first)" priority)
-                 (const :tag "Scheduled date (earliest first)" scheduled)
-                 (const :tag "Deadline (earliest first)" deadline)
-                 (const :tag "Alphabetical" alpha)
-                 (const :tag "No sorting (file order)" none))
-  :group 'org-kanban)
-
-;;; Faces
-
-(defface org-kanban-column-header
-  '((t :inherit fixed-pitch :weight bold :underline t))
-  "Face for column headers."
-  :group 'org-kanban)
-
-(defface org-kanban-card
-  '((t :inherit default))
-  "Face for card text."
-  :group 'org-kanban)
-
-(defface org-kanban-card-selected
-  '((((class color) (background dark))
-     :background "#44475a" :extend t)
-    (((class color) (background light))
-     :background "#dde4ff" :extend t)
-    (t :inverse-video t :extend t))
-  "Face for the currently selected card."
-  :group 'org-kanban)
-
-(defface org-kanban-indicator
-  '((((background dark))
-     :foreground "#ff79c6" :weight bold)
-    (((background light))
-     :foreground "#a626a4" :weight bold))
-  "Face for the active card indicator."
-  :group 'org-kanban)
-
-(defface org-kanban-priority-1
-  '((t :foreground "#ff5555" :weight bold))
-  "Face for priority 1 (highest)."
-  :group 'org-kanban)
-
-(defface org-kanban-priority-2
-  '((t :foreground "#ffb86c" :weight bold))
-  "Face for priority 2."
-  :group 'org-kanban)
-
-(defface org-kanban-priority-3
-  '((t :foreground "#f1fa8c"))
-  "Face for priority 3."
-  :group 'org-kanban)
-
-(defface org-kanban-state-todo
-  '((t :foreground "#8be9fd"))
-  "Face for TODO state."
-  :group 'org-kanban)
-
-(defface org-kanban-state-next
-  '((t :foreground "#50fa7b" :weight bold))
-  "Face for NEXT state."
-  :group 'org-kanban)
-
-(defface org-kanban-state-strt
-  '((t :foreground "#ff79c6" :weight bold))
-  "Face for STRT state."
-  :group 'org-kanban)
-
-(defface org-kanban-state-wait
-  '((t :foreground "#ffb86c"))
-  "Face for WAIT state."
-  :group 'org-kanban)
-
-(defface org-kanban-state-done
-  '((t :foreground "#6272a4"))
-  "Face for DONE state."
-  :group 'org-kanban)
-
-(defface org-kanban-state-canx
-  '((t :foreground "#6272a4" :strike-through t))
-  "Face for CANX state."
-  :group 'org-kanban)
-
-(defface org-kanban-tag
-  '((t :inherit org-tag))
-  "Face for tags on cards."
-  :group 'org-kanban)
-
-(defface org-kanban-date
-  '((t :inherit org-date))
-  "Face for dates on cards."
-  :group 'org-kanban)
-
-(defface org-kanban-section-filter
-  '((t :inherit font-lock-keyword-face))
-  "Face for the active section filter indicator."
-  :group 'org-kanban)
-
-;;; Internal variables
-
-(defvar-local org-kanban--cards nil
-  "Alist of (STATE . cards) for current board.")
-
-(defvar-local org-kanban--show-done nil
-  "Whether to show DONE/CANX columns.")
-
-(defvar-local org-kanban--section-filter nil
-  "Current section filter, or nil for all.")
-
-(defvar-local org-kanban--selected-card nil
-  "Plist of the currently selected card (:heading :state :col :row).")
-
-(defvar-local org-kanban--card-positions nil
-  "Hash table mapping (col . row) to card plist.")
-
-(defvar-local org-kanban--selected-col nil
-  "Column index of the currently selected card.")
-
-(defvar-local org-kanban--selected-row nil
-  "Row index of the currently selected card.")
-
-(defvar-local org-kanban--sort nil
-  "Current sort order. Nil means use `org-kanban-sort' default.")
-
-;;; Data fetching
-
-(defun org-kanban--fetch-cards ()
-  "Fetch all TODO items from `org-kanban-file' using org-ql.
-Returns an alist of (STATE . list-of-card-plists)."
-  (let ((all-states (append org-kanban-columns
-                            (when org-kanban--show-done
-                              org-kanban-done-columns)))
-        (results '()))
-    (dolist (state all-states)
-      (let ((cards (org-ql-select org-kanban-file
-                     `(and (todo ,state)
-                           (level ,org-kanban-level)
-                           ,@(when org-kanban--section-filter
-                               `((ancestors
-                                  (and (level 1)
-                                       (heading ,org-kanban--section-filter))))))
-                     :action (lambda ()
-                               (let* ((element (org-element-at-point))
-                                      (priority-raw (org-element-property :priority element))
-                                      (priority-num (when priority-raw
-                                                      (if (< priority-raw 10)
-                                                          priority-raw       ;; already a number (1-5)
-                                                        (- priority-raw 48)))) ;; ASCII char (?1=49 -> 1)
-                                      (tags (mapcar #'substring-no-properties (org-get-tags nil t)))
-                                      (scheduled (org-entry-get nil "SCHEDULED"))
-                                      (deadline (org-entry-get nil "DEADLINE"))
-                                      (section (save-excursion
-                                                 (while (> (org-current-level) 1)
-                                                   (org-up-heading-safe))
-                                                 (substring-no-properties (org-get-heading t t t t)))))
-                                 (list :heading (org-kanban--render-links
-                                                  (substring-no-properties (org-get-heading t t t t)))
-                                       :state state
-                                       :priority priority-num
-                                       :tags tags
-                                       :scheduled scheduled
-                                       :deadline deadline
-                                       :section section
-                                       :marker (point-marker)))))))
-        (push (cons state (org-kanban--sort-cards cards)) results)))
-    (nreverse results)))
-
-(defun org-kanban--sort-cards (cards)
-  "Sort CARDS according to the current sort order."
-  (let ((order (or org-kanban--sort org-kanban-sort)))
-    (pcase order
-      ('none cards)
-      ('priority
-       (sort cards
-             (lambda (a b)
-               (let ((pa (or (plist-get a :priority) 99))
-                     (pb (or (plist-get b :priority) 99)))
-                 (< pa pb)))))
-      ('scheduled
-       (sort cards
-             (lambda (a b)
-               (let ((sa (or (plist-get a :scheduled) "9999"))
-                     (sb (or (plist-get b :scheduled) "9999")))
-                 (string< sa sb)))))
-      ('deadline
-       (sort cards
-             (lambda (a b)
-               (let ((da (or (plist-get a :deadline) "9999"))
-                     (db (or (plist-get b :deadline) "9999")))
-                 (string< da db)))))
-      ('alpha
-       (sort cards
-             (lambda (a b)
-               (string< (plist-get a :heading) (plist-get b :heading)))))
-      (_ cards))))
-
-;;; Text processing
-
-(defun org-kanban--render-links (str)
-  "Replace org links in STR with their description.
-\=[[url][desc]] becomes desc, [[url]] becomes url."
-  (let ((result str))
-    ;; [[target][description]] → description
-    (setq result (replace-regexp-in-string
-                  "\\[\\[\\([^]]*\\)\\]\\[\\([^]]*\\)\\]\\]"
-                  "\\2" result))
-    ;; [[target]] → target
-    (setq result (replace-regexp-in-string
-                  "\\[\\[\\([^]]*\\)\\]\\]"
-                  "\\1" result))
-    result))
-
-;;; Rendering
-
-(defun org-kanban--truncate (str width)
-  "Truncate STR to WIDTH display columns, adding ellipsis if needed."
-  (if (> (string-width str) width)
-      (concat (truncate-string-to-width str (- width 1)) "…")
-    str))
-
-(defun org-kanban--pad (str width)
-  "Pad STR to WIDTH display columns with spaces."
-  (let ((truncated (org-kanban--truncate str width)))
-    (concat truncated (make-string (max 0 (- width (string-width truncated))) ?\s))))
-
-(defun org-kanban--state-face (state)
-  "Return the face for STATE."
-  (pcase state
-    ("TODO" 'org-kanban-state-todo)
-    ("NEXT" 'org-kanban-state-next)
-    ("STRT" 'org-kanban-state-strt)
-    ("WAIT" 'org-kanban-state-wait)
-    ("DONE" 'org-kanban-state-done)
-    ("CANX" 'org-kanban-state-canx)
-    (_ 'default)))
-
-(defun org-kanban--priority-face (priority)
-  "Return the face for PRIORITY number."
-  (pcase priority
-    (1 'org-kanban-priority-1)
-    (2 'org-kanban-priority-2)
-    (3 'org-kanban-priority-3)
-    (_ nil)))
-
-(defun org-kanban--format-card-line1 (card width &optional selected)
-  "Format first line of CARD (heading) to fit WIDTH.
-If SELECTED is non-nil, prepend a visible indicator."
-  (let* ((indicator (if selected
-                       (propertize "▶ " 'face 'org-kanban-indicator)
-                     "  "))
-         (inner-width (- width 2))  ;; reserve 2 chars for indicator
-         (heading (plist-get card :heading))
-         (priority (plist-get card :priority))
-         (prefix (if priority (format "[#%d] " priority) ""))
-         (avail (- inner-width (string-width prefix)))
-         (truncated (org-kanban--truncate heading avail))
-         (line (concat prefix truncated))
-         (result (org-kanban--pad line inner-width)))
-    ;; Apply priority face to prefix
-    (when (and priority (org-kanban--priority-face priority))
-      (put-text-property 0 (min (length prefix) (length result)) 'face
-                         (org-kanban--priority-face priority) result))
-    (concat indicator result)))
-
-(defun org-kanban--format-card-line2 (card width &optional selected)
-  "Format second line of CARD (metadata) to fit WIDTH.
-If SELECTED is non-nil, prepend a continuation indicator."
-  (let* ((tags (plist-get card :tags))
-         (scheduled (plist-get card :scheduled))
-         (deadline (plist-get card :deadline))
-         (section (plist-get card :section))
-         (parts '()))
-    ;; Add date info
-    (when deadline
-      (push (propertize (format "⚑%s" (org-kanban--short-date deadline))
-                        'face 'org-kanban-date)
-            parts))
-    (when scheduled
-      (push (propertize (format "▸%s" (org-kanban--short-date scheduled))
-                        'face 'org-kanban-date)
-            parts))
-    ;; Add section
-    (when (and section (not org-kanban--section-filter))
-      (push (propertize (org-kanban--truncate section 10)
-                        'face 'org-kanban-section-filter)
-            parts))
-    ;; Add first tag
-    (when tags
-      (push (propertize (format ":%s:" (car tags))
-                        'face 'org-kanban-tag)
-            parts))
-    (let* ((indicator (if selected
-                         (propertize "│ " 'face 'org-kanban-indicator)
-                       "  "))
-           (inner-width (- width 2))
-           (line (string-join (nreverse parts) " ")))
-      (concat indicator (org-kanban--pad line inner-width)))))
-
-(defun org-kanban--short-date (date-str)
-  "Extract short date from DATE-STR like '<2026-04-07 Tue>'."
-  (if (and date-str (string-match "\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" date-str))
-      (let ((date (match-string 1 date-str)))
-        (format "%s/%s" (substring date 5 7) (substring date 8 10)))
-    ""))
-
-(defun org-kanban--render ()
-  "Render the kanban board into the current buffer."
-  (let* ((inhibit-read-only t)
-         (columns (append org-kanban-columns
-                          (when org-kanban--show-done
-                            org-kanban-done-columns)))
-         (col-width org-kanban-column-width)
-         (separator "│")
-         (card-positions (make-hash-table :test 'equal))
-         ;; Calculate max rows
-         (max-rows (apply #'max 0
-                          (mapcar (lambda (state)
-                                    (length (alist-get state org-kanban--cards
-                                                       nil nil #'string=)))
-                                  columns))))
-    (erase-buffer)
-
-    ;; Title line
-    (insert (propertize "  Org Kanban" 'face '(:inherit fixed-pitch :weight bold :height 1.3)))
-    (when org-kanban--section-filter
-      (insert "  "
-              (propertize (format "⟨%s⟩" org-kanban--section-filter)
-                          'face 'org-kanban-section-filter)))
-    ;; Show current sort
-    (let ((sort-name (or org-kanban--sort org-kanban-sort)))
-      (unless (eq sort-name 'none)
-        (insert "  "
-                (propertize (format "↕%s" sort-name) 'face 'shadow))))
-    (unless org-kanban--show-done
-      (insert "  "
-              (propertize "(D to show done)" 'face 'shadow)))
-    (insert "\n\n")
-
-    ;; Column headers
-    (insert "  ")
-    (dotimes (col-idx (length columns))
-      (let* ((state (nth col-idx columns))
-             (count (length (alist-get state org-kanban--cards nil nil #'string=)))
-             (header (format "%s (%d)" state count)))
-        (insert (propertize (org-kanban--pad header col-width)
-                            'face (list 'org-kanban-column-header
-                                        (org-kanban--state-face state))))
-        (when (< col-idx (1- (length columns)))
-          (insert " " separator " "))))
-    (insert "\n")
-
-    ;; Separator line
-    (insert "  ")
-    (dotimes (col-idx (length columns))
-      (insert (make-string col-width ?─))
-      (when (< col-idx (1- (length columns)))
-        (insert "─┼─")))
-    (insert "\n")
-
-    ;; Card rows
-    (dotimes (row max-rows)
-      ;; Line 1: heading
-      (insert "  ")
-      (dotimes (col-idx (length columns))
-        (let* ((state (nth col-idx columns))
-               (cards (alist-get state org-kanban--cards nil nil #'string=))
-               (card (nth row cards)))
-          (if card
-              (let* ((sel (and (eql col-idx org-kanban--selected-col)
-                              (eql row org-kanban--selected-row)))
-                     (start (point)))
-                (insert (org-kanban--format-card-line1 card col-width sel))
-                ;; Store card position
-                (puthash (cons col-idx row) card card-positions)
-                ;; Add text properties for navigation
-                (put-text-property start (point) 'org-kanban-card card)
-                (put-text-property start (point) 'org-kanban-col col-idx)
-                (put-text-property start (point) 'org-kanban-row row))
-            (insert (make-string col-width ?\s)))
-          (when (< col-idx (1- (length columns)))
-            (insert " " separator " "))))
-      (insert "\n")
-
-      ;; Line 2: metadata
-      (insert "  ")
-      (dotimes (col-idx (length columns))
-        (let* ((state (nth col-idx columns))
-               (cards (alist-get state org-kanban--cards nil nil #'string=))
-               (card (nth row cards)))
-          (if card
-              (let* ((sel (and (eql col-idx org-kanban--selected-col)
-                              (eql row org-kanban--selected-row)))
-                     (start (point)))
-                (insert (org-kanban--format-card-line2 card col-width sel))
-                (put-text-property start (point) 'org-kanban-card card)
-                (put-text-property start (point) 'org-kanban-col col-idx)
-                (put-text-property start (point) 'org-kanban-row row))
-            (insert (make-string col-width ?\s)))
-          (when (< col-idx (1- (length columns)))
-            (insert " " separator " "))))
-      (insert "\n")
-
-      ;; Blank line between cards
-      (insert "  ")
-      (dotimes (col-idx (length columns))
-        (insert (make-string col-width ?\s))
-        (when (< col-idx (1- (length columns)))
-          (insert " " separator " ")))
-      (insert "\n"))
-
-    (setq org-kanban--card-positions card-positions)
-
-    ;; Footer with keybinding help
-    (insert "\n")
-    (insert (propertize "  hjkl/←↑↓→" 'face 'font-lock-keyword-face) " nav  "
-            (propertize "</H" 'face 'font-lock-keyword-face) " state←  "
-            (propertize ">/L" 'face 'font-lock-keyword-face) " state→  "
-            (propertize "RET" 'face 'font-lock-keyword-face) " goto  "
-            (propertize "v/SPC" 'face 'font-lock-keyword-face) " preview  "
-            (propertize "f" 'face 'font-lock-keyword-face) " filter  "
-            (propertize "s" 'face 'font-lock-keyword-face) " sort  "
-            (propertize "D" 'face 'font-lock-keyword-face) " done  "
-            (propertize "g" 'face 'font-lock-keyword-face) " refresh  "
-            (propertize "q" 'face 'font-lock-keyword-face) " quit")))
-
-;;; Selection and preview
-
-(defun org-kanban--update-preview (card)
-  "Update the preview window with CARD content, if visible."
-  (let ((preview-win (get-buffer-window "*org-kanban-preview*")))
-    (when preview-win
-      (let ((marker (plist-get card :marker)))
-        (when (and marker (marker-buffer marker))
-          (let ((content (with-current-buffer (marker-buffer marker)
-                           (save-excursion
-                             (goto-char marker)
-                             (org-fold-show-entry)
-                             (buffer-substring
-                              (point)
-                              (save-excursion (org-end-of-subtree t t) (point)))))))
-            (with-current-buffer (get-buffer-create "*org-kanban-preview*")
-              (let ((inhibit-read-only t))
-                (erase-buffer)
-                (insert content)
-                (org-mode)
-                (goto-char (point-min))
-                (org-fold-show-all)
-                (org-cycle-hide-drawers 'all)
-                (read-only-mode 1)))))))))
-
-;;; Navigation
-
-(defun org-kanban--goto-card (col row)
-  "Select the card at COL and ROW.
-Re-renders the board to update the indicator, then moves point."
-  (when (gethash (cons col row) org-kanban--card-positions)
-    (setq org-kanban--selected-col col
-          org-kanban--selected-row row)
-    ;; Re-render to show the indicator (uses cached card data)
-    (org-kanban--render)
-    ;; Move point to the selected card
-    (goto-char (point-min))
-    (let ((found nil))
-      (while (and (not found) (not (eobp)))
-        (when (and (equal (get-text-property (point) 'org-kanban-col) col)
-                   (equal (get-text-property (point) 'org-kanban-row) row))
-          (setq found t))
-        (unless found
-          (goto-char (next-single-property-change (point) 'org-kanban-card nil (point-max))))))
-    ;; Update selected card and preview
-    (let ((card (gethash (cons col row) org-kanban--card-positions)))
-      (when card
-        (setq org-kanban--selected-card card)
-        (org-kanban--update-preview card)))))
-
-(defun org-kanban-next-card ()
-  "Move to the next card (down or next column)."
-  (interactive)
-  (let ((col (get-text-property (point) 'org-kanban-col))
-        (row (get-text-property (point) 'org-kanban-row)))
-    (cond
-     ;; Try next row in same column
-     ((and col row (gethash (cons col (1+ row)) org-kanban--card-positions))
-      (org-kanban--goto-card col (1+ row)))
-     ;; Try first card in next column
-     ((and col (gethash (cons (1+ col) 0) org-kanban--card-positions))
-      (org-kanban--goto-card (1+ col) 0))
-     ;; Wrap to first card
-     ((gethash (cons 0 0) org-kanban--card-positions)
-      (org-kanban--goto-card 0 0))
-     ;; No card at point, find first card
-     (t (org-kanban--goto-card 0 0)))))
-
-(defun org-kanban-prev-card ()
-  "Move to the previous card (up or previous column)."
-  (interactive)
-  (let ((col (get-text-property (point) 'org-kanban-col))
-        (row (get-text-property (point) 'org-kanban-row)))
-    (cond
-     ;; Try previous row in same column
-     ((and col row (> row 0)
-           (gethash (cons col (1- row)) org-kanban--card-positions))
-      (org-kanban--goto-card col (1- row)))
-     ;; Try last card in previous column
-     ((and col (> col 0))
-      (let ((prev-col (1- col))
-            (r 0))
-        (while (gethash (cons prev-col (1+ r)) org-kanban--card-positions)
-          (cl-incf r))
-        (org-kanban--goto-card prev-col r)))
-     ;; No card at point, find first card
-     (t (org-kanban--goto-card 0 0)))))
-
-(defun org-kanban--col-count (col)
-  "Return the number of cards in column COL."
-  (let ((count 0))
-    (while (gethash (cons col count) org-kanban--card-positions)
-      (cl-incf count))
-    count))
-
-(defun org-kanban-next-column ()
-  "Move to the next column (right), keeping the same row or nearest card."
-  (interactive)
-  (let ((col (or org-kanban--selected-col 0))
-        (row (or org-kanban--selected-row 0))
-        (ncols (length (append org-kanban-columns
-                               (when org-kanban--show-done org-kanban-done-columns)))))
-    (cl-loop for c from (1+ col) below ncols
-             for cnt = (org-kanban--col-count c)
-             when (> cnt 0)
-             do (org-kanban--goto-card c (min row (1- cnt)))
-             and return nil
-             finally
-             ;; Wrap around from the start
-             (cl-loop for c from 0 below col
-                      for cnt = (org-kanban--col-count c)
-                      when (> cnt 0)
-                      do (org-kanban--goto-card c (min row (1- cnt)))
-                      and return nil))))
-
-(defun org-kanban-prev-column ()
-  "Move to the previous column (left), keeping the same row or nearest card."
-  (interactive)
-  (let ((col (or org-kanban--selected-col 0))
-        (row (or org-kanban--selected-row 0))
-        (ncols (length (append org-kanban-columns
-                               (when org-kanban--show-done org-kanban-done-columns)))))
-    (cl-loop for c downfrom (1- col) to 0
-             for cnt = (org-kanban--col-count c)
-             when (> cnt 0)
-             do (org-kanban--goto-card c (min row (1- cnt)))
-             and return nil
-             finally
-             ;; Wrap around from the end
-             (cl-loop for c downfrom (1- ncols) above col
-                      for cnt = (org-kanban--col-count c)
-                      when (> cnt 0)
-                      do (org-kanban--goto-card c (min row (1- cnt)))
-                      and return nil))))
-
-;;; Card operations
-
-(defun org-kanban--move-card (direction)
-  "Move the card at point in DIRECTION (:next or :prev) in the state order."
-  (let ((card (get-text-property (point) 'org-kanban-card)))
-    (when card
-      (let* ((current-state (plist-get card :state))
-             (marker (plist-get card :marker))
-             (heading (plist-get card :heading))
-             (idx (cl-position current-state org-kanban-state-order :test #'string=))
-             (new-idx (pcase direction
-                        (:next (min (1- (length org-kanban-state-order)) (1+ idx)))
-                        (:prev (max 0 (1- idx)))))
-             (new-state (nth new-idx org-kanban-state-order)))
-        (unless (string= current-state new-state)
-          ;; Change the state in the org file
-          (with-current-buffer (marker-buffer marker)
-            (save-excursion
-              (goto-char marker)
-              (org-todo new-state)))
-          (message "Moved \"%s\" → %s" heading new-state)
-          ;; Refresh board, try to stay near current position
-          (let ((col (get-text-property (point) 'org-kanban-col))
-                (row (get-text-property (point) 'org-kanban-row)))
-            (org-kanban-refresh)
-            ;; Try to navigate back to a reasonable position
-            (or (ignore-errors (org-kanban--goto-card col row) t)
-                (ignore-errors (org-kanban--goto-card col (max 0 (1- row))) t)
-                (org-kanban--goto-card 0 0))))))))
-
-(defun org-kanban-move-right ()
-  "Move the card at point to the next state."
-  (interactive)
-  (org-kanban--move-card :next))
-
-(defun org-kanban-move-left ()
-  "Move the card at point to the previous state."
-  (interactive)
-  (org-kanban--move-card :prev))
-
-(defun org-kanban-goto-heading ()
-  "Jump to the org heading for the card at point.
-Opens in a new tab if `org-kanban-open-in-tab' is non-nil."
-  (interactive)
-  (let ((card (get-text-property (point) 'org-kanban-card)))
-    (when card
-      (let ((marker (plist-get card :marker)))
-        (when (and marker (marker-buffer marker))
-          (if org-kanban-open-in-tab
-              (progn
-                (tab-bar-new-tab)
-                (switch-to-buffer (marker-buffer marker)))
-            (pop-to-buffer (marker-buffer marker)))
-          (goto-char marker)
-          (org-reveal)
-          (org-fold-show-entry))))))
-
-(defun org-kanban-preview ()
-  "Preview the card at point in a side window (toggle)."
-  (interactive)
-  (let ((win (get-buffer-window "*org-kanban-preview*")))
-    (if win
-        (quit-window nil win)
-      (let* ((card (get-text-property (point) 'org-kanban-card))
-             (marker (and card (plist-get card :marker))))
-        (when (and marker (marker-buffer marker))
-          (let ((content
-                 (with-current-buffer (marker-buffer marker)
-                   (save-excursion
-                     (goto-char marker)
-                     (org-fold-show-entry)
-                     (buffer-substring
-                      (point)
-                      (save-excursion
-                        (org-end-of-subtree t t)
-                        (point)))))))
-            (with-current-buffer (get-buffer-create "*org-kanban-preview*")
-              (let ((inhibit-read-only t))
-                (erase-buffer)
-                (insert content)
-                (org-mode)
-                (goto-char (point-min))
-                (org-fold-show-all)
-                (org-cycle-hide-drawers 'all)
-                (read-only-mode 1)))
-            (display-buffer "*org-kanban-preview*"
-                            '((display-buffer-in-side-window)
-                              (side . bottom)
-                              (window-height . 0.35)))))))))
-
-;;; Filtering
-
-(defun org-kanban-filter-section ()
-  "Filter the board to show only items from a specific section."
-  (interactive)
-  (let* ((sections (org-ql-select org-kanban-file
-                     '(level 1)
-                     :action (lambda () (substring-no-properties (org-get-heading t t t t)))))
-         (choice (completing-read "Filter by section (empty to clear): "
-                                  sections nil nil)))
-    (setq org-kanban--section-filter (if (string-empty-p choice) nil choice))
-    (org-kanban-refresh)))
-
-(defun org-kanban-clear-filter ()
-  "Clear the section filter."
-  (interactive)
-  (setq org-kanban--section-filter nil)
-  (org-kanban-refresh))
-
-(defun org-kanban-cycle-sort ()
-  "Cycle through sort orders: priority → scheduled → deadline → alpha → none."
-  (interactive)
-  (let* ((current (or org-kanban--sort org-kanban-sort))
-         (order '(priority scheduled deadline alpha none))
-         (idx (cl-position current order))
-         (next (nth (mod (1+ (or idx 0)) (length order)) order)))
-    (setq org-kanban--sort next)
-    (setq org-kanban--cards (org-kanban--fetch-cards))
-    (setq org-kanban--selected-col nil
-          org-kanban--selected-row nil)
-    (org-kanban--render)
-    (goto-char (point-min))
-    (when (gethash (cons 0 0) org-kanban--card-positions)
-      (org-kanban--goto-card 0 0))
-    (message "Sort: %s" next)))
-
-(defun org-kanban-toggle-done ()
-  "Toggle visibility of DONE/CANX columns."
-  (interactive)
-  (setq org-kanban--show-done (not org-kanban--show-done))
-  (org-kanban-refresh))
-
-;;; Refresh
-
-(defun org-kanban-refresh ()
-  "Refresh the kanban board."
-  (interactive)
-  (setq org-kanban--cards (org-kanban--fetch-cards))
-  (setq org-kanban--selected-col nil
-        org-kanban--selected-row nil)
-  (org-kanban--render)
-  (goto-char (point-min))
-  ;; Navigate to first card
-  (when (gethash (cons 0 0) org-kanban--card-positions)
-    (org-kanban--goto-card 0 0)))
-
-;;; Major mode
-
-(defvar org-kanban-mode-map
-  (let ((map (make-sparse-keymap)))
-    ;; Navigation
-    (define-key map (kbd "j") #'org-kanban-next-card)
-    (define-key map (kbd "n") #'org-kanban-next-card)
-    (define-key map (kbd "<down>") #'org-kanban-next-card)
-    (define-key map (kbd "k") #'org-kanban-prev-card)
-    (define-key map (kbd "p") #'org-kanban-prev-card)
-    (define-key map (kbd "<up>") #'org-kanban-prev-card)
-    ;; Column navigation
-    (define-key map (kbd "l") #'org-kanban-next-column)
-    (define-key map (kbd "<right>") #'org-kanban-next-column)
-    (define-key map (kbd "h") #'org-kanban-prev-column)
-    (define-key map (kbd "<left>") #'org-kanban-prev-column)
-    ;; Move cards (change state)
-    (define-key map (kbd ">") #'org-kanban-move-right)
-    (define-key map (kbd "L") #'org-kanban-move-right)
-    (define-key map (kbd "<") #'org-kanban-move-left)
-    (define-key map (kbd "H") #'org-kanban-move-left)
-    ;; Actions
-    (define-key map (kbd "RET") #'org-kanban-goto-heading)
-    (define-key map (kbd "v") #'org-kanban-preview)
-    (define-key map (kbd "SPC") #'org-kanban-preview)
-    (define-key map (kbd "f") #'org-kanban-filter-section)
-    (define-key map (kbd "F") #'org-kanban-clear-filter)
-    (define-key map (kbd "s") #'org-kanban-cycle-sort)
-    (define-key map (kbd "D") #'org-kanban-toggle-done)
-    (define-key map (kbd "g") #'org-kanban-refresh)
-    (define-key map (kbd "q") #'quit-window)
-    map)
-  "Keymap for `org-kanban-mode'.")
-
-(define-derived-mode org-kanban-mode special-mode "OrgKanban"
-  "Major mode for the org-mode kanban board.
-\\{org-kanban-mode-map}"
-  (setq-local buffer-read-only t)
-  (setq-local truncate-lines t)
-  (setq-local cursor-type nil)
-  ;; Force monospace fixed-pitch font at uniform height to fix alignment
-  ;; with mixed-fonts themes (modus-themes-mixed-fonts, variable-pitch headings)
-  (face-remap-add-relative 'default :inherit 'fixed-pitch :height 1.0)
-)
-
-;;; Entry points
-
-;;;###autoload
-(defun org-kanban (&optional file)
-  "Open the kanban board for FILE (defaults to `org-kanban-file')."
-  (interactive)
-  (let ((buf (get-buffer-create "*org-kanban*")))
-    (with-current-buffer buf
-      (org-kanban-mode)
-      (when file
-        (setq-local org-kanban-file file))
-      (org-kanban-refresh))
-    (pop-to-buffer buf)))
-
-;;;###autoload
-(defun org-kanban-work ()
-  "Open the kanban board filtered to the Work section."
-  (interactive)
-  (let ((buf (get-buffer-create "*org-kanban*")))
-    (with-current-buffer buf
-      (org-kanban-mode)
-      (setq org-kanban--section-filter "Work")
-      (org-kanban-refresh))
-    (pop-to-buffer buf)))
-
-;;;###autoload
-(defun org-kanban-projects ()
-  "Open the kanban board filtered to the Projects section."
-  (interactive)
-  (let ((buf (get-buffer-create "*org-kanban*")))
-    (with-current-buffer buf
-      (org-kanban-mode)
-      (setq org-kanban--section-filter "Projects")
-      (org-kanban-refresh))
-    (pop-to-buffer buf)))
-
-;;;###autoload
-(defun org-kanban-systems ()
-  "Open the kanban board filtered to the Systems section."
-  (interactive)
-  (let ((buf (get-buffer-create "*org-kanban*")))
-    (with-current-buffer buf
-      (org-kanban-mode)
-      (setq org-kanban--section-filter "Systems")
-      (org-kanban-refresh))
-    (pop-to-buffer buf)))
-
-(provide 'org-kanban)
-;;; org-kanban.el ends here
dots/config/emacs/site-lisp/whisper.el
@@ -1,208 +0,0 @@
-;;; whisper.el --- Record audio and transcribe using whisper-cli -*- lexical-binding: t; -*-
-
-;; Package-Requires: ((emacs "25.1"))
-
-;;; Commentary:
-;; This library provides functions to record audio using ffmpeg and
-;; transcribe it using the whisper-cli command-line tool.
-
-;;; Code:
-
-(defgroup whisper nil
-  "Settings for the whisper audio transcription library."
-  :group 'tools)
-
-(defcustom whisper-cli-executable "whisper-cli"
-  "Path to the whisper-cli executable."
-  :type 'string
-  :group 'whisper)
-
-(defcustom whisper-model "base"
-  "The whisper model to use for transcription (e.g., tiny, base, small, medium, large)."
-  :type 'string
-  :group 'whisper)
-
-(defcustom whisper-language "en"
-  "The language for transcription (e.g., en, es, fr, de)."
-  :type 'string
-  :group 'whisper)
-
-(defcustom whisper-ffmpeg-timeout 300
-  "Default timeout in seconds for ffmpeg recording."
-  :type 'integer
-  :group 'whisper)
-
-(defcustom whisper-ffmpeg-audio-input-source "pulse"
-  "FFmpeg audio input source. For PulseAudio, usually 'pulse'.
-For ALSA, it might be 'hw:0'. For macOS, it might be ':0' (for default input).
-You might need to adjust this based on your system's ffmpeg configuration."
-  :type 'string
-  :group 'whisper)
-
-(defcustom whisper-ffmpeg-audio-input-device "default"
-  "FFmpeg audio input device. For PulseAudio, often 'default'.
-For ALSA, it might be something like 'plughw:1,0'.
-For macOS, check available devices with `ffmpeg -f avfoundation -list_devices true -i \"\"`."
-  :type 'string
-  :group 'whisper)
-
-
-(defvar whisper--recording-process nil
-  "Holds the ffmpeg recording process.")
-(defvar whisper--original-mode-line-format mode-line-format
-  "To store the original mode-line format.")
-
-(defun whisper--start-mode-line-indicator (indicator)
-  "Display an INDICATOR in the mode line."
-  (setq whisper--original-mode-line-format mode-line-format)
-  (setq-default mode-line-format
-                (cons (format " %s " indicator)
-                      (if (listp mode-line-format) mode-line-format (list mode-line-format)))))
-
-(defun whisper--stop-mode-line-indicator ()
-  "Restore the original mode line."
-  (setq-default mode-line-format whisper--original-mode-line-format))
-
-(defun whisper--record-audio (output-file-basename timeout callback)
-  "Record audio using ffmpeg.
-A temporary WAV file will be created based on OUTPUT-FILE-BASENAME.
-Recording runs for TIMEOUT seconds, or until the process is interrupted.
-Then, execute CALLBACK function with the path to the recorded audio file.
-The CALLBACK is responsible for processing and eventually deleting the audio file."
-  (whisper--start-mode-line-indicator "")
-  (message "Recording audio for up to %d seconds (or run 'whisper-run' again to stop early)..." timeout)
-  (let* ((temp-wav-file (make-temp-file "whisper-audio-" nil ".wav"))
-         (process-environment (copy-sequence process-environment))
-         (ffmpeg-command
-          (list "ffmpeg"
-                "-y" ; Overwrite output files without asking
-                "-f" whisper-ffmpeg-audio-input-source
-                "-i" whisper-ffmpeg-audio-input-device
-                "-t" (number-to-string timeout)
-                temp-wav-file)))
-    (setenv "LC_ALL" "C" process-environment) ; Ensure consistent ffmpeg output
-    (setq whisper--recording-process
-          (apply #'start-process "whisper-ffmpeg" "*whisper-ffmpeg-output*" ffmpeg-command))
-
-    (set-process-sentinel
-     whisper--recording-process
-     (lambda (proc _event)
-       (let ((status (process-status proc))
-             (audio-file-processed nil)) ; Flag to track if callback was called
-         (unwind-protect
-             (cond
-              ((memq status '(exit signal)) ; Process has definitely terminated
-               (if (and (file-exists-p temp-wav-file)
-                        ;; Check if file has content (size > 0)
-                        (> (nth 7 (file-attributes temp-wav-file)) 0))
-                   (progn
-                     (message "Recording finished/stopped. Audio file: %s" temp-wav-file)
-                     (setq audio-file-processed t)
-                     (funcall callback temp-wav-file)) ; Pass to callback for transcription
-                 (progn
-                   (message "Recording failed or produced no usable audio data."))))
-              (t ; Other statuses - should not happen often for a finished process
-               (message "Recording process ended in unexpected state: %s" status)))
-           ;; Cleanup actions
-           (setq whisper--recording-process nil) ; Clear the process variable
-           (whisper--stop-mode-line-indicator)   ; Always restore mode line
-           ;; Delete the temp wav file only if it was not passed to the callback
-           (when (and (not audio-file-processed) (file-exists-p temp-wav-file))
-             (message "Deleting unused/empty temp audio file: %s" temp-wav-file)
-             (delete-file temp-wav-file))
-           ))))
-    whisper--recording-process))
-
-(defun whisper--transcribe (audio-file callback)
-  "Transcribe AUDIO-FILE using whisper-cli and call CALLBACK with transcription."
-  (whisper--start-mode-line-indicator "")
-  (message "Transcribing audio...")
-  (let* ((temp-output-file (make-temp-file "whisper-transcription-" nil ".txt"))
-         (command (list whisper-cli-executable
-                        audio-file
-                        "--model" whisper-model
-                        "--language" whisper-language
-                        "--output_txt" ; Ensure whisper-cli outputs a .txt file
-                        "--output_dir" (file-name-directory temp-output-file))))
-    (message "Running command: %s" (string-join command " "))
-    (let ((process (apply #'start-process "whisper-cli" "*whisper-cli-output*" command)))
-      (set-process-sentinel
-       process
-       (lambda (_proc _event)
-         (whisper--stop-mode-line-indicator)
-         (unwind-protect
-             (if (and (eq (process-status process) 'exit)
-                      (= (process-exit-status process) 0))
-                 (let* ((expected-txt-name (concat (file-name-sans-extension audio-file) ".txt"))
-                        (transcription-file (expand-file-name expected-txt-name (file-name-directory temp-output-file))))
-                   (if (file-exists-p transcription-file)
-                       (progn
-                         (message "Transcription successful.")
-                         (with-temp-buffer
-                           (insert-file-contents transcription-file)
-                           (funcall callback (buffer-string)))
-                         (delete-file transcription-file)) ; Delete the .txt file created by whisper-cli
-                     (message "Transcription output file not found: %s" transcription-file)))
-               (message "whisper-cli transcription failed. Check *whisper-cli-output* buffer."))
-           (when (file-exists-p audio-file)
-             (delete-file audio-file)) ; Clean up the audio file
-           (when (file-exists-p temp-output-file)
-             (delete-file temp-output-file)) ; Clean up the placeholder temp file
-           ))))))
-
-;;;###autoload
-;;;###autoload
-(defun whisper-run ()
-  "Record audio, transcribe it, and insert the text into the current buffer.
-If a recording is already in progress (started by this command),
-running `whisper-run` again will stop the current recording, and
-transcription will proceed on the audio captured so far.
-Uses `whisper-ffmpeg-timeout` for recording duration if starting anew."
-  (interactive)
-  (if (and whisper--recording-process (process-live-p whisper--recording-process))
-      (progn
-        (message "Stopping current recording...")
-        (interrupt-process whisper--recording-process)
-        ;; The sentinel of the existing whisper--recording-process will handle
-        ;; the audio file and initiate transcription.
-        )
-    ;; Else, no recording in progress, so start a new one.
-    (whisper--record-audio
-     "whisper-rec-" ; Base name for make-temp-file
-     whisper-ffmpeg-timeout
-     (lambda (audio-file) ; This is the callback from whisper--record-audio
-       ;; audio-file here is the temp-wav-file from whisper--record-audio
-       (if (and audio-file (file-exists-p audio-file))
-           (whisper--transcribe
-            audio-file ; whisper--transcribe is now responsible for this audio-file
-            (lambda (transcription)
-              (if (string-empty-p transcription)
-                  (message "Transcription is empty.")
-                (insert transcription))
-              (message "Transcription inserted.")))
-         (message "No valid audio file was recorded to transcribe."))))))
-
-;;;###autoload
-(defun whisper-file (file)
-  "Record audio, transcribe it, and append the text to the specified FILE.
-Uses `whisper-ffmpeg-timeout` for recording duration."
-  (interactive "FAppend transcription to file: ")
-  (unless (file-writable-p (file-name-directory file))
-    (error "Directory for file %s is not writable" file))
-  (whisper--record-audio
-   "whisper-temp-output.wav" ; Not directly used
-   whisper-ffmpeg-timeout
-   (lambda (audio-file)
-     (whisper--transcribe
-      audio-file
-      (lambda (transcription)
-        (if (string-empty-p transcription)
-            (message "Transcription is empty. Nothing appended to %s." file)
-          (with-temp-buffer
-            (insert transcription)
-            (append-to-file nil nil file))
-          (message "Transcription appended to %s." file)))))))
-
-(provide 'whisper)
-
-;;; whisper.el ends here
home/common/dev/emacs.nix
@@ -18,26 +18,18 @@ let
   '';
   myExtraPackages =
     epkgs: with epkgs; [
-      ace-window
       acp
-      adoc-mode
       age
       agent-shell
       aggressive-indent
-      aidermacs
       alert
       async
       avy
       beginend
       cape
-      casual
-      casual-avy
-      chatgpt-shell
-      shell-maker
       consult
       consult-dir
       consult-denote
-      consult-project-extra
       consult-vc-modified-files
       consult-gh
       consult-gh-embark
@@ -45,27 +37,18 @@ let
       # copilot
       # copilot-chat
       corfu
-      corfu-candidate-overlay
       dape
       dash
       denote
       denote-org
       # denote-journal
       # denote-sequence # maybe ?
-      denote-menu
-      detached
       devdocs
       diff-hl
-      dired-collapse
       dired-narrow
-      dired-rsync
-      diredfl
-      dockerfile-mode
       # doom-modeline
       dwim-shell-command
-      easy-kill
       eat
-      edit-indirect
       editorconfig
       eldoc-box
       pr-review
@@ -77,53 +60,25 @@ let
       esup
       # flimenu
       flymake-yamllint
-      (ghostel.overrideAttrs (old: {
-        # Exclude evil integration files to avoid build failure when evil is not installed
-        preBuild = (old.preBuild or "") + ''
-          rm -f "$NIX_BUILD_TOP/working/ghostel/evil-ghostel.el" "$NIX_BUILD_TOP/working/ghostel/ghostel-evil.el" 2>/dev/null || true
-        '';
-        # Install pre-built native module (Nix store is read-only, can't download at runtime)
-        postInstall = (old.postInstall or "") + ''
-          local dest=$(find $out/share/emacs/site-lisp/elpa -maxdepth 1 -name 'ghostel-*' -type d)
-          install -m444 ${
-            pkgs.fetchurl {
-              url = "https://github.com/dakra/ghostel/releases/download/v0.13.0/ghostel-module-x86_64-linux.so";
-              hash = "sha256-C+hinkx7uKIQw04TNevVZ6ybutpqm62wqo62q2KSbUk=";
-            }
-          } $dest/ghostel-module.so
-        '';
-      }))
       git-modes
-      go-mode
-      gotest
       gotest-ts
       gptel
-      hardhat
       helpful
-      highlight
-      highlight-indentation
       htmlize
       # ibuffer-vc
-      indent-bars
       jinx
       # jira
       # jiralib2
-      json-mode
-      kubed
       kkp
       # ligature
       macrostep
       magit
-      magit-popup
       # marginalia
       markdown-mode
       mcp
       minions
       modus-themes
-      doom-themes
-      multi-vterm
       mu4e
-      mwim
       nix-mode
       nix-ts-mode
       nixpkgs-fmt
@@ -136,23 +91,15 @@ let
       orderless
       org
       org-appear
-      org-contrib
-      org-download
-      org-gcal
-      org-modern
       org-nix-shell
       org-ql
-      org-review
       org-rich-yank
       org-tree-slide
       org-web-tools
       orgalist
       orgit
       outline-indent
-      ox-pandoc
-      ox-jira
       ox-tufte
-      pandoc-mode
       pi-coding-agent
       pkgs.phscroll
       popon
@@ -160,10 +107,7 @@ let
       rg
       ready-player
       scopeline
-      scratch
       shr-tag-pre-highlight
-      smartparens
-      substitute
       surround
       symbol-overlay
       tempel
@@ -174,13 +118,11 @@ let
       treesit-fold
       treesit-grammars.with-all-grammars # see how much it weight
       # vc-jj
-      verb
       # vertico
       visual-fill-column
       visual-regexp
       vterm
       vundo
-      web-mode
       wgrep
       with-editor
       # xeft