;;; gds.el --- Emacs interface to Google Desktop Search ;; ;; Copyright (C) 2006 Mathias Dahl ;; ;; Version: 0.2 ;; Keywords: search, convenience, files, gds, google ;; Author: Mathias Dahl ;; This file is not part of GNU Emacs. ;; GNU Emacs 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 2, or (at your option) ;; any later version. ;; GNU Emacs 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; see the file COPYING. If not, write to the ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;;; Commentary: ;; ;; gds.el provides a couple of commands to interface Google Desktop ;; Search (GDS) from Emacs. ;; ;; gds.el should work on all operating systems where GDS exist. ;; Currently it seems only to exist officially in Microsoft Windows ;; and Mac OSX but there is supposedly some sort of beta for GNU/Linux ;; as well. ;; ;; Basically all it does is to build a URL with the search, send it to ;; GDS and do different fun things with the result. The commands ;; currently implemented are `gds', `gds-iswitchb-open-file' and ;; `gds-dired'. ;; ;; It is easy to use, just customize `gds-search-url' and get going! I ;; recommend starting with the `gds' command. ;; ;;; Todo: ;; ;;; History: ;; ;; Version 0.1, 2006-02-08 ;; ;; * First release. ;; ;; Version 0.1.1, 2006-02-09 ;; ;; * Finished first version of `gds-dired'. ;; ;; Version 0.2, 2007-07-06 ;; ;; * Added the `gds' command. ;; * Made some refactoring. ;; * Added some helper functions. ;; ;;; Code: (require 'url) (require 'xml) (require 'cl) (defcustom gds-search-url "" "Search URL for the GDS installation. You must specify this value. It can be found in the Windows registry, under HKEY_CURRENT_USER\\Software\\Google\\Google Desktop\\AP\I\search_url" :type 'string :group 'gds) (defun gds-search-url () "Get GDS search url. Raise error if empty or nil. Maybe this is ugly, I don't know, but it sure should make the user configure this..." (if (or (string= "" gds-search-url) (not gds-search-url)) (error "Option gds-search-url is empty or nil") gds-search-url)) (defun gds-get-xml-response (search &optional num) "Do a Google Desktop Search for SEARCH, return result as xml. Optional argument NUM is used to set the number of hits to get." (with-temp-buffer (url-insert-file-contents (format "%s%s&format=xml%s" (gds-search-url) search (if num (format "&num=%d" num) ""))) (goto-char (point-min)) (if (not (search-forward-regexp "^<\\?xml" nil t)) (message "No XML data from GDS search") (xml-parse-region (match-beginning 0) (point-max))))) (defcustom gds-ignored-file-names-regexp "" "Regexp of file names to remove from the hit list." :type 'string :group 'gds) (defun gds-get-matching-files (search &optional num) "Search GDS for files containing SEARCH. Returns a list of file names. Optional argument NUM sets the number of search results. Default is 10 hits. Only hits of category \"file\" are returned. File names matching `gds-ignored-file-names-regexp' will be removed." (let ((xml (xml-get-children (car (gds-get-xml-response search num)) 'result)) result results) (while xml (setq result (car xml)) (let ((category (car (xml-node-children (assoc 'category result)))) (file-name (car (xml-node-children (assoc 'url result))))) (if (and (string= "file" category) (not (string-match gds-ignored-file-names-regexp file-name))) (setq results (append results (list file-name))))) (setq xml (cdr xml))) results)) (defvar gds-ignored-categories nil "List of strings of ignored categories. Example of categories are file, web and email. The whole list can be found here: http://desktop.google.com/dev/queryapi.html. For some reason the category can sometimes be nil, so you can add this as an item in the list as well.") (defun gds-get-matching-items (search &optional num) "Search GDS for items containing SEARCH. Returns a list of items names. Optional argument NUM sets the number of search results. Default is 10 hits. Hits with a category in `gds-ignored-categories' will not be returned. File names matching `gds-ignored-file-names-regexp' will be removed." (let ((xml (xml-get-children (car (gds-get-xml-response search num)) 'result)) result results) (while xml (setq result (car xml)) (let ((category (car (xml-node-children (assoc 'category result)))) (file-name (car (xml-node-children (assoc 'url result)))) (title (car (xml-node-children (assoc 'title result)))) (cache-url (car (xml-node-children (assoc 'cache_url result))))) (if (and (not (member category gds-ignored-categories)) (not (and (string= category "file") (string-match gds-ignored-file-names-regexp file-name)))) (setq results (append results (list (list category title file-name cache-url)))))) (setq xml (cdr xml))) results)) (defun gds-browse-url () "Browse to URL given by the title's URL property." (interactive) (browse-url (get-text-property (point) 'url))) (defun gds-mouse-browse-url (start-event) "Browse to URL given by the title's URL property. Argument START-EVENT is not documented." (interactive "e") (gds-browse-url)) (defvar gds-title-map nil "Keymap for the title in the result list.") (progn (setq gds-title-map (make-sparse-keymap)) (define-key gds-title-map (kbd "RET") 'gds-browse-url) (define-key gds-title-map (kbd "") 'gds-mouse-browse-url)) (define-derived-mode gds-display-mode fundamental-mode "GDS Display" "Major mode for browsing hits from GDS") (progn (setq gds-display-mode-map (make-sparse-keymap)) (define-key gds-display-mode-map (kbd "q") 'bury-buffer) (define-key gds-display-mode-map (kbd "TAB") 'gds-find-next-title)) (defface gds-title-face '((t (:foreground "Red" :weight bold))) "*Face used to highlight the title of search results.." :group 'gds :group 'faces) (defface gds-url-face '((t (:foreground "Gray"))) "*Face used to highlight the URL of search results.." :group 'gds :group 'faces) (defcustom gds-max-results 40 "Maximum number of results to search for. Note that this is the number of hits to get from GDS *before* filtering non-file hits and file names matching `gds-ignored-file-names-regexp'. This means that you probably want to make this number higher that the number of files you want to choose from at the prompt." :type 'integer :group 'gds) (defun gds (search-string) "Make a Google Desktop search and display the results. Argument SEARCH-STRING is the string to search for." (interactive "MSearch string: ") (let ((results (gds-get-matching-items search-string gds-max-results)) (buf (get-buffer-create "*gds-search*"))) (switch-to-buffer buf) (gds-display-mode) (toggle-truncate-lines 1) (toggle-read-only 0) (erase-buffer) (insert "\n*** Click mouse-1 or type `RET' on an item title to find the item in GDS. Type `TAB' to jump to the next search result or use the normal movement keys to move around. Type `q' to quit.\n\n") (mapc (lambda (x) (insert (format "%-5s %-60s (%s)\n" (if (car x) (capitalize (car x)) "Other") (propertize (cadr x) 'face 'gds-title-face 'mouse-face 'highlight 'url (cadddr x) 'keymap gds-title-map) (propertize (cadddr x) 'face 'gds-url-face)))) results)) (goto-char (point-min)) (set-buffer-modified-p nil) (toggle-read-only 1) (gds-find-next-title)) (defun gds-next-url-property-change () "Return position of next change in the url property." (next-single-property-change (point) 'url nil (point-max))) (defun gds-find-next-title () "Go to next search result. If at the bottom, wrap around to the top." (interactive) (goto-char (gds-next-url-property-change)) (let (p) (when (not (get-text-property (point) 'url)) (setq p (gds-next-url-property-change)) (if (eq p (point-max)) (progn (goto-char (point-min)) (goto-char (gds-next-url-property-change))) (goto-char p))))) (defcustom gds-iswitchb-open-file-number-of-hits 20 "Number of hits to fetch from GDS when using `gds-iswitchb-open-file'. Note that this is the number of hits to get from GDS *before* filtering non-file hits and file names matching `gds-ignored-file-names-regexp'. This means that you probably want to make this number higher that the number of files you want to choose from at the prompt." :type 'integer :group 'gds) (defun gds-iswitchb-open-file (search) "Use iswitchb to open file matching SEARCH in GDS." (interactive "sSearch for: ") (let ((iswitchb-make-buflist-hook (lambda () (setq iswitchb-temp-buflist (gds-get-matching-files search 20))))) (find-file (iswitchb-read-buffer "Find file: ")))) (defcustom gds-dired-number-of-hits 100 "Number of hits to fetch from GDS when using `gds-dired'. Note that this is the number of hits to get from GDS *before* filtering non-file hits and file names matching `gds-ignored-file-names-regexp'. This means that you probably want to make this number higher that the number of files you want to choose from at the prompt." :type 'integer :group 'gds) (defun gds-dired () "Use Google Desktop Search to find files and list them in dired. It generates a result like `find-dired' does, but uses Google Desktop Search to find matching files." (interactive) (let ((dir (read-directory-name "Set current directory: ")) (search (read-string "Search string: ")) (buf (get-buffer-create "*gds-dired*"))) (switch-to-buffer buf) (kill-all-local-variables) (setq buffer-read-only nil) (erase-buffer) (setq default-directory dir) (dired-mode dir) (use-local-map (append (make-sparse-keymap) (current-local-map))) (define-key (current-local-map) "g" 'undefined) ;; Set subdir-alist so that Tree Dired will work: (if (fboundp 'dired-simple-subdir-alist) ;; will work even with nested dired format (dired-nstd.el,v ;; 1.15 and later) (dired-simple-subdir-alist) ;; else we have an ancient tree dired (or classic dired, where ;; this does no harm) (set (make-local-variable 'dired-subdir-alist) (list (cons default-directory (point-min-marker))))) (setq buffer-read-only nil) (insert " " dir ":\n") ;; Make second line a ``dir'' line in analogy to the ``total'' or ;; ``wildcard'' line. (insert " GDS search results for \"" search "\"\n") (let ((buffer-read-only nil) (saved-pos nil)) (goto-char (point-max)) (setq mode-line-process (concat ": GDS search")) (mapc (lambda (x) (when (file-exists-p x) (insert " ") (insert-directory (expand-file-name x) ""))) (gds-get-matching-files search gds-dired-number-of-hits)) (insert " at " (substring (current-time-string) 0 19)) (force-mode-line-update)) (goto-char (point-min)) (forward-line 1) (dired-next-line 1))) ;; Experimental (defun gds-dired-2 () "Use Google Desktop Search to find files and list them in dired. It generates a result like `find-dired' does, but uses Google Desktop Search to find matching files." (interactive) (let ((dir (read-directory-name "Set current directory: ")) (search (read-string "Search string: "))) (dired (cons dir (gds-get-matching-files search gds-dired-number-of-hits))))) (provide 'gds) ;;; gds.el ends here