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