aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Allred <code@seanallred.com>2014-11-14 09:34:14 -0500
committerSean Allred <code@seanallred.com>2014-11-14 09:34:14 -0500
commit77e519de7b1219767a25399b727c2aece6189e6c (patch)
tree5c99eb09549561a549e93c19aaa788cd81a16073
parent525a304f9aca38e1101a1d02ade26ec3b8e91b23 (diff)
parent8d14a6b425d27b7b942f3922d583877ec8b00690 (diff)
Merge pull request #46 from vermiculus/sx-question-mode
Question Mode
-rw-r--r--Cask1
-rw-r--r--sx-encoding.el35
-rw-r--r--sx-question-list.el30
-rw-r--r--sx-question-mode.el436
-rw-r--r--sx-question.el76
-rw-r--r--sx-request.el2
-rw-r--r--sx-time.el22
7 files changed, 542 insertions, 60 deletions
diff --git a/Cask b/Cask
index 3e939ef..f0c70fb 100644
--- a/Cask
+++ b/Cask
@@ -8,6 +8,7 @@
(depends-on "json" "1.4")
(depends-on "url")
(depends-on "cl-lib")
+(depends-on "markdown-mode")
(development
(depends-on "ert"))
diff --git a/sx-encoding.el b/sx-encoding.el
index 0b72365..9d48e60 100644
--- a/sx-encoding.el
+++ b/sx-encoding.el
@@ -23,6 +23,8 @@
;;; Code:
+(require 'cl-lib)
+
(defcustom sx-encoding-html-entities-plist
'(Aacute "Á" aacute "á" Acirc "Â" acirc "â" acute "´" AElig "Æ" aelig "æ"
Agrave "À" agrave "à" alefsym "ℵ" Alpha "Α" alpha "α" amp "&" and "∧"
@@ -74,6 +76,39 @@
(substring ss 1))))))))
(replace-regexp-in-string "&[^; ]*;" get-function string)))
+(defun sx-encoding-normalize-line-endings (string)
+ "Normalize the line endings for STRING"
+ (delete ?\r string))
+
+(defun sx-encoding-clean-content (string)
+ "Cleans STRING for display.
+Applies `sx-encoding-normalize-line-endings' and
+`sx-encoding-decode-entities'."
+ (sx-encoding-decode-entities
+ (sx-encoding-normalize-line-endings
+ string)))
+
+(defun sx-encoding-clean-content-deep (data)
+ "Clean DATA recursively where necessary.
+
+See `sx-encoding-clean-content'."
+ (if (consp data)
+ ;; If we're looking at a cons cell, test to see if is a list. If
+ ;; it is, map ourselves over the entire list. If it is not,
+ ;; reconstruct the cons cell using a cleaned cdr.
+ (if (listp (cdr data))
+ (cl-map #'list #'sx-encoding-clean-content-deep data)
+ (cons (car data) (sx-encoding-clean-content-deep (cdr data))))
+ ;; If we're looking at an atom, clean and return if we're looking
+ ;; at a string, map if we're looking at a vector, and just return
+ ;; if we aren't looking at either.
+ (cond
+ ((stringp data)
+ (sx-encoding-clean-content data))
+ ((vectorp data)
+ (cl-map #'vector #'sx-encoding-clean-content-deep data))
+ (t data))))
+
(defun sx-encoding-gzipped-p (data)
"Checks for magic bytes in DATA.
diff --git a/sx-question-list.el b/sx-question-list.el
index bdbc2f1..ff1bdaa 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -26,6 +26,7 @@
(require 'sx)
(require 'sx-time)
(require 'sx-question)
+(require 'sx-question-mode)
;;; Customization
@@ -69,7 +70,7 @@
:group 'sx-question-list-faces)
(defface sx-question-list-tags
- '((t :inherit font-lock-function-name-face))
+ '((t :inherit sx-question-mode-tags))
""
:group 'sx-question-list-faces)
@@ -179,6 +180,9 @@ Letters do not insert themselves; instead, they are commands.
(defvar sx-question-list--current-site "emacs"
"Site being displayed in the *question-list* buffer.")
+(defvar sx-question-list--current-dataset nil
+ "")
+
(defun sx-question-list-refresh (&optional redisplay no-update)
"Update the list of questions.
If REDISPLAY is non-nil, also call `tabulated-list-print'.
@@ -187,8 +191,12 @@ a new list before redisplaying."
(interactive "pP")
;; Reset the mode-line unread count (we rebuild it here).
(setq sx-question-list--unread-count 0)
- (let ((question-list (sx-question-get-questions
- sx-question-list--current-site)))
+ (let ((question-list
+ (if (and no-update sx-question-list--current-dataset)
+ sx-question-list--current-dataset
+ (sx-question-get-questions
+ sx-question-list--current-site))))
+ (setq sx-question-list--current-dataset question-list)
;; Print the result.
(setq tabulated-list-entries
(mapcar #'sx-question-list--print-info question-list)))
@@ -233,7 +241,8 @@ Used in the questions list to indicate a question was updated \"4d ago\"."
(propertize (concat (sx-time-since .last_activity_date)
sx-question-list-ago-string)
'face 'sx-question-list-date)
- (propertize (concat " [" (mapconcat #'identity .tags "] [") "]")
+ " "
+ (propertize (mapconcat #'sx-question--tag-format .tags " ")
'face 'sx-question-list-tags)
(propertize " " 'display "\n"))))))
@@ -270,8 +279,9 @@ focus the relevant window."
(when (sx-question--read-p data)
(cl-decf sx-question-list--unread-count)
(sx-question--mark-read data))
- (unless (window-live-p sx-question--window)
- (setq sx-question--window
+ (unless (and (window-live-p sx-question-mode--window)
+ (null (equal sx-question-mode--window (selected-window))))
+ (setq sx-question-mode--window
(condition-case er
(split-window-below sx-question-list-height)
(error
@@ -281,11 +291,11 @@ focus the relevant window."
(car (cdr-safe er)))
nil
(error (cdr er)))))))
- (sx-question--display data sx-question--window)
+ (sx-question-mode--display data sx-question-mode--window)
(when focus
- (if sx-question--window
- (select-window sx-question--window)
- (switch-to-buffer sx-question--buffer))))
+ (if sx-question-mode--window
+ (select-window sx-question-mode--window)
+ (switch-to-buffer sx-question-mode--buffer))))
(defvar sx-question-list--buffer nil
"Buffer where the list of questions is displayed.")
diff --git a/sx-question-mode.el b/sx-question-mode.el
new file mode 100644
index 0000000..20d3035
--- /dev/null
+++ b/sx-question-mode.el
@@ -0,0 +1,436 @@
+;;; sx-question-mode.el --- Creating the buffer that displays questions -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2014 Artur Malabarba
+
+;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
+
+;; This program 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.
+
+;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+
+;;; Code:
+(require 'markdown-mode)
+
+(require 'sx)
+(require 'sx-question)
+
+(defgroup sx-question-mode nil
+ "Customization group for sx-question-mode."
+ :prefix "sx-question-mode-"
+ :group 'sx)
+
+(defgroup sx-question-mode-faces nil
+ "Customization group for the faces of `sx-question-mode'."
+ :prefix "sx-question-mode-"
+ :group 'sx-question-mode)
+
+
+;;; Displaying a question
+(defvar sx-question-mode--window nil
+ "Window where the content of questions is displayed.")
+
+(defvar sx-question-mode--buffer nil
+ "Buffer being used to display questions.")
+
+(defvar sx-question-mode--data nil
+ "The data of the question being displayed.")
+
+(defun sx-question-mode--display (data &optional window)
+ "Display question given by DATA on WINDOW.
+If WINDOW is nil, use selected one.
+Returns the question buffer."
+ (let ((inhibit-read-only t))
+ (with-current-buffer
+ (sx-question-mode--display-buffer window)
+ (erase-buffer)
+ (sx-question-mode)
+ (sx-question-mode--print-question data)
+ (current-buffer))))
+
+(defun sx-question-mode--display-buffer (window)
+ "Display and return the buffer used for displaying a question.
+Create the buffer if necessary.
+If WINDOW is given, use that to display the buffer."
+ ;; Create the buffer if necessary.
+ (unless (buffer-live-p sx-question-mode--buffer)
+ (setq sx-question-mode--buffer
+ (generate-new-buffer "*stack-question*")))
+ (cond
+ ;; Window was given, use it.
+ ((window-live-p window)
+ (set-window-buffer window sx-question-mode--buffer))
+ ;; No window, but the buffer is already being displayed somewhere.
+ ((get-buffer-window sx-question-mode--buffer 'visible))
+ ;; Neither, so we create the window.
+ (t (switch-to-buffer sx-question-mode--buffer)))
+ sx-question-mode--buffer)
+
+
+;;; Printing a question's content
+;;;; Faces and Variables
+(defvar sx-question-mode--overlays nil
+ "")
+(make-variable-buffer-local 'sx-question-mode--overlays)
+
+(defface sx-question-mode-header
+ '((t :inherit font-lock-variable-name-face))
+ "Face used on the question headers in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defface sx-question-mode-title
+ '((t :height 1.3 :weight bold :inherit default))
+ "Face used on the question title in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defface sx-question-mode-title-comments
+ '((t :height 1.1 :inherit sx-question-mode-title))
+ "Face used on the question title in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-header-title "\n"
+ "String used before the question title at the header."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defface sx-question-mode-author
+ '((t :inherit font-lock-string-face))
+ "Face used on the question author in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-header-author "\nAuthor: "
+ "String used before the question author at the header."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defface sx-question-mode-date
+ '((t :inherit font-lock-string-face))
+ "Face used on the question date in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-header-date "\nAsked on: "
+ "String used before the question date at the header."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defface sx-question-mode-tags
+ '((t :underline nil :inherit font-lock-function-name-face))
+ "Face used on the question tags in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defface sx-question-mode-author
+ '((t :inherit font-lock-variable-name-face))
+ "Face used for author names in the question buffer."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-header-tags "\nTags: "
+ "String used before the question tags at the header."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defface sx-question-mode-content-face
+ '((((background dark)) :background "#090909")
+ (((background light)) :background "#f4f4f4"))
+ "Face used on the question body in the question buffer.
+Shouldn't have a foreground, or this will interfere with
+font-locking."
+ :group 'sx-question-mode-faces)
+
+(defcustom sx-question-mode-last-edit-format " (edited %s ago by %s)"
+ "Format used to describe last edit date in the header.
+First %s is replaced with the date, and the second %s with the
+editor's name."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defcustom sx-question-mode-separator
+ (concat "\n" (make-string 80 ?_) "\n")
+ "Separator used between header and body."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defcustom sx-question-mode-answer-title "Answer"
+ "Title used at the start of \"Answer\" sections."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defcustom sx-question-mode-comments-title " Comments"
+ "Title used at the start of \"Comments\" sections."
+ :type 'string
+ :group 'sx-question-mode)
+
+(defcustom sx-question-mode-comments-format "%s: %s\n"
+ "Format used to display comments.
+First \"%s\" is replaced with user name.
+Second \"%s\" is replaced with the comment."
+ :type 'string
+ :group 'sx-question-mode)
+
+
+;;; Printing a question's content
+;;;; Functions
+;; This is where most of the work is still left to be done! Need to
+;; insert more data from QUESTION.
+(defun sx-question-mode--print-question (question)
+ "Print a buffer describing QUESTION.
+QUESTION must be a data structure returned by `json-read'."
+ ;; Clear the overlays
+ (mapc #'delete-overlay sx-question-mode--overlays)
+ (setq sx-question-mode--overlays nil)
+ ;; Print everything
+ (sx-question-mode--print-section question)
+ (sx-assoc-let question
+ (mapc #'sx-question-mode--print-section .answers))
+ (goto-char (point-min))
+ (with-selected-window sx-question-mode--window
+ (sx-question-mode-next-section)))
+
+(defun sx-question-mode--print-section (data)
+ "Print a section corresponding to DATA.
+DATA can represent a question or an answer."
+ (sx-assoc-let data
+ (insert sx-question-mode-header-title
+ (if .title
+ ;; Questions have title
+ (propertize
+ .title
+ 'font-lock-face 'sx-question-mode-title
+ 'sx-question-mode--section 1)
+ ;; Answers don't
+ (propertize
+ sx-question-mode-answer-title
+ 'font-lock-face 'sx-question-mode-title
+ 'sx-question-mode--section 2)))
+ ;; Sections can be hidden with overlays
+ (sx-question-mode--wrap-in-overlay
+ '(sx-question-mode--section-content t)
+ (sx-question-mode--insert-header
+ ;; Author
+ sx-question-mode-header-author
+ (sx-question-mode--propertized-display-name .owner)
+ 'sx-question-mode-author
+ ;; Date
+ sx-question-mode-header-date
+ (concat
+ (sx-time-seconds-to-date .creation_date)
+ (when .last_edit_date
+ (format sx-question-mode-last-edit-format
+ (sx-time-since .last_edit_date)
+ (sx-question-mode--propertized-display-name .last_editor))))
+ 'sx-question-mode-date)
+ (when .title
+ ;; Tags
+ (sx-question-mode--insert-header
+ sx-question-mode-header-tags
+ (mapconcat #'sx-question--tag-format .tags " ")
+ 'sx-question-mode-tags))
+ ;; Body
+ (insert (propertize sx-question-mode-separator
+ 'face 'sx-question-mode-header))
+ (sx-question-mode--wrap-in-overlay
+ '(face sx-question-mode-content-face)
+ (insert "\n"
+ (sx-question-mode--fill-string
+ .body_markdown)
+ (propertize sx-question-mode-separator
+ 'face 'sx-question-mode-header))))
+ ;; Comments
+ (when .comments
+ (insert
+ "\n"
+ (propertize
+ sx-question-mode-comments-title
+ 'font-lock-face 'sx-question-mode-title-comments
+ 'sx-question-mode--section 3))
+ (sx-question-mode--wrap-in-overlay
+ '(sx-question-mode--section-content t)
+ (insert "\n")
+ (sx-question-mode--wrap-in-overlay
+ '(face sx-question-mode-content-face)
+ (mapc #'sx-question-mode--print-comment .comments))))))
+
+(defun sx-question-mode--fill-string (text)
+ "Fill TEXT according to `markdown-mode' and return it."
+ (with-temp-buffer
+ (insert text)
+ (markdown-mode)
+ (goto-char (point-min))
+ ;; ;; Do something here
+ ;; (while (null (eobp))
+ ;; (skip-chars-forward "\r\n[:blank:]")
+ ;; (markdown-pre-region))
+ (buffer-string)))
+
+(defun sx-question-mode--propertized-display-name (author)
+ "Return display_name of AUTHOR with `sx-question-mode-author' face."
+ (sx-assoc-let author
+ (propertize .display_name
+ 'font-lock-face 'sx-question-mode-author)))
+
+(defun sx-question-mode--print-comment (data)
+ "Print the comment described by alist DATA."
+ (sx-assoc-let data
+ (insert
+ (format
+ sx-question-mode-comments-format
+ (sx-question-mode--propertized-display-name .owner)
+ (substring
+ ;; We fill with three spaces at the start, so the comment is
+ ;; slightly indented.
+ (sx-question-mode--fill-string
+ (concat " " .body_markdown))
+ ;; Then we remove the spaces from the first line, since we'll
+ ;; add the username there anyway.
+ 3)))))
+
+(defmacro sx-question-mode--wrap-in-overlay (properties &rest body)
+ "Execute BODY and wrap any inserted text in an overlay.
+Overlay is pushed on `sx-question-mode--overlays' and given PROPERTIES.
+Return the result of BODY."
+ (declare (indent 1)
+ (debug t))
+ `(let ((p (point-marker))
+ (result (progn ,@body)))
+ (let ((ov (make-overlay p (point)))
+ (props ,properties))
+ (while props
+ (overlay-put ov (pop props) (pop props)))
+ (push ov sx-question-mode--overlays))
+ result))
+
+(defun sx-question-mode--insert-header (&rest args)
+ "Insert HEADER and VALUE.
+HEADER is given `sx-question-mode-header' face, and value is given FACE.
+\(fn header value face [header value face] [header value face] ...)"
+ (while args
+ (insert
+ (propertize (pop args) 'font-lock-face 'sx-question-mode-header)
+ (propertize (pop args) 'font-lock-face (pop args)))))
+
+
+;;; Movement commands
+;; Sections are headers placed above a question's content or an
+;; answer's content, or above the list of comments. They are
+;; identified with the `sx-question-mode--section' text property.
+;; To move between sections, just search for the property. The value
+;; of the text-property is the depth of the section (1 for contents, 2
+;; for comments).
+(defcustom sx-question-mode-recenter-line 1
+ "Screen line to which we recenter after moving between sections.
+This is used as an argument to `recenter', only used if the end
+of section is outside the window.
+If nil, no recentering is performed."
+ :type '(choice (const :tag "Don't recenter" nil)
+ integer)
+ :group 'sx-question-mode)
+
+(defun sx-question-mode-next-section (&optional n)
+ "Move down to next section (question or answer) of this buffer.
+Prefix argument N moves N sections down or up."
+ (interactive "p")
+ (unless n (setq n 1))
+ (dotimes (_ (abs n))
+ ;; This will either move us to the next section, or move out of
+ ;; the current one.
+ (unless (sx-question-mode--goto-propety-change 'section n)
+ ;; If all we did was move out the current one, then move again
+ ;; and we're guaranteed to reach the next section.
+ (sx-question-mode--goto-propety-change 'section n)))
+ (when sx-question-mode-recenter-line
+ (let ((ov (car-safe (sx-question-mode--section-overlays-at (line-end-position)))))
+ (when (and (overlayp ov) (> (overlay-end ov) (window-end)))
+ (recenter sx-question-mode-recenter-line)))))
+
+(defun sx-question-mode-previous-section (&optional n)
+ "Move down to previous section (question or answer) of this buffer.
+Prefix argument N moves N sections up or down."
+ (interactive "p")
+ (sx-question-mode-next-section (- (or n 1))))
+
+(defun sx-question-mode--goto-propety-change (prop &optional direction)
+ "Move forward until the value of text-property `sx-question-mode--PROP' changes.
+Return the new value of PROP at point.
+If DIRECTION is negative, move backwards instead."
+ (let ((prop (intern (format "sx-question-mode--%s" prop)))
+ (func (if (and (numberp direction)
+ (< direction 0))
+ #'previous-single-property-change
+ #'next-single-property-change))
+ (limit (if (and (numberp direction)
+ (< direction 0))
+ (point-min) (point-max))))
+ (goto-char (funcall func (point) prop nil limit))
+ (get-text-property (point) prop)))
+
+
+(defun sx-question-mode-hide-show-section ()
+ "Hide or show section under point."
+ (interactive)
+ (let ((ov (car (or (sx-question-mode--section-overlays-at (point))
+ (sx-question-mode--section-overlays-at
+ (line-end-position))))))
+ (goto-char (overlay-start ov))
+ (forward-line 0)
+ (overlay-put
+ ov 'invisible
+ (null (overlay-get ov 'invisible)))))
+
+(defun sx-question-mode--section-overlays-at (pos)
+ "Return a list of `sx-question-mode--section-content' overlays at POS."
+ (cl-remove-if (lambda (x) (null (overlay-get x 'sx-question-mode--section-content)))
+ (overlays-at pos)))
+
+
+;;; Major-mode
+(define-derived-mode sx-question-mode markdown-mode "Question"
+ "Major mode for a question and its answers.
+Letters do not insert themselves; instead, they are commands.
+\\<sx-question-mode>
+\\{sx-question-mode}"
+ (remove-hook 'after-change-functions 'markdown-check-change-for-wiki-link t)
+ (remove-hook 'window-configuration-change-hook
+ 'markdown-fontify-buffer-wiki-links t)
+ (read-only-mode))
+
+(mapc
+ (lambda (x) (define-key sx-question-mode-map
+ (car x) (cadr x)))
+ `(("n" sx-question-mode-next-section)
+ ("p" sx-question-mode-previous-section)
+ ("j" sx-question-mode-next-section)
+ ("k" sx-question-mode-previous-section)
+ ("g" sx-question-mode-refresh)
+ ("q" quit-window)
+ (" " scroll-up-command)
+ (,(kbd "S-SPC") scroll-down-command)
+ ([backspace] scroll-down-command)
+ ([tab] sx-question-mode-hide-show-section)))
+
+(defun sx-question-mode-refresh ()
+ "Refresh currently displayed question.
+Queries the API for any changes to the question or its answers or
+comments, and redisplays it."
+ (interactive)
+ (unless (derived-mode-p 'sx-question-mode)
+ (error "Not in `sx-question-mode'"))
+ (sx-assoc-let sx-question-mode--data
+ (sx-question-mode--display
+ (sx-question-get-question
+ sx-question-list--current-site .question_id)
+ (selected-window))))
+
+(provide 'sx-question-mode)
+;;; sx-question-mode.el ends here
diff --git a/sx-question.el b/sx-question.el
index 20a71cc..d15cc80 100644
--- a/sx-question.el
+++ b/sx-question.el
@@ -30,7 +30,19 @@
(require 'sx-method)
(defvar sx-question-browse-filter
- '(nil (user.profile_image shallow_user.profile_image)))
+ '((question.body_markdown
+ question.comments
+ question.answers
+ question.last_editor
+ user.display_name
+ comment.owner
+ comment.body_markdown
+ comment.body
+ answer.last_editor
+ answer.owner
+ answer.body_markdown
+ answer.comments)
+ (user.profile_image shallow_user.profile_image)))
(defun sx-question-get-questions (site &optional page)
"Get the page PAGE of questions from SITE."
@@ -40,6 +52,16 @@
(page . ,page))
sx-question-browse-filter))
+(defun sx-question-get-question (site id)
+ "Get the question ID from SITE."
+ (let ((res (sx-method-call
+ (format "questions/%s" id)
+ `((site . ,site))
+ sx-question-browse-filter)))
+ (if (vectorp res)
+ (elt res 0)
+ (error "Couldn't find question %S in %S" id site))))
+
;;; Question Properties
(defun sx-question--read-p (question)
@@ -62,54 +84,10 @@ With optional argument predicate, use it instead of `<'."
(funcall (or pred #'<)
(cdr (assoc property x))
(cdr (assoc property y))))
-
-;;; Displaying a question
-(defvar sx-question--window nil
- "Window where the content of questions is displayed.")
-
-(defvar sx-question--buffer nil
- "Buffer being used to display questions.")
-
-(defcustom sx-question-use-html t
- "If nil, markdown is used for the body."
- :type 'boolean
- :group 'sx-question)
-
-(defun sx-question--display (data &optional window)
- "Display question given by DATA on WINDOW.
-If WINDOW is nil, use selected one."
- (let ((sx-lto--body-src-block
- (if sx-question-use-html nil
- sx-lto--body-src-block))
- (inhibit-read-only t))
- (with-current-buffer
- (sx-question--display-buffer window)
- (erase-buffer)
- (insert
- (org-element-interpret-data
- (sx-lto--question data)))
- (org-mode)
- (show-all)
- (view-mode)
- (current-buffer))))
-
-(defun sx-question--display-buffer (window)
- "Display and return the buffer used for displaying a question.
-Create the buffer if necessary.
-If WINDOW is given, use that to display the buffer."
- ;; Create the buffer if necessary.
- (unless (buffer-live-p sx-question--buffer)
- (setq sx-question--buffer
- (generate-new-buffer "*sx-question*")))
- (cond
- ;; Window was given, use it.
- ((window-live-p window)
- (set-window-buffer window sx-question--buffer))
- ;; No window, but the buffer is already being displayed somewhere.
- ((get-buffer-window sx-question--buffer 'visible))
- ;; Neither, so we create the window.
- (t (switch-to-buffer sx-question--buffer)))
- sx-question--buffer)
+
+(defun sx-question--tag-format (tag)
+ "Formats TAG for display"
+ (concat "[" tag "]"))
(provide 'sx-question)
;;; sx-question.el ends here
diff --git a/sx-request.el b/sx-request.el
index dd98ead..b16fd9a 100644
--- a/sx-request.el
+++ b/sx-request.el
@@ -115,7 +115,7 @@ number of requests left every time it finishes a call.")
sx-request-remaining-api-requests-message-threshold)
(sx-message "%d API requests reamining"
sx-request-remaining-api-requests))
- .items)))))))
+ (sx-encoding-clean-content-deep .items))))))))
;;; Support Functions
diff --git a/sx-time.el b/sx-time.el
index 1c8e353..9c4dfaa 100644
--- a/sx-time.el
+++ b/sx-time.el
@@ -49,5 +49,27 @@
(concat (format "%.0f" (/ delay (car (cddr here))))
(cadr here)))))))
+(defcustom sx-time-date-format-year "%H:%M %e %b %Y"
+ "Format used for dates on a past year.
+See also `sx-time-date-format'."
+ :type 'string
+ :group 'sx-time)
+
+(defcustom sx-time-date-format "%H:%M - %d %b"
+ "Format used for dates on this year.
+See also `sx-time-date-format-year'."
+ :type 'string
+ :group 'sx-time)
+
+(defun sx-time-seconds-to-date (seconds)
+ "Return the integer SECONDS as a date string."
+ (let ((time (seconds-to-time seconds)))
+ (format-time-string
+ (if (string= (format-time-string "%Y")
+ (format-time-string "%Y" time))
+ sx-time-date-format
+ sx-time-date-format-year)
+ time)))
+
(provide 'sx-time)
;;; sx-time.el ends here