]> git.notmuchmail.org Git - notmuch/blobdiff - emacs/notmuch-lib.el
emacs: Use the minibuffer for CLI error reporting
[notmuch] / emacs / notmuch-lib.el
index e99b48d107e164b1ab56c92dc9da654f75d862e4..d78bcf8065aae699a162241e9e7e92b114a3267a 100644 (file)
@@ -23,7 +23,8 @@
 
 (require 'mm-view)
 (require 'mm-decode)
-(eval-when-compile (require 'cl))
+(require 'json)
+(require 'cl)
 
 (defvar notmuch-command "notmuch"
   "Command to run the notmuch binary.")
 (defvar notmuch-search-history nil
   "Variable to store notmuch searches history.")
 
-(defcustom notmuch-saved-searches nil
+(defcustom notmuch-saved-searches '(("inbox" . "tag:inbox")
+                                   ("unread" . "tag:unread"))
   "A list of saved searches to display."
   :type '(alist :key-type string :value-type string)
   :group 'notmuch-hello)
 
-(defvar notmuch-folders nil
-  "Deprecated name for what is now known as `notmuch-saved-searches'.")
+(defcustom notmuch-archive-tags '("-inbox")
+  "List of tag changes to apply to a message or a thread when it is archived.
 
-(defun notmuch-saved-searches ()
-  "Common function for querying the notmuch-saved-searches variable.
+Tags starting with \"+\" (or not starting with either \"+\" or
+\"-\") in the list will be added, and tags starting with \"-\"
+will be removed from the message or thread being archived.
 
-We do this as a function to support the old name of the
-variable (`notmuch-folders') as well as for the default value if
-the user hasn't set this variable with the old or new value."
-  (if notmuch-saved-searches
-      notmuch-saved-searches
-    (if notmuch-folders
-       notmuch-folders
-      '(("inbox" . "tag:inbox")
-       ("unread" . "tag:unread")))))
+For example, if you wanted to remove an \"inbox\" tag and add an
+\"archived\" tag, you would set:
+    (\"-inbox\" \"+archived\")"
+  :type '(repeat string)
+  :group 'notmuch-search
+  :group 'notmuch-show)
 
 (defun notmuch-version ()
   "Return a string with the notmuch version number."
@@ -146,16 +146,36 @@ the user hasn't set this variable with the old or new value."
        "[No Subject]"
       subject)))
 
+(defun notmuch-escape-boolean-term (term)
+  "Escape a boolean term for use in a query.
+
+The caller is responsible for prepending the term prefix and a
+colon.  This performs minimal escaping in order to produce
+user-friendly queries."
+
+  (save-match-data
+    (if (or (equal term "")
+           (string-match "[ ()]\\|^\"" term))
+       ;; Requires escaping
+       (concat "\"" (replace-regexp-in-string "\"" "\"\"" term t t) "\"")
+      term)))
+
 (defun notmuch-id-to-query (id)
   "Return a query that matches the message with id ID."
-  (concat "id:\"" (replace-regexp-in-string "\"" "\"\"" id t t) "\""))
+  (concat "id:" (notmuch-escape-boolean-term id)))
 
 ;;
 
 (defun notmuch-common-do-stash (text)
   "Common function to stash text in kill ring, and display in minibuffer."
-  (kill-new text)
-  (message "Stashed: %s" text))
+  (if text
+      (progn
+       (kill-new text)
+       (message "Stashed: %s" text))
+    ;; There is nothing to stash so stash an empty string so the user
+    ;; doesn't accidentally paste something else somewhere.
+    (kill-new "")
+    (message "Nothing to stash!")))
 
 ;;
 
@@ -239,6 +259,19 @@ the given type."
   (or (plist-get part :content)
       (notmuch-get-bodypart-internal (notmuch-id-to-query (plist-get msg :id)) nth process-crypto)))
 
+;; Workaround: The call to `mm-display-part' below triggers a bug in
+;; Emacs 24 if it attempts to use the shr renderer to display an HTML
+;; part with images in it (demonstrated in 24.1 and 24.2 on Debian and
+;; Fedora 17, though unreproducable in other configurations).
+;; `mm-shr' references the variable `gnus-inhibit-images' without
+;; first loading gnus-art, which defines it, resulting in a
+;; void-variable error.  Hence, we advise `mm-shr' to ensure gnus-art
+;; is loaded.
+(if (>= emacs-major-version 24)
+    (defadvice mm-shr (before load-gnus-arts activate)
+      (require 'gnus-art nil t)
+      (ad-disable-advice 'mm-shr 'before 'load-gnus-arts)))
+
 (defun notmuch-mm-display-part-inline (msg part nth content-type process-crypto)
   "Use the mm-decode/mm-view functions to display a part in the
 current buffer, if possible."
@@ -268,6 +301,130 @@ current buffer, if possible."
   (loop for (key value . rest) on plist by #'cddr
        collect (cons (intern (substring (symbol-name key) 1)) value)))
 
+(defun notmuch-combine-face-text-property (start end face)
+  "Combine FACE into the 'face text property between START and END.
+
+This function combines FACE with any existing faces between START
+and END.  Attributes specified by FACE take precedence over
+existing attributes.  FACE must be a face name (a symbol or
+string), a property list of face attributes, or a list of these."
+
+  (let ((pos start))
+    (while (< pos end)
+      (let ((cur (get-text-property pos 'face))
+           (next (next-single-property-change pos 'face nil end)))
+       (put-text-property pos next 'face (cons face cur))
+       (setq pos next)))))
+
+(defun notmuch-logged-error (msg &optional extra)
+  "Log MSG and EXTRA to *Notmuch errors* and signal MSG.
+
+This logs MSG and EXTRA to the *Notmuch errors* buffer and
+signals MSG as an error.  If EXTRA is non-nil, text referring the
+user to the *Notmuch errors* buffer will be appended to the
+signaled error.  This function does not return."
+
+  (with-current-buffer (get-buffer-create "*Notmuch errors*")
+    (goto-char (point-max))
+    (unless (bobp)
+      (newline))
+    (save-excursion
+      (insert "[" (current-time-string) "]\n" msg)
+      (unless (bolp)
+       (newline))
+      (when extra
+       (insert extra)
+       (unless (bolp)
+         (newline)))))
+  (error "%s" (concat msg (when extra
+                           " (see *Notmuch errors* for more details)"))))
+
+(defun notmuch-check-async-exit-status (proc msg)
+  "If PROC exited abnormally, pop up an error buffer and signal an error.
+
+This is a wrapper around `notmuch-check-exit-status' for
+asynchronous process sentinels.  PROC and MSG must be the
+arguments passed to the sentinel."
+  (let ((exit-status
+        (case (process-status proc)
+          ((exit) (process-exit-status proc))
+          ((signal) msg))))
+    (when exit-status
+      (notmuch-check-exit-status exit-status (process-command proc)))))
+
+(defun notmuch-check-exit-status (exit-status command &optional output err-file)
+  "If EXIT-STATUS is non-zero, pop up an error buffer and signal an error.
+
+If EXIT-STATUS is non-zero, pop up a notmuch error buffer
+describing the error and signal an Elisp error.  EXIT-STATUS must
+be a number indicating the exit status code of a process or a
+string describing the signal that terminated the process (such as
+returned by `call-process').  COMMAND must be a list giving the
+command and its arguments.  OUTPUT, if provided, is a string
+giving the output of command.  ERR-FILE, if provided, is the name
+of a file containing the error output of command.  OUTPUT and the
+contents of ERR-FILE will be included in the error message."
+
+  (cond
+   ((eq exit-status 0) t)
+   ((eq exit-status 20)
+    (notmuch-logged-error "notmuch CLI version mismatch
+Emacs requested an older output format than supported by the notmuch CLI.
+You may need to restart Emacs or upgrade your notmuch Emacs package."))
+   ((eq exit-status 21)
+    (notmuch-logged-error "notmuch CLI version mismatch
+Emacs requested a newer output format than supported by the notmuch CLI.
+You may need to restart Emacs or upgrade your notmuch package."))
+   (t
+    (let* ((err (when err-file
+                 (with-temp-buffer
+                   (insert-file-contents err-file)
+                   (unless (eobp)
+                     (buffer-string)))))
+          (extra
+           (concat
+            "command: " (mapconcat #'shell-quote-argument command " ") "\n"
+            (if (integerp exit-status)
+                (format "exit status: %s\n" exit-status)
+              (format "exit signal: %s\n" exit-status))
+            (when err
+              (concat "stderr:\n" err))
+            (when output
+              (concat "stdout:\n" output)))))
+       (if err
+           ;; We have an error message straight from the CLI.
+           (notmuch-logged-error
+            (replace-regexp-in-string "\\s $" "" err) extra)
+         ;; We only have combined output from the CLI; don't inundate
+         ;; the user with it.  Mimic `process-lines'.
+         (notmuch-logged-error (format "%s exited with status %s"
+                                       (car command) exit-status)
+                               extra))
+       ;; `notmuch-logged-error' does not return.
+       ))))
+
+(defun notmuch-call-notmuch-json (&rest args)
+  "Invoke `notmuch-command' with `args' and return the parsed JSON output.
+
+The returned output will represent objects using property lists
+and arrays as lists.  If notmuch exits with a non-zero status,
+this will pop up a buffer containing notmuch's output and signal
+an error."
+
+  (with-temp-buffer
+    (let ((err-file (make-temp-file "nmerr")))
+      (unwind-protect
+         (let ((status (apply #'call-process
+                              notmuch-command nil (list t err-file) nil args)))
+           (notmuch-check-exit-status status (cons notmuch-command args)
+                                      (buffer-string) err-file)
+           (goto-char (point-min))
+           (let ((json-object-type 'plist)
+                 (json-array-type 'list)
+                 (json-false 'nil))
+             (json-read)))
+       (delete-file err-file)))))
+
 ;; Compatibility functions for versions of emacs before emacs 23.
 ;;
 ;; Both functions here were copied from emacs 23 with the following copyright:
@@ -296,5 +453,274 @@ was called."
 (defvar notmuch-show-process-crypto nil)
 (make-variable-buffer-local 'notmuch-show-process-crypto)
 
+;; Incremental JSON parsing
+
+;; These two variables are internal variables to the parsing
+;; routines. They are always used buffer local but need to be declared
+;; globally to avoid compiler warnings.
+
+(defvar notmuch-json-parser nil
+  "Internal incremental JSON parser object: local to the buffer being parsed.")
+
+(defvar notmuch-json-state nil
+  "State of the internal JSON parser: local to the buffer being parsed.")
+
+(defun notmuch-json-create-parser (buffer)
+  "Return a streaming JSON parser that consumes input from BUFFER.
+
+This parser is designed to read streaming JSON whose structure is
+known to the caller.  Like a typical JSON parsing interface, it
+provides a function to read a complete JSON value from the input.
+However, it extends this with an additional function that
+requires the next value in the input to be a compound value and
+descends into it, allowing its elements to be read one at a time
+or further descended into.  Both functions can return 'retry to
+indicate that not enough input is available.
+
+The parser always consumes input from BUFFER's point.  Hence, the
+caller is allowed to delete and data before point and may
+resynchronize after an error by moving point."
+
+  (list buffer
+       ;; Terminator stack: a stack of characters that indicate the
+       ;; end of the compound values enclosing point
+       '()
+       ;; Next: One of
+       ;; * 'expect-value if the next token must be a value, but a
+       ;;   value has not yet been reached
+       ;; * 'value if point is at the beginning of a value
+       ;; * 'expect-comma if the next token must be a comma
+       'expect-value
+       ;; Allow terminator: non-nil if the next token may be a
+       ;; terminator
+       nil
+       ;; Partial parse position: If state is 'value, a marker for
+       ;; the position of the partial parser or nil if no partial
+       ;; parsing has happened yet
+       nil
+       ;; Partial parse state: If state is 'value, the current
+       ;; `parse-partial-sexp' state
+       nil))
+
+(defmacro notmuch-json-buffer (jp) `(first ,jp))
+(defmacro notmuch-json-term-stack (jp) `(second ,jp))
+(defmacro notmuch-json-next (jp) `(third ,jp))
+(defmacro notmuch-json-allow-term (jp) `(fourth ,jp))
+(defmacro notmuch-json-partial-pos (jp) `(fifth ,jp))
+(defmacro notmuch-json-partial-state (jp) `(sixth ,jp))
+
+(defvar notmuch-json-syntax-table
+  (let ((table (make-syntax-table)))
+    ;; The standard syntax table is what we need except that "." needs
+    ;; to have word syntax instead of punctuation syntax.
+    (modify-syntax-entry ?. "w" table)
+    table)
+  "Syntax table used for incremental JSON parsing.")
+
+(defun notmuch-json-scan-to-value (jp)
+  ;; Helper function that consumes separators, terminators, and
+  ;; whitespace from point.  Returns nil if it successfully reached
+  ;; the beginning of a value, 'end if it consumed a terminator, or
+  ;; 'retry if not enough input was available to reach a value.  Upon
+  ;; nil return, (notmuch-json-next jp) is always 'value.
+
+  (if (eq (notmuch-json-next jp) 'value)
+      ;; We're already at a value
+      nil
+    ;; Drive the state toward 'expect-value
+    (skip-chars-forward " \t\r\n")
+    (or (when (eobp) 'retry)
+       ;; Test for the terminator for the current compound
+       (when (and (notmuch-json-allow-term jp)
+                  (eq (char-after) (car (notmuch-json-term-stack jp))))
+         ;; Consume it and expect a comma or terminator next
+         (forward-char)
+         (setf (notmuch-json-term-stack jp) (cdr (notmuch-json-term-stack jp))
+               (notmuch-json-next jp) 'expect-comma
+               (notmuch-json-allow-term jp) t)
+         'end)
+       ;; Test for a separator
+       (when (eq (notmuch-json-next jp) 'expect-comma)
+         (when (/= (char-after) ?,)
+           (signal 'json-readtable-error (list "expected ','")))
+         ;; Consume it, switch to 'expect-value, and disallow a
+         ;; terminator
+         (forward-char)
+         (skip-chars-forward " \t\r\n")
+         (setf (notmuch-json-next jp) 'expect-value
+               (notmuch-json-allow-term jp) nil)
+         ;; We moved point, so test for eobp again and fall through
+         ;; to the next test if there's more input
+         (when (eobp) 'retry))
+       ;; Next must be 'expect-value and we know this isn't
+       ;; whitespace, EOB, or a terminator, so point must be on a
+       ;; value
+       (progn
+         (assert (eq (notmuch-json-next jp) 'expect-value))
+         (setf (notmuch-json-next jp) 'value)
+         nil))))
+
+(defun notmuch-json-begin-compound (jp)
+  "Parse the beginning of a compound value and traverse inside it.
+
+Returns 'retry if there is insufficient input to parse the
+beginning of the compound.  If this is able to parse the
+beginning of a compound, it moves point past the token that opens
+the compound and returns t.  Later calls to `notmuch-json-read'
+will return the compound's elements.
+
+Entering JSON objects is currently unimplemented."
+
+  (with-current-buffer (notmuch-json-buffer jp)
+    ;; Disallow terminators
+    (setf (notmuch-json-allow-term jp) nil)
+    ;; Save "next" so we can restore it if there's a syntax error
+    (let ((saved-next (notmuch-json-next jp)))
+      (or (notmuch-json-scan-to-value jp)
+         (if (/= (char-after) ?\[)
+             (progn
+               (setf (notmuch-json-next jp) saved-next)
+               (signal 'json-readtable-error (list "expected '['")))
+           (forward-char)
+           (push ?\] (notmuch-json-term-stack jp))
+           ;; Expect a value or terminator next
+           (setf (notmuch-json-next jp) 'expect-value
+                 (notmuch-json-allow-term jp) t)
+           t)))))
+
+(defun notmuch-json-read (jp)
+  "Parse the value at point in JP's buffer.
+
+Returns 'retry if there is insufficient input to parse a complete
+JSON value (though it may still move point over separators or
+whitespace).  If the parser is currently inside a compound value
+and the next token ends the list or object, this moves point just
+past the terminator and returns 'end.  Otherwise, this moves
+point to just past the end of the value and returns the value."
+
+  (with-current-buffer (notmuch-json-buffer jp)
+    (or
+     ;; Get to a value state
+     (notmuch-json-scan-to-value jp)
+
+     ;; Can we parse a complete value?
+     (let ((complete
+           (if (looking-at "[-+0-9tfn]")
+               ;; This is a number or a keyword, so the partial
+               ;; parser isn't going to help us because a truncated
+               ;; number or keyword looks like a complete symbol to
+               ;; it.  Look for something that clearly ends it.
+               (save-excursion
+                 (skip-chars-forward "^]},: \t\r\n")
+                 (not (eobp)))
+
+             ;; We're looking at a string, object, or array, which we
+             ;; can partial parse.  If we just reached the value, set
+             ;; up the partial parser.
+             (when (null (notmuch-json-partial-state jp))
+               (setf (notmuch-json-partial-pos jp) (point-marker)))
+
+             ;; Extend the partial parse until we either reach EOB or
+             ;; get the whole value
+             (save-excursion
+               (let ((pstate
+                      (with-syntax-table notmuch-json-syntax-table
+                        (parse-partial-sexp
+                         (notmuch-json-partial-pos jp) (point-max) 0 nil
+                         (notmuch-json-partial-state jp)))))
+                 ;; A complete value is available if we've reached
+                 ;; depth 0 or less and encountered a complete
+                 ;; subexpression.
+                 (if (and (<= (first pstate) 0) (third pstate))
+                     t
+                   ;; Not complete.  Update the partial parser state
+                   (setf (notmuch-json-partial-pos jp) (point-marker)
+                         (notmuch-json-partial-state jp) pstate)
+                   nil))))))
+
+       (if (not complete)
+          'retry
+        ;; We have a value.  Reset the partial parse state and expect
+        ;; a comma or terminator after the value.
+        (setf (notmuch-json-next jp) 'expect-comma
+              (notmuch-json-allow-term jp) t
+              (notmuch-json-partial-pos jp) nil
+              (notmuch-json-partial-state jp) nil)
+        ;; Parse the value
+        (let ((json-object-type 'plist)
+              (json-array-type 'list)
+              (json-false nil))
+          (json-read)))))))
+
+(defun notmuch-json-eof (jp)
+  "Signal a json-error if there is more data in JP's buffer.
+
+Moves point to the beginning of any trailing data or to the end
+of the buffer if there is only trailing whitespace."
+
+  (with-current-buffer (notmuch-json-buffer jp)
+    (skip-chars-forward " \t\r\n")
+    (unless (eobp)
+      (signal 'json-error (list "Trailing garbage following JSON data")))))
+
+(defun notmuch-json-parse-partial-list (result-function error-function results-buf)
+  "Parse a partial JSON list from current buffer.
+
+This function consumes a JSON list from the current buffer,
+applying RESULT-FUNCTION in buffer RESULT-BUFFER to each complete
+value in the list.  It operates incrementally and should be
+called whenever the buffer has been extended with additional
+data.
+
+If there is a syntax error, this will attempt to resynchronize
+with the input and will apply ERROR-FUNCTION in buffer
+RESULT-BUFFER to any input that was skipped.
+
+It sets up all the needed internal variables: the caller just
+needs to call it with point in the same place that the parser
+left it."
+  (let (done)
+    (unless (local-variable-p 'notmuch-json-parser)
+      (set (make-local-variable 'notmuch-json-parser)
+          (notmuch-json-create-parser (current-buffer)))
+      (set (make-local-variable 'notmuch-json-state) 'begin))
+    (while (not done)
+      (condition-case nil
+         (case notmuch-json-state
+               ((begin)
+                ;; Enter the results list
+                (if (eq (notmuch-json-begin-compound
+                         notmuch-json-parser) 'retry)
+                    (setq done t)
+                  (setq notmuch-json-state 'result)))
+               ((result)
+                ;; Parse a result
+                (let ((result (notmuch-json-read notmuch-json-parser)))
+                  (case result
+                        ((retry) (setq done t))
+                        ((end) (setq notmuch-json-state 'end))
+                        (otherwise (with-current-buffer results-buf
+                                     (funcall result-function result))))))
+               ((end)
+                ;; Any trailing data is unexpected
+                (notmuch-json-eof notmuch-json-parser)
+                (setq done t)))
+       (json-error
+        ;; Do our best to resynchronize and ensure forward
+        ;; progress
+        (let ((bad (buffer-substring (line-beginning-position)
+                                     (line-end-position))))
+          (forward-line)
+          (with-current-buffer results-buf
+            (funcall error-function "%s" bad))))))
+    ;; Clear out what we've parsed
+    (delete-region (point-min) (point))))
+
+
+
+
 (provide 'notmuch-lib)
 
+;; Local Variables:
+;; byte-compile-warnings: (not cl-functions)
+;; End: