seam

Personal wiki toolkit for Emacs
Log | Files | Refs | LICENSE

seam.el (26716B)


      1 ;;; seam.el --- Personal Org mode wiki  -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2025 Spencer Williams
      4 
      5 ;; Author: Spencer Williams <spnw@plexwave.org>
      6 ;; Homepage: https://wiki.plexwave.org/seam
      7 ;; Keywords: hypermedia, outlines
      8 
      9 ;; Version: 0.1.0
     10 
     11 ;; SPDX-License-Identifier: GPL-3.0-or-later
     12 
     13 ;; This file is not part of GNU Emacs.
     14 
     15 ;; This program is free software: you can redistribute it and/or modify
     16 ;; it under the terms of the GNU General Public License as published by
     17 ;; the Free Software Foundation, either version 3 of the License, or
     18 ;; (at your option) any later version.
     19 
     20 ;; This program is distributed in the hope that it will be useful,
     21 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     23 ;; GNU General Public License for more details.
     24 
     25 ;; You should have received a copy of the GNU General Public License
     26 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
     27 
     28 ;;; Commentary:
     29 
     30 ;; Seam is a package that lets you easily create, link, and publish
     31 ;; Org notes in the form of a personal wiki.
     32 
     33 ;;; Code:
     34 
     35 (require 'seam-export)
     36 (require 'org)
     37 (require 'cl-lib)
     38 (require 'grep)
     39 
     40 (defgroup seam nil
     41   "Options for Seam."
     42   :group 'org
     43   :tag "Seam")
     44 
     45 (defcustom seam-note-directory nil
     46   "Seam note directory."
     47   :group 'seam
     48   :type '(choice directory (const nil)))
     49 
     50 (defcustom seam-default-note-type "private"
     51   "Default type for Seam notes."
     52   :group 'seam
     53   :type 'string)
     54 
     55 (defcustom seam-note-types '("private" "public")
     56   "List of valid Seam note types.  Each element can either be a
     57 string (the name of the type), or an alist.  If using an alist,
     58 the car should be the type name, and the cdr should be a plist
     59 containing any number of these properties:
     60 
     61   `:create-as-draft'
     62 
     63    When this is non-nil, new Seam notes of this type will be
     64    created as drafts.  If this is missing, falls back to
     65    `seam-create-as-draft'."
     66   :group 'seam
     67   :type '(repeat
     68           (choice string
     69                   (alist :key-type string :value-type plist))))
     70 
     71 (defcustom seam-create-as-draft nil
     72   "When non-nil, new Seam notes will be created as drafts."
     73   :group 'seam
     74   :type 'boolean)
     75 
     76 (defun seam-format-title-default (title type draft-p)
     77   "Default Seam title formatter.  Formats like this: \"TITLE (TYPE[ draft])\"."
     78   (format "%s %s"
     79           title
     80           (propertize
     81            (format "(%s%s)" type (if draft-p " draft" ""))
     82            'face 'font-lock-comment-face)))
     83 
     84 (defcustom seam-title-formatter
     85   #'seam-format-title-default
     86   "Function used by Seam to format note titles for completion and buffer
     87 naming.  Must be a function taking two arguments: TITLE and TYPE."
     88   :group 'seam
     89   :type 'function)
     90 
     91 (defun seam-html-directories ()
     92   (mapcar #'car seam-export-alist))
     93 
     94 (defun seam-slugify (title)
     95   (setq title (string-replace "'" "" title))
     96   (setq title (string-split title "\\W+" t))
     97   (setq title (string-join title "-"))
     98   (downcase title))
     99 
    100 (defun seam-lookup-slug (slug)
    101   (cl-dolist (type (seam-get-all-note-type-names))
    102     (let ((file (file-name-concat seam-note-directory type (concat slug ".org")))
    103           (draft-file (file-name-concat seam-note-directory type (concat "-" slug ".org"))))
    104       (cond
    105        ((file-exists-p file)
    106         (cl-return (expand-file-name file)))
    107        ((file-exists-p draft-file)
    108         (cl-return (expand-file-name draft-file)))))))
    109 
    110 (defun seam--check-conflict (slug)
    111   (when (seam-lookup-slug slug)
    112     (error "A note called `%s.org' already exists" slug)))
    113 
    114 (defun seam-link-open (path _prefix)
    115   (org-mark-ring-push)
    116   (if-let ((file (seam-lookup-slug path)))
    117       (find-file file)
    118     (seam-create-note path nil t (seam-draft-p path)))
    119   (seam-set-buffer-name))
    120 
    121 (defvar seam-note-file-regexp "\\`[^.].+\\.org\\'")
    122 (defvar seam--subset nil)
    123 
    124 (defcustom seam-completing-read-function #'completing-read
    125   "The completion function used by Seam."
    126   :group 'seam
    127   :type 'function)
    128 
    129 (defun seam-ensure-directory-exists (dir)
    130   (unless (file-directory-p dir)
    131     (make-directory dir t)))
    132 
    133 (defun seam-ensure-note-subdirectories-exist ()
    134   (unless seam-note-directory
    135     (error "Please set `seam-note-directory'"))
    136   (cl-dolist (type (seam-get-all-note-type-names))
    137     (let ((dir (file-name-concat seam-note-directory type)))
    138       (seam-ensure-directory-exists dir))))
    139 
    140 (defcustom seam-sort-method 'title
    141   "The method used by Seam to sort notes."
    142   :group 'seam
    143   :type '(choice (const :tag "Sort by title" title)
    144                  (const :tag "Sort by modification date" modified)))
    145 
    146 (defun seam-get-all-note-type-names ()
    147   (mapcar (lambda (x) (car (ensure-list x))) seam-note-types))
    148 
    149 (cl-defgeneric seam-get-all-notes (sort-by))
    150 
    151 (cl-defmethod seam-get-all-notes ((sort-by (eql 't)))
    152   (ignore sort-by)
    153   (seam-get-all-notes seam-sort-method))
    154 
    155 (cl-defmethod seam-get-all-notes ((sort-by (eql 'modified)))
    156   (ignore sort-by)
    157   (let ((files (cl-loop for type in (seam--active-subset)
    158                         append (directory-files-and-attributes
    159                                 (file-name-concat seam-note-directory type)
    160                                 t
    161                                 seam-note-file-regexp))))
    162     (cl-loop for (file . _attributes)
    163              in (cl-sort
    164                  files
    165                  (lambda (f1 f2)
    166                    (time-less-p (file-attribute-modification-time f2)
    167                                 (file-attribute-modification-time f1)))
    168                  :key #'cdr)
    169              collect (cons (seam-get-title-from-file file) file))))
    170 
    171 (cl-defmethod seam-get-all-notes ((sort-by (eql 'title)))
    172   (ignore sort-by)
    173   (let ((files (cl-loop for type in (seam--active-subset)
    174                         append (directory-files
    175                                 (file-name-concat seam-note-directory type)
    176                                 t
    177                                 seam-note-file-regexp))))
    178     (cl-sort
    179      (cl-loop for file in files
    180               collect (cons (seam-get-title-from-file file) file))
    181      #'string<
    182      :key #'car)))
    183 
    184 (cl-defun seam-get-title-from-buffer (&optional (buffer (current-buffer)))
    185   (with-current-buffer buffer
    186     (save-mark-and-excursion
    187       (save-restriction
    188         (widen)
    189         (goto-char 1)
    190         (when (re-search-forward "^\\* " nil t)
    191           (let ((start (point)))
    192             (end-of-line)
    193             (let ((title (string-trim (buffer-substring-no-properties start (point)))))
    194               (unless (string-empty-p title)
    195                 title))))))))
    196 
    197 (defun seam-get-title-from-file (file)
    198   (with-temp-buffer
    199     (insert-file-contents file)
    200     (seam-get-title-from-buffer)))
    201 
    202 (defun seam-get-slug-from-file-name (file)
    203   (string-remove-prefix "-" (file-name-base file)))
    204 
    205 (cl-defun seam-get-slug-from-buffer (&optional (buffer (current-buffer)))
    206   (or (with-current-buffer buffer
    207         (save-mark-and-excursion
    208           (save-restriction
    209             (widen)
    210             (goto-char 1)
    211             (when (re-search-forward "^\\* " nil t)
    212               (org-element-property :SEAM_SLUG (org-element-at-point))))))
    213       (seam-slugify (seam-get-title-from-buffer buffer))))
    214 
    215 (defun seam-format-title (title type draft-p)
    216   (funcall seam-title-formatter title type draft-p))
    217 
    218 (defun seam-validate-note-type (type)
    219   (unless (member type (seam-get-all-note-type-names))
    220     (error "`%s' is not a valid Seam note type" type)))
    221 
    222 (cl-defun seam-create-note (title &optional type select (draft-p nil draft-supplied-p))
    223   (unless type
    224     (setq type seam-default-note-type))
    225   (seam-validate-note-type type)
    226   (seam-ensure-note-subdirectories-exist)
    227   (let* ((slug (seam-slugify title))
    228          (draft-p
    229           (if draft-supplied-p
    230               draft-p
    231             (cl-getf (cdr (assoc type (mapcar #'ensure-list seam-note-types)))
    232                      :create-as-draft
    233                      seam-create-as-draft)))
    234          (file (file-name-concat seam-note-directory
    235                                  type
    236                                  (concat (when draft-p "-") slug ".org"))))
    237     (when (string= "" slug)
    238       (error "Cannot create a note with an empty slug"))
    239     (seam--check-conflict slug)
    240     (let ((buffer (funcall (if select #'find-file #'find-file-noselect) file)))
    241       (with-current-buffer buffer
    242         (insert (format "* %s\n" title))
    243         (save-buffer)
    244         buffer))))
    245 
    246 (defun seam-read-title (prompt)
    247   (seam-ensure-note-subdirectories-exist)
    248   (let* ((notes (seam-get-all-notes t))
    249          (self (cl-find (buffer-file-name) notes :key #'cdr :test #'equal)))
    250     (let ((notes
    251            (append (cl-remove self
    252                               notes
    253                               :test #'equal)
    254                    (and self (list self)))))
    255       (let ((files (cl-loop for (title . file) in notes
    256                             collect (cons (seam-format-title
    257                                            title
    258                                            (seam-get-note-type file)
    259                                            (seam-draft-p file))
    260                                           file))))
    261         (let ((completion (string-trim (funcall seam-completing-read-function prompt (mapcar #'car files)))))
    262           (or (assoc completion files)
    263               (cons completion nil)))))))
    264 
    265 (defun seam--read-type (prompt arg &optional choices)
    266   (when arg
    267     (if (listp arg)
    268         (let ((type (funcall seam-completing-read-function
    269                              prompt
    270                              (or choices (seam-get-all-note-type-names))
    271                              nil
    272                              t)))
    273           (seam-validate-note-type type)
    274           type)
    275       (nth (1- arg)
    276            (seam-get-all-note-type-names)))))
    277 
    278 ;;;###autoload
    279 (defun seam-find-note (arg)
    280   "Find Seam note interactively by title, creating it if it does not exist.
    281 `seam-completing-read-function' is used for completion.
    282 
    283 A prefix argument can be used to show only a specific note type (and to
    284 use that type if a new note is created).  With a numeric argument N, the
    285 Nth type in `seam-note-types' is chosen (counting from 1).  With C-u, a
    286 completion prompt is given to choose the type."
    287   (interactive "P")
    288   (let* ((type (seam--read-type "Type: " arg))
    289          (seam--subset
    290           (if type (list type) (seam-get-all-note-type-names))))
    291     (cl-destructuring-bind (completion . file)
    292         (seam-read-title "Open note: ")
    293       (if file
    294           (with-current-buffer (find-file file)
    295             ;; Ensure buffer name is up to date (e.g. after changing
    296             ;; formatter function) (NOTE: Redundant if buffer wasn't
    297             ;; already open, as `seam-setup-buffer' does this too.)
    298             (seam-set-buffer-name))
    299         (seam-create-note (string-trim completion) (or type seam-default-note-type) t)))))
    300 
    301 (cl-defun seam-get-note-type (file &optional no-error)
    302   (when (and file (equal "org" (file-name-extension file)))
    303     (let ((type (cadr (nreverse (file-name-split file)))))
    304       (when (member type (seam-get-all-note-type-names))
    305         (cl-return-from seam-get-note-type type))))
    306   (unless no-error
    307     (error "%s is not a Seam note" file)))
    308 
    309 (defun seam-make-file-name (slug type &optional draft)
    310   (expand-file-name
    311    (file-name-concat
    312     seam-note-directory type
    313     (concat (when draft "-") slug ".org"))))
    314 
    315 (defun seam-get-links-to-file (file &optional include-drafts)
    316   "Return filename of each note which links to FILE."
    317   (cl-loop for file in (remove (expand-file-name file)
    318                                (seam-note-files-containing-string
    319                                 (format "[[seam:%s]" (file-name-base file))))
    320            when (or include-drafts
    321                     seam-export--include-drafts
    322                     (not (seam-draft-p file)))
    323            collect file))
    324 
    325 (cl-defun seam-get-links-from-buffer (&optional (buffer (current-buffer)))
    326   "Return filename of each existing note which is linked to from BUFFER."
    327   (let ((links (with-current-buffer buffer
    328                  (save-mark-and-excursion
    329                    (save-restriction
    330                      (widen)
    331                      (goto-char 1)
    332                      (delete-dups
    333                       (cl-loop for ret = (re-search-forward "\\[\\[seam:\\(.*?\\)\\]" nil t)
    334                                while ret collect (match-string 1))))))))
    335     (let ((file (buffer-file-name buffer)))
    336       (remove (and file (expand-file-name file))
    337               (cl-loop for link in links
    338                        as f = (seam-lookup-slug link)
    339                        when f collect f)))))
    340 
    341 (defun seam-get-links-from-file (file)
    342   "Return filename of each existing note which is linked to from FILE."
    343   (with-temp-buffer
    344     (insert-file-contents file)
    345     (seam-get-links-from-buffer)))
    346 
    347 (defun seam-delete-html-files-for-note (note-file)
    348   (let ((html-nd (concat (seam-get-slug-from-file-name note-file) ".html")))
    349     (dolist (dir (seam-html-directories))
    350       (let ((html (file-name-concat dir html-nd)))
    351         (when (file-exists-p html)
    352           (delete-file html)
    353           (message "Deleted %s" html))))))
    354 
    355 (defun seam--rename-file (old new interactive)
    356   (rename-file old new)
    357   (when interactive
    358     (set-visited-file-name new nil t)
    359     (seam-set-buffer-name))
    360   (seam-post-save-or-rename old new))
    361 
    362 (defun seam-post-save-or-rename (old new &optional previous-links-from-file slug-or-title-changed)
    363   (unless (string= old new)
    364     (seam-update-links old new))
    365   (seam-delete-html-files-for-note old)
    366   (seam-export-note new)
    367   (let* ((current-links (seam-get-links-from-file new))
    368          (added-links (cl-set-difference current-links
    369                                          previous-links-from-file
    370                                          :test #'string=))
    371          (removed-links (cl-set-difference previous-links-from-file
    372                                            current-links
    373                                            :test #'string=)))
    374     (let ((type-changed
    375            (not (string= (seam-get-note-type old)
    376                          (seam-get-note-type new)))))
    377       (mapc #'seam-export-note
    378             (delete-dups
    379              (append
    380               removed-links
    381 
    382               ;; Backlinks sections must be updated when either
    383               ;; slug or title changes.
    384               (if slug-or-title-changed
    385                   current-links
    386                 added-links)
    387 
    388               ;; `seam-update-links' inherently triggers
    389               ;; re-exporting of notes when links change.
    390               ;; However, note type is not encoded in the link,
    391               ;; so we must handle that case manually.
    392               (when type-changed
    393                 (seam-get-links-to-file new))))))))
    394 
    395 (defun seam-draft-p (file)
    396   (string-prefix-p "-" (file-name-base file)))
    397 
    398 (defun seam-save-buffer ()
    399   (let* ((old (buffer-file-name))
    400          (type (seam-get-note-type old t))
    401          (draft-p (seam-draft-p old)))
    402     (when type
    403       (unless (seam-get-title-from-buffer)
    404         (error "Note must have a title"))
    405       (let* ((slug (seam-get-slug-from-buffer))
    406              (new (seam-make-file-name slug type draft-p))
    407              (newly-created-p (not (file-exists-p old)))
    408              (slug-changed-p (not (string= slug (file-name-base old))))
    409              (title-changed-p (unless newly-created-p
    410                                 (not (string= (seam-get-title-from-buffer)
    411                                               (seam-get-title-from-file old))))))
    412         (unless (string= old new)       ;This is valid because
    413                                         ;`seam-save-buffer' cannot
    414                                         ;change type or draft status.
    415           (seam--check-conflict slug)
    416           (rename-file old new)
    417           (set-visited-file-name new nil t))
    418         (let ((previous-links-from-file
    419                ;; If we've yet to create the file, don't check it.
    420                (unless newly-created-p
    421                  (seam-get-links-from-file new))))
    422           (let ((write-contents-functions
    423                  (remove 'seam-save-buffer write-contents-functions)))
    424             (save-buffer))
    425           (seam-post-save-or-rename old
    426                                     new
    427                                     previous-links-from-file
    428                                     (or slug-changed-p title-changed-p))
    429           (seam-set-buffer-name)
    430           t)))))
    431 
    432 (defun seam--set-note-type (file new-type interactive)
    433   (let ((old-type (seam-get-note-type file))
    434         (new-file (seam-make-file-name (file-name-base file) new-type)))
    435     (if (string= new-type old-type)
    436         file
    437       (seam--rename-file file new-file interactive)
    438       new-file)))
    439 
    440 ;;;###autoload
    441 (defun seam-set-note-type (file new-type &optional interactive)
    442   "Set Seam note FILE to NEW-TYPE.  Error if file is not a Seam note.
    443 
    444 When called interactively, FILE is the currently visited file.  A
    445 numeric argument N chooses the Nth type in `seam-note-types' (counting
    446 from 1).  Otherwise a completion prompt is given for the desired type."
    447   (interactive
    448    (let* ((file (buffer-file-name))
    449           (old-type (seam-get-note-type file)))
    450      (list file
    451            (or (seam--read-type "New type: "
    452                                 ;; HACK: Treat nil prefix as C-u.
    453                                 (or current-prefix-arg '(4))
    454                                 (remove old-type (seam-get-all-note-type-names)))
    455                old-type)
    456            t)))
    457   (seam--set-note-type file new-type interactive))
    458 
    459 ;;;###autoload
    460 (defun seam-toggle-draft (file &optional interactive)
    461   "Toggle the draft status of Seam note FILE."
    462   (interactive (list (buffer-file-name) t))
    463   (seam-get-note-type file)          ;Error if file isn't a Seam note.
    464   (let* ((base (file-name-nondirectory file))
    465          (new-file (file-name-concat
    466                     (file-name-directory file)
    467                     (if (string-prefix-p "-" base)
    468                         (string-remove-prefix "-" base)
    469                       (concat "-" base)))))
    470     (seam--rename-file file new-file interactive)))
    471 
    472 (defun seam-update-links (old new)
    473   (let* ((old-link (file-name-base old))
    474          (new-link (file-name-base new)))
    475     (unless (string= old-link new-link)
    476       (let ((count (seam-replace-string-in-all-notes
    477                     (format "[[seam:%s]" old-link)
    478                     (format "[[seam:%s]" new-link)
    479                     t)))
    480         (unless (zerop count)
    481           (message "Updated links in %d file%s"
    482                    count (if (= count 1) "" "s")))))))
    483 
    484 (defun seam--active-subset ()
    485   (or seam--subset (seam-get-all-note-type-names)))
    486 
    487 (defun seam-note-subdirectories ()
    488   (cl-loop for type in (seam--active-subset)
    489            collect (expand-file-name
    490                     (file-name-as-directory
    491                      (file-name-concat seam-note-directory type)))))
    492 
    493 (defun seam-note-files-containing-string (string)
    494   "Search all Seam note files for literal STRING.  Case-sensitive."
    495   (seam-ensure-note-subdirectories-exist)
    496   (with-temp-buffer
    497     (apply #'call-process find-program
    498            nil t nil
    499            (append
    500             (seam-note-subdirectories)
    501             (list "-type" "f" "-name" "*.org" "-and" "-not" "-name" ".*"
    502                   "-exec" grep-program "-F" "-l" "-s" "-e" string "{}" "+")))
    503     (string-lines (string-trim (buffer-string)) t)))
    504 
    505 ;;;###autoload
    506 (defun seam-search (query &optional delimited)
    507   "Search all Seam notes for the regexp QUERY (case-insensitively).  If
    508 DELIMITED is non-nil, only search at word boundaries.
    509 
    510 When called interactively, DELIMITED is t if a prefix argument is given.
    511 Otherwise, it's nil."
    512   (interactive (list (read-string (format "Search all notes%s: "
    513                                           (if current-prefix-arg
    514                                               " for word"
    515                                             "")))
    516                      current-prefix-arg))
    517   (when (eq grep-highlight-matches 'auto-detect)
    518     (grep-compute-defaults))
    519   (let ((default-directory seam-note-directory))
    520     (grep
    521      (format "%s %s -type f -name %s -and -not -name %s -exec %s %s -n -i -e %s \\{\\} \\+"
    522              find-program
    523              (string-join (mapcar (lambda (type)
    524                                     (shell-quote-argument (concat type "/")))
    525                                   (seam-get-all-note-type-names))
    526                           " ")
    527              (shell-quote-argument "*.org")
    528              (shell-quote-argument ".*")
    529              grep-program
    530              (if grep-highlight-matches "--color=always" "")
    531              (shell-quote-argument
    532               (if delimited
    533                   (concat "\\b" query "\\b")
    534                 query))))))
    535 
    536 (defun seam-visited-notes ()
    537   (let ((subdirs (seam-note-subdirectories)))
    538     (cl-loop for buf in (buffer-list)
    539              as file = (buffer-file-name buf)
    540              when (and file
    541                        (member (file-name-directory file) subdirs)
    542                        (string-match seam-note-file-regexp file))
    543              collect file)))
    544 
    545 (defun seam-replace-string-in-all-notes (old new preserve-modtime)
    546   (let ((hash (make-hash-table :test 'equal)))
    547     (dolist (file (seam-note-files-containing-string old))
    548       (puthash file nil hash))
    549     (dolist (file (seam-visited-notes))
    550       (puthash file t hash))
    551     (let ((update-count 0))
    552       (maphash
    553        (lambda (file was-open-p)
    554          (with-current-buffer (find-file-noselect file)
    555            (let ((was-modified-p (buffer-modified-p)))
    556              (save-mark-and-excursion
    557                (without-restriction
    558                  (goto-char (point-min))
    559                  (let ((updated-p nil))
    560                    (while (search-forward old nil t)
    561                      (setq updated-p t)
    562                      (replace-match new))
    563                    (when updated-p
    564                      (setq update-count (1+ update-count))))))
    565              (when (and (not was-modified-p)
    566                         (buffer-modified-p))
    567                (if preserve-modtime
    568                    (let ((modtime (visited-file-modtime)))
    569                      (save-buffer)
    570                      (set-file-times file modtime)
    571                      (set-visited-file-modtime modtime))
    572                  (save-buffer)))
    573              (unless was-open-p
    574                (kill-buffer)))))
    575        hash)
    576       update-count)))
    577 
    578 (cl-defun seam-set-buffer-name (&optional (buffer (current-buffer)))
    579   (when-let ((title (seam-get-title-from-buffer)))
    580     (let ((file (buffer-file-name buffer)))
    581       (with-current-buffer buffer
    582         (rename-buffer
    583          (seam-format-title title
    584                             (seam-get-note-type file)
    585                             (seam-draft-p file)))))))
    586 
    587 (defun seam-setup-buffer ()
    588   "Setup hooks when loading a Seam file."
    589   (add-hook 'write-contents-functions 'seam-save-buffer nil t)
    590   ;; NOTE: Needed for when note w/o using Seam commands.  Redundant otherwise.
    591   (seam-set-buffer-name))
    592 
    593 (defun seam--watch-note-directory-var (_symbol newval operation _where)
    594   "Install necessary hooks when `seam-note-directory' is set, removing any
    595 old ones."
    596   (when (member operation '(set makunbound))
    597     (setq dir-locals-directory-cache
    598           (cl-remove 'seam-note-directory dir-locals-directory-cache :key #'cadr))
    599     (when newval
    600       (dir-locals-set-directory-class newval 'seam-note-directory))))
    601 
    602 (defun seam--delete-note (file)
    603   (seam-get-note-type file)           ;Error if file isn't a Seam note.
    604   (let ((to-update (delete-dups
    605                     (append
    606                      (seam-get-links-to-file file)
    607                      (seam-get-links-from-file file)))))
    608     (delete-file file t)
    609     (seam-delete-html-files-for-note file)
    610     (mapc #'seam-export-note to-update)))
    611 
    612 ;;;###autoload
    613 (defun seam-delete-note (file &optional interactive)
    614   "Delete Seam note FILE.  Error if file is not a Seam note.
    615 `delete-by-moving-to-trash' is respected.
    616 
    617 When called interactively, FILE is the currently visited file, and the
    618 buffer is killed after deletion."
    619   (interactive
    620    (let ((file (buffer-file-name)))
    621      (seam-get-note-type file)        ;Error if file isn't a Seam note.
    622      (list
    623       (let ((incoming (length (seam-get-links-to-file file t))))
    624         (and (yes-or-no-p
    625               (format "Really %s `%s' and kill buffer%s?"
    626                       (if delete-by-moving-to-trash
    627                           "trash"
    628                         "delete")
    629                       (seam-get-title-from-buffer)
    630                       (if (> incoming 0)
    631                           (format " (breaking links from %d note%s)"
    632                                   incoming
    633                                   (if (= incoming 1) "" "s"))
    634                         "")))
    635              file))
    636       t)))
    637   (unless (and interactive (null file))
    638     (seam--delete-note file)
    639     (when interactive
    640       (kill-buffer))))
    641 
    642 ;;;###autoload
    643 (defun seam-insert-link ()
    644   "Interactively insert an Org link at point to the given Seam note,
    645 creating the note if it does not exist.  If any text is selected, the
    646 link will replace it."
    647   (interactive)
    648   (cl-destructuring-bind (completion . file) (seam-read-title "Insert note link: ")
    649     (let* ((new-buffer
    650             (unless file
    651               (seam-create-note completion seam-default-note-type nil)))
    652            (selection (when (use-region-p)
    653                         (buffer-substring
    654                          (region-beginning)
    655                          (region-end))))
    656            (file (if new-buffer
    657                      (buffer-file-name new-buffer)
    658                    file))
    659            (slug (file-name-base file))
    660            (initial (or selection
    661                         (seam-get-title-from-file file)))
    662            (desc (read-string "Description: " initial)))
    663       (when selection
    664         (delete-region (region-beginning) (region-end)))
    665       (insert (format "[[seam:%s][%s]]" slug desc))
    666       (when new-buffer
    667         (pop-to-buffer new-buffer)))))
    668 
    669 (defvar-keymap seam-prefix-map
    670   "f" #'seam-find-note
    671   "k" #'seam-delete-note
    672   "l" #'seam-insert-link
    673   "s" #'seam-search
    674   "t" #'seam-set-note-type
    675   "d" #'seam-toggle-draft)
    676 
    677 (org-link-set-parameters "seam" :follow #'seam-link-open)
    678 
    679 (dir-locals-set-class-variables
    680  'seam-note-directory
    681  '((org-mode . ((eval . (seam-setup-buffer))))))
    682 
    683 (add-variable-watcher 'seam-note-directory #'seam--watch-note-directory-var)
    684 
    685 ;;; If `seam-note-directory' was set before loading package, ensure
    686 ;;; directory class is set up.
    687 (when (and seam-note-directory
    688            (not (cl-find 'seam-note-directory dir-locals-directory-cache :key #'cadr)))
    689   (dir-locals-set-directory-class seam-note-directory 'seam-note-directory))
    690 
    691 (provide 'seam)
    692 
    693 ;;; seam.el ends here