1 ;;; org-interactive-query.el --- Interactive modification of agenda query
3 ;; Copyright 2007-2012 Free Software Foundation, Inc.
5 ;; Author: Christopher League <league at contrapunctus dot net>
9 ;; This file is not part of GNU Emacs.
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)
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.
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.
28 ;; This library implements interactive modification of a tags/todo query
29 ;; in the org-agenda. It adds 4 keys to the agenda
31 ;; / add a keyword as a positive selection criterion
32 ;; \ add a keyword as a newgative selection criterion
33 ;; = clear a keyword from the selection string
38 (org-defkey org-agenda-mode-map
"=" 'org-agenda-query-clear-cmd
)
39 (org-defkey org-agenda-mode-map
"/" 'org-agenda-query-and-cmd
)
40 (org-defkey org-agenda-mode-map
";" 'org-agenda-query-or-cmd
)
41 (org-defkey org-agenda-mode-map
"\\" 'org-agenda-query-not-cmd
)
43 ;;; Agenda interactive query manipulation
45 (defcustom org-agenda-query-selection-single-key t
46 "Non-nil means query manipulation exits after first change.
47 When nil, you have to press RET to exit it.
48 During query selection, you can toggle this flag with `C-c'.
49 This variable can also have the value `expert'. In this case, the window
50 displaying the tags menu is not even shown, until you press C-c again."
55 (const :tag
"Expert" expert
)))
57 (defun org-agenda-query-selection (current op table
&optional todo-table
)
58 "Fast query manipulation with single keys.
59 CURRENT is the current query string, OP is the initial
60 operator (one of \"+|-=\"), TABLE is an alist of tags and
61 corresponding keys, possibly with grouping information.
62 TODO-TABLE is a similar table with TODO keywords, should these
63 have keys assigned to them. If the keys are nil, a-z are
64 automatically assigned. Returns the new query string, or nil to
65 not change the current one."
66 (let* ((fulltable (append table todo-table
))
67 (maxlen (apply 'max
(mapcar
69 (if (stringp (car x
)) (string-width (car x
)) 0))
71 (fwidth (+ maxlen
3 1 3))
72 (ncol (/ (- (window-width) 4) fwidth
))
73 (expert (eq org-agenda-query-selection-single-key
'expert
))
74 (exit-after-next org-agenda-query-selection-single-key
)
75 (done-keywords org-done-keywords
)
76 tbl char cnt e groups ingroup
77 tg c2 c c1 ntable rtn
)
78 (save-window-excursion
80 (set-buffer (get-buffer-create " *Org tags*"))
81 (delete-other-windows)
82 (split-window-vertically)
83 (org-switch-to-buffer-other-window (get-buffer-create " *Org tags*")))
85 (org-set-local 'org-done-keywords done-keywords
)
86 (insert "Query: " current
"\n")
87 (org-agenda-query-op-line op
)
89 (org-fast-tag-show-exit exit-after-next
)
90 (setq tbl fulltable char ?a cnt
0)
91 (while (setq e
(pop tbl
))
93 ((equal e
'(:startgroup
))
94 (push '() groups
) (setq ingroup t
)
99 ((equal e
'(:endgroup
))
100 (setq ingroup nil cnt
0)
103 (setq tg
(car e
) c2 nil
)
106 ;; automatically assign a character.
107 (setq c1
(string-to-char
109 tg
(if (= (string-to-char tg
) ?
@) 1 0)))))
110 (if (or (rassoc c1 ntable
) (rassoc c1 table
))
111 (while (or (rassoc char ntable
) (rassoc char table
))
112 (setq char
(1+ char
)))
114 (setq c
(or c2 char
)))
115 (if ingroup
(push tg
(car groups
)))
116 (setq tg
(org-add-props tg nil
'face
118 ((not (assoc tg table
))
119 (org-get-todo-face tg
))
121 (if (and (= cnt
0) (not ingroup
)) (insert " "))
122 (insert "[" c
"] " tg
(make-string
123 (- fwidth
4 (length tg
)) ?\
))
124 (push (cons tg c
) ntable
)
125 (when (= (setq cnt
(1+ cnt
)) ncol
)
127 (if ingroup
(insert " "))
129 (setq ntable
(nreverse ntable
))
131 (goto-char (point-min))
132 (if (and (not expert
) (fboundp 'fit-window-to-buffer
))
133 (fit-window-to-buffer))
137 (message "[a-z..]:Toggle [SPC]:clear [RET]:accept [TAB]:free%s%s"
138 (if groups
" [!] no groups" " [!]groups")
139 (if expert
" [C-c]:window" (if exit-after-next
" [C-c]:single" " [C-c]:multi")))
140 (setq c
(let ((inhibit-quit t
)) (read-char-exclusive)))
142 ((= c ?
\r) (throw 'exit t
))
144 (setq groups
(not groups
))
145 (goto-char (point-min))
146 (while (re-search-forward "[{}]" nil t
) (replace-match " ")))
149 (org-fast-tag-show-exit
150 (setq exit-after-next
(not exit-after-next
)))
152 (delete-other-windows)
153 (split-window-vertically)
154 (org-switch-to-buffer-other-window " *Org tags*")
155 (and (fboundp 'fit-window-to-buffer
)
156 (fit-window-to-buffer))))
158 (and (= c ?q
) (not (rassoc c ntable
))))
162 (if exit-after-next
(setq exit-after-next
'now
)))
163 ((= c ?\
[) ; clear left
164 (org-agenda-query-decompose current
)
165 (setq current
(concat "/" (match-string 2 current
)))
166 (if exit-after-next
(setq exit-after-next
'now
)))
167 ((= c ?\
]) ; clear right
168 (org-agenda-query-decompose current
)
169 (setq current
(match-string 1 current
))
170 (if exit-after-next
(setq exit-after-next
'now
)))
173 (setq current
(read-string "Query: " current
))
175 (if exit-after-next
(setq exit-after-next
'now
)))
177 ((or (= c ?
/) (= c ?
+)) (setq op
"+"))
178 ((or (= c ?\
;) (= c ?|)) (setq op "|"))
179 ((or (= c ?
\\) (= c ?-
)) (setq op
"-"))
180 ((= c ?
=) (setq op
"="))
182 ((setq e
(rassoc c todo-table
) tg
(car e
))
183 (setq current
(org-agenda-query-manip
184 current op groups
'todo tg
))
185 (if exit-after-next
(setq exit-after-next
'now
)))
187 ((setq e
(rassoc c ntable
) tg
(car e
))
188 (setq current
(org-agenda-query-manip
189 current op groups
'tag tg
))
190 (if exit-after-next
(setq exit-after-next
'now
))))
191 (if (eq exit-after-next
'now
) (throw 'exit t
))
192 (goto-char (point-min))
193 (beginning-of-line 1)
194 (delete-region (point) (point-at-eol))
195 (insert "Query: " current
)
196 (beginning-of-line 2)
197 (delete-region (point) (point-at-eol))
198 (org-agenda-query-op-line op
)
199 (goto-char (point-min)))))
200 (if rtn current nil
))))
202 (defun org-agenda-query-op-line (op)
204 (org-agenda-query-op-entry (equal op
"+") "/+" "and")
205 (org-agenda-query-op-entry (equal op
"|") ";|" "or")
206 (org-agenda-query-op-entry (equal op
"-") "\\-" "not")
207 (org-agenda-query-op-entry (equal op
"=") "=" "clear")))
209 (defun org-agenda-query-op-entry (matchp chars str
)
211 (org-add-props (format "[%s %s] " chars
(upcase str
))
213 (format "[%s]%s " chars str
)))
215 (defun org-agenda-query-decompose (current)
216 (string-match "\\([^/]*\\)/?\\(.*\\)" current
))
218 (defun org-agenda-query-clear (current prefix tag
)
219 (if (string-match (concat prefix
"\\b" (regexp-quote tag
) "\\b") current
)
220 (replace-match "" t t current
)
223 (defun org-agenda-query-manip (current op groups kind tag
)
224 "Apply an operator to a query string and a tag.
225 CURRENT is the current query string, OP is the operator, GROUPS is a
226 list of lists of tags that are mutually exclusive. KIND is 'tag for a
227 regular tag, or 'todo for a TODO keyword, and TAG is the tag or
229 ;; If this tag is already in query string, remove it.
230 (setq current
(org-agenda-query-clear current
"[-\\+&|]?" tag
))
231 (if (equal op
"=") current
232 ;; When using AND, also remove mutually exclusive tags.
234 (loop for g in groups do
238 (org-agenda-query-clear current
"\\+" x
)))
240 ;; Decompose current query into q1 (tags) and q2 (TODOs).
241 (org-agenda-query-decompose current
)
242 (let* ((q1 (match-string 1 current
))
243 (q2 (match-string 2 current
)))
246 (concat q1 op tag
"/" q2
))
247 ;; It's a TODO; when using AND, drop all other TODOs.
249 (concat q1
"/+" tag
))
251 (concat q1
"/" q2 op tag
))))))
253 (defun org-agenda-query-global-todo-keys (&optional files
)
254 "Return alist of all TODO keywords and their fast keys, in all FILES."
256 (unless (and files
(car files
))
257 (setq files
(org-agenda-files)))
259 (loop for f in files do
260 (set-buffer (find-file-noselect f
))
261 (loop for k in org-todo-key-alist do
262 (setq alist
(org-agenda-query-merge-todo-key
266 (defun org-agenda-query-merge-todo-key (alist entry
)
269 ;; if this is not a keyword (:startgroup, etc), ignore it
270 ((not (stringp (car entry
))))
271 ;; if keyword already exists, replace char if it's null
272 ((setq e
(assoc (car entry
) alist
))
273 (when (null (cdr e
)) (setcdr e
(cdr entry
))))
274 ;; if char already exists, prepend keyword but drop char
275 ((rassoc (cdr entry
) alist
)
276 (message "TRACE POSITION 2")
277 (setq alist
(cons (cons (car entry
) nil
) alist
)))
278 ;; else, prepend COPY of entry
280 (setq alist
(cons (cons (car entry
) (cdr entry
)) alist
)))))
283 (defun org-agenda-query-generic-cmd (op)
284 "Activate query manipulation with OP as initial operator."
285 (let ((q (org-agenda-query-selection org-agenda-query-string op
287 (org-agenda-query-global-todo-keys))))
289 (setq org-agenda-query-string q
)
292 (defun org-agenda-query-clear-cmd ()
293 "Activate query manipulation, to clear a tag from the string."
295 (org-agenda-query-generic-cmd "="))
297 (defun org-agenda-query-and-cmd ()
298 "Activate query manipulation, initially using the AND (+) operator."
300 (org-agenda-query-generic-cmd "+"))
302 (defun org-agenda-query-or-cmd ()
303 "Activate query manipulation, initially using the OR (|) operator."
305 (org-agenda-query-generic-cmd "|"))
307 (defun org-agenda-query-not-cmd ()
308 "Activate query manipulation, initially using the NOT (-) operator."
310 (org-agenda-query-generic-cmd "-"))
312 (provide 'org-interactive-query
)