org-interactive-query.el 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. ;;; org-interactive-query.el --- Interactive modification of agenda query
  2. ;;
  3. ;; Copyright 2007-2013 Free Software Foundation, Inc.
  4. ;;
  5. ;; Author: Christopher League <league at contrapunctus dot net>
  6. ;; Version: 1.0
  7. ;; Keywords: org, wp
  8. ;;
  9. ;; This file is not part of GNU Emacs.
  10. ;;
  11. ;; This program is free software; you can redistribute it and/or modify
  12. ;; it under the terms of the GNU General Public License as published by
  13. ;; the Free Software Foundation; either version 3, or (at your option)
  14. ;; any later version.
  15. ;;
  16. ;; This program is distributed in the hope that it will be useful,
  17. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. ;; GNU General Public License for more details.
  20. ;;
  21. ;; You should have received a copy of the GNU General Public License
  22. ;; along with this program; if not, write to the Free Software
  23. ;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  24. ;;
  25. ;;; Commentary:
  26. ;;
  27. ;; This library implements interactive modification of a tags/todo query
  28. ;; in the org-agenda. It adds 4 keys to the agenda
  29. ;;
  30. ;; / add a keyword as a positive selection criterion
  31. ;; \ add a keyword as a newgative selection criterion
  32. ;; = clear a keyword from the selection string
  33. ;; ;
  34. (require 'org)
  35. (org-defkey org-agenda-mode-map "=" 'org-agenda-query-clear-cmd)
  36. (org-defkey org-agenda-mode-map "/" 'org-agenda-query-and-cmd)
  37. (org-defkey org-agenda-mode-map ";" 'org-agenda-query-or-cmd)
  38. (org-defkey org-agenda-mode-map "\\" 'org-agenda-query-not-cmd)
  39. ;;; Agenda interactive query manipulation
  40. (defcustom org-agenda-query-selection-single-key t
  41. "Non-nil means query manipulation exits after first change.
  42. When nil, you have to press RET to exit it.
  43. During query selection, you can toggle this flag with `C-c'.
  44. This variable can also have the value `expert'. In this case, the window
  45. displaying the tags menu is not even shown, until you press C-c again."
  46. :group 'org-agenda
  47. :type '(choice
  48. (const :tag "No" nil)
  49. (const :tag "Yes" t)
  50. (const :tag "Expert" expert)))
  51. (defun org-agenda-query-selection (current op table &optional todo-table)
  52. "Fast query manipulation with single keys.
  53. CURRENT is the current query string, OP is the initial
  54. operator (one of \"+|-=\"), TABLE is an alist of tags and
  55. corresponding keys, possibly with grouping information.
  56. TODO-TABLE is a similar table with TODO keywords, should these
  57. have keys assigned to them. If the keys are nil, a-z are
  58. automatically assigned. Returns the new query string, or nil to
  59. not change the current one."
  60. (let* ((fulltable (append table todo-table))
  61. (maxlen (apply 'max (mapcar
  62. (lambda (x)
  63. (if (stringp (car x)) (string-width (car x)) 0))
  64. fulltable)))
  65. (fwidth (+ maxlen 3 1 3))
  66. (ncol (/ (- (window-width) 4) fwidth))
  67. (expert (eq org-agenda-query-selection-single-key 'expert))
  68. (exit-after-next org-agenda-query-selection-single-key)
  69. (done-keywords org-done-keywords)
  70. tbl char cnt e groups ingroup
  71. tg c2 c c1 ntable rtn)
  72. (save-window-excursion
  73. (if expert
  74. (set-buffer (get-buffer-create " *Org tags*"))
  75. (delete-other-windows)
  76. (split-window-vertically)
  77. (org-switch-to-buffer-other-window (get-buffer-create " *Org tags*")))
  78. (erase-buffer)
  79. (org-set-local 'org-done-keywords done-keywords)
  80. (insert "Query: " current "\n")
  81. (org-agenda-query-op-line op)
  82. (insert "\n\n")
  83. (org-fast-tag-show-exit exit-after-next)
  84. (setq tbl fulltable char ?a cnt 0)
  85. (while (setq e (pop tbl))
  86. (cond
  87. ((equal e '(:startgroup))
  88. (push '() groups) (setq ingroup t)
  89. (when (not (= cnt 0))
  90. (setq cnt 0)
  91. (insert "\n"))
  92. (insert "{ "))
  93. ((equal e '(:endgroup))
  94. (setq ingroup nil cnt 0)
  95. (insert "}\n"))
  96. (t
  97. (setq tg (car e) c2 nil)
  98. (if (cdr e)
  99. (setq c (cdr e))
  100. ;; automatically assign a character.
  101. (setq c1 (string-to-char
  102. (downcase (substring
  103. tg (if (= (string-to-char tg) ?@) 1 0)))))
  104. (if (or (rassoc c1 ntable) (rassoc c1 table))
  105. (while (or (rassoc char ntable) (rassoc char table))
  106. (setq char (1+ char)))
  107. (setq c2 c1))
  108. (setq c (or c2 char)))
  109. (if ingroup (push tg (car groups)))
  110. (setq tg (org-add-props tg nil 'face
  111. (cond
  112. ((not (assoc tg table))
  113. (org-get-todo-face tg))
  114. (t nil))))
  115. (if (and (= cnt 0) (not ingroup)) (insert " "))
  116. (insert "[" c "] " tg (make-string
  117. (- fwidth 4 (length tg)) ?\ ))
  118. (push (cons tg c) ntable)
  119. (when (= (setq cnt (1+ cnt)) ncol)
  120. (insert "\n")
  121. (if ingroup (insert " "))
  122. (setq cnt 0)))))
  123. (setq ntable (nreverse ntable))
  124. (insert "\n")
  125. (goto-char (point-min))
  126. (if (and (not expert) (fboundp 'fit-window-to-buffer))
  127. (fit-window-to-buffer))
  128. (setq rtn
  129. (catch 'exit
  130. (while t
  131. (message "[a-z..]:Toggle [SPC]:clear [RET]:accept [TAB]:free%s%s"
  132. (if groups " [!] no groups" " [!]groups")
  133. (if expert " [C-c]:window" (if exit-after-next " [C-c]:single" " [C-c]:multi")))
  134. (setq c (let ((inhibit-quit t)) (read-char-exclusive)))
  135. (cond
  136. ((= c ?\r) (throw 'exit t))
  137. ((= c ?!)
  138. (setq groups (not groups))
  139. (goto-char (point-min))
  140. (while (re-search-forward "[{}]" nil t) (replace-match " ")))
  141. ((= c ?\C-c)
  142. (if (not expert)
  143. (org-fast-tag-show-exit
  144. (setq exit-after-next (not exit-after-next)))
  145. (setq expert nil)
  146. (delete-other-windows)
  147. (split-window-vertically)
  148. (org-switch-to-buffer-other-window " *Org tags*")
  149. (and (fboundp 'fit-window-to-buffer)
  150. (fit-window-to-buffer))))
  151. ((or (= c ?\C-g)
  152. (and (= c ?q) (not (rassoc c ntable))))
  153. (setq quit-flag t))
  154. ((= c ?\ )
  155. (setq current "")
  156. (if exit-after-next (setq exit-after-next 'now)))
  157. ((= c ?\[) ; clear left
  158. (org-agenda-query-decompose current)
  159. (setq current (concat "/" (match-string 2 current)))
  160. (if exit-after-next (setq exit-after-next 'now)))
  161. ((= c ?\]) ; clear right
  162. (org-agenda-query-decompose current)
  163. (setq current (match-string 1 current))
  164. (if exit-after-next (setq exit-after-next 'now)))
  165. ((= c ?\t)
  166. (condition-case nil
  167. (setq current (read-string "Query: " current))
  168. (quit))
  169. (if exit-after-next (setq exit-after-next 'now)))
  170. ;; operators
  171. ((or (= c ?/) (= c ?+)) (setq op "+"))
  172. ((or (= c ?\;) (= c ?|)) (setq op "|"))
  173. ((or (= c ?\\) (= c ?-)) (setq op "-"))
  174. ((= c ?=) (setq op "="))
  175. ;; todos
  176. ((setq e (rassoc c todo-table) tg (car e))
  177. (setq current (org-agenda-query-manip
  178. current op groups 'todo tg))
  179. (if exit-after-next (setq exit-after-next 'now)))
  180. ;; tags
  181. ((setq e (rassoc c ntable) tg (car e))
  182. (setq current (org-agenda-query-manip
  183. current op groups 'tag tg))
  184. (if exit-after-next (setq exit-after-next 'now))))
  185. (if (eq exit-after-next 'now) (throw 'exit t))
  186. (goto-char (point-min))
  187. (beginning-of-line 1)
  188. (delete-region (point) (point-at-eol))
  189. (insert "Query: " current)
  190. (beginning-of-line 2)
  191. (delete-region (point) (point-at-eol))
  192. (org-agenda-query-op-line op)
  193. (goto-char (point-min)))))
  194. (if rtn current nil))))
  195. (defun org-agenda-query-op-line (op)
  196. (insert "Operator: "
  197. (org-agenda-query-op-entry (equal op "+") "/+" "and")
  198. (org-agenda-query-op-entry (equal op "|") ";|" "or")
  199. (org-agenda-query-op-entry (equal op "-") "\\-" "not")
  200. (org-agenda-query-op-entry (equal op "=") "=" "clear")))
  201. (defun org-agenda-query-op-entry (matchp chars str)
  202. (if matchp
  203. (org-add-props (format "[%s %s] " chars (upcase str))
  204. nil 'face 'org-todo)
  205. (format "[%s]%s " chars str)))
  206. (defun org-agenda-query-decompose (current)
  207. (string-match "\\([^/]*\\)/?\\(.*\\)" current))
  208. (defun org-agenda-query-clear (current prefix tag)
  209. (if (string-match (concat prefix "\\b" (regexp-quote tag) "\\b") current)
  210. (replace-match "" t t current)
  211. current))
  212. (defun org-agenda-query-manip (current op groups kind tag)
  213. "Apply an operator to a query string and a tag.
  214. CURRENT is the current query string, OP is the operator, GROUPS is a
  215. list of lists of tags that are mutually exclusive. KIND is 'tag for a
  216. regular tag, or 'todo for a TODO keyword, and TAG is the tag or
  217. keyword string."
  218. ;; If this tag is already in query string, remove it.
  219. (setq current (org-agenda-query-clear current "[-\\+&|]?" tag))
  220. (if (equal op "=") current
  221. ;; When using AND, also remove mutually exclusive tags.
  222. (if (equal op "+")
  223. (loop for g in groups do
  224. (if (member tag g)
  225. (mapc (lambda (x)
  226. (setq current
  227. (org-agenda-query-clear current "\\+" x)))
  228. g))))
  229. ;; Decompose current query into q1 (tags) and q2 (TODOs).
  230. (org-agenda-query-decompose current)
  231. (let* ((q1 (match-string 1 current))
  232. (q2 (match-string 2 current)))
  233. (cond
  234. ((eq kind 'tag)
  235. (concat q1 op tag "/" q2))
  236. ;; It's a TODO; when using AND, drop all other TODOs.
  237. ((equal op "+")
  238. (concat q1 "/+" tag))
  239. (t
  240. (concat q1 "/" q2 op tag))))))
  241. (defun org-agenda-query-global-todo-keys (&optional files)
  242. "Return alist of all TODO keywords and their fast keys, in all FILES."
  243. (let (alist)
  244. (unless (and files (car files))
  245. (setq files (org-agenda-files)))
  246. (save-excursion
  247. (loop for f in files do
  248. (set-buffer (find-file-noselect f))
  249. (loop for k in org-todo-key-alist do
  250. (setq alist (org-agenda-query-merge-todo-key
  251. alist k)))))
  252. alist))
  253. (defun org-agenda-query-merge-todo-key (alist entry)
  254. (let (e)
  255. (cond
  256. ;; if this is not a keyword (:startgroup, etc), ignore it
  257. ((not (stringp (car entry))))
  258. ;; if keyword already exists, replace char if it's null
  259. ((setq e (assoc (car entry) alist))
  260. (when (null (cdr e)) (setcdr e (cdr entry))))
  261. ;; if char already exists, prepend keyword but drop char
  262. ((rassoc (cdr entry) alist)
  263. (message "TRACE POSITION 2")
  264. (setq alist (cons (cons (car entry) nil) alist)))
  265. ;; else, prepend COPY of entry
  266. (t
  267. (setq alist (cons (cons (car entry) (cdr entry)) alist)))))
  268. alist)
  269. (defun org-agenda-query-generic-cmd (op)
  270. "Activate query manipulation with OP as initial operator."
  271. (let ((q (org-agenda-query-selection org-agenda-query-string op
  272. org-tag-alist
  273. (org-agenda-query-global-todo-keys))))
  274. (when q
  275. (setq org-agenda-query-string q)
  276. (org-agenda-redo))))
  277. (defun org-agenda-query-clear-cmd ()
  278. "Activate query manipulation, to clear a tag from the string."
  279. (interactive)
  280. (org-agenda-query-generic-cmd "="))
  281. (defun org-agenda-query-and-cmd ()
  282. "Activate query manipulation, initially using the AND (+) operator."
  283. (interactive)
  284. (org-agenda-query-generic-cmd "+"))
  285. (defun org-agenda-query-or-cmd ()
  286. "Activate query manipulation, initially using the OR (|) operator."
  287. (interactive)
  288. (org-agenda-query-generic-cmd "|"))
  289. (defun org-agenda-query-not-cmd ()
  290. "Activate query manipulation, initially using the NOT (-) operator."
  291. (interactive)
  292. (org-agenda-query-generic-cmd "-"))
  293. (provide 'org-interactive-query)