]> code.delx.au - gnu-emacs/blob - lisp/net/xesam.el
* net/xesam.el (xesam-highlight-string): Precise doc string.
[gnu-emacs] / lisp / net / xesam.el
1 ;;; xesam.el --- Xesam interface to search engines.
2
3 ;; Copyright (C) 2008 Free Software Foundation, Inc.
4
5 ;; Author: Michael Albinus <michael.albinus@gmx.de>
6 ;; Keywords: tools, hypermedia
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This package provides an interface to Xesam, a D-Bus based "eXtEnsible
26 ;; Search And Metadata specification". It has been tested with
27 ;;
28 ;; xesam-glib 0.3.4, xesam-tools 0.6.1
29 ;; beagle 0.3.7, beagle-xesam 0.2
30 ;; strigi 0.5.11
31
32 ;; The precondition for this package is a D-Bus aware Emacs. This is
33 ;; configured per default, when Emacs is built on a machine running
34 ;; D-Bus. Furthermore, there must be at least one search engine
35 ;; running, which supports the Xesam interface. Beagle and strigi have
36 ;; been tested; tracker, pinot and recoll are also said to support
37 ;; Xesam. You can check the existence of such a search engine by
38 ;;
39 ;; (dbus-list-queued-owners :session "org.freedesktop.xesam.searcher")
40
41 ;; In order to start a search, you must load xesam.el:
42 ;;
43 ;; (require 'xesam)
44
45 ;; xesam.el supports two types of queries, which are explained *very* short:
46 ;;
47 ;; * Full text queries. Just search keys shall be given, like
48 ;;
49 ;; hello world
50 ;;
51 ;; A full text query in xesam.el is restricted to files.
52 ;;
53 ;; * Xesam End User Search Language queries. The Xesam query language
54 ;; is described at <http://xesam.org/main/XesamUserSearchLanguage>,
55 ;; which must be consulted for the whole features.
56 ;;
57 ;; A query string consists of search keys, collectors, selectors,
58 ;; and phrases. Search keys are words like in a full text query:
59 ;;
60 ;; hello word
61 ;;
62 ;; A selector is a tuple <keyword><relation>. <keyword> can be any
63 ;; predefined Xesam keyword, the most common keywords are "ext"
64 ;; (file name extension), "format " (mime type), "tag" (user
65 ;; keywords) and "type" (types of items, like "audio", "file",
66 ;; "picture", "attachment"). <relation> is a comparison to a value,
67 ;; which must be a string (relation ":" or "=") or number (relation
68 ;; "<=", ">=", "<", ">"):
69 ;;
70 ;; type:attachment ext=el
71 ;;
72 ;; A collector is one of the items "AND", "and", "&&", "OR", "or",
73 ;; "||", or "-". The default collector on multiple terms is "AND";
74 ;; "-" means "AND NOT".
75 ;;
76 ;; albinus -type:file
77 ;;
78 ;; A phrase is a string enclosed in quotes, with appended modifiers
79 ;; (single letters). Examples of modifiers are "c" (case
80 ;; sensitive), "C" (case insensitive), "e" (exact match), "r"
81 ;; (regular expression):
82 ;;
83 ;; "Hello world"c
84
85 ;; You can customize, whether you want to apply a Xesam user query, or
86 ;; a full text query. Note, that not every search engine supports
87 ;; both query types.
88 ;;
89 ;; (setq xesam-query-type 'fulltext-query)
90 ;;
91 ;; Another option to be customised is the number of hits to be
92 ;; presented at once.
93 ;;
94 ;; (setq xesam-hits-per-page 50)
95
96 ;; A search can be started by the command
97 ;;
98 ;; M-x xesam-search
99 ;;
100 ;; When several search engines are registered, the engine to be used
101 ;; can be selected via minibuffer completion. Afterwards, the query
102 ;; shall be entered in the minibuffer.
103
104 ;;; Code:
105
106 ;; D-Bus support in the Emacs core can be disabled with configuration
107 ;; option "--without-dbus". Declare used subroutines and variables.
108 (declare-function dbus-call-method "dbusbind.c")
109 (declare-function dbus-register-signal "dbusbind.c")
110
111 (require 'dbus)
112
113 ;; Pacify byte compiler.
114 (eval-when-compile
115 (require 'cl))
116
117 ;; Widgets are used to highlight the search results.
118 (require 'widget)
119 (require 'wid-edit)
120
121 ;; `run-at-time' is used in the signal handler.
122 (require 'timer)
123
124 ;; The default search field is "xesam:url". It must be inspected.
125 (require 'url)
126
127 (defgroup xesam nil
128 "Xesam compatible interface to search engines."
129 :group 'extensions
130 :group 'hypermedia
131 :version "23.1")
132
133 (defcustom xesam-query-type 'user-query
134 "Xesam query language type."
135 :group 'xesam
136 :type '(choice
137 (const :tag "Xesam user query" user-query)
138 (const :tag "Xesam fulltext query" fulltext-query)))
139
140 (defcustom xesam-hits-per-page 20
141 "Number of search hits to be displayed in the result buffer."
142 :group 'xesam
143 :type 'integer)
144
145 (defface xesam-mode-line '((t :inherit mode-line-emphasis))
146 "Face to highlight mode line."
147 :group 'xesam)
148
149 (defface xesam-highlight '((t :inherit match))
150 "Face to highlight query entries.
151 It will be overlayed by `widget-documentation-face', so it shall
152 be different at least in one face property not set in that face."
153 :group 'xesam)
154
155 (defvar xesam-debug nil
156 "Insert debug information in the help echo.")
157
158 (defconst xesam-service-search "org.freedesktop.xesam.searcher"
159 "The D-Bus name used to talk to Xesam.")
160
161 (defconst xesam-path-search "/org/freedesktop/xesam/searcher/main"
162 "The D-Bus object path used to talk to Xesam.")
163
164 ;; Methods: "NewSession", "SetProperty", "GetProperty",
165 ;; "CloseSession", "NewSearch", "StartSearch", "GetHitCount",
166 ;; "GetHits", "GetHitData", "CloseSearch" and "GetState".
167 ;; Signals: "HitsAdded", "HitsRemoved", "HitsModified", "SearchDone"
168 ;; and "StateChanged".
169 (defconst xesam-interface-search "org.freedesktop.xesam.Search"
170 "The D-Bus Xesam search interface.")
171
172 (defconst xesam-all-fields
173 '("xesam:35mmEquivalent" "xesam:aimContactMedium" "xesam:aperture"
174 "xesam:aspectRatio" "xesam:attachmentEncoding" "xesam:attendee"
175 "xesam:audioBirate" "xesam:audioChannels" "xesam:audioCodec"
176 "xesam:audioCodecType" "xesam:audioSampleFormat" "xesam:audioSampleRate"
177 "xesam:author" "xesam:bcc" "xesam:birthDate" "xesam:blogContactURL"
178 "xesam:cameraManufacturer" "xesam:cameraModel" "xesam:cc" "xesam:ccdWidth"
179 "xesam:cellPhoneNumber" "xesam:characterCount" "xesam:charset"
180 "xesam:colorCount" "xesam:colorSpace" "xesam:columnCount" "xesam:comment"
181 "xesam:commentCharacterCount" "xesam:conflicts" "xesam:contactMedium"
182 "xesam:contactName" "xesam:contactNick" "xesam:contactPhoto"
183 "xesam:contactURL" "xesam:contains" "xesam:contenKeyword"
184 "xesam:contentComment" "xesam:contentCreated" "xesam:contentModified"
185 "xesam:contentType" "xesam:contributor" "xesam:copyright" "xesam:creator"
186 "xesam:definesClass" "xesam:definesFunction" "xesam:definesGlobalVariable"
187 "xesam:deletionTime" "xesam:depends" "xesam:description" "xesam:device"
188 "xesam:disclaimer" "xesam:documentCategory" "xesam:duration"
189 "xesam:emailAddress" "xesam:eventEnd" "xesam:eventLocation"
190 "xesam:eventStart" "xesam:exposureBias" "xesam:exposureProgram"
191 "xesam:exposureTime" "xesam:faxPhoneNumber" "xesam:fileExtension"
192 "xesam:fileSystemType" "xesam:flashUsed" "xesam:focalLength"
193 "xesam:focusDistance" "xesam:formatSubtype" "xesam:frameCount"
194 "xesam:frameRate" "xesam:freeSpace" "xesam:gender" "xesam:generator"
195 "xesam:generatorOptions" "xesam:group" "xesam:hash" "xesam:hash"
196 "xesam:height" "xesam:homeEmailAddress" "xesam:homePhoneNumber"
197 "xesam:homePostalAddress" "xesam:homepageContactURL"
198 "xesam:horizontalResolution" "xesam:icqContactMedium" "xesam:id"
199 "xesam:imContactMedium" "xesam:interests" "xesam:interlaceMode"
200 "xesam:isEncrypted" "xesam:isImportant" "xesam:isInProgress"
201 "xesam:isPasswordProtected" "xesam:isRead" "xesam:isoEquivalent"
202 "xesam:jabberContactMedium" "xesam:keyword" "xesam:language" "xesam:legal"
203 "xesam:license" "xesam:licenseType" "xesam:lineCount" "xesam:links"
204 "xesam:mailingPostalAddress" "xesam:maintainer" "xesam:md5Hash"
205 "xesam:mediaCodec" "xesam:mediaCodecBitrate" "xesam:mediaCodecType"
206 "xesam:meteringMode" "xesam:mimeType" "xesam:mountPoint"
207 "xesam:msnContactMedium" "xesam:name" "xesam:occupiedSpace"
208 "xesam:orientation" "xesam:originalLocation" "xesam:owner"
209 "xesam:pageCount" "xesam:permissions" "xesam:phoneNumber"
210 "xesam:physicalAddress" "xesam:pixelFormat" "xesam:primaryRecipient"
211 "xesam:programmingLanguage" "xesam:rating" "xesam:receptionTime"
212 "xesam:recipient" "xesam:related" "xesam:remoteUser" "xesam:rowCount"
213 "xesam:sampleBitDepth" "xesam:sampleFormat" "xesam:secondaryRecipient"
214 "xesam:sha1Hash" "xesam:size" "xesam:skypeContactMedium"
215 "xesam:sourceCreated" "xesam:sourceModified" "xesam:storageSize"
216 "xesam:subject" "xesam:supercedes" "xesam:title" "xesam:to"
217 "xesam:totalSpace" "xesam:totalUncompressedSize" "xesam:url"
218 "xesam:usageIntensity" "xesam:userComment" "xesam:userKeyword"
219 "xesam:uuid" "xesam:version" "xesam:verticalResolution" "xesam:videoBirate"
220 "xesam:videoCodec" "xesam:videoCodecType" "xesam:whiteBalance"
221 "xesam:width" "xesam:wordCount" "xesam:workEmailAddress"
222 "xesam:workPhoneNumber" "xesam:workPostalAddress"
223 "xesam:yahooContactMedium")
224 "All fields from the Xesam Core Ontology.
225 This defconst can be used to check for a new search engine, which
226 fields are supported.")
227
228 (defconst xesam-user-query
229 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
230 <request xmlns=\"http://freedesktop.org/standards/xesam/1.0/query\">
231 <userQuery>
232 %s
233 </userQuery>
234 </request>"
235 "The Xesam user query XML.")
236
237 (defconst xesam-fulltext-query
238 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
239 <request xmlns=\"http://freedesktop.org/standards/xesam/1.0/query\">
240 <query content=\"xesam:Document\" source=\"xesam:File\">
241 <fullText>
242 <string>%s</string>
243 </fullText>
244 </query>
245 </request>"
246 "The Xesam fulltext query XML.")
247
248 (defvar xesam-dbus-unique-names
249 (list (cons :system (dbus-get-unique-name :system))
250 (cons :session (dbus-get-unique-name :session)))
251 "The unique names, under which Emacs is registered at D-Bus.")
252
253 (defun xesam-dbus-call-method (&rest args)
254 "Apply a D-Bus method call.
255 `dbus-call-method' is to be preferred, because it is more
256 performant. If the target D-Bus service is owned by Emacs, this
257 is not applicable, and `dbus-call-method-non-blocking' must be
258 used instead. ARGS are identical to the argument list of both
259 functions."
260 (apply
261 ;; The first argument is the bus, the second argument the targt service.
262 (if (string-equal (cdr (assoc (car args) xesam-dbus-unique-names))
263 (cadr args))
264 'dbus-call-method-non-blocking 'dbus-call-method)
265 args))
266
267 (defun xesam-get-property (engine property)
268 "Return the PROPERTY value of ENGINE."
269 ;; "GetProperty" returns a variant, so we must use the car.
270 (car (xesam-dbus-call-method
271 :session (car engine) xesam-path-search
272 xesam-interface-search "GetProperty"
273 (xesam-get-cached-property engine "session") property)))
274
275 (defun xesam-set-property (engine property value)
276 "Set the PROPERTY of ENGINE to VALUE.
277 VALUE can be a string, a non-negative integer, a boolean
278 value (nil or t), or a list of them. It returns the new value of
279 PROPERTY in the search engine. This new value can be different
280 from VALUE, depending on what the search engine accepts."
281 ;; "SetProperty" returns a variant, so we must use the car.
282 (car (xesam-dbus-call-method
283 :session (car engine) xesam-path-search
284 xesam-interface-search "SetProperty"
285 (xesam-get-cached-property engine "session") property
286 ;; The value must be a variant. It can be only a string, an
287 ;; unsigned int, a boolean, or an array of them. So we need
288 ;; no type keyword; we let the type check to the search
289 ;; engine.
290 (list :variant value))))
291
292 (defvar xesam-minibuffer-vendor-history nil
293 "Interactive vendor history.")
294
295 (defvar xesam-minibuffer-query-history nil
296 "Interactive query history.")
297
298 ;; Pacify byte compiler.
299 (defvar xesam-vendor nil)
300 (make-variable-buffer-local 'xesam-vendor)
301 (put 'xesam-vendor 'permanent-local t)
302
303 (defvar xesam-engine nil)
304 (defvar xesam-search nil)
305 (defvar xesam-type nil)
306 (defvar xesam-query nil)
307 (defvar xesam-xml-string nil)
308 (defvar xesam-objects nil)
309 (defvar xesam-current nil)
310 (defvar xesam-count nil)
311 (defvar xesam-to nil)
312 (defvar xesam-notify-function nil)
313 (defvar xesam-refreshing nil)
314
315 \f
316 ;;; Search engines.
317
318 (defvar xesam-search-engines nil
319 "List of available Xesam search engines.
320 Every entry is an association list, with a car denoting the
321 unique D-Bus service name of the engine. The rest of the entry
322 are cached associations of engine attributes, like the session
323 identifier, and the display name. Example:
324
325 \(\(\":1.59\"
326 \(\"session\" . \"0t1214948020ut358230u0p2698r3912347765k3213849828\")
327 \(\"vendor.display\" . \"Tracker Xesam Service\"))
328 \(\":1.27\"
329 \(\"session\" . \"strigisession1369133069\")
330 \(\"vendor.display\" . \"Strigi Desktop Search\")))
331
332 A Xesam-compatible search engine is identified as a queued D-Bus
333 service of the known service `xesam-service-search'.")
334
335 (defun xesam-get-cached-property (engine property)
336 "Return the PROPERTY value of ENGINE from the cache.
337 If PROPERTY is not existing, retrieve it from ENGINE first."
338 ;; If the property has not been cached yet, we retrieve it from the
339 ;; engine, and cache it.
340 (unless (assoc property engine)
341 (xesam-set-cached-property
342 engine property (xesam-get-property engine property)))
343 (cdr (assoc property engine)))
344
345 (defun xesam-set-cached-property (engine property value)
346 "Set the PROPERTY of ENGINE to VALUE in the cache."
347 (setcdr engine (append (cdr engine) (list (cons property value)))))
348
349 (defun xesam-delete-search-engine (&rest args)
350 "Remove service from `xesam-search-engines'."
351 (setq xesam-search-engines
352 (delete (assoc (car args) xesam-search-engines) xesam-search-engines)))
353
354 (defun xesam-search-engines ()
355 "Return Xesam search engines, stored in `xesam-search-engines'.
356 The first search engine is the name owner of `xesam-service-search'.
357 If there is no registered search engine at all, the function returns `nil'."
358 (let ((services (dbus-ignore-errors
359 (dbus-list-queued-owners
360 :session xesam-service-search)))
361 engine vendor-id hit-fields)
362 (dolist (service services)
363 (unless (assoc-string service xesam-search-engines)
364
365 ;; Open a new session, and add it to the search engines list.
366 (add-to-list 'xesam-search-engines (list service) 'append)
367 (setq engine (assoc service xesam-search-engines))
368
369 ;; Add the session string.
370 (xesam-set-cached-property
371 engine "session"
372 (xesam-dbus-call-method
373 :session service xesam-path-search
374 xesam-interface-search "NewSession"))
375
376 ;; Unset the "search.live" property; we don't want to be
377 ;; informed by changed results.
378 (xesam-set-property engine "search.live" nil)
379
380 ;; Check the vendor properties.
381 (setq vendor-id (xesam-get-property engine "vendor.id")
382 hit-fields (xesam-get-property engine "hit.fields"))
383
384 ;; Ususally, `hit.fields' shall describe supported fields.
385 ;; That is not the case now, so we set it ourselves.
386 ;; Hopefully, this will change later.
387 (setq hit-fields
388 (case (intern vendor-id)
389 ('Beagle
390 '("xesam:mimeType" "xesam:url"))
391 ('Strigi
392 '("xesam:author" "xesam:cc" "xesam:charset"
393 "xesam:contentType" "xesam:fileExtension"
394 "xesam:id" "xesam:lineCount" "xesam:links"
395 "xesam:mimeType" "xesam:name" "xesam:size"
396 "xesam:sourceModified" "xesam:subject" "xesam:to"
397 "xesam:url"))
398 ('TrackerXesamSession
399 '("xesam:relevancyRating" "xesam:url"))
400 ('Debbugs
401 '("xesam:keyword" "xesam:owner" "xesam:title"
402 "xesam:url" "xesam:sourceModified" "xesam:mimeType"
403 "debbugs:key"))
404 ;; xesam-tools yahoo service.
405 (t '("xesam:contentModified" "xesam:mimeType" "xesam:summary"
406 "xesam:title" "xesam:url" "yahoo:displayUrl"))))
407
408 (xesam-set-property engine "hit.fields" hit-fields)
409 (xesam-set-property engine "hit.fields.extended" '("xesam:snippet"))
410
411 ;; Let us notify, when the search engine disappears.
412 (dbus-register-signal
413 :session dbus-service-dbus dbus-path-dbus
414 dbus-interface-dbus "NameOwnerChanged"
415 'xesam-delete-search-engine service))))
416 xesam-search-engines)
417
418 \f
419 ;;; Search buffers.
420
421 (define-derived-mode xesam-mode nil "Xesam"
422 "Major mode for presenting search results of a Xesam search.
423 In this mode, widgets represent the search results.
424
425 \\{xesam-mode-map}
426 Turning on Xesam mode runs the normal hook `xesam-mode-hook'. It
427 can be used to set `xesam-notify-function', which must a search
428 engine specific, widget :notify function to visualize xesam:url."
429 (set (make-local-variable 'xesam-notify-function) nil)
430
431 ;; Keymap.
432 (setq xesam-mode-map (copy-keymap special-mode-map))
433 (set-keymap-parent xesam-mode-map widget-keymap)
434 (define-key xesam-mode-map "z" 'kill-this-buffer)
435
436 ;; Maybe we implement something useful, later on.
437 (set (make-local-variable 'revert-buffer-function) 'ignore)
438 ;; `xesam-engine', `xesam-search', `xesam-type', `xesam-query', and
439 ;; `xesam-xml-string' will be set in `xesam-new-search'.
440 (set (make-local-variable 'xesam-engine) nil)
441 (set (make-local-variable 'xesam-search) nil)
442 (set (make-local-variable 'xesam-type) "")
443 (set (make-local-variable 'xesam-query) "")
444 (set (make-local-variable 'xesam-xml-string) "")
445 (set (make-local-variable 'xesam-objects) nil)
446 ;; `xesam-current' is the last hit put into the search buffer,
447 (set (make-local-variable 'xesam-current) 0)
448 ;; `xesam-count' is the number of hits reported by the search engine.
449 (set (make-local-variable 'xesam-count) 0)
450 ;; `xesam-to' is the upper hit number to be presented.
451 (set (make-local-variable 'xesam-to) xesam-hits-per-page)
452 ;; `xesam-notify-function' can be a search engine specific function
453 ;; to visualize xesam:url. It can be overwritten in `xesam-mode'.
454 (set (make-local-variable 'xesam-notify-function) nil)
455 ;; `xesam-refreshing' is an indicator, whether the buffer is just
456 ;; being updated. Needed, because `xesam-refresh-search-buffer'
457 ;; can be triggered by an event.
458 (set (make-local-variable 'xesam-refreshing) nil)
459 ;; Mode line position returns hit counters.
460 (set (make-local-variable 'mode-line-position)
461 (list '(-3 "%p%")
462 '(10 (:eval (format " (%d/%d)" xesam-current xesam-count)))))
463 ;; Header line contains the query string.
464 (set (make-local-variable 'header-line-format)
465 (list '(20
466 (:eval
467 (list "Type: "
468 (propertize xesam-type 'face 'xesam-mode-line))))
469 '(10
470 (:eval
471 (list " Query: "
472 (propertize
473 xesam-query
474 'face 'xesam-mode-line
475 'help-echo (when xesam-debug xesam-xml-string)))))))
476
477 (when (not (interactive-p))
478 ;; Initialize buffer.
479 (setq buffer-read-only t)
480 (let ((inhibit-read-only t))
481 (erase-buffer))))
482
483 ;; It doesn't make sense to call it interactively.
484 (put 'xesam-mode 'disabled t)
485
486 ;; The very first buffer created with `xesam-mode' does not have the
487 ;; keymap etc. So we create a dummy buffer. Stupid.
488 (with-temp-buffer (xesam-mode))
489
490 (defun xesam-buffer-name (service search)
491 "Return the buffer name where to present search results.
492 SERVICE is the D-Bus unique service name of the Xesam search engine.
493 SEARCH is the search identification in that engine. Both must be strings."
494 (format "*%s/%s*" service search))
495
496 (defun xesam-highlight-string (string)
497 "Highlight text enclosed by <b> and </b>.
498 Return propertized STRING."
499 (while (string-match "\\(.*\\)\\(<b>\\)\\(.*\\)\\(</b>\\)\\(.*\\)" string)
500 (setq string
501 (format
502 "%s%s%s"
503 (match-string 1 string)
504 (propertize (match-string 3 string) 'face 'xesam-highlight)
505 (match-string 5 string))))
506 string)
507
508 (defun xesam-highlight-buffer (regexp &optional buffer)
509 "Highlight text matching REGEXP in BUFFER.
510 If BUFFER is nil, use the current buffer"
511 (with-current-buffer (or buffer (current-buffer))
512 (save-excursion
513 (let ((case-fold-search t))
514 (goto-char (point-min))
515 (while (re-search-forward regexp nil t)
516 (overlay-put
517 (make-overlay
518 (match-beginning 0) (match-end 0)) 'face 'xesam-highlight))))))
519
520 (defun xesam-refresh-entry (engine entry)
521 "Refreshes one entry in the search buffer."
522 (let* ((result (nth (1- xesam-current) xesam-objects))
523 widget)
524
525 ;; Create widget.
526 (setq widget (widget-convert 'link))
527 (when xesam-debug
528 (widget-put widget :help-echo ""))
529
530 ;; Take all results.
531 (dolist (field (xesam-get-cached-property engine "hit.fields"))
532 (when (cond
533 ((stringp (caar result)) (not (zerop (length (caar result)))))
534 ((numberp (caar result)) (not (zerop (caar result))))
535 ((caar result) t))
536 (when xesam-debug
537 (widget-put
538 widget :help-echo
539 (format "%s%s: %s\n"
540 (widget-get widget :help-echo) field (caar result))))
541 (widget-put widget (intern (concat ":" field)) (caar result)))
542 (setq result (cdr result)))
543
544 ;; Strigi doesn't return URLs in xesam:url. We must fix this.
545 (when
546 (not (url-type (url-generic-parse-url (widget-get widget :xesam:url))))
547 (widget-put
548 widget :xesam:url (concat "file://" (widget-get widget :xesam:url))))
549
550 ;; Strigi returns xesam:size as string. We must fix this.
551 (when (and (widget-member widget :xesam:size)
552 (stringp (widget-get widget :xesam:size)))
553 (widget-put
554 widget :xesam:size (string-to-number (widget-get widget :xesam:url))))
555
556 ;; First line: :tag.
557 (cond
558 ((widget-member widget :xesam:title)
559 (widget-put widget :tag (widget-get widget :xesam:title)))
560 ((widget-member widget :xesam:subject)
561 (widget-put widget :tag (widget-get widget :xesam:subject)))
562 ((widget-member widget :xesam:mimeType)
563 (widget-put widget :tag (widget-get widget :xesam:mimeType)))
564 ((widget-member widget :xesam:name)
565 (widget-put widget :tag (widget-get widget :xesam:name))))
566
567 ;; Highlight the search items.
568 (when (widget-member widget :tag)
569 (widget-put
570 widget :tag (xesam-highlight-string (widget-get widget :tag))))
571
572 ;; Last Modified.
573 (when (and (widget-member widget :xesam:sourceModified)
574 (not
575 (zerop
576 (string-to-number (widget-get widget :xesam:sourceModified)))))
577 (widget-put
578 widget :tag
579 (format
580 "%s\nLast Modified: %s"
581 (or (widget-get widget :tag) "")
582 (format-time-string
583 "%d %B %Y, %T"
584 (seconds-to-time
585 (string-to-number (widget-get widget :xesam:sourceModified)))))))
586
587 ;; Second line: :value.
588 (widget-put widget :value (widget-get widget :xesam:url))
589
590 (cond
591 ;; A search engine can set `xesam-notify-function' via
592 ;; `xesam-mode-hooks'.
593 (xesam-notify-function
594 (widget-put widget :notify xesam-notify-function))
595
596 ;; In case of HTML, we use a URL link.
597 ((and (widget-member widget :xesam:mimeType)
598 (string-equal "text/html" (widget-get widget :xesam:mimeType)))
599 (setcar widget 'url-link))
600
601 ;; For local files, we will open the file as default action.
602 ((string-match "file"
603 (url-type (url-generic-parse-url
604 (widget-get widget :xesam:url))))
605 (widget-put
606 widget :notify
607 (lambda (widget &rest ignore)
608 (let ((query xesam-query))
609 (find-file
610 (url-filename (url-generic-parse-url (widget-value widget))))
611 (xesam-highlight-buffer (regexp-opt (split-string query nil t))))))
612 (widget-put
613 widget :value
614 (url-filename (url-generic-parse-url (widget-get widget :xesam:url))))))
615
616 ;; Third line: :doc.
617 (cond
618 ((widget-member widget :xesam:summary)
619 (widget-put widget :doc (widget-get widget :xesam:summary)))
620 ((widget-member widget :xesam:snippet)
621 (widget-put widget :doc (widget-get widget :xesam:snippet))))
622
623 (when (widget-member widget :doc)
624 (with-temp-buffer
625 (insert
626 (xesam-highlight-string (widget-get widget :doc)))
627 (fill-region-as-paragraph (point-min) (point-max))
628 (widget-put widget :doc (buffer-string)))
629 (widget-put widget :help-echo (widget-get widget :doc)))
630
631 ;; Format the widget.
632 (widget-put
633 widget :format
634 (format "%d. %s%%[%%v%%]\n%s\n" xesam-current
635 (if (widget-member widget :tag) "%{%t%}\n" "")
636 (if (widget-member widget :doc) "%h" "")))
637
638 ;; Write widget.
639 (goto-char (point-max))
640 (widget-default-create widget)
641 (set-buffer-modified-p nil)
642 (force-mode-line-update)
643 (redisplay)))
644
645 (defun xesam-get-hits (engine search hits)
646 "Retrieve hits from ENGINE."
647 (with-current-buffer (xesam-buffer-name (car engine) search)
648 (setq xesam-objects
649 (append xesam-objects
650 (xesam-dbus-call-method
651 :session (car engine) xesam-path-search
652 xesam-interface-search "GetHits" search hits)))))
653
654 (defun xesam-refresh-search-buffer (engine search)
655 "Refreshes the buffer, presenting results of SEARCH."
656 (with-current-buffer (xesam-buffer-name (car engine) search)
657 ;; Work only if nobody else is here.
658 (unless (or xesam-refreshing (>= xesam-current xesam-to))
659 (setq xesam-refreshing t)
660 (unwind-protect
661 (let (widget)
662
663 ;; Retrieve needed hits for visualization.
664 (while (> (min xesam-to xesam-count) (length xesam-objects))
665 (xesam-get-hits
666 engine search
667 (min xesam-hits-per-page
668 (- (min xesam-to xesam-count) (length xesam-objects)))))
669
670 ;; Add all result widgets.
671 (while (< xesam-current (min xesam-to xesam-count))
672 (setq xesam-current (1+ xesam-current))
673 (xesam-refresh-entry engine search))
674
675 ;; Add "NEXT" widget.
676 (when (> xesam-count xesam-to)
677 (goto-char (point-max))
678 (widget-create
679 'link
680 :notify
681 (lambda (widget &rest ignore)
682 (setq xesam-to (+ xesam-to xesam-hits-per-page))
683 (widget-delete widget)
684 (xesam-refresh-search-buffer xesam-engine xesam-search))
685 "NEXT")
686 (widget-beginning-of-line))
687
688 ;; Prefetch next hits.
689 (when (> (min (+ xesam-hits-per-page xesam-to) xesam-count)
690 (length xesam-objects))
691 (xesam-get-hits
692 engine search
693 (min xesam-hits-per-page
694 (- (min (+ xesam-hits-per-page xesam-to) xesam-count)
695 (length xesam-objects)))))
696
697 ;; Add "DONE" widget.
698 (when (= xesam-current xesam-count)
699 (goto-char (point-max))
700 (widget-create 'link :notify 'ignore "DONE")
701 (widget-beginning-of-line)))
702
703 ;; Return with save settings.
704 (setq xesam-refreshing nil)))))
705
706 \f
707 ;;; Search functions.
708
709 (defun xesam-signal-handler (&rest args)
710 "Handles the different D-Bus signals of a Xesam search."
711 (let* ((service (dbus-event-service-name last-input-event))
712 (member (dbus-event-member-name last-input-event))
713 (search (nth 0 args))
714 (buffer (xesam-buffer-name service search)))
715
716 (when (get-buffer buffer)
717 (with-current-buffer buffer
718 (cond
719
720 ((string-equal member "HitsAdded")
721 (setq xesam-count (+ xesam-count (nth 1 args)))
722 ;; We use `run-at-time' in order to not block the event queue.
723 (run-at-time
724 0 nil
725 'xesam-refresh-search-buffer
726 (assoc service xesam-search-engines) search))
727
728 ((string-equal member "SearchDone")
729 (setq mode-line-process
730 (propertize " Done" 'face 'xesam-mode-line))
731 (force-mode-line-update)))))))
732
733 (defun xesam-kill-buffer-function ()
734 "Send the CloseSearch indication."
735 (when (and (eq major-mode 'xesam-mode) (stringp xesam-search))
736 (xesam-dbus-call-method
737 :session (car xesam-engine) xesam-path-search
738 xesam-interface-search "CloseSearch" xesam-search)))
739
740 (defun xesam-new-search (engine type query)
741 "Create a new search session.
742 ENGINE identifies the search engine. TYPE is the query type, it
743 can be either `fulltext-query', or `user-query'. QUERY is a
744 string in the Xesam query language. A string, identifying the
745 search, is returned."
746 (let* ((service (car engine))
747 (session (xesam-get-cached-property engine "session"))
748 (xml-string
749 (format
750 (if (eq type 'user-query) xesam-user-query xesam-fulltext-query)
751 (url-insert-entities-in-string query)))
752 (search (xesam-dbus-call-method
753 :session service xesam-path-search
754 xesam-interface-search "NewSearch" session xml-string)))
755
756 ;; Let us notify for relevant signals. We ignore "HitsRemoved",
757 ;; "HitsModified" and "StateChanged"; there is nothing to do for
758 ;; us.
759 (dbus-register-signal
760 :session service xesam-path-search
761 xesam-interface-search "HitsAdded"
762 'xesam-signal-handler search)
763 (dbus-register-signal
764 :session service xesam-path-search
765 xesam-interface-search "SearchDone"
766 'xesam-signal-handler search)
767
768 ;; Create the search buffer.
769 (with-current-buffer
770 (generate-new-buffer (xesam-buffer-name service search))
771 (switch-to-buffer-other-window (current-buffer))
772 ;; Inialize buffer with `xesam-mode'. `xesam-vendor' must be
773 ;; set before calling `xesam-mode', because we want to give the
774 ;; hook functions a chance to identify their search engine.
775 (setq xesam-vendor (xesam-get-cached-property engine "vendor.id"))
776 (xesam-mode)
777 (setq xesam-engine engine
778 xesam-search search
779 ;; `xesam-type', `xesam-query' and `xesam-xml-string'
780 ;; are displayed in the header line.
781 xesam-type (symbol-name type)
782 xesam-query query
783 xesam-xml-string xml-string
784 xesam-objects nil
785 ;; The buffer identification shall indicate the search
786 ;; engine. The `help-echo' property is used for debug
787 ;; information, when applicable.
788 mode-line-buffer-identification
789 (if (not xesam-debug)
790 (list 12 (propertized-buffer-identification xesam-vendor))
791 (propertize
792 xesam-vendor
793 'help-echo
794 (mapconcat
795 (lambda (x)
796 (format "%s: %s" x (xesam-get-cached-property engine x)))
797 '("vendor.id" "vendor.version" "vendor.display" "vendor.xesam"
798 "vendor.ontology.fields" "vendor.ontology.contents"
799 "vendor.ontology.sources" "vendor.extensions"
800 "vendor.ontologies" "vendor.maxhits")
801 "\n"))))
802 (add-hook 'kill-buffer-hook 'xesam-kill-buffer-function)
803 (force-mode-line-update))
804
805 ;; Start the search.
806 (xesam-dbus-call-method
807 :session (car engine) xesam-path-search
808 xesam-interface-search "StartSearch" search)
809
810 ;; Return search id.
811 search))
812
813 ;;;###autoload
814 (defun xesam-search (engine query)
815 "Perform an interactive search.
816 ENGINE is the Xesam search engine to be applied, it must be one of the
817 entries of `xesam-search-engines'. QUERY is the search string in the
818 Xesam user query language. If the search engine does not support
819 the Xesam user query language, a Xesam fulltext search is applied.
820
821 The default search engine is the first entry in `xesam-search-engines'.
822 Example:
823
824 (xesam-search (car (xesam-search-engines)) \"emacs\")"
825 (interactive
826 (let* ((vendors (mapcar
827 (lambda (x) (xesam-get-cached-property x "vendor.display"))
828 (xesam-search-engines)))
829 (vendor
830 (if (> (length vendors) 1)
831 (completing-read
832 "Enter search engine: " vendors nil t
833 (try-completion "" vendors) 'xesam-minibuffer-vendor-history)
834 (car vendors))))
835 (list
836 ;; ENGINE.
837 (when vendor
838 (dolist (elt (xesam-search-engines) engine)
839 (when (string-equal
840 (xesam-get-cached-property elt "vendor.display") vendor)
841 (setq engine elt))))
842 ;; QUERY.
843 (when vendor
844 (read-from-minibuffer
845 "Enter search string: " nil nil nil
846 'xesam-minibuffer-query-history)))))
847
848 (if (null engine)
849 (message "No search engine running")
850 (if (zerop (length query))
851 (message "No query applied")
852 (xesam-new-search engine xesam-query-type query))))
853
854 (provide 'xesam)
855
856 ;;; TODO:
857
858 ;; * Buffer highlighting needs better analysis of query string.
859 ;; * Accept input while retrieving prefetched hits. `run-at-time'?
860 ;; * With prefix, let's choose search engine.
861 ;; * Minibuffer completion for user queries.
862 ;; * `revert-buffer-function' implementation.
863 ;;
864 ;; * Mid term
865 ;; - If available, use ontologies for field selection.
866 ;; - Search engines for Emacs bugs database, wikipedia, google,
867 ;; yahoo, ebay, ...
868 ;; - Construct complex queries via widgets, like in mairix.el.
869
870 ;; arch-tag: 7fb9fc6c-c2ff-4bc7-bb42-bacb80cce2b2
871 ;;; xesam.el ends here