| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 | ;;; org-interactive-query.el --- Interactive modification of agenda query;;;; Copyright 2007 Free Software Foundation, Inc.;;;; Author: Christopher League <league at contrapunctus dot net>;; Version: 1.0;; Keywords: org, wp;;;; 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, 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 this program; if not, write to the Free Software;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.;;;;; Commentary:;;;; This library implements interactive modification of a tags/todo query;; in the org-agenda.  It adds 4 keys to the agenda;;;; /   add a keyword as a positive selection criterion;; \   add a keyword as a newgative selection criterion;; =   clear a keyword from the selection string;; ;   (require 'org)(org-defkey org-agenda-mode-map "=" 'org-agenda-query-clear-cmd)(org-defkey org-agenda-mode-map "/" 'org-agenda-query-and-cmd)(org-defkey org-agenda-mode-map ";" 'org-agenda-query-or-cmd)(org-defkey org-agenda-mode-map "\\" 'org-agenda-query-not-cmd) ;;; Agenda interactive query manipulation(defcustom org-agenda-query-selection-single-key t  "Non-nil means query manipulation exits after first change.When nil, you have to press RET to exit it.During query selection, you can toggle this flag with `C-c'.This variable can also have the value `expert'.  In this case, the windowdisplaying the tags menu is not even shown, until you press C-c again."  :group 'org-agenda  :type '(choice	  (const :tag "No" nil)	  (const :tag "Yes" t)	  (const :tag "Expert" expert)))(defun org-agenda-query-selection (current op table &optional todo-table)  "Fast query manipulation with single keys.CURRENT is the current query string, OP is the initialoperator (one of \"+|-=\"), TABLE is an alist of tags andcorresponding keys, possibly with grouping information.TODO-TABLE is a similar table with TODO keywords, should thesehave keys assigned to them.  If the keys are nil, a-z areautomatically assigned.  Returns the new query string, or nil tonot change the current one."  (let* ((fulltable (append table todo-table))	 (maxlen (apply 'max (mapcar			      (lambda (x)				(if (stringp (car x)) (string-width (car x)) 0))			      fulltable)))	 (fwidth (+ maxlen 3 1 3))	 (ncol (/ (- (window-width) 4) fwidth))	 (expert (eq org-agenda-query-selection-single-key 'expert))	 (exit-after-next org-agenda-query-selection-single-key)	 (done-keywords org-done-keywords)         tbl char cnt e groups ingroup	 tg c2 c c1 ntable rtn)    (save-window-excursion      (if expert	  (set-buffer (get-buffer-create " *Org tags*"))	(delete-other-windows)	(split-window-vertically)	(org-switch-to-buffer-other-window (get-buffer-create " *Org tags*")))      (erase-buffer)      (org-set-local 'org-done-keywords done-keywords)      (insert "Query:    " current "\n")      (org-agenda-query-op-line op)      (insert "\n\n")      (org-fast-tag-show-exit exit-after-next)      (setq tbl fulltable char ?a cnt 0)      (while (setq e (pop tbl))	(cond	 ((equal e '(:startgroup))	  (push '() groups) (setq ingroup t)	  (when (not (= cnt 0))	    (setq cnt 0)	    (insert "\n"))	  (insert "{ "))	 ((equal e '(:endgroup))	  (setq ingroup nil cnt 0)	  (insert "}\n"))	 (t	  (setq tg (car e) c2 nil)	  (if (cdr e)	      (setq c (cdr e))	    ;; automatically assign a character.	    (setq c1 (string-to-char		      (downcase (substring				 tg (if (= (string-to-char tg) ?@) 1 0)))))	    (if (or (rassoc c1 ntable) (rassoc c1 table))		(while (or (rassoc char ntable) (rassoc char table))		  (setq char (1+ char)))	      (setq c2 c1))	    (setq c (or c2 char)))	  (if ingroup (push tg (car groups)))	  (setq tg (org-add-props tg nil 'face				  (cond				   ((not (assoc tg table))				    (org-get-todo-face tg))				   (t nil))))	  (if (and (= cnt 0) (not ingroup)) (insert "  "))	  (insert "[" c "] " tg (make-string				 (- fwidth 4 (length tg)) ?\ ))	  (push (cons tg c) ntable)	  (when (= (setq cnt (1+ cnt)) ncol)	    (insert "\n")	    (if ingroup (insert "  "))	    (setq cnt 0)))))      (setq ntable (nreverse ntable))      (insert "\n")      (goto-char (point-min))      (if (and (not expert) (fboundp 'fit-window-to-buffer))	  (fit-window-to-buffer))      (setq rtn	    (catch 'exit	      (while t		(message "[a-z..]:Toggle [SPC]:clear [RET]:accept [TAB]:free%s%s"			 (if groups " [!] no groups" " [!]groups")			 (if expert " [C-c]:window" (if exit-after-next " [C-c]:single" " [C-c]:multi")))		(setq c (let ((inhibit-quit t)) (read-char-exclusive)))		(cond		 ((= c ?\r) (throw 'exit t))		 ((= c ?!)		  (setq groups (not groups))		  (goto-char (point-min))		  (while (re-search-forward "[{}]" nil t) (replace-match " ")))		 ((= c ?\C-c)		  (if (not expert)		      (org-fast-tag-show-exit		       (setq exit-after-next (not exit-after-next)))		    (setq expert nil)		    (delete-other-windows)		    (split-window-vertically)		    (org-switch-to-buffer-other-window " *Org tags*")		    (and (fboundp 'fit-window-to-buffer)			 (fit-window-to-buffer))))		 ((or (= c ?\C-g)		      (and (= c ?q) (not (rassoc c ntable))))		  (setq quit-flag t))		 ((= c ?\ )		  (setq current "")		  (if exit-after-next (setq exit-after-next 'now)))		 ((= c ?\[)             ; clear left                  (org-agenda-query-decompose current)                  (setq current (concat "/" (match-string 2 current)))		  (if exit-after-next (setq exit-after-next 'now)))		 ((= c ?\])             ; clear right                  (org-agenda-query-decompose current)                  (setq current (match-string 1 current))		  (if exit-after-next (setq exit-after-next 'now)))		 ((= c ?\t)		  (condition-case nil		      (setq current (read-string "Query: " current))		    (quit))		  (if exit-after-next (setq exit-after-next 'now)))                 ;; operators                 ((or (= c ?/) (= c ?+)) (setq op "+"))                 ((or (= c ?\;) (= c ?|)) (setq op "|"))                 ((or (= c ?\\) (= c ?-)) (setq op "-"))                 ((= c ?=) (setq op "="))                 ;; todos                 ((setq e (rassoc c todo-table) tg (car e))                  (setq current (org-agenda-query-manip                                 current op groups 'todo tg))                  (if exit-after-next (setq exit-after-next 'now)))                 ;; tags                 ((setq e (rassoc c ntable) tg (car e))                  (setq current (org-agenda-query-manip                                 current op groups 'tag tg))                  (if exit-after-next (setq exit-after-next 'now))))		(if (eq exit-after-next 'now) (throw 'exit t))		(goto-char (point-min))		(beginning-of-line 1)		(delete-region (point) (point-at-eol))                (insert "Query:    " current)                (beginning-of-line 2)                (delete-region (point) (point-at-eol))                (org-agenda-query-op-line op)		(goto-char (point-min)))))      (if rtn current nil))))(defun org-agenda-query-op-line (op)  (insert "Operator: "          (org-agenda-query-op-entry (equal op "+") "/+" "and")          (org-agenda-query-op-entry (equal op "|") ";|" "or")          (org-agenda-query-op-entry (equal op "-") "\\-" "not")          (org-agenda-query-op-entry (equal op "=") "=" "clear")))(defun org-agenda-query-op-entry (matchp chars str)  (if matchp      (org-add-props (format "[%s %s]  " chars (upcase str))          nil 'face 'org-todo)    (format "[%s]%s   " chars str)))(defun org-agenda-query-decompose (current)  (string-match "\\([^/]*\\)/?\\(.*\\)" current))(defun org-agenda-query-clear (current prefix tag)  (if (string-match (concat prefix "\\b" (regexp-quote tag) "\\b") current)      (replace-match "" t t current)    current))(defun org-agenda-query-manip (current op groups kind tag)  "Apply an operator to a query string and a tag.CURRENT is the current query string, OP is the operator, GROUPS is alist of lists of tags that are mutually exclusive.  KIND is 'tag for aregular tag, or 'todo for a TODO keyword, and TAG is the tag orkeyword string."  ;; If this tag is already in query string, remove it.  (setq current (org-agenda-query-clear current "[-\\+&|]?" tag))  (if (equal op "=") current    ;; When using AND, also remove mutually exclusive tags.    (if (equal op "+")        (loop for g in groups do              (if (member tag g)                  (mapc (lambda (x)                          (setq current                                (org-agenda-query-clear current "\\+" x)))                        g))))    ;; Decompose current query into q1 (tags) and q2 (TODOs).    (org-agenda-query-decompose current)    (let* ((q1 (match-string 1 current))           (q2 (match-string 2 current)))      (cond       ((eq kind 'tag)        (concat q1 op tag "/" q2))       ;; It's a TODO; when using AND, drop all other TODOs.       ((equal op "+")        (concat q1 "/+" tag))       (t        (concat q1 "/" q2 op tag))))))(defun org-agenda-query-global-todo-keys (&optional files)  "Return alist of all TODO keywords and their fast keys, in all FILES."  (let (alist)    (unless (and files (car files))      (setq files (org-agenda-files)))    (save-excursion      (loop for f in files do            (set-buffer (find-file-noselect f))            (loop for k in org-todo-key-alist do                  (setq alist (org-agenda-query-merge-todo-key                               alist k)))))    alist))(defun org-agenda-query-merge-todo-key (alist entry)  (let (e)    (cond     ;; if this is not a keyword (:startgroup, etc), ignore it     ((not (stringp (car entry))))     ;; if keyword already exists, replace char if it's null     ((setq e (assoc (car entry) alist))      (when (null (cdr e)) (setcdr e (cdr entry))))     ;; if char already exists, prepend keyword but drop char     ((rassoc (cdr entry) alist)      (message "TRACE POSITION 2")      (setq alist (cons (cons (car entry) nil) alist)))     ;; else, prepend COPY of entry     (t      (setq alist (cons (cons (car entry) (cdr entry)) alist)))))  alist)(defun org-agenda-query-generic-cmd (op)  "Activate query manipulation with OP as initial operator."  (let ((q (org-agenda-query-selection org-agenda-query-string op                                       org-tag-alist                                        (org-agenda-query-global-todo-keys))))    (when q      (setq org-agenda-query-string q)      (org-agenda-redo))))(defun org-agenda-query-clear-cmd ()  "Activate query manipulation, to clear a tag from the string."  (interactive)  (org-agenda-query-generic-cmd "="))(defun org-agenda-query-and-cmd ()  "Activate query manipulation, initially using the AND (+) operator."  (interactive)  (org-agenda-query-generic-cmd "+"))(defun org-agenda-query-or-cmd ()  "Activate query manipulation, initially using the OR (|) operator."  (interactive)  (org-agenda-query-generic-cmd "|"))(defun org-agenda-query-not-cmd ()  "Activate query manipulation, initially using the NOT (-) operator."  (interactive)  (org-agenda-query-generic-cmd "-"))(provide 'org-interactive-query)
 |