aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sx-auth.el1
-rw-r--r--sx-question-list.el310
-rw-r--r--sx-question-mode.el3
-rw-r--r--sx-tab.el102
-rw-r--r--sx.org16
-rw-r--r--test/tests.el3
6 files changed, 341 insertions, 94 deletions
diff --git a/sx-auth.el b/sx-auth.el
index ac4bd31..217da7d 100644
--- a/sx-auth.el
+++ b/sx-auth.el
@@ -111,6 +111,7 @@ parsed and displayed prominently on the page)."
`((client_id . ,sx-auth-client-id)
(scope . (read_inbox
no_expiry
+ private_info
write_access))
(redirect_uri . ,(url-hexify-string
sx-auth-redirect-uri)))
diff --git a/sx-question-list.el b/sx-question-list.el
index 9e94536..fc6d16c 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -52,7 +52,7 @@
:group 'sx-question-list-faces)
(defface sx-question-list-answers-accepted
- '((t :underline t :overline t :inherit sx-question-list-answers))
+ '((t :box 1 :inherit sx-question-list-answers))
""
:group 'sx-question-list-faces)
@@ -93,14 +93,158 @@
:group 'sx-question-list-faces)
+;;; Backend variables
+(defvar sx-question-list--print-function #'sx-question-list--print-info
+ "Function to convert a question alist into a tabulated-list entry.
+Used by `sx-question-list-mode', the default value is
+`sx-question-list--print-info'.
+
+If this is set to a different value, it may be necessary to
+change `tabulated-list-format' accordingly.")
+(make-variable-buffer-local 'sx-question-list--print-function)
+
+(defun sx-question-list--print-info (question-data)
+ "Convert `json-read' QUESTION-DATA into tabulated-list format.
+
+This is the default printer used by `sx-question-list'. It
+assumes QUESTION-DATA is an alist containing (at least) the
+elements:
+ `site', `score', `upvoted', `answer_count', `title',
+ `last_activity_date', `tags', `uestion_id'.
+
+Also see `sx-question-list-refresh'."
+ (sx-assoc-let question-data
+ (let ((favorite (if (member .question_id
+ (assoc .site
+ sx-favorites--user-favorite-list))
+ (if (char-displayable-p ?\x2b26) "\x2b26" "*") " ")))
+ (list
+ question-data
+ (vector
+ (list (int-to-string .score)
+ 'face (if .upvoted 'sx-question-list-score-upvoted
+ 'sx-question-list-score))
+ (list (int-to-string .answer_count)
+ 'face (if (sx-question--accepted-answer-id question-data)
+ 'sx-question-list-answers-accepted
+ 'sx-question-list-answers))
+ (concat
+ (propertize
+ .title
+ 'face (if (sx-question--read-p question-data)
+ 'sx-question-list-read-question
+ ;; Increment `sx-question-list--unread-count' for
+ ;; the mode-line.
+ (cl-incf sx-question-list--unread-count)
+ 'sx-question-list-unread-question))
+ (propertize " " 'display "\n ")
+ (propertize favorite 'face 'sx-question-list-favorite)
+ " "
+ (propertize (concat (sx-time-since .last_activity_date)
+ sx-question-list-ago-string)
+ 'face 'sx-question-list-date)
+ " "
+ (propertize (mapconcat #'sx-question--tag-format .tags " ")
+ 'face 'sx-question-list-tags)
+ (propertize " " 'display "\n")))))))
+
+(defvar sx-question-list--pages-so-far 0
+ "Number of pages currently being displayed.
+This variable gets reset to 0 before every refresh.
+It should be used by `sx-question-list--next-page-function'.")
+(make-variable-buffer-local 'sx-question-list--pages-so-far)
+
+(defvar sx-question-list--refresh-function nil
+ "Function used to refresh the list of questions to be displayed.
+Used by `sx-question-list-mode', this is a function, called with
+no arguments, which returns a list questions to be displayed,
+like the one returned by `sx-question-get-questions'.
+
+If this is not set, the value of `sx-question-list--dataset' is
+used, and the list is simply redisplayed.")
+(make-variable-buffer-local 'sx-question-list--refresh-function)
+
+(defvar sx-question-list--next-page-function nil
+ "Function used to fetch the next page of questions to be displayed.
+Used by `sx-question-list-mode'. This is a function, called with
+no arguments, which returns a list questions to be displayed,
+like the one returned by `sx-question-get-questions'.
+
+This function will be called when the user presses \\<sx-question-list-mode-map>\\[sx-question-list-next] at the end
+of the question list. It should either return nil (indicating
+\"no more questions\") or return a list of questions which will
+appended to the currently displayed list.
+
+If this is not set, it's the same as a function which always
+returns nil.")
+(make-variable-buffer-local 'sx-question-list--next-page-function)
+
+(defvar sx-question-list--dataset nil
+ "The logical data behind the displayed list of questions.
+This dataset contains even questions that are hidden by the user,
+and thus not displayed in the list of questions.
+
+This is ignored if `sx-question-list--refresh-function' is set.")
+(make-variable-buffer-local 'sx-question-list--dataset)
+
+
;;; Mode Definition
-(define-derived-mode sx-question-list-mode tabulated-list-mode "Question List"
+(define-derived-mode sx-question-list-mode
+ tabulated-list-mode "Question List"
"Major mode for browsing a list of questions from StackExchange.
Letters do not insert themselves; instead, they are commands.
-\\<sx-question-list>
-\\{sx-question-list}"
+
+The recommended way of using this mode is to activate it and then
+set `sx-question-list--next-page-function'. The return value of
+this function is mapped with `sx-question-list--print-function',
+so you may need to customize the latter if the former does not
+return a list of questions.
+
+The full list of variables which can be set is:
+ 1. `sx-question-list--site'
+ Set this to the name of the site if that makes sense. If it
+ doesn't leave it as nil.
+ 2. `sx-question-list--print-function'
+ Change this if the data you're dealing with is not strictly a
+ list of questions (see the doc for details).
+ 3. `sx-question-list--refresh-function'
+ This is used to populate the initial list. It is only necessary
+ if item 4 does not fit your needs.
+ 4. `sx-question-list--next-page-function'
+ This is used to fetch further questions. If item 3 is nil, it is
+ also used to populate the initial list.
+ 5. `sx-question-list--dataset'
+ This is only used if both 3 and 4 are nil. It can be used to
+ display a static list.
+\\<sx-question-list-mode-map>
+If none of these is configured, the behaviour is that of a
+\"Frontpage\", for the site given by
+`sx-question-list--site'.
+
+Item 2 is mandatory, but it also has a sane default which is
+usually enough.
+
+As long as one of 3, 4, or 5 is provided, the other two are
+entirely optional. Populating or refreshing the list of questions
+is done in the following way:
+ - Set `sx-question-list--pages-so-far' to 1.
+ - Call function 2.
+ - If function 2 is not given, call function 3 with argument 1.
+ - If 3 is not given use the value of 4.
+
+Adding further questions to the bottom of the list is done by:
+ - Increment `sx-question-list--pages-so-far'.
+ - Call function 3 with argument `sx-question-list--pages-so-far'.
+ - If it returns anything, append to the dataset and refresh the
+ display; otherwise, decrement `sx-question-list--pages-so-far'.
+
+If `sx-question-list--site' is given, items 3 and 4 should take it
+into consideration.
+
+\\{sx-question-list-mode-map}"
(hl-line-mode 1)
(sx-question-list--update-mode-line)
+ (setq sx-question-list--pages-so-far 0)
(setq tabulated-list-format
[(" V" 3 t :right-align t)
(" A" 3 t :right-align t)
@@ -108,7 +252,7 @@ Letters do not insert themselves; instead, they are commands.
(setq tabulated-list-padding 1)
;; Sorting by title actually sorts by date. It's what we want, but
;; it's not terribly intuitive.
- (setq tabulated-list-sort-key '("Title" . nil))
+ (setq tabulated-list-sort-key nil)
(add-hook 'tabulated-list-revert-hook
#'sx-question-list-refresh nil t)
(add-hook 'tabulated-list-revert-hook
@@ -128,6 +272,8 @@ Letters do not insert themselves; instead, they are commands.
sx-question-list-date-sort-method
(car x) (car y) #'>))
+
+;;; Keybinds
(mapc
(lambda (x) (define-key sx-question-list-mode-map
(car x) (cadr x)))
@@ -135,6 +281,10 @@ Letters do not insert themselves; instead, they are commands.
("p" sx-question-list-previous)
("j" sx-question-list-view-next)
("k" sx-question-list-view-previous)
+ ("N" sx-question-list-next-far)
+ ("P" sx-question-list-previous-far)
+ ("J" sx-question-list-next-far)
+ ("K" sx-question-list-previous-far)
("g" sx-question-list-refresh)
(":" sx-question-list-switch-site)
("v" sx-question-list-visit)
@@ -165,10 +315,10 @@ Non-interactively, DATA is a question alist."
(when (called-interactively-p 'any)
(sx-question-list-refresh 'redisplay 'noupdate)))
-(defvar sx-question-list--current-page "Latest"
+(defvar sx-question-list--current-tab "Latest"
;; @TODO Other values (once we implement them) are "Top Voted",
;; "Unanswered", etc.
- "Variable describing current page being viewed.")
+ "Variable describing current tab being viewed.")
(defvar sx-question-list--unread-count 0
"Holds the number of unread questions in the current buffer.")
@@ -182,7 +332,7 @@ Non-interactively, DATA is a question alist."
'(" "
mode-name
" "
- (:propertize sx-question-list--current-page
+ (:propertize sx-question-list--current-tab
face mode-line-buffer-id)
" ["
"Unread: "
@@ -206,14 +356,9 @@ Non-interactively, DATA is a question alist."
(setq sx-question-list--total-count
(length tabulated-list-entries))))
-(defvar sx-question-list--current-site "emacs"
+(defvar sx-question-list--site nil
"Site being displayed in the *question-list* buffer.")
-(defvar sx-question-list--current-dataset nil
- "The logical data behind the displayed list of questions.
-This dataset contains even questions that are hidden by the user,
-and thus not displayed in the list of questions.")
-
(defun sx-question-list-refresh (&optional redisplay no-update)
"Update the list of questions.
If REDISPLAY is non-nil (or if interactive), also call `tabulated-list-print'.
@@ -222,15 +367,19 @@ a new list before redisplaying."
(interactive "p\nP")
;; Reset the mode-line unread count (we rebuild it here).
(setq sx-question-list--unread-count 0)
+ (unless no-update
+ (setq sx-question-list--pages-so-far 1))
(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)
+ (or (and no-update sx-question-list--dataset)
+ (and (functionp sx-question-list--refresh-function)
+ (funcall sx-question-list--refresh-function))
+ (and (functionp sx-question-list--next-page-function)
+ (funcall sx-question-list--next-page-function 1))
+ sx-question-list--dataset)))
+ (setq sx-question-list--dataset question-list)
;; Print the result.
(setq tabulated-list-entries
- (mapcar #'sx-question-list--print-info
+ (mapcar sx-question-list--print-function
(cl-remove-if #'sx-question--hidden-p question-list))))
(when redisplay (tabulated-list-print 'remember)))
@@ -251,44 +400,6 @@ Used in the questions list to indicate a question was updated
:type 'string
:group 'sx-question-list)
-(defun sx-question-list--print-info (question-data)
- "Convert `json-read' QUESTION-DATA into tabulated-list format.
-See `sx-question-list-refresh'."
- (sx-assoc-let question-data
- (let ((favorite (if (member .question_id
- (assoc .site
- sx-favorites--user-favorite-list))
- (if (char-displayable-p ?\x2b26) "\x2b26" "*") " ")))
- (list
- question-data
- (vector
- (list (int-to-string .score)
- 'face (if .upvoted 'sx-question-list-score-upvoted
- 'sx-question-list-score))
- (list (int-to-string .answer_count)
- 'face (if (sx-question--accepted-answer-id question-data)
- 'sx-question-list-answers-accepted
- 'sx-question-list-answers))
- (concat
- (propertize
- .title
- 'face (if (sx-question--read-p question-data)
- 'sx-question-list-read-question
- ;; Increment `sx-question-list--unread-count' for
- ;; the mode-line.
- (cl-incf sx-question-list--unread-count)
- 'sx-question-list-unread-question))
- (propertize " " 'display "\n ")
- (propertize favorite 'face 'sx-question-list-favorite)
- " "
- (propertize (concat (sx-time-since .last_activity_date)
- sx-question-list-ago-string)
- 'face 'sx-question-list-date)
- " "
- (propertize (mapconcat #'sx-question--tag-format .tags " ")
- 'face 'sx-question-list-tags)
- (propertize " " 'display "\n")))))))
-
(defun sx-question-list-view-previous (n)
"Move cursor up N questions up and display this question.
Displayed in `sx-question-mode--window', replacing any question
@@ -308,7 +419,34 @@ that may currently be there."
"Move cursor down N questions.
This does not update `sx-question-mode--window'."
(interactive "p")
- (forward-line n))
+ (if (and (< n 0) (bobp))
+ (sx-question-list-refresh 'redisplay)
+ (forward-line n)
+ ;; If we were trying to move forward, but we hit the end.
+ (when (eobp)
+ ;; Try to get more questions.
+ (sx-question-list-next-page))))
+
+(defun sx-question-list-next-page ()
+ "Fetch and display the next page of questions."
+ (interactive)
+ ;; Stay at the last line.
+ (goto-char (point-max))
+ (forward-line -1)
+ (when (functionp sx-question-list--next-page-function)
+ ;; Try to get more questions
+ (let ((list (funcall sx-question-list--next-page-function
+ (1+ sx-question-list--pages-so-far))))
+ (if (null list)
+ (message "No further questions.")
+ ;; If it worked, increment the variable.
+ (cl-incf sx-question-list--pages-so-far)
+ ;; And update the dataset.
+ ;; @TODO: Check for duplicates.
+ (setq sx-question-list--dataset
+ (append sx-question-list--dataset list))
+ (sx-question-list-refresh 'redisplay 'no-update)
+ (forward-line 1)))))
(defun sx-question-list-previous (n)
"Move cursor up N questions.
@@ -316,6 +454,24 @@ This does not update `sx-question-mode--window'."
(interactive "p")
(sx-question-list-next (- n)))
+(defcustom sx-question-list-far-step-size 5
+ "How many questions `sx-question-list-next-far' skips."
+ :type 'integer
+ :group 'sx-question-list
+ :package-version '(sx-question-list . ""))
+
+(defun sx-question-list-next-far (n)
+ "Move cursor up N*`sx-question-list-far-step-size' questions.
+This does not update `sx-question-mode--window'."
+ (interactive "p")
+ (sx-question-list-next (* n sx-question-list-far-step-size)))
+
+(defun sx-question-list-previous-far (n)
+ "Move cursor up N questions.
+This does not update `sx-question-mode--window'."
+ (interactive "p")
+ (sx-question-list-next-far (- n)))
+
(defun sx-question-list-display-question (&optional data focus)
"Display question given by DATA.
When DATA is nil, display question under point. When FOCUS is
@@ -356,36 +512,18 @@ relevant window."
(defun sx-question-list-switch-site (site)
"Switch the current site to SITE and display its questions.
-Uses `ido-completing-read' if variable `ido-mode' is active. Retrieves
-completions from `sx-site-get-api-tokens'. Sets
-`sx-question-list--current-site' and then
+Use `ido-completing-read' if variable `ido-mode' is active.
+Retrieve completions from `sx-site-get-api-tokens'.
+Sets `sx-question-list--site' and then call
`sx-question-list-refresh' with `redisplay'."
(interactive
(list (funcall (if ido-mode #'ido-completing-read #'completing-read)
- "Switch to site: " (sx-site-get-api-tokens)
- (lambda (site)
- (not (equal site sx-question-list--current-site)))
- t)))
- (setq sx-question-list--current-site site)
- (sx-question-list-refresh 'redisplay))
-
-(defvar sx-question-list--buffer nil
- "Buffer where the list of questions is displayed.")
-
-(defun list-questions (no-update)
- "Display a list of StackExchange questions.
-NO-UPDATE is passed to `sx-question-list-refresh'."
- (interactive "P")
- (sx-initialize)
- (unless (buffer-live-p sx-question-list--buffer)
- (setq sx-question-list--buffer
- (generate-new-buffer "*question-list*")))
- (with-current-buffer sx-question-list--buffer
- (sx-question-list-mode)
- (sx-question-list-refresh 'redisplay no-update))
- (switch-to-buffer sx-question-list--buffer))
-
-(defalias 'sx-list-questions #'list-questions)
+ "Switch to site: " (sx-site-get-api-tokens)
+ (lambda (site) (not (equal site sx-question-list--site)))
+ t)))
+ (when (and (stringp site) (> (length site) 0))
+ (setq sx-question-list--site site)
+ (sx-question-list-refresh 'redisplay)))
(provide 'sx-question-list)
;;; sx-question-list.el ends here
diff --git a/sx-question-mode.el b/sx-question-mode.el
index f1705a5..a9e14e7 100644
--- a/sx-question-mode.el
+++ b/sx-question-mode.el
@@ -604,8 +604,7 @@ comments, and redisplays it."
(sx-question-mode--ensure-mode)
(sx-assoc-let sx-question-mode--data
(sx-question-mode--display
- (sx-question-get-question
- sx-question-list--current-site .question_id)
+ (sx-question-get-question .site .question_id)
(selected-window))))
(defun sx-question-mode--ensure-mode ()
diff --git a/sx-tab.el b/sx-tab.el
new file mode 100644
index 0000000..8a51236
--- /dev/null
+++ b/sx-tab.el
@@ -0,0 +1,102 @@
+;;; sx-tab.el --- Functions for viewing different tabs. -*- 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 'sx)
+(require 'sx-question-list)
+
+(defcustom sx-tab-default-site "emacs"
+ "Name of the site to use by default when listing questions."
+ :type 'string
+ :group 'stack-exchange)
+
+(defmacro sx-tab--define (tab pager &optional printer refresher
+ &rest body)
+ "Define a stack-exchange tab called TAB.
+TAB is a capitalized string.
+
+This defines a command `sx-tab-TAB' for displaying the tab,
+and a variable `sx-tab--TAB-buffer' for holding the bufer.
+
+The arguments PAGER, PRINTER, and REFRESHER, if non-nil, are
+respectively used to set the value of the variables
+`sx-question-list--print-function',
+`sx-question-list--refresh-function', and
+`sx-question-list--next-page-function'.
+
+BODY is evaluated after activating the mode and setting these
+variables, but before refreshing the display."
+ (declare (indent 1) (debug t))
+ (let* ((name (downcase tab))
+ (buffer-variable
+ (intern (concat "sx-tab--" name "-buffer"))))
+ `(progn
+ (defvar ,buffer-variable nil
+ ,(format "Buffer where the %s questions are displayed."
+ tab))
+ (defun
+ ,(intern (concat "sx-tab-" name))
+ (&optional no-update site)
+ ,(format "Display a list of %s questions for SITE.
+
+NO-UPDATE (the prefix arg) is passed to `sx-question-list-refresh'.
+If SITE is nil, use `sx-tab-default-site'."
+ tab)
+ (interactive
+ (list current-prefix-arg
+ (funcall (if ido-mode #'ido-completing-read #'completing-read)
+ (format "Site (%s): " sx-tab-default-site)
+ (sx-site-get-api-tokens) nil t nil nil
+ sx-tab-default-site)))
+ (sx-initialize)
+ (unless site (setq site sx-tab-default-site))
+ ;; Create the buffer
+ (unless (buffer-live-p ,buffer-variable)
+ (setq ,buffer-variable
+ (generate-new-buffer "*question-list*")))
+ ;; Fill the buffer with content.
+ (with-current-buffer ,buffer-variable
+ (sx-question-list-mode)
+ ,(when printer
+ `(setq sx-question-list--print-function ,printer))
+ ,(when refresher
+ `(setq sx-question-list--refresh-function ,refresher))
+ ,(when pager
+ `(setq sx-question-list--next-page-function ,pager))
+ (setq sx-question-list--site site)
+ (setq sx-question-list--current-tab ,tab)
+ ,@body
+ (sx-question-list-refresh 'redisplay no-update))
+ (switch-to-buffer ,buffer-variable)))))
+
+
+;;; FrontPage
+(sx-tab--define "FrontPage"
+ (lambda (page)
+ (sx-question-get-questions
+ sx-question-list--site page)))
+
+(provide 'sx-tab)
+;;; sx-tab.el ends here
diff --git a/sx.org b/sx.org
index b646b2c..cb1c109 100644
--- a/sx.org
+++ b/sx.org
@@ -66,11 +66,16 @@ a cache file.
** Browsing Questions
To browse a list of questions retrieved from the site, use
-~list-questions~. This pulls the first page of questions from the
-current site and displays them in a list. Refresh the page with =g=,
-use =n= and =p= to navigate without viewing the question, =j= and =k=
-to do the same /with/ viewing the question, and =RET= to visit the
-question at point.
+~sx-tab-frontpage~. This queries for a site, pulls the first page of
+questions for that site, and displays them in a list.
+
+- Refresh the page with =g= or by scrolling past the top.
+- Use =n=, =p=, =N=, =P= to navigate without viewing the question.
+- =j=, =k=, =J=, =K= to do the same /with/ viewing the question.
+- =RET= to open the question with Emacs.
+- =v= to visit it with a browser.
+
+Scrolling past the bottom of the list fetches more questions.
* List of Hooks
:PROPERTIES:
@@ -123,4 +128,5 @@ The language for Emacs Lisp source code blocks should be given as
# Local Variables:
# org-export-date-timestamp-format: "$B %e %Y"
+# sentence-end-double-space: t
# End:
diff --git a/test/tests.el b/test/tests.el
index c76e137..75238fe 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -48,6 +48,7 @@
(require 'sx)
(require 'sx-question)
(require 'sx-question-list)
+(require 'sx-tab)
(ert-deftest test-basic-request ()
"Test basic request functionality"
@@ -100,7 +101,7 @@
(ert-deftest question-list-display ()
(cl-letf (((symbol-function #'sx-request-make)
(lambda (&rest _) sx-test-data-questions)))
- (list-questions nil)
+ (sx-tab-frontpage nil "emacs")
(switch-to-buffer "*question-list*")
(goto-char (point-min))
(should (equal (buffer-name) "*question-list*"))