180 lines
7.2 KiB
EmacsLisp
180 lines
7.2 KiB
EmacsLisp
|
;;; anki-editor-ui.el --- UI for anki-editor -*- lexical-binding: t; -*-
|
||
|
|
||
|
;; Author: orgtre
|
||
|
;; URL: https://github.com/orgtre/anki-editor
|
||
|
;; Package-Requires: ((emacs "25.1"))
|
||
|
|
||
|
;; 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
||
|
;;; Code:
|
||
|
|
||
|
(require 'transient)
|
||
|
(require 'anki-editor)
|
||
|
|
||
|
(defgroup anki-editor-ui nil
|
||
|
"Customizations for anki-editor-ui."
|
||
|
:group 'anki-editor)
|
||
|
|
||
|
(defcustom anki-editor-ui-match-preprompt
|
||
|
"Syntax: [+-&|][tag|{tagregex}|property[=|<>|<|>|<=|>=]value]\n"
|
||
|
"Extra syntax explanation shown before the match prompt."
|
||
|
:type 'string)
|
||
|
|
||
|
(defcustom anki-editor-ui-deck-preprompt
|
||
|
"Use 'TAB' to complete, ',' to select multiple, and 'RET' to finalize.\n"
|
||
|
"Extra syntax explanation shown before the deck prompt."
|
||
|
:type 'string)
|
||
|
|
||
|
(transient-define-prefix anki-editor-ui ()
|
||
|
"Transient for anki-editor."
|
||
|
[["Org"
|
||
|
("i" " insert default note" anki-editor-insert-default-note :level 1)
|
||
|
("t" " set default note type" anki-editor-set-default-note-type
|
||
|
:level 1)
|
||
|
("c" " cloze dwim" anki-editor-cloze-dwim :level 2)]
|
||
|
[""
|
||
|
("I" " insert note" anki-editor-insert-note :level 1)
|
||
|
("T" " set note type" anki-editor-set-note-type :level 1)
|
||
|
("d" " delete note" anki-editor-delete-notes :level 1)]]
|
||
|
[["Push"
|
||
|
("." " note at point " anki-editor-push-note-at-point :level 2)
|
||
|
("n" " new notes " anki-editor-push-new-notes :level 2)
|
||
|
("p" " push with ui" anki-editor-ui-push :level 1)]
|
||
|
[""
|
||
|
("b" " notes in buffer" anki-editor-push-notes :level 2)
|
||
|
("f" " failed notes" anki-editor-retry-failed-notes :level 2)
|
||
|
""]]
|
||
|
[["Anki"
|
||
|
("g" " gui browse " anki-editor-gui-browse :level 2)
|
||
|
("s" " sync AnkiWeb " anki-editor-sync-collection :level 2)]
|
||
|
[""
|
||
|
("G" " gui add card" anki-editor-gui-add-cards :level 3)
|
||
|
("a" " api check" anki-editor-api-check 3)]]
|
||
|
[["Misc"
|
||
|
("h" " subtree to html " anki-editor-export-subtree-to-html
|
||
|
:level 4)
|
||
|
("C" " copy styles " anki-editor-copy-styles :level 4)
|
||
|
("F" " toggle format " anki-editor-toggle-format :level 4)]
|
||
|
[""
|
||
|
("H" " region to html" anki-editor-convert-region-to-html :level 4)
|
||
|
("R" " remove styles" anki-editor-remove-styles :level 4)
|
||
|
""]])
|
||
|
|
||
|
(transient-define-prefix anki-editor-ui-push ()
|
||
|
"Transient for pushing anki-editor notes."
|
||
|
:incompatible '(("new" "failed" "existing"))
|
||
|
["Push which notes?"
|
||
|
("n" " new" "new")
|
||
|
("f" " failed" "failed")
|
||
|
("e" " existing" "existing")
|
||
|
("t" " note-type" "note-type=" anki-editor-ui--read-note-type)
|
||
|
("d" " decks" "decks=" anki-editor-ui--read-decks :multi-value rest)
|
||
|
("m" " match" "match=" anki-editor-ui--read-match)]
|
||
|
["From where?"
|
||
|
("." " point" anki-editor-push-note-at-point)
|
||
|
("r" " active region" anki-editor-ui-push-region :if region-active-p)
|
||
|
("s" " subtree" anki-editor-ui-push-subtree)
|
||
|
("b" " narrowed buffer" anki-editor-ui-push-narrowed-buffer)
|
||
|
("B" " full buffer" anki-editor-ui-push-full-buffer)
|
||
|
("A" " agenda files" anki-editor-ui-push-agenda-files)])
|
||
|
|
||
|
(defun anki-editor-ui--read-note-type (prompt initial-input history)
|
||
|
(completing-read prompt (anki-editor-note-types)
|
||
|
nil nil initial-input history))
|
||
|
|
||
|
(defun anki-editor-ui--read-decks (prompt initial-input history)
|
||
|
(completing-read-multiple (concat anki-editor-ui-deck-preprompt prompt)
|
||
|
(anki-editor-deck-names)
|
||
|
nil nil initial-input history))
|
||
|
|
||
|
(defun anki-editor-ui--read-match (prompt initial-input history)
|
||
|
(read-string (concat anki-editor-ui-match-preprompt prompt)
|
||
|
initial-input history))
|
||
|
|
||
|
(defun anki-editor-ui-push-region (&optional args)
|
||
|
"Used by `anki-editor-ui-push' to push region with ARGS."
|
||
|
(interactive (list (transient-args transient-current-command)))
|
||
|
(anki-editor-ui-push--pass-args-and-push args 'region))
|
||
|
|
||
|
(defun anki-editor-ui-push-subtree (&optional args)
|
||
|
"Used by `anki-editor-ui-push' to push subtree with ARGS."
|
||
|
(interactive (list (transient-args transient-current-command)))
|
||
|
(anki-editor-ui-push--pass-args-and-push args 'tree))
|
||
|
|
||
|
(defun anki-editor-ui-push-narrowed-buffer (&optional args)
|
||
|
"Used by `anki-editor-ui-push' to push narrowed buffer with ARGS."
|
||
|
(interactive (list (transient-args transient-current-command)))
|
||
|
(anki-editor-ui-push--pass-args-and-push args nil))
|
||
|
|
||
|
(defun anki-editor-ui-push-full-buffer (&optional args)
|
||
|
"Used by `anki-editor-ui-push' to push full buffer with ARGS."
|
||
|
(interactive (list (transient-args transient-current-command)))
|
||
|
(anki-editor-ui-push--pass-args-and-push args 'file))
|
||
|
|
||
|
(defun anki-editor-ui-push-agenda-files (&optional args)
|
||
|
"Used by `anki-editor-ui-push' to push agenda files with ARGS."
|
||
|
(interactive (list (transient-args transient-current-command)))
|
||
|
(anki-editor-ui-push--pass-args-and-push args 'agenda))
|
||
|
|
||
|
(defun anki-editor-ui-push--pass-args-and-push (args scope)
|
||
|
"Pass the `transient-args` ARGS to `anki-editor-push-notes`.
|
||
|
Also pass SCOPE."
|
||
|
(let-alist (anki-editor-ui-push--parse-args args)
|
||
|
(anki-editor-push-notes
|
||
|
scope .fullmatch
|
||
|
(when .decks `(apply #'anki-editor-ui--skip-unless-decks
|
||
|
(quote ,.decks))))))
|
||
|
|
||
|
(defun anki-editor-ui-push--parse-args (args)
|
||
|
"Parse the `transient-args` ARGS from `anki-editor-push`.
|
||
|
Return an alist with the full match pattern and deck."
|
||
|
(let ((new (transient-arg-value "new" args))
|
||
|
(failed (transient-arg-value "failed" args))
|
||
|
(existing (transient-arg-value "existing" args))
|
||
|
(note-type (transient-arg-value "note-type=" args))
|
||
|
(decks (alist-get "decks=" args nil nil 'equal))
|
||
|
(match (transient-arg-value "match=" args)))
|
||
|
(let ((fullmatch
|
||
|
(concat
|
||
|
(when new
|
||
|
(concat "+" anki-editor-prop-note-id "=\"\""))
|
||
|
(when failed
|
||
|
(concat "+" anki-editor-prop-failure-reason "<>\"\""))
|
||
|
(when existing
|
||
|
(concat "+" anki-editor-prop-note-id "<>\"\""))
|
||
|
(when note-type
|
||
|
(concat "+" anki-editor-prop-note-type "=\"" note-type "\""))
|
||
|
;; note that the match syntax doesn't allows us to specify
|
||
|
;; several alternative note types here
|
||
|
(when match
|
||
|
(if (string-match "^[-+&|/]" match)
|
||
|
match
|
||
|
(concat "+" match))))))
|
||
|
(list (cons 'fullmatch fullmatch)(cons 'decks decks)))))
|
||
|
|
||
|
(defun anki-editor-ui--skip-unless-decks (&rest filter-decks)
|
||
|
"Skip function passed to `org-map-entries`.
|
||
|
A note (subtree) is skipped unless its deck is in FILTER-DECKS.
|
||
|
We can't just filter by deck using `org-map-entries` match argument,
|
||
|
since we need to turn off property inheritance when mapping notes."
|
||
|
(let ((deck (org-entry-get nil anki-editor-prop-deck t)))
|
||
|
(unless (member deck filter-decks)
|
||
|
(save-excursion
|
||
|
(org-end-of-subtree t)
|
||
|
(point)))))
|
||
|
|
||
|
(provide 'anki-editor-ui)
|
||
|
|
||
|
;;; anki-editor-ui.el ends here
|