;;; xesam.el --- Xesam interface to search engines. ;; Copyright (C) 2008, 2009 Free Software Foundation, Inc. ;; Author: Michael Albinus ;; Keywords: tools, hypermedia ;; This file is 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 3 of the License, 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. If not, see . ;;; Commentary: ;; This package provides an interface to Xesam, a D-Bus based "eXtEnsible ;; Search And Metadata specification". It has been tested with ;; ;; xesam-glib 0.3.4, xesam-tools 0.6.1 ;; beagle 0.3.7, beagle-xesam 0.2 ;; strigi 0.5.11 ;; The precondition for this package is a D-Bus aware Emacs. This is ;; configured per default, when Emacs is built on a machine running ;; D-Bus. Furthermore, there must be at least one search engine ;; running, which supports the Xesam interface. Beagle and strigi have ;; been tested; tracker, pinot and recoll are also said to support ;; Xesam. You can check the existence of such a search engine by ;; ;; (dbus-list-queued-owners :session "org.freedesktop.xesam.searcher") ;; In order to start a search, you must load xesam.el: ;; ;; (require 'xesam) ;; xesam.el supports two types of queries, which are explained *very* short: ;; ;; * Full text queries. Just search keys shall be given, like ;; ;; hello world ;; ;; A full text query in xesam.el is restricted to files. ;; ;; * Xesam End User Search Language queries. The Xesam query language ;; is described at , ;; which must be consulted for the whole features. ;; ;; A query string consists of search keys, collectors, selectors, ;; and phrases. Search keys are words like in a full text query: ;; ;; hello word ;; ;; A selector is a tuple . can be any ;; predefined Xesam keyword, the most common keywords are "ext" ;; (file name extension), "format " (mime type), "tag" (user ;; keywords) and "type" (types of items, like "audio", "file", ;; "picture", "attachment"). is a comparison to a value, ;; which must be a string (relation ":" or "=") or number (relation ;; "<=", ">=", "<", ">"): ;; ;; type:attachment ext=el ;; ;; A collector is one of the items "AND", "and", "&&", "OR", "or", ;; "||", or "-". The default collector on multiple terms is "AND"; ;; "-" means "AND NOT". ;; ;; albinus -type:file ;; ;; A phrase is a string enclosed in quotes, with appended modifiers ;; (single letters). Examples of modifiers are "c" (case ;; sensitive), "C" (case insensitive), "e" (exact match), "r" ;; (regular expression): ;; ;; "Hello world"c ;; You can customize, whether you want to apply a Xesam user query, or ;; a full text query. Note, that not every search engine supports ;; both query types. ;; ;; (setq xesam-query-type 'fulltext-query) ;; ;; Another option to be customised is the number of hits to be ;; presented at once. ;; ;; (setq xesam-hits-per-page 50) ;; A search can be started by the command ;; ;; M-x xesam-search ;; ;; When several search engines are registered, the engine to be used ;; can be selected via minibuffer completion. Afterwards, the query ;; shall be entered in the minibuffer. ;; Search results are presented in a new buffer. This buffer has the ;; major mode `xesam-mode', with the following keybindings: ;; SPC `scroll-up' ;; DEL `scroll-down' ;; < `beginning-of-buffer' ;; > `end-of-buffer' ;; q `quit-window' ;; z `kill-this-buffer' ;; g `revert-buffer' ;; The search results are represented by widgets. Navigation commands ;; are the usual widget navigation commands: ;; TAB `widget-forward' ;; `widget-backward' ;; Applying RET, , or on a URL belonging ;; to the widget, brings up more details of the search hit. The way, ;; how this hit is presented, depends on the type of the hit. HTML ;; files are opened via `browse-url'. Local files are opened in a new ;; buffer, with highlighted search hits (highlighting can be toggled ;; by `xesam-minor-mode' in that buffer). ;;; Code: ;; D-Bus support in the Emacs core can be disabled with configuration ;; option "--without-dbus". Declare used subroutines and variables. (declare-function dbus-call-method "dbusbind.c") (declare-function dbus-register-signal "dbusbind.c") (require 'dbus) ;; Pacify byte compiler. (eval-when-compile (require 'cl)) ;; Widgets are used to highlight the search results. (require 'widget) (require 'wid-edit) ;; `run-at-time' is used in the signal handler. (require 'timer) ;; The default search field is "xesam:url". It must be inspected. (require 'url) (defgroup xesam nil "Xesam compatible interface to search engines." :group 'extensions :group 'hypermedia :version "23.1") (defcustom xesam-query-type 'user-query "Xesam query language type." :group 'xesam :type '(choice (const :tag "Xesam user query" user-query) (const :tag "Xesam fulltext query" fulltext-query))) (defcustom xesam-hits-per-page 20 "Number of search hits to be displayed in the result buffer." :group 'xesam :type 'integer) (defface xesam-mode-line '((t :inherit mode-line-emphasis)) "Face to highlight mode line." :group 'xesam) (defface xesam-highlight '((t :inherit match)) "Face to highlight query entries. It will be overlayed by `widget-documentation-face', so it shall be different at least in one face property not set in that face." :group 'xesam) (defvar xesam-debug nil "Insert debug information in the help echo.") (defconst xesam-service-search "org.freedesktop.xesam.searcher" "The D-Bus name used to talk to Xesam.") (defconst xesam-path-search "/org/freedesktop/xesam/searcher/main" "The D-Bus object path used to talk to Xesam.") ;; Methods: "NewSession", "SetProperty", "GetProperty", ;; "CloseSession", "NewSearch", "StartSearch", "GetHitCount", ;; "GetHits", "GetHitData", "CloseSearch" and "GetState". ;; Signals: "HitsAdded", "HitsRemoved", "HitsModified", "SearchDone" ;; and "StateChanged". (defconst xesam-interface-search "org.freedesktop.xesam.Search" "The D-Bus Xesam search interface.") (defconst xesam-all-fields '("xesam:35mmEquivalent" "xesam:aimContactMedium" "xesam:aperture" "xesam:aspectRatio" "xesam:attachmentEncoding" "xesam:attendee" "xesam:audioBirate" "xesam:audioChannels" "xesam:audioCodec" "xesam:audioCodecType" "xesam:audioSampleFormat" "xesam:audioSampleRate" "xesam:author" "xesam:bcc" "xesam:birthDate" "xesam:blogContactURL" "xesam:cameraManufacturer" "xesam:cameraModel" "xesam:cc" "xesam:ccdWidth" "xesam:cellPhoneNumber" "xesam:characterCount" "xesam:charset" "xesam:colorCount" "xesam:colorSpace" "xesam:columnCount" "xesam:comment" "xesam:commentCharacterCount" "xesam:conflicts" "xesam:contactMedium" "xesam:contactName" "xesam:contactNick" "xesam:contactPhoto" "xesam:contactURL" "xesam:contains" "xesam:contenKeyword" "xesam:contentComment" "xesam:contentCreated" "xesam:contentModified" "xesam:contentType" "xesam:contributor" "xesam:copyright" "xesam:creator" "xesam:definesClass" "xesam:definesFunction" "xesam:definesGlobalVariable" "xesam:deletionTime" "xesam:depends" "xesam:description" "xesam:device" "xesam:disclaimer" "xesam:documentCategory" "xesam:duration" "xesam:emailAddress" "xesam:eventEnd" "xesam:eventLocation" "xesam:eventStart" "xesam:exposureBias" "xesam:exposureProgram" "xesam:exposureTime" "xesam:faxPhoneNumber" "xesam:fileExtension" "xesam:fileSystemType" "xesam:flashUsed" "xesam:focalLength" "xesam:focusDistance" "xesam:formatSubtype" "xesam:frameCount" "xesam:frameRate" "xesam:freeSpace" "xesam:gender" "xesam:generator" "xesam:generatorOptions" "xesam:group" "xesam:hash" "xesam:hash" "xesam:height" "xesam:homeEmailAddress" "xesam:homePhoneNumber" "xesam:homePostalAddress" "xesam:homepageContactURL" "xesam:horizontalResolution" "xesam:icqContactMedium" "xesam:id" "xesam:imContactMedium" "xesam:interests" "xesam:interlaceMode" "xesam:isEncrypted" "xesam:isImportant" "xesam:isInProgress" "xesam:isPasswordProtected" "xesam:isRead" "xesam:isoEquivalent" "xesam:jabberContactMedium" "xesam:keyword" "xesam:language" "xesam:legal" "xesam:license" "xesam:licenseType" "xesam:lineCount" "xesam:links" "xesam:mailingPostalAddress" "xesam:maintainer" "xesam:md5Hash" "xesam:mediaCodec" "xesam:mediaCodecBitrate" "xesam:mediaCodecType" "xesam:meteringMode" "xesam:mimeType" "xesam:mountPoint" "xesam:msnContactMedium" "xesam:name" "xesam:occupiedSpace" "xesam:orientation" "xesam:originalLocation" "xesam:owner" "xesam:pageCount" "xesam:permissions" "xesam:phoneNumber" "xesam:physicalAddress" "xesam:pixelFormat" "xesam:primaryRecipient" "xesam:programmingLanguage" "xesam:rating" "xesam:receptionTime" "xesam:recipient" "xesam:related" "xesam:remoteUser" "xesam:rowCount" "xesam:sampleBitDepth" "xesam:sampleFormat" "xesam:secondaryRecipient" "xesam:sha1Hash" "xesam:size" "xesam:skypeContactMedium" "xesam:sourceCreated" "xesam:sourceModified" "xesam:storageSize" "xesam:subject" "xesam:supercedes" "xesam:title" "xesam:to" "xesam:totalSpace" "xesam:totalUncompressedSize" "xesam:url" "xesam:usageIntensity" "xesam:userComment" "xesam:userKeyword" "xesam:uuid" "xesam:version" "xesam:verticalResolution" "xesam:videoBirate" "xesam:videoCodec" "xesam:videoCodecType" "xesam:whiteBalance" "xesam:width" "xesam:wordCount" "xesam:workEmailAddress" "xesam:workPhoneNumber" "xesam:workPostalAddress" "xesam:yahooContactMedium") "All fields from the Xesam Core Ontology. This defconst can be used to check for a new search engine, which fields are supported.") (defconst xesam-user-query " %s " "The Xesam user query XML.") (defconst xesam-fulltext-query " %s " "The Xesam fulltext query XML.") (declare-function dbus-get-unique-name "dbusbind.c" (bus)) (defvar xesam-dbus-unique-names (list (cons :system (dbus-get-unique-name :system)) (cons :session (dbus-get-unique-name :session))) "The unique names, under which Emacs is registered at D-Bus.") (defun xesam-dbus-call-method (&rest args) "Apply a D-Bus method call. `dbus-call-method' is to be preferred, because it is more performant. If the target D-Bus service is owned by Emacs, this is not applicable, and `dbus-call-method-non-blocking' must be used instead. ARGS are identical to the argument list of both functions." (apply ;; The first argument is the bus, the second argument the targt service. (if (string-equal (cdr (assoc (car args) xesam-dbus-unique-names)) (cadr args)) 'dbus-call-method-non-blocking 'dbus-call-method) args)) (defun xesam-get-property (engine property) "Return the PROPERTY value of ENGINE." ;; "GetProperty" returns a variant, so we must use the car. (car (xesam-dbus-call-method :session (car engine) xesam-path-search xesam-interface-search "GetProperty" (xesam-get-cached-property engine "session") property))) (defun xesam-set-property (engine property value) "Set the PROPERTY of ENGINE to VALUE. VALUE can be a string, a non-negative integer, a boolean value (nil or t), or a list of them. It returns the new value of PROPERTY in the search engine. This new value can be different from VALUE, depending on what the search engine accepts." ;; "SetProperty" returns a variant, so we must use the car. (car (xesam-dbus-call-method :session (car engine) xesam-path-search xesam-interface-search "SetProperty" (xesam-get-cached-property engine "session") property ;; The value must be a variant. It can be only a string, an ;; unsigned int, a boolean, or an array of them. So we need ;; no type keyword; we let the type check to the search ;; engine. (list :variant value)))) (defvar xesam-minibuffer-vendor-history nil "Interactive vendor history.") (defvar xesam-minibuffer-query-history nil "Interactive query history.") ;; Pacify byte compiler. (defvar xesam-vendor nil) (make-variable-buffer-local 'xesam-vendor) (put 'xesam-vendor 'permanent-local t) (defvar xesam-engine nil) (defvar xesam-search nil) (defvar xesam-type nil) (defvar xesam-query nil) (defvar xesam-xml-string nil) (defvar xesam-objects nil) (defvar xesam-current nil) (defvar xesam-count nil) (defvar xesam-to nil) (defvar xesam-notify-function nil) (defvar xesam-refreshing nil) ;;; Search engines. (defvar xesam-search-engines nil "List of available Xesam search engines. Every entry is an association list, with a car denoting the unique D-Bus service name of the engine. The rest of the entry are cached associations of engine attributes, like the session identifier, and the display name. Example: \(\(\":1.59\" \(\"session\" . \"0t1214948020ut358230u0p2698r3912347765k3213849828\") \(\"vendor.display\" . \"Tracker Xesam Service\")) \(\":1.27\" \(\"session\" . \"strigisession1369133069\") \(\"vendor.display\" . \"Strigi Desktop Search\"))) A Xesam-compatible search engine is identified as a queued D-Bus service of the known service `xesam-service-search'.") (defun xesam-get-cached-property (engine property) "Return the PROPERTY value of ENGINE from the cache. If PROPERTY is not existing, retrieve it from ENGINE first." ;; If the property has not been cached yet, we retrieve it from the ;; engine, and cache it. (unless (assoc property engine) (xesam-set-cached-property engine property (xesam-get-property engine property))) (cdr (assoc property engine))) (defun xesam-set-cached-property (engine property value) "Set the PROPERTY of ENGINE to VALUE in the cache." (setcdr engine (append (cdr engine) (list (cons property value))))) (defun xesam-delete-search-engine (&rest args) "Remove service from `xesam-search-engines'." (setq xesam-search-engines (delete (assoc (car args) xesam-search-engines) xesam-search-engines))) (defvar dbus-debug) (defun xesam-search-engines () "Return Xesam search engines, stored in `xesam-search-engines'. The first search engine is the name owner of `xesam-service-search'. If there is no registered search engine at all, the function returns `nil'." (let ((services (dbus-ignore-errors (dbus-list-queued-owners :session xesam-service-search))) engine vendor-id hit-fields) (dolist (service services) (unless (assoc-string service xesam-search-engines) ;; Open a new session, and add it to the search engines list. (add-to-list 'xesam-search-engines (list service) 'append) (setq engine (assoc service xesam-search-engines)) ;; Add the session string. (xesam-set-cached-property engine "session" (xesam-dbus-call-method :session service xesam-path-search xesam-interface-search "NewSession")) ;; Unset the "search.live" property; we don't want to be ;; informed by changed results. (xesam-set-property engine "search.live" nil) ;; Check the vendor properties. (setq vendor-id (xesam-get-property engine "vendor.id") hit-fields (xesam-get-property engine "hit.fields")) ;; Ususally, `hit.fields' shall describe supported fields. ;; That is not the case now, so we set it ourselves. ;; Hopefully, this will change later. (setq hit-fields (case (intern vendor-id) ('Beagle '("xesam:mimeType" "xesam:url")) ('Strigi '("xesam:author" "xesam:cc" "xesam:charset" "xesam:contentType" "xesam:fileExtension" "xesam:id" "xesam:lineCount" "xesam:links" "xesam:mimeType" "xesam:name" "xesam:size" "xesam:sourceModified" "xesam:subject" "xesam:to" "xesam:url")) ('TrackerXesamSession '("xesam:relevancyRating" "xesam:url")) ('Debbugs '("xesam:keyword" "xesam:owner" "xesam:title" "xesam:url" "xesam:sourceModified" "xesam:mimeType" "debbugs:key")) ;; xesam-tools yahoo service. (t '("xesam:contentModified" "xesam:mimeType" "xesam:summary" "xesam:title" "xesam:url" "yahoo:displayUrl")))) (xesam-set-property engine "hit.fields" hit-fields) (xesam-set-property engine "hit.fields.extended" '("xesam:snippet")) ;; Let us notify, when the search engine disappears. (dbus-register-signal :session dbus-service-dbus dbus-path-dbus dbus-interface-dbus "NameOwnerChanged" 'xesam-delete-search-engine service)))) xesam-search-engines) ;;; Search buffers. (define-derived-mode xesam-mode nil "Xesam" "Major mode for presenting search results of a Xesam search. In this mode, widgets represent the search results. \\{xesam-mode-map} Turning on Xesam mode runs the normal hook `xesam-mode-hook'. It can be used to set `xesam-notify-function', which must a search engine specific, widget :notify function to visualize xesam:url." (set (make-local-variable 'xesam-notify-function) nil) ;; Keymap. (setq xesam-mode-map (copy-keymap special-mode-map)) (set-keymap-parent xesam-mode-map widget-keymap) (define-key xesam-mode-map "z" 'kill-this-buffer) ;; Maybe we implement something useful, later on. (set (make-local-variable 'revert-buffer-function) 'ignore) ;; `xesam-engine', `xesam-search', `xesam-type', `xesam-query', and ;; `xesam-xml-string' will be set in `xesam-new-search'. (set (make-local-variable 'xesam-engine) nil) (set (make-local-variable 'xesam-search) nil) (set (make-local-variable 'xesam-type) "") (set (make-local-variable 'xesam-query) "") (set (make-local-variable 'xesam-xml-string) "") (set (make-local-variable 'xesam-objects) nil) ;; `xesam-current' is the last hit put into the search buffer, (set (make-local-variable 'xesam-current) 0) ;; `xesam-count' is the number of hits reported by the search engine. (set (make-local-variable 'xesam-count) 0) ;; `xesam-to' is the upper hit number to be presented. (set (make-local-variable 'xesam-to) xesam-hits-per-page) ;; `xesam-notify-function' can be a search engine specific function ;; to visualize xesam:url. It can be overwritten in `xesam-mode'. (set (make-local-variable 'xesam-notify-function) nil) ;; `xesam-refreshing' is an indicator, whether the buffer is just ;; being updated. Needed, because `xesam-refresh-search-buffer' ;; can be triggered by an event. (set (make-local-variable 'xesam-refreshing) nil) ;; Mode line position returns hit counters. (set (make-local-variable 'mode-line-position) (list '(-3 "%p%") '(10 (:eval (format " (%d/%d)" xesam-current xesam-count))))) ;; Header line contains the query string. (set (make-local-variable 'header-line-format) (list '(20 (:eval (list "Type: " (propertize xesam-type 'face 'xesam-mode-line)))) '(10 (:eval (list " Query: " (propertize xesam-query 'face 'xesam-mode-line 'help-echo (when xesam-debug xesam-xml-string))))))) (when (not (interactive-p)) ;; Initialize buffer. (setq buffer-read-only t) (let ((inhibit-read-only t)) (erase-buffer)))) ;; It doesn't make sense to call it interactively. (put 'xesam-mode 'disabled t) ;; The very first buffer created with `xesam-mode' does not have the ;; keymap etc. So we create a dummy buffer. Stupid. (with-temp-buffer (xesam-mode)) (define-minor-mode xesam-minor-mode "Toggle Xesam minor mode. With no argument, this command toggles the mode. Non-null prefix argument turns on the mode. Null prefix argument turns off the mode. When Xesam minor mode is enabled, all text which matches a previous Xesam query in this buffer is highlighted." :group 'xesam :init-value nil :lighter " Xesam" (when (local-variable-p 'xesam-query) ;; Run only if the buffer is related to a Xesam search. (save-excursion (if xesam-minor-mode ;; Highlight hits. (let ((query-regexp (regexp-opt (split-string xesam-query nil t) t)) (case-fold-search t)) ;; I have no idea whether people will like setting ;; `isearch-case-fold-search' and `query-regexp'. Maybe ;; this shall be controlled by a custom option. (unless isearch-case-fold-search (isearch-toggle-case-fold)) (isearch-update-ring query-regexp t) ;; Create overlays. (goto-char (point-min)) (while (re-search-forward query-regexp nil t) (overlay-put (make-overlay (match-beginning 0) (match-end 0)) 'face 'xesam-highlight))) ;; Remove overlays. (dolist (ov (overlays-in (point-min) (point-max))) (delete-overlay ov)))))) (defun xesam-buffer-name (service search) "Return the buffer name where to present search results. SERVICE is the D-Bus unique service name of the Xesam search engine. SEARCH is the search identification in that engine. Both must be strings." (format "*%s/%s*" service search)) (defun xesam-highlight-string (string) "Highlight text enclosed by and . Return propertized STRING." (while (string-match "\\(.*\\)\\(\\)\\(.*\\)\\(\\)\\(.*\\)" string) (setq string (format "%s%s%s" (match-string 1 string) (propertize (match-string 3 string) 'face 'xesam-highlight) (match-string 5 string)))) string) (defun xesam-refresh-entry (engine entry) "Refreshes one entry in the search buffer." (let* ((result (nth (1- xesam-current) xesam-objects)) widget) ;; Create widget. (setq widget (widget-convert 'link)) (when xesam-debug (widget-put widget :help-echo "")) ;; Take all results. (dolist (field (xesam-get-cached-property engine "hit.fields")) (when (cond ((stringp (caar result)) (not (zerop (length (caar result))))) ((numberp (caar result)) (not (zerop (caar result)))) ((caar result) t)) (when xesam-debug (widget-put widget :help-echo (format "%s%s: %s\n" (widget-get widget :help-echo) field (caar result)))) (widget-put widget (intern (concat ":" field)) (caar result))) (setq result (cdr result))) ;; Strigi doesn't return URLs in xesam:url. We must fix this. (when (not (url-type (url-generic-parse-url (widget-get widget :xesam:url)))) (widget-put widget :xesam:url (concat "file://" (widget-get widget :xesam:url)))) ;; Strigi returns xesam:size as string. We must fix this. (when (and (widget-member widget :xesam:size) (stringp (widget-get widget :xesam:size))) (widget-put widget :xesam:size (string-to-number (widget-get widget :xesam:url)))) ;; First line: :tag. (cond ((widget-member widget :xesam:title) (widget-put widget :tag (widget-get widget :xesam:title))) ((widget-member widget :xesam:subject) (widget-put widget :tag (widget-get widget :xesam:subject))) ((widget-member widget :xesam:mimeType) (widget-put widget :tag (widget-get widget :xesam:mimeType))) ((widget-member widget :xesam:name) (widget-put widget :tag (widget-get widget :xesam:name)))) ;; Highlight the search items. (when (widget-member widget :tag) (widget-put widget :tag (xesam-highlight-string (widget-get widget :tag)))) ;; Last Modified. (when (and (widget-member widget :xesam:sourceModified) (not (zerop (string-to-number (widget-get widget :xesam:sourceModified))))) (widget-put widget :tag (format "%s\nLast Modified: %s" (or (widget-get widget :tag) "") (format-time-string "%d %B %Y, %T" (seconds-to-time (string-to-number (widget-get widget :xesam:sourceModified))))))) ;; Second line: :value. (widget-put widget :value (widget-get widget :xesam:url)) (cond ;; A search engine can set `xesam-notify-function' via ;; `xesam-mode-hooks'. (xesam-notify-function (widget-put widget :notify xesam-notify-function)) ;; In case of HTML, we use a URL link. ((and (widget-member widget :xesam:mimeType) (string-equal "text/html" (widget-get widget :xesam:mimeType))) (setcar widget 'url-link)) ;; For local files, we will open the file as default action. ((string-match "file" (url-type (url-generic-parse-url (widget-get widget :xesam:url)))) (widget-put widget :notify (lambda (widget &rest ignore) (let ((query xesam-query)) (find-file (url-filename (url-generic-parse-url (widget-value widget)))) (set (make-local-variable 'xesam-query) query) (xesam-minor-mode 1)))) (widget-put widget :value (url-filename (url-generic-parse-url (widget-get widget :xesam:url)))))) ;; Third line: :doc. (cond ((widget-member widget :xesam:summary) (widget-put widget :doc (widget-get widget :xesam:summary))) ((widget-member widget :xesam:snippet) (widget-put widget :doc (widget-get widget :xesam:snippet)))) (when (widget-member widget :doc) (with-temp-buffer (insert (xesam-highlight-string (widget-get widget :doc))) (fill-region-as-paragraph (point-min) (point-max)) (widget-put widget :doc (buffer-string))) (widget-put widget :help-echo (widget-get widget :doc))) ;; Format the widget. (widget-put widget :format (format "%d. %s%%[%%v%%]\n%s\n" xesam-current (if (widget-member widget :tag) "%{%t%}\n" "") (if (widget-member widget :doc) "%h" ""))) ;; Write widget. (goto-char (point-max)) (widget-default-create widget) (set-buffer-modified-p nil) (force-mode-line-update) (redisplay))) (defun xesam-get-hits (engine search hits) "Retrieve hits from ENGINE." (with-current-buffer (xesam-buffer-name (car engine) search) (setq xesam-objects (append xesam-objects (xesam-dbus-call-method :session (car engine) xesam-path-search xesam-interface-search "GetHits" search hits))))) (defun xesam-refresh-search-buffer (engine search) "Refreshes the buffer, presenting results of SEARCH." (with-current-buffer (xesam-buffer-name (car engine) search) ;; Work only if nobody else is here. (unless (or xesam-refreshing (>= xesam-current xesam-to)) (setq xesam-refreshing t) (unwind-protect (let (widget) ;; Retrieve needed hits for visualization. (while (> (min xesam-to xesam-count) (length xesam-objects)) (xesam-get-hits engine search (min xesam-hits-per-page (- (min xesam-to xesam-count) (length xesam-objects))))) ;; Add all result widgets. (while (< xesam-current (min xesam-to xesam-count)) (setq xesam-current (1+ xesam-current)) (xesam-refresh-entry engine search)) ;; Add "NEXT" widget. (when (> xesam-count xesam-to) (goto-char (point-max)) (widget-create 'link :notify (lambda (widget &rest ignore) (setq xesam-to (+ xesam-to xesam-hits-per-page)) (widget-delete widget) (xesam-refresh-search-buffer xesam-engine xesam-search)) "NEXT") (widget-beginning-of-line)) ;; Prefetch next hits. (when (> (min (+ xesam-hits-per-page xesam-to) xesam-count) (length xesam-objects)) (xesam-get-hits engine search (min xesam-hits-per-page (- (min (+ xesam-hits-per-page xesam-to) xesam-count) (length xesam-objects))))) ;; Add "DONE" widget. (when (= xesam-current xesam-count) (goto-char (point-max)) (widget-create 'link :notify 'ignore "DONE") (widget-beginning-of-line))) ;; Return with save settings. (setq xesam-refreshing nil))))) ;;; Search functions. (defun xesam-signal-handler (&rest args) "Handles the different D-Bus signals of a Xesam search." (let* ((service (dbus-event-service-name last-input-event)) (member (dbus-event-member-name last-input-event)) (search (nth 0 args)) (buffer (xesam-buffer-name service search))) (when (get-buffer buffer) (with-current-buffer buffer (cond ((string-equal member "HitsAdded") (setq xesam-count (+ xesam-count (nth 1 args))) ;; We use `run-at-time' in order to not block the event queue. (run-at-time 0 nil 'xesam-refresh-search-buffer (assoc service xesam-search-engines) search)) ((string-equal member "SearchDone") (setq mode-line-process (propertize " Done" 'face 'xesam-mode-line)) (force-mode-line-update))))))) (defun xesam-kill-buffer-function () "Send the CloseSearch indication." (when (and (eq major-mode 'xesam-mode) (stringp xesam-search)) (ignore-errors ;; The D-Bus service could have disappeared. (xesam-dbus-call-method :session (car xesam-engine) xesam-path-search xesam-interface-search "CloseSearch" xesam-search)))) (defun xesam-new-search (engine type query) "Create a new search session. ENGINE identifies the search engine. TYPE is the query type, it can be either `fulltext-query', or `user-query'. QUERY is a string in the Xesam query language. A string, identifying the search, is returned." (let* ((service (car engine)) (session (xesam-get-cached-property engine "session")) (xml-string (format (if (eq type 'user-query) xesam-user-query xesam-fulltext-query) (url-insert-entities-in-string query))) (search (xesam-dbus-call-method :session service xesam-path-search xesam-interface-search "NewSearch" session xml-string))) ;; Let us notify for relevant signals. We ignore "HitsRemoved", ;; "HitsModified" and "StateChanged"; there is nothing to do for ;; us. (dbus-register-signal :session service xesam-path-search xesam-interface-search "HitsAdded" 'xesam-signal-handler search) (dbus-register-signal :session service xesam-path-search xesam-interface-search "SearchDone" 'xesam-signal-handler search) ;; Create the search buffer. (with-current-buffer (generate-new-buffer (xesam-buffer-name service search)) (switch-to-buffer-other-window (current-buffer)) ;; Inialize buffer with `xesam-mode'. `xesam-vendor' must be ;; set before calling `xesam-mode', because we want to give the ;; hook functions a chance to identify their search engine. (setq xesam-vendor (xesam-get-cached-property engine "vendor.id")) (xesam-mode) (setq xesam-engine engine xesam-search search ;; `xesam-type', `xesam-query' and `xesam-xml-string' ;; are displayed in the header line. xesam-type (symbol-name type) xesam-query query xesam-xml-string xml-string xesam-objects nil ;; The buffer identification shall indicate the search ;; engine. The `help-echo' property is used for debug ;; information, when applicable. mode-line-buffer-identification (if (not xesam-debug) (list 12 (propertized-buffer-identification xesam-vendor)) (propertize xesam-vendor 'help-echo (mapconcat (lambda (x) (format "%s: %s" x (xesam-get-cached-property engine x))) '("vendor.id" "vendor.version" "vendor.display" "vendor.xesam" "vendor.ontology.fields" "vendor.ontology.contents" "vendor.ontology.sources" "vendor.extensions" "vendor.ontologies" "vendor.maxhits") "\n")))) (add-hook 'kill-buffer-hook 'xesam-kill-buffer-function) (force-mode-line-update)) ;; Start the search. (xesam-dbus-call-method :session (car engine) xesam-path-search xesam-interface-search "StartSearch" search) ;; Return search id. search)) ;;;###autoload (defun xesam-search (engine query) "Perform an interactive search. ENGINE is the Xesam search engine to be applied, it must be one of the entries of `xesam-search-engines'. QUERY is the search string in the Xesam user query language. If the search engine does not support the Xesam user query language, a Xesam fulltext search is applied. The default search engine is the first entry in `xesam-search-engines'. Example: (xesam-search (car (xesam-search-engines)) \"emacs\")" (interactive (let* ((vendors (mapcar (lambda (x) (xesam-get-cached-property x "vendor.display")) (xesam-search-engines))) (vendor (if (> (length vendors) 1) (completing-read "Enter search engine: " vendors nil t (try-completion "" vendors) 'xesam-minibuffer-vendor-history) (car vendors)))) (list ;; ENGINE. (when vendor (dolist (elt (xesam-search-engines) engine) (when (string-equal (xesam-get-cached-property elt "vendor.display") vendor) (setq engine elt)))) ;; QUERY. (when vendor (read-from-minibuffer "Enter search string: " nil nil nil 'xesam-minibuffer-query-history))))) (if (null engine) (message "No search engine running") (if (zerop (length query)) (message "No query applied") (xesam-new-search engine xesam-query-type query)))) (provide 'xesam) ;;; TODO: ;; * Buffer highlighting needs better analysis of query string. ;; * Accept input while retrieving prefetched hits. `run-at-time'? ;; * With prefix, let's choose search engine. ;; * Minibuffer completion for user queries. ;; * `revert-buffer-function' implementation. ;; ;; * Mid term ;; - If available, use ontologies for field selection. ;; - Search engines for Emacs bugs database, wikipedia, google, ;; yahoo, ebay, ... ;; - Construct complex queries via widgets, like in mairix.el. ;; arch-tag: 7fb9fc6c-c2ff-4bc7-bb42-bacb80cce2b2 ;;; xesam.el ends here