diff --git a/zk-desktop.el b/zk-desktop.el index 25cead7..9baddaa 100644 --- a/zk-desktop.el +++ b/zk-desktop.el @@ -52,13 +52,63 @@ The names of all ZK-Desktops should begin with this string." :type 'string) -(defcustom zk-desktop-prefix "" - "String to prepend to note names in ZK-Desktop." +(defcustom zk-desktop-entry-prefix "" + "String to prepend to entries in Zk-Desktop." :type 'string) -(defcustom zk-desktop-invisible-ids t - "If non-nil, IDs will not be visible in the index." - :type 'boolean) +(defcustom zk-desktop-entry-suffix "" + "String to append to entries in Zk-Desktop." + :type 'string) + +(defcustom zk-desktop-entry-format "%t %i" + "Format string for entries in ZK-Desktop. +This is the part of each line in ZK-Desktop buffer that +become buttons (see `zk-desktop-make-buttons'); use +`zk-desktop-entry-prefix' and `zk-desktop-entry-suffix' to +add arbitary text around the entry, and which would not be +part of the buttons themselves. + +See `zk-format-function' and `zk-format-id-and-title' for +valid control strings." + :type 'string) + +(defcustom zk-desktop-make-buttons t + "If non-nil, Zk-Desktop will make buttons. +Possible values are t (make normal buttons), 'invisible +\(make buttons with invisible IDs), or nil (don't make any +buttons)." + :type '(choice (const :tag "Yes" t) + (const :tag "Yes, with invisible IDs" invisible) + (const :tag "No" nil))) + +(make-obsolete-variable 'zk-desktop-invisible-ids 'zk-desktop-make-buttons "0.6") + +(defcustom zk-desktop-mark-missing "<- ID NOT FOUND" + "If non-nil, Zk-Desktop will mark missing IDs. +Possible values are a string for the text of an overlay to +add at the end of lines with missing IDs, non-nil to merely +display their buttons with `zk-desktop-missing-button' face, +or nil to eschew checking for missing IDs at all." + :type '(choice (string :tag "Add overlay text" "<- ID NOT FOUND") + (const :tag "Propertize missing" t) + (const :tag "Do not mark" nil))) + +(defun zk-desktop-line-regexp () + "Return the regexp for the relevant Zk-Desktop lines. +The value is computed from `zk-desktop-entry-prefix', +`zk-desktop-entry-suffix', `zk-desktop-entry-format', and +`zk-id-regexp'. + +Group 1 is the note zk-ID. +Group 2 is the note title. +Group 3 is the entire entry." + (zk--format (concat (regexp-quote zk-desktop-entry-prefix) + "\\(?3:" + (regexp-quote zk-desktop-entry-format) + "\\)" + (regexp-quote zk-desktop-entry-suffix)) + (concat "\\(?1:" zk-id-regexp "\\)") + (concat "\\(?2:" ".*" "\\)"))) ; FIXME: `zk-title-regexp' (PR #68) (defcustom zk-desktop-major-mode nil "Name of major-mode for ZK-Desktop buffers. @@ -96,11 +146,14 @@ To quickly change this setting, call `zk-desktop-add-toggle'." '((t :inherit default)) "Face used for buttons in `zk-desktop-mode'.") +(defface zk-desktop-missing-button + '((t :inherit error)) + "Face used for buttons in `zk-desktop-mode' with missing IDs.") ;;; Declarations -(defvar zk-desktop-current nil) - +(defvar zk-desktop-current nil + "Buffer object of the current Zk-Desktop.") ;;; Embark Integration @@ -130,13 +183,17 @@ To quickly change this setting, call `zk-desktop-add-toggle'." (define-minor-mode zk-desktop-mode "Minor mode for `zk-desktop'." + :lighter " Zk-Desktop" :init-value nil :keymap zk-desktop-map - (zk-desktop-make-buttons) - (when-let ((mode zk-desktop-major-mode)) - (funcall mode)) - ;;(setq truncate-lines t) - (setq-local zk-desktop-mode t)) + (cond (zk-desktop-mode ; enabled + (when zk-desktop-make-buttons + (zk-desktop-make-buttons)) + (when-let ((major-mode zk-desktop-major-mode)) + (funcall major-mode)) + (setq zk-desktop-mode t)) + (t ; disabled + (zk-desktop--clear)))) (eval-and-compile (defvar zk-desktop-button-map @@ -177,28 +234,26 @@ To quickly change this setting, call `zk-desktop-add-toggle'." ;;;###autoload (defun zk-desktop-select () - "Select a ZK-Desktop to work with." + "Select a ZK-Desktop to work with. +Return the buffer object visiting the selected or created +desktop." (interactive) (unless zk-desktop-directory (error "Please set `zk-desktop-directory' first")) (let* ((last-command last-command) (desktop (completing-read "Select or Create ZK-Desktop: " - (directory-files - zk-desktop-directory - nil - (concat - zk-desktop-basename - ".*")) - nil nil nil nil - (concat zk-desktop-basename " "))) + (directory-files zk-desktop-directory + nil + (concat + zk-desktop-basename + ".*")) + nil nil zk-desktop-basename nil)) (file (concat zk-desktop-directory "/" desktop))) - (if (file-exists-p (expand-file-name file)) - (setq zk-desktop-current - (find-file-noselect file)) - (progn - (generate-new-buffer desktop) - (setq zk-desktop-current desktop))) + (setq zk-desktop-current + (if (file-exists-p (expand-file-name file)) + (find-file-noselect file) + (generate-new-buffer desktop))) (with-current-buffer zk-desktop-current (setq require-final-newline 'visit-save) (unless (bound-and-true-p truncate-lines) @@ -218,83 +273,96 @@ To quickly change this setting, call `zk-desktop-add-toggle'." 'read-only t 'front-sticky t 'rear-sticky t + 'button-data nil ; filled by `zk-desktop--make-button' 'keymap zk-desktop-button-map 'action 'zk-desktop-button-action 'face 'zk-desktop-button 'cursor-face 'highlight)) +(defun zk-desktop--make-button () + "Try to make a ZK-Desktop button after point. +Return nil if there are no more buttons to be made in the +buffer. Otherwise, move point after the button created and +return a tuple of button boundaries." + (save-match-data + (when-let* ((beg (point)) + (_ (re-search-forward (zk-desktop-line-regexp) nil t)) + (id (match-string-no-properties 1)) + (id-beg (match-beginning 1)) + (id-end (match-end 1)) + (title (match-string-no-properties 2)) + (button-beg (match-beginning 3)) + (button-end (match-end 3))) + (replace-match (save-match-data + (zk--format zk-desktop-entry-format id title)) + nil t nil 3) + (if (not (eq 'invisible zk-desktop-make-buttons)) + ;; I.e. can add text in front of the button? + (add-text-properties button-beg (1+ button-beg) '(front-sticky nil)) + ;; Make entire link invisible, not just the ID + (goto-char beg) + (when (re-search-forward (zk-link-regexp) (line-end-position) t) + (setq id-beg (match-beginning 0) + id-end (match-end 0))) + ;; I.e. can add text in the rear of invisible IDs, but not in the front? + (add-text-properties id-beg id-end '(invisible t rear-nonsticky t)) + ;; Org-mode requires more drastic measures + (when (eq zk-desktop-major-mode 'org-mode) + (let ((overlay (make-overlay (match-beginning 0) (match-end 0)))) + (overlay-put overlay 'invisible t) + (overlay-put overlay 'type 'zk-desktop)))) + (make-text-button button-beg button-end + 'type 'zk-desktop + 'button-data (list id title nil) ; matches `zk--alist' + 'help-echo zk-desktop-help-echo-function) + (goto-char button-end) + (cons button-beg button-end)))) + +(defun zk-desktop--clear () + "Clear special text properties added by `zk-desktop-make-buttons'. +This removes buttons, overlays, and text properties from the +entire buffer." + (save-excursion + (let ((inhibit-read-only t)) + (remove-overlays (point-min) (point-max) 'type 'zk-desktop) + (set-text-properties (point-min) (point-max) '())))) + ;;;###autoload (defun zk-desktop-make-buttons () - "Re-make buttons in ZK-Desktop." + "Re-make buttons in ZK-Desktop. +If `zk-desktop-make-buttons' is nil, just clear any existing +buttons and overlays." (interactive) - (when (and (string-match-p zk-desktop-basename (buffer-name)) - (file-in-directory-p default-directory zk-desktop-directory)) - (let ((inhibit-read-only t)) + (unless (and (string-match-p zk-desktop-basename (buffer-name)) + (file-in-directory-p default-directory zk-desktop-directory)) + (user-error "Can only make buttons in Zk desktop file; %s isn't" + (buffer-name))) + (let* ((inhibit-read-only t) + (ids (if zk-desktop-mark-missing + (zk--id-list nil (zk--alist)) + nil)) + button-bounds) + (zk-desktop--clear) + (when zk-desktop-make-buttons (save-excursion - ;; replace titles (goto-char (point-min)) - (let* ((zk-alist (zk--alist)) - (ids (zk--id-list))) - (while (re-search-forward zk-id-regexp nil t) - (let* ((beg (line-beginning-position)) - (end (line-end-position)) - (id (progn - (save-match-data - (beginning-of-line) - (when (re-search-forward "\\[\\[" end t) - (replace-match "")) - (when (re-search-forward "]]" end t) - (replace-match ""))) - (match-string-no-properties 1))) - (title (buffer-substring-no-properties beg (match-beginning 0))) - (new-title (when (member id ids) - (concat zk-desktop-prefix - (zk--parse-id 'title id zk-alist) " ")))) - (beginning-of-line) - (if new-title - (unless (string= title new-title) - (progn - (search-forward title end) - (replace-match new-title))) - (progn - (search-forward title end) - (replace-match (propertize title 'face 'error)))) - (end-of-line))) - ;; make buttons - (goto-char (point-min)) - (while (re-search-forward zk-id-regexp nil t) - (let* ((beg (line-beginning-position)) - (end (line-end-position)) - (id (match-string-no-properties 1))) - (if (member id ids) - (progn - (make-text-button beg end - 'type 'zk-desktop - 'help-echo zk-desktop-help-echo-function) - (when zk-desktop-invisible-ids - (beginning-of-line) - ;; find zk-links and plain zk-ids - (if (re-search-forward (zk-link-regexp) (line-end-position) t) - (replace-match - (propertize (match-string 0) 'invisible t) nil t) - (progn - (re-search-forward id) - (replace-match - (propertize id - 'read-only t - 'front-sticky t - 'rear-nonsticky t)) - ;; enable invisibility in org-mode - (overlay-put - (make-overlay (match-beginning 0) (match-end 0)) - 'invisible t)))) - (add-text-properties beg (+ beg 1) - '(front-sticky nil))) - (end-of-line) - (overlay-put (make-overlay (point) (point)) - 'before-string - (propertize" <- ID NOT FOUND" 'font-lock-face 'error)))) - (end-of-line))))))) + (while (setq button-bounds (zk-desktop--make-button)) + (let* ((button-data (get-text-property (car button-bounds) 'button-data)) + (button-id (car button-data))) + (cond ((and (stringp zk-desktop-mark-missing) + (not (member button-id ids))) + (let ((overlay (make-overlay (line-end-position) (line-end-position)))) + (overlay-put overlay 'type 'zk-desktop) + (overlay-put overlay 'before-string + (propertize zk-desktop-mark-missing + 'font-lock-face 'zk-desktop-missing-button)))) + ((and zk-desktop-mark-missing + (not (member button-id ids))) + (add-text-properties (car button-bounds) (cdr button-bounds) + '(face zk-desktop-missing-button))) + (t + ;; do nothing + )))))))) ;;; Utilities @@ -321,68 +389,71 @@ To quickly change this setting, call `zk-desktop-add-toggle'." (with-selected-window win (goto-char pos) (let* ((beg (+ (line-beginning-position) - (length zk-desktop-prefix))) - (end (line-end-position)) - (title (buffer-substring beg end))) + (length zk-desktop-entry-prefix))) + (end (- (line-end-position) + (length zk-desktop-entry-suffix))) + (title (buffer-substring-no-properties beg end))) (format "%s" title))))) ;;; Commands +(defun zk-desktop--gather-items (arg) + "Normalize ARG into a list of files." + (cond ((stringp arg) + (zk--formatter arg zk-desktop-entry-format)) + ((eq major-mode 'zk-index-mode) + (let ((ids (if (use-region-p) + (zk-index--current-id-list (current-buffer) + (region-beginning) + (region-end)) + (zk-index--current-id-list (current-buffer) + (line-beginning-position) + (line-end-position))))) + (zk--formatter ids zk-desktop-entry-format))) + ((zk-file-p) + (zk--formatter buffer-file-name zk-desktop-entry-format)) + (t (user-error "No item to send to desktop")))) + ;;;###autoload -(defun zk-desktop-send-to-desktop (&optional arg) - "Send notes from ZK-Index to ZK-Desktop. -In ZK-Index, works on note at point or notes in active region. -Also works on files or group of files in minibuffer, as ARG, and -on zk-id at point." +(defun zk-desktop-send-to-desktop (&optional items suffix) + "Add ITEMS to the current ZK-Desktop. +In ZK-Index, works on note at point or notes in active +region. Also works on files or group of files in minibuffer, +passed as ITEMS, and on Zk-ID at point. With non-nil SUFFIX, +insert it after each entry. New entries are inserted +according to `zk-desktop-add-pos'. + +See `zk-desktop-entry-format', `zk-desktop-entry-prefix', +and `zk-desktop-entry-suffix' for the format of each line." (interactive) (unless zk-desktop-directory (error "Please set `zk-desktop-directory' first")) (let ((inhibit-read-only t) - buffer - (items - (cond - (arg (zk--formatted-string arg zk-index-format)) - ((eq major-mode 'zk-index-mode) - (if (use-region-p) - (buffer-substring - (save-excursion - (goto-char (region-beginning)) - (line-beginning-position)) - (save-excursion - (goto-char (region-end)) - (line-end-position))) - (buffer-substring - (line-beginning-position) - (line-end-position)))) - ((zk-file-p) - (car - (funcall - zk-index-format-function - (list buffer-file-name)))) - (t (user-error "No item to send to desktop"))))) - (if (and zk-desktop-current - (buffer-live-p (get-buffer zk-desktop-current))) - (setq buffer zk-desktop-current) - (setq buffer (zk-desktop-select))) - (unless (get-buffer buffer) - (generate-new-buffer buffer)) + (items (zk-desktop--gather-items items)) + (buffer (if (buffer-live-p zk-desktop-current) + zk-desktop-current + (zk-desktop-select)))) (with-current-buffer buffer (setq require-final-newline 'visit-save) (pcase zk-desktop-add-pos - ('append (progn - (goto-char (point-max)) - (beginning-of-line) - (when (looking-at-p ".") - (end-of-line) - (newline)))) - ('prepend (progn - (goto-char (point-min)))) + ('append (goto-char (point-max)) + (beginning-of-line) + (when (looking-at-p ".") + (end-of-line) + (newline))) + ('prepend (goto-char (point-min))) ('at-point (goto-char (point)))) - (insert items "\n") + (mapc (lambda (item) + (insert (concat zk-desktop-entry-prefix + item + (or suffix zk-desktop-entry-suffix) + "\n"))) + items) (beginning-of-line) (unless (bound-and-true-p truncate-lines) (toggle-truncate-lines)) - (zk-desktop-mode)) + (when zk-desktop-make-buttons + (zk-desktop--make-button))) (if (eq major-mode 'zk-index-mode) (message "Sent to %s - press D to switch" buffer) (message "Sent to %s" buffer)))) @@ -427,7 +498,7 @@ With prefix-argument, raise ZK-Desktop in other frame." (forward-line 1) (transpose-lines 1) (forward-line -1) - (when zk-desktop-invisible-ids + (when zk-desktop-make-buttons (zk-desktop-make-buttons)))) (defun zk-desktop-move-line-up () @@ -436,7 +507,7 @@ With prefix-argument, raise ZK-Desktop in other frame." (let ((inhibit-read-only t)) (transpose-lines 1) (forward-line -2) - (when zk-desktop-invisible-ids + (when zk-desktop-make-buttons (zk-desktop-make-buttons)))) (defun zk-desktop-delete-region-maybe () @@ -548,8 +619,8 @@ With prefix-argument, raise ZK-Desktop in other frame." (interactive) (let ((inhibit-read-only t)) (yank) - (zk-desktop-make-buttons))) - + (when zk-desktop-make-buttons + (zk-desktop-make-buttons)))) (provide 'zk-desktop) diff --git a/zk-index.el b/zk-index.el index fa324da..f60bf5f 100644 --- a/zk-index.el +++ b/zk-index.el @@ -463,17 +463,26 @@ with query term STRING." (setq zk-index-query-mode-line nil zk-index-query-terms nil)) -(defun zk-index--current-id-list (buf-name) - "Return list of IDs for index in BUF-NAME, as filepaths." - (let (ids) - (with-current-buffer (or buf-name - zk-index-buffer-name) +(defun zk-index--current-id-list (buf-name &optional beg end) + "Return list of IDs for index in BUF-NAME. +If BEG and END are given, only return the IDs in the lines +between those positions, inclusive." + (with-current-buffer buf-name + (let ((beg (if (not beg) + (point-min) + (goto-char beg) + (line-beginning-position))) + (end (if (not end) + (point-max) + (goto-char end) + (line-end-position))) + ids) (save-excursion - (goto-char (point-min)) + (goto-char beg) (save-match-data - (while (re-search-forward zk-id-regexp nil t) - (push (match-string-no-properties 0) ids))) - ids)))) + (while (re-search-forward zk-id-regexp end t) + (push (match-string-no-properties 0) ids)))) + (nreverse ids)))) ;;; Index Sort Functions