changeset 96989:691e8bf892cb

* net/xesam.el: New file.
author Michael Albinus <michael.albinus@gmx.de>
date Thu, 24 Jul 2008 21:45:01 +0000
parents 77625aa3eac4
children c9371b294083
files lisp/net/xesam.el
diffstat 1 files changed, 696 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lisp/net/xesam.el	Thu Jul 24 21:45:01 2008 +0000
@@ -0,0 +1,696 @@
+;;; xesam.el --- Xesam interface to search engines.
+
+;; Copyright (C) 2008 Free Software Foundation, Inc.
+
+;; Author: Michael Albinus <michael.albinus@gmx.de>
+;; 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, 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, see
+;; <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package provides an interface to the 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.10
+
+;; 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 support 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 <http://xesam.org/main/XesamUserSearchLanguage>,
+;;   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 <keyword><relation>.  <keyword> 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").  <relation> 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.
+
+;;; 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)
+
+(eval-when-compile
+  (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)
+
+(defvar xesam-debug nil
+  "Insert debug information.")
+
+(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:Alarm" "xesam:Archive" "xesam:Audio"
+    "xesam:AudioList" "xesam:Content" "xesam:DataObject" "xesam:DeletedFile"
+    "xesam:Document" "xesam:Email" "xesam:EmailAttachment" "xesam:Event"
+    "xesam:File" "xesam:FileSystem" "xesam:FreeBusy" "xesam:IMMessage"
+    "xesam:Image" "xesam:Journal" "xesam:Mailbox" "xesam:Media"
+    "xesam:MediaList" "xesam:Message" "xesam:PIM" "xesam:Partition"
+    "xesam:Photo" "xesam:Presentation" "xesam:Project" "xesam:RemoteResource"
+    "xesam:Software" "xesam:SourceCode" "xesam:Spreadsheet" "xesam:Storage"
+    "xesam:Task" "xesam:TextDocument" "xesam:Video" "xesam:Visual"
+    "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
+  "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+<request xmlns=\"http://freedesktop.org/standards/xesam/1.0/query\">
+  <userQuery>
+    %s
+  </userQuery>
+</request>"
+  "The Xesam user query XML.")
+
+(defconst xesam-fulltext-query
+  "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+<request xmlns=\"http://freedesktop.org/standards/xesam/1.0/query\">
+  <query content=\"xesam:Document\" source=\"xesam:File\">
+    <fullText>
+      <string>%s</string>
+    </fullText>
+  </query>
+</request>"
+  "The Xesam fulltext query XML.")
+
+(defun xesam-get-property (engine property)
+  "Return the PROPERTY value of ENGINE."
+  ;; "GetProperty" returns a variant, so we must use the car.
+  (car (dbus-call-method
+	:session (car engine) xesam-path-search
+	xesam-interface-search  "GetProperty"
+	(cdr engine) 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 (dbus-call-method
+	:session (car engine) xesam-path-search
+	xesam-interface-search  "SetProperty"
+	(cdr engine) 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-engine nil)
+(defvar xesam-search nil)
+(defvar xesam-current nil)
+(defvar xesam-count nil)
+(defvar xesam-from nil)
+(defvar xesam-to nil)
+(defvar xesam-refreshing nil)
+
+
+;;; Search engines.
+
+(defvar xesam-search-engines nil
+  "List of available Xesam search engines.
+Every entry is a triple of the unique D-Bus service name of the
+engine, the session identifier, and the display name.  Example:
+
+  \(\(\":1.59\" \"0t1214948020ut358230u0p2698r3912347765k3213849828\" \"Tracker Xesam Service\")
+   \(\":1.27\" \"strigisession1369133069\" \"Strigi Desktop Search\"))
+
+A Xesam-compatible search engine is identified as a queued D-Bus
+service of `xesam-service-search'.")
+
+(defun xesam-delete-search-engine (&rest args)
+  "Removes service from `xesam-search-engines'."
+  (when (and (= (length args) 3) (stringp (car args)))
+    (setq xesam-search-engines
+	  (delete (assoc (car args) xesam-search-engines)
+		  xesam-search-engines))))
+
+(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
+	 (setq engine
+	       (cons service
+		     (dbus-call-method
+		      :session service xesam-path-search
+		      xesam-interface-search "NewSession")))
+	 'append)
+
+	;; Set the "search.live" property; otherwise the search engine
+	;; might refuse to answer.
+;	(xesam-set-property engine "search.live" t)
+
+	;; 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
+	      (cond
+	       ((string-equal vendor-id "Beagle")
+		'("xesam:mimeType" "xesam:url"))
+	       ((string-equal vendor-id "Strigi")
+		'("xesam:author" "xesam:cc" "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"))
+	       ((string-equal vendor-id "TrackerXesamSession")
+		'("xesam:relevancyRating" "xesam:url"))
+	       ;; 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'."
+  ;; Initialize buffer.
+  (setq buffer-read-only t)
+  (let ((inhibit-read-only t))
+    (erase-buffer))
+
+  ;; Keymap.
+  (set-keymap-parent xesam-mode-map widget-keymap)
+  (define-key xesam-mode-map "q" 'quit-window)
+
+  ;; Local variables.
+  (set (make-local-variable 'mode-line-position)  (list "%p (0/0)"))
+  ;; `xesam-engine' and `xesam-search' 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-current) 1)
+  (set (make-local-variable 'xesam-count) 0)
+  (set (make-local-variable 'xesam-from) 1)
+  (set (make-local-variable 'xesam-to) xesam-hits-per-page)
+  ;; `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))
+
+(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-refresh-entry (engine search hit-number)
+  "Refreshes one entry in the search buffer."
+  (let* ((result
+	  (car
+	   (dbus-call-method
+	    :session (car engine) xesam-path-search
+	    xesam-interface-search "GetHits" search 1)))
+	 (snippet)
+	 ;; We must disable this for the time being; the search
+	 ;; engines don't return usable values so far.
+;	  (caaar
+;	   (dbus-ignore-errors
+;	     (dbus-call-method
+;	      :session  (car engine) xesam-path-search
+;	      xesam-interface-search "GetHitData"
+;	      search (list hit-number) '("snippet")))))
+	 widget)
+
+    ;; Create widget.
+    (setq widget (widget-convert 'link))
+
+    ;; Take all results.
+    (dolist (field (xesam-get-property engine "hit.fields"))
+      (when (not (zerop (length (caar result))))
+	(when xesam-debug
+	  (widget-insert (format "%s: %s\n" 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))))
+
+    ;; 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))))
+
+    ;; Last Modified.
+    (when (widget-member 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
+     ;; 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)
+	  (find-file
+	   (url-filename (url-generic-parse-url (widget-value widget))))))
+      (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)
+      (widget-put widget :help-echo (widget-get widget :doc))
+      (with-temp-buffer
+	(insert (widget-get widget :doc))
+	(fill-region-as-paragraph (point-min) (point-max))
+	(widget-put widget :doc (buffer-string))))
+
+    ;; Format the widget.
+    (widget-put
+     widget :format
+     (format "%d. %s%%[%%v%%]\n%s\n" hit-number
+	     (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)
+    (setq mode-line-position
+	  (list (format "%%p (%d/%d)" xesam-current xesam-count)))
+    (redisplay)))
+
+(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 xesam-refreshing
+      (setq xesam-refreshing t)
+      (unwind-protect
+	  ;; `xesam-from' is the first result id to be presented.
+	  ;; `xesam-current' is the last result which has been presented.
+	  ;; `xesam-to' is the upper result to be presented.
+	  ;; All of them are buffer-local variables.
+	  (let ((from (max xesam-from xesam-current))
+		widget next)
+	    ;; Add all result widgets.  The upper boundary is always
+	    ;; computed, because new hits might have arrived while
+	    ;; running.
+	    (while (<= from (min xesam-to xesam-count))
+	      (xesam-refresh-entry engine search from)
+	      (setq next t
+		    from (1+ from)
+		    xesam-current from))
+
+	    ;; Add "NEXT" widget.
+	    (when (and next (> xesam-count xesam-to))
+	      (goto-char (point-max))
+	      (widget-create
+	       'link
+	       :notify
+	       '(lambda (widget &rest ignore)
+		  (setq xesam-from (+ xesam-from xesam-hits-per-page)
+			xesam-to   (+ xesam-to   xesam-hits-per-page))
+		  (widget-delete widget)
+		  (xesam-refresh-search-buffer xesam-engine xesam-search))
+	       "NEXT")
+	      (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))
+		mode-line-position
+		(list (format "%%p (%d/%d)" (1- xesam-current) xesam-count)))
+	  ;; 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 'font-lock-type-face))
+	  (force-mode-line-update)))))))
+
+(defun xesam-new-search (engine query)
+  "Create a new search session.
+ENGINE identifies the search engine.  QUERY is a string in the
+Xesam user query language.  A string, identifying the search, is
+returned."
+  (let* ((service (car engine))
+	 (session (cdr engine))
+	 (search (dbus-call-method
+		  :session service xesam-path-search
+		  xesam-interface-search "NewSearch" session query)))
+    ;; 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)
+    (dbus-call-method
+     :session (car engine) xesam-path-search
+     xesam-interface-search "StartSearch" search)
+    ;; Create the search buffer.
+    (with-current-buffer
+	(generate-new-buffer (xesam-buffer-name service search))
+      (switch-to-buffer-other-window (current-buffer))
+      (xesam-mode)
+      (setq xesam-engine engine
+	    xesam-search search
+	    mode-line-buffer-identification
+	    (xesam-get-property engine "vendor.id"))
+      (when xesam-debug
+	(widget-insert
+	 (format "vendor.id: %s\n"
+		 (xesam-get-property engine "vendor.id"))
+	 (format "vendor.version: %s\n"
+		 (xesam-get-property engine "vendor.version"))
+	 (format "vendor.display: %s\n"
+		 (xesam-get-property engine "vendor.display"))
+	 (format "vendor.xesam: %s\n"
+		 (xesam-get-property engine "vendor.xesam"))
+	 (format "vendor.ontology.fields: %s\n"
+		 (xesam-get-property engine "vendor.ontology.fields"))
+	 (format "vendor.ontology.contents: %s\n"
+		 (xesam-get-property engine "vendor.ontology.contents"))
+	 (format "vendor.ontology.sources: %s\n"
+		 (xesam-get-property engine "vendor.ontology.sources"))
+	 (format "vendor.extensions: %s\n"
+		 (xesam-get-property engine "vendor.extensions"))
+	 (format "vendor.ontologies: %s\n"
+		 (xesam-get-property engine "vendor.ontologies"))
+	 (format "vendor.maxhits: %s\n\n"
+		 (xesam-get-property engine "vendor.maxhits")))))
+
+    ;; Return search id.
+    search))
+
+(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-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-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 (and engine (stringp query))
+      (if (eq xesam-query-type 'user-query)
+	  (xesam-new-search engine (format xesam-user-query query))
+	(xesam-new-search engine (format xesam-fulltext-query query)))
+    ;; There might be no search engine available ATM.
+    (message "No query applied")))
+
+(provide 'xesam)
+
+;;; TODO:
+
+;; * Retrieve several results at once.
+;; * Improve mode-line handling. Show search string etc.
+;; * Minibuffer completion for user queries.
+
+;; * Mid term
+;;   - If available, use ontologies for field selection.
+;;   - Search engines for Emacs bugs database, wikipedia, google,
+;;     yahoo, ebay, ...
+
+;;; xesam.el ends here