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