sbbs.el

Check-in [8aaa2a71b5]
Login
Overview
Comment:initial import many thanks to anon who fixed submission issues in http://textboard.org/prog/81
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | descendants | master | trunk
Files: files | file ages | folders
SHA3-256: 8aaa2a71b555870630164e1317c9064251d5f0a086d7a404f0f4b5bbed72468a
User & Date: philip@warpmail.net on 2020-02-21 22:15:34
Other Links: branch diff | manifest | tags
Context
2020-02-21
23:09
added more issues and improvments to readme check-in: 3010b30422 user: philip@warpmail.net tags: master, trunk
22:15
initial import many thanks to anon who fixed submission issues in http://textboard.org/prog/81 check-in: 8aaa2a71b5 user: philip@warpmail.net tags: master, trunk
Changes
Hide Diffs Side-by-Side Diffs Ignore Whitespace

Added .dir-locals.el version [1d789c6a91].

            1  +;;; Directory Local Variables
            2  +;;; For more information see (info "(emacs) Directory Variables")
            3  +
            4  +((emacs-lisp-mode
            5  +  (tab-width . 4)
            6  +  (indent-tabs-mode . nil)))

Added .gitignore version [4aeab8f511].

            1  +*.elc
            2  +*~
            3  +\#*\#

Added LICENSE version [38a1a1ee40].

            1  +Creative Commons Legal Code
            2  +
            3  +CC0 1.0 Universal
            4  +
            5  +    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
            6  +    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
            7  +    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
            8  +    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
            9  +    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
           10  +    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
           11  +    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
           12  +    HEREUNDER.
           13  +
           14  +Statement of Purpose
           15  +
           16  +The laws of most jurisdictions throughout the world automatically confer
           17  +exclusive Copyright and Related Rights (defined below) upon the creator
           18  +and subsequent owner(s) (each and all, an "owner") of an original work of
           19  +authorship and/or a database (each, a "Work").
           20  +
           21  +Certain owners wish to permanently relinquish those rights to a Work for
           22  +the purpose of contributing to a commons of creative, cultural and
           23  +scientific works ("Commons") that the public can reliably and without fear
           24  +of later claims of infringement build upon, modify, incorporate in other
           25  +works, reuse and redistribute as freely as possible in any form whatsoever
           26  +and for any purposes, including without limitation commercial purposes.
           27  +These owners may contribute to the Commons to promote the ideal of a free
           28  +culture and the further production of creative, cultural and scientific
           29  +works, or to gain reputation or greater distribution for their Work in
           30  +part through the use and efforts of others.
           31  +
           32  +For these and/or other purposes and motivations, and without any
           33  +expectation of additional consideration or compensation, the person
           34  +associating CC0 with a Work (the "Affirmer"), to the extent that he or she
           35  +is an owner of Copyright and Related Rights in the Work, voluntarily
           36  +elects to apply CC0 to the Work and publicly distribute the Work under its
           37  +terms, with knowledge of his or her Copyright and Related Rights in the
           38  +Work and the meaning and intended legal effect of CC0 on those rights.
           39  +
           40  +1. Copyright and Related Rights. A Work made available under CC0 may be
           41  +protected by copyright and related or neighboring rights ("Copyright and
           42  +Related Rights"). Copyright and Related Rights include, but are not
           43  +limited to, the following:
           44  +
           45  +  i. the right to reproduce, adapt, distribute, perform, display,
           46  +     communicate, and translate a Work;
           47  + ii. moral rights retained by the original author(s) and/or performer(s);
           48  +iii. publicity and privacy rights pertaining to a person's image or
           49  +     likeness depicted in a Work;
           50  + iv. rights protecting against unfair competition in regards to a Work,
           51  +     subject to the limitations in paragraph 4(a), below;
           52  +  v. rights protecting the extraction, dissemination, use and reuse of data
           53  +     in a Work;
           54  + vi. database rights (such as those arising under Directive 96/9/EC of the
           55  +     European Parliament and of the Council of 11 March 1996 on the legal
           56  +     protection of databases, and under any national implementation
           57  +     thereof, including any amended or successor version of such
           58  +     directive); and
           59  +vii. other similar, equivalent or corresponding rights throughout the
           60  +     world based on applicable law or treaty, and any national
           61  +     implementations thereof.
           62  +
           63  +2. Waiver. To the greatest extent permitted by, but not in contravention
           64  +of, applicable law, Affirmer hereby overtly, fully, permanently,
           65  +irrevocably and unconditionally waives, abandons, and surrenders all of
           66  +Affirmer's Copyright and Related Rights and associated claims and causes
           67  +of action, whether now known or unknown (including existing as well as
           68  +future claims and causes of action), in the Work (i) in all territories
           69  +worldwide, (ii) for the maximum duration provided by applicable law or
           70  +treaty (including future time extensions), (iii) in any current or future
           71  +medium and for any number of copies, and (iv) for any purpose whatsoever,
           72  +including without limitation commercial, advertising or promotional
           73  +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
           74  +member of the public at large and to the detriment of Affirmer's heirs and
           75  +successors, fully intending that such Waiver shall not be subject to
           76  +revocation, rescission, cancellation, termination, or any other legal or
           77  +equitable action to disrupt the quiet enjoyment of the Work by the public
           78  +as contemplated by Affirmer's express Statement of Purpose.
           79  +
           80  +3. Public License Fallback. Should any part of the Waiver for any reason
           81  +be judged legally invalid or ineffective under applicable law, then the
           82  +Waiver shall be preserved to the maximum extent permitted taking into
           83  +account Affirmer's express Statement of Purpose. In addition, to the
           84  +extent the Waiver is so judged Affirmer hereby grants to each affected
           85  +person a royalty-free, non transferable, non sublicensable, non exclusive,
           86  +irrevocable and unconditional license to exercise Affirmer's Copyright and
           87  +Related Rights in the Work (i) in all territories worldwide, (ii) for the
           88  +maximum duration provided by applicable law or treaty (including future
           89  +time extensions), (iii) in any current or future medium and for any number
           90  +of copies, and (iv) for any purpose whatsoever, including without
           91  +limitation commercial, advertising or promotional purposes (the
           92  +"License"). The License shall be deemed effective as of the date CC0 was
           93  +applied by Affirmer to the Work. Should any part of the License for any
           94  +reason be judged legally invalid or ineffective under applicable law, such
           95  +partial invalidity or ineffectiveness shall not invalidate the remainder
           96  +of the License, and in such case Affirmer hereby affirms that he or she
           97  +will not (i) exercise any of his or her remaining Copyright and Related
           98  +Rights in the Work or (ii) assert any associated claims and causes of
           99  +action with respect to the Work, in either case contrary to Affirmer's
          100  +express Statement of Purpose.
          101  +
          102  +4. Limitations and Disclaimers.
          103  +
          104  + a. No trademark or patent rights held by Affirmer are waived, abandoned,
          105  +    surrendered, licensed or otherwise affected by this document.
          106  + b. Affirmer offers the Work as-is and makes no representations or
          107  +    warranties of any kind concerning the Work, express, implied,
          108  +    statutory or otherwise, including without limitation warranties of
          109  +    title, merchantability, fitness for a particular purpose, non
          110  +    infringement, or the absence of latent or other defects, accuracy, or
          111  +    the present or absence of errors, whether or not discoverable, all to
          112  +    the greatest extent permissible under applicable law.
          113  + c. Affirmer disclaims responsibility for clearing rights of other persons
          114  +    that may apply to the Work or any use thereof, including without
          115  +    limitation any person's Copyright and Related Rights in the Work.
          116  +    Further, Affirmer disclaims responsibility for obtaining any necessary
          117  +    consents, permissions or other rights required for any use of the
          118  +    Work.
          119  + d. Affirmer understands and acknowledges that Creative Commons is not a
          120  +    party to this document and has no duty or obligation with respect to
          121  +    this CC0 or use of the Work.

Added README.md version [61806f711a].

            1  +`sbbs.el`
            2  +=========
            3  +
            4  +`sbbs` is a [SchemeBBS][schemebbs] client in Emacs. It implements
            5  +browsing the thread list, threads and creating responses.
            6  +
            7  +Any bugs, patches or questions can be submitted to my [public
            8  +inbox][mail].
            9  +
           10  +Issues and Improvments
           11  +----------------------
           12  +
           13  +- Due to issues with `url.el`, some versions of Emacs take
           14  +  quite long to load threads/thread lists.
           15  +- No highlighting in response buffers.
           16  +- Spoilers aren't shown unless manually highlighted.
           17  +
           18  +Copying
           19  +-------
           20  +
           21  +`sbbs.el` is distributed under the [CC0 1.0 Universal (CC0 1.0) Public
           22  +Domain Dedication][cc0] license.
           23  +
           24  +[schemebbs]: https://textboard.org/
           25  +[public inbox]: https://lists.sr.ht/~zge/public-inbox
           26  +[cc0]: https://creativecommons.org/publicdomain/zero/1.0/deed

Added sbbs.el version [7a0569766c].

            1  +;;; sbbs.el --- SchemeBBS client -*- lexical-binding: t -*-
            2  +
            3  +;; Author: Philip K. <philip@warpmail.net>
            4  +;; Version: 0.1.0
            5  +;; Keywords: comm
            6  +;; Package-Requires: ((emacs "24.4"))
            7  +;; URL: https://git.sr.ht/~zge/sbbs
            8  +
            9  +;; This file is NOT part of Emacs.
           10  +;;
           11  +;; This file is in the public domain, to the extent possible under law,
           12  +;; published under the CC0 1.0 Universal license.
           13  +;;
           14  +;; For a full copy of the CC0 license see
           15  +;; https://creativecommons.org/publicdomain/zero/1.0/legalcode
           16  +
           17  +;;; Commentary:
           18  +;;
           19  +;; sbbs is a SchemeBBS (https://textboard.org) client in Emacs.
           20  +;;
           21  +;; Start browsing a board by invoking M-x `sbbs'.
           22  +
           23  +;; Open a board
           24  +
           25  +;;; Code:
           26  +
           27  +(require 'tabulated-list)
           28  +(require 'button)
           29  +(require 'url)
           30  +(require 'hl-line)
           31  +
           32  + ;; CUSTOMIZABLE DATA
           33  +
           34  +(defgroup sbbs nil
           35  +  "SchemeBBS client."
           36  +  :group 'applications
           37  +  :prefix "sbbs-")
           38  +
           39  +(defcustom sbbs-boards
           40  +  '(("textboard.org" ("sol" "prog") nil))
           41  +  "List of SchemeBBS sites and boards."
           42  +  :type '(repeat (list (string :tag "Board Domain")
           43  +                       (repeat (string :tag "Board Name"))
           44  +                       (boolean :tag "Use TLS?")))
           45  +  :risky t)
           46  +
           47  +(defface sbbs--spoiler-face
           48  +  '((((background light)) :background "black" :foreground "black")
           49  +    (((background dark)) :background "white" :foreground "white"))
           50  +  "Face for spoiler text in threads.")
           51  +
           52  +(defface sbbs--code-face
           53  +  '((((background light)) :background "gray89")
           54  +    (((background dark)) :background "gray11"))
           55  +  "Face for code blocks in threads.")
           56  +
           57  + ;; VARIABLES
           58  +
           59  +(defvar-local sbbs--board nil
           60  +  "Buffer local reference to current board.
           61  +
           62  +See `sbbs-make-board'.")
           63  +
           64  +(defvar-local sbbs--thread-id nil
           65  +  "Buffer local reference to current thread id.
           66  +
           67  +Used in thread and reply buffers.")
           68  +
           69  +(defvar sbbs--ornamentum nil
           70  +  "Global cache of the ornamentum, used for posting.")
           71  +
           72  + ;; BOARD OBJECT AND FUNCTIONS
           73  +
           74  +(defun sbbs-make-board (domain name &optional tls)
           75  +  "Create board object, using DOMAIN, NAME and TLS flag."
           76  +  (vector domain name tls))
           77  +
           78  +(defsubst sbbs--board-domain (board)
           79  +  "Get domain part of a BOARD object."
           80  +  (aref board 0))
           81  +
           82  +(defsubst sbbs--board-name (board)
           83  +  "Get board name part of a BOARD object."
           84  +  (aref board 1))
           85  +
           86  +(defsubst sbbs--board-protocol (board)
           87  +  "Determine protocol to be used for BOARD object."
           88  +  (if (aref board 2) "https" "http"))
           89  +
           90  +(defun sbbs--board-url (&optional path api-p board)
           91  +  "Generate URL for BOARD to access PATH.
           92  +
           93  +If API-P is non-nil, prefix path with \"sexp\"."
           94  +  (let ((board (or board sbbs--board)))
           95  +    (format "%s://%s/%s%s/%s"
           96  +            (sbbs--board-protocol board)
           97  +            (sbbs--board-domain board)
           98  +            (if api-p "sexp/" "")
           99  +            (sbbs--board-name board)
          100  +            (or path ""))))
          101  +
          102  +(defun sbbs-read-board ()
          103  +  "Read in a board using `completing-read'.
          104  +
          105  +The list will be generated using `sbbs-boards', and the result
          106  +will be a board object generated with `sbbs-make-board'."
          107  +  (let (boards)
          108  +    (dolist (ent sbbs-boards)
          109  +      (dolist (board (cadr ent))
          110  +        (push (cons (format "/%s/ (%s)" board (car ent))
          111  +                    (sbbs-make-board (car ent) board (caddr ent)))
          112  +              boards)))
          113  +    (cdr (assoc (completing-read "Board: " boards nil t) boards))))
          114  +
          115  + ;; UTILITY FUNCTIONS
          116  +
          117  +(defun sbbs--reload-thread (&optional _ignore-auto _noconfirm)
          118  +  "Function to reload an opened thread."
          119  +  (when sbbs--thread-id (sbbs-view-open sbbs--thread-id)))
          120  +
          121  +(defun sbbs--reload-board ()
          122  +  "Function to regenerate thread index.
          123  +
          124  +Called by `tabulated-list-mode' hooks."
          125  +  (when sbbs--board (sbbs-browse sbbs--board)))
          126  +
          127  + ;; UI GENERATOR
          128  +
          129  +(defun sbbs--insert-link (text link)
          130  +  "Insert link to LINK as TEXT into buffer.
          131  +
          132  +If LINK is a (board, thread or site) local link, modify opening
          133  +behaviour accordingly."
          134  +  (let ((domain (sbbs--board-domain sbbs--board))
          135  +        (name (sbbs--board-name sbbs--board))
          136  +        (id sbbs--thread-id)
          137  +        (local-re "\\`/%s/%d/\\([[:digit:]]+\\)")
          138  +        (thread-re "\\`/%s/\\([[:digit:]]+\\)")
          139  +        (board-re "\\`http\\(s?\\)://\\([-._[:alnum:]]+\\)/sexp/\\([[:alnum:]]+\\)/\\([[:digit:]]+\\)"))
          140  +    (save-match-data
          141  +      (cond ((string-match (format local-re name id) link)
          142  +             (insert-button
          143  +              text
          144  +              'action (lambda (x)
          145  +                        (goto-char (point-min))
          146  +                        (sbbs-read-next (1- (button-get x 'sbbs-post-id))))
          147  +              'sbbs-post-id (string-to-number (match-string 1 link))))
          148  +            ((string-match (format thread-re name id) link)
          149  +             (insert-button
          150  +              text
          151  +              'action (lambda (x)
          152  +                        (sbbs-view-open (button-get x 'sbbs-link-id)))
          153  +              'sbbs-link-id (string-to-number (match-string 1 link))))
          154  +            ((string-match (format board-re domain name) link)
          155  +             (insert-button
          156  +              text
          157  +              'action (lambda (x)
          158  +                        (let ((sbbs--board (button-get x 'sbbs-board)))
          159  +                          (sbbs-view-open (button-get x 'sbbs-link-id))))
          160  +              'sbbs-board (sbbs-make-board (match-string 2 link)
          161  +                                           (match-string 3 link)
          162  +                                           (string= (match-string 1 link) "s"))
          163  +              'sbbs-link-id (string-to-number (match-string 3 link))))
          164  +            (t (insert-button
          165  +                text
          166  +                'action (lambda (x) (browse-url (button-get x 'url)))
          167  +                'url link))))))
          168  +
          169  +(defun sbbs--insert-sxml-par (sxml)
          170  +  "Insert paragraph contents SXML at point."
          171  +  (dolist (it sxml)
          172  +    (cond ((stringp it)
          173  +           (insert (propertize it 'face 'variable-pitch)))
          174  +          ((eq (car it) 'br)
          175  +           (fill-paragraph)
          176  +           (newline))
          177  +          ((eq (car it) 'b)
          178  +           (insert (propertize (cadr it) 'face '(bold variable-pitch))))
          179  +          ((eq (car it) 'i)
          180  +           (insert (propertize (cadr it) 'face '(italic variable-pitch))))
          181  +          ((eq (car it) 'code)
          182  +           (insert (propertize (cadr it) 'face 'fixed-pitch)))
          183  +          ((eq (car it) 'del)
          184  +           (insert (propertize (cadr it) 'face 'sbbs--spoiler-face)))
          185  +          ((eq (car it) 'a)
          186  +           (let* ((text (caddr it))
          187  +                  (link (plist-get (cadadr it) 'href)))
          188  +             (sbbs--insert-link text link)))
          189  +          (t (insert (prin1-to-string it)))))
          190  +  (insert ?\n))
          191  +
          192  +(defun sbbs--insert-sxml (sxml)
          193  +  "Insert top-level SXML into buffer at point."
          194  +  (dolist (par sxml)
          195  +    (cond ((eq (car par) 'p)
          196  +           (sbbs--insert-sxml-par (cdr par)))
          197  +          ((eq (car par) 'blockquote)
          198  +           (let ((start (point))
          199  +                 (comment-start "> "))
          200  +             (sbbs--insert-sxml-par (cdadr par))
          201  +             (comment-region start (point))
          202  +             (add-face-text-property start (point)
          203  +                                     'font-lock-comment-face)))
          204  +          ((eq (car par) 'pre)
          205  +           (let ((start (point)))
          206  +             (insert (propertize (cadadr par)
          207  +                                 'face 'fixed-pitch))
          208  +             (newline)
          209  +             (add-face-text-property start (point) 'sbbs--code-face)))
          210  +          (t (error "Unknown top-level element")))
          211  +    (insert ?\n)))
          212  +
          213  +(defun sbbs--thread-insert-post (post)
          214  +  "Prepare and Insert header and contents of POST at point."
          215  +  (let ((start (point)))
          216  +    (insert (format "#%d\t%s" (car post)
          217  +                    (cdr (assq 'date (cdr post)))))
          218  +    (when (cdr (assq 'vip (cdr post)))
          219  +      (insert " (VIP)"))
          220  +    (newline 2)
          221  +    (add-text-properties start (1- (point)) '(face highlight))
          222  +    (set-text-properties (1- (point)) (point) nil)
          223  +    (sbbs--insert-sxml (cdr (assq 'content (cdr post))))
          224  +    (add-text-properties start (point) (list 'sbbs-thread-nr (car post)))))
          225  +
          226  + ;; URL.EL CALLBACKS
          227  +
          228  +(defun sbbs--board-loader (status buf)
          229  +  "Callback function for `url-retrieve' when loading board.
          230  +
          231  +Load results into buffer BUF."
          232  +  (when (plist-get status :error)
          233  +    (message "Error while loading: %s"
          234  +             (cdr (plist-get status :error))))
          235  +  (forward-paragraph)
          236  +  (let ((list (read (current-buffer))))
          237  +    (kill-buffer)
          238  +    (with-current-buffer buf
          239  +      (let (ent)
          240  +        (dolist (thread list)
          241  +          (push (list (car thread)
          242  +                      (vector (substring (cdr (assq 'date (cdr thread)))
          243  +                                         0 16)
          244  +                              (number-to-string
          245  +                               (cdr (assq 'messages (cdr thread))))
          246  +                              (propertize
          247  +                               (cdr (assq 'headline (cdr thread)))
          248  +                               'face 'variable-pitch)))
          249  +                ent))
          250  +        (setq-local tabulated-list-entries ent)
          251  +        (tabulated-list-print t t)))
          252  +    (switch-to-buffer buf)
          253  +    (hl-line-highlight)))
          254  +
          255  +(defun sbbs--thread-loader (status board id)
          256  +  "Callback function for `url-retrieve' when loading thread.
          257  +
          258  +The attribute ID determines what thread from board BOARD to
          259  +load."
          260  +  (when (plist-get status :error)
          261  +    (message "Error while loading: %s"
          262  +             (cdr (plist-get status :error))))
          263  +  (prog-mode)
          264  +  (forward-paragraph)
          265  +  (save-excursion
          266  +    (save-match-data
          267  +      (while (search-forward "#f" nil t)
          268  +        (unless (cadddr (syntax-ppss))
          269  +          (replace-match "nil")))))
          270  +  (save-excursion
          271  +    (save-match-data
          272  +      (while (search-forward "#f" nil t)
          273  +        (unless (cadddr (syntax-ppss))
          274  +          (replace-match "t")))))
          275  +  (let ((thread (read (current-buffer)))
          276  +        (buf (get-buffer-create
          277  +              (format "*reading /%s/%d*"
          278  +                      (sbbs--board-name board)
          279  +                      id))))
          280  +    (kill-buffer)
          281  +    (with-current-buffer buf
          282  +      (sbbs-read-mode)
          283  +      (setq sbbs--board board
          284  +            sbbs--thread-id id)
          285  +      (let ((buffer-read-only nil))
          286  +        (erase-buffer)
          287  +        (setq header-line-format
          288  +              (format "Thread %d: %s" id
          289  +                      (cdr (assq 'headline thread))))
          290  +        (dolist (post (cadr (assq 'posts thread)))
          291  +          (sbbs--thread-insert-post post))
          292  +        (delete-blank-lines)
          293  +        (goto-char (point-min))))
          294  +    (switch-to-buffer buf)))
          295  +
          296  + ;; INTERACTIVE FUNCTIONS
          297  +
          298  +(defun sbbs-view-open (id)
          299  +  "Open thread ID in new buffer."
          300  +  (interactive (list (tabulated-list-get-id)))
          301  +  (let ((url (sbbs--board-url (format "/%d" id) t)))
          302  +    (url-retrieve url #'sbbs--thread-loader
          303  +                  (list sbbs--board id))))
          304  +
          305  +(defun sbbs-view-compose ()
          306  +  "Create buffer to start a new thread."
          307  +  (interactive)
          308  +  (let ((board sbbs--board))
          309  +    (with-current-buffer (generate-new-buffer "*new thread*")
          310  +      (sbbs-compose-mode)
          311  +      (setq sbbs--board board)
          312  +      (switch-to-buffer (current-buffer)))))
          313  +
          314  +(defun sbbs-read-reply (arg)
          315  +  "Create buffer to start a reply in current thread.
          316  +
          317  +With \\[universal-argument] add a reply reference to thread at
          318  +point."
          319  +  (interactive "P")
          320  +  (let ((id sbbs--thread-id)
          321  +        (nr (get-text-property (point) 'sbbs-thread-nr))
          322  +        (board sbbs--board))
          323  +    (with-current-buffer (generate-new-buffer "*new response*")
          324  +      (sbbs-compose-mode)
          325  +      (when (and arg (= (car arg) 4))
          326  +        (insert (format ">>%d" nr))
          327  +        (newline))
          328  +      (setq header-line-format (format "Responding to Thread %d" id)
          329  +            sbbs--thread-id id
          330  +            sbbs--board board)
          331  +      (switch-to-buffer (current-buffer)))))
          332  +
          333  +(defun sbbs-compose-create ()
          334  +  "Upload response or thread to board."
          335  +  (interactive)
          336  +  (let ((board sbbs--board)
          337  +        (url-request-method "POST")
          338  +        (url-request-extra-headers
          339  +         '(("Content-Type" . "application/x-www-form-urlencoded")))
          340  +        (url-request-data
          341  +         (url-build-query-string
          342  +          `((epistula ,(buffer-string))
          343  +            (ornamentum "") (name "") (message "")
          344  +            (frontpage ,(if sbbs--thread-id "true" "false"))
          345  +            . ,(and (not sbbs--thread-id)
          346  +                    `((titulus ,(read-string "Headline: ")))))))
          347  +        (url (if sbbs--thread-id
          348  +                 (sbbs--board-url (format "%d/post" sbbs--thread-id))
          349  +               (sbbs--board-url "/post"))))
          350  +    (url-retrieve url (lambda (status buf)
          351  +                        (if (plist-get status :error)
          352  +                            (message "Error while submitting: %s"
          353  +                                     (cdr (plist-get status :error)))
          354  +                          (kill-buffer buf)
          355  +                          (let ((sbbs--board board))
          356  +                            (sbbs--reload-thread))))
          357  +                  (list (current-buffer)))))
          358  +
          359  +(defun sbbs-read-next (arg)
          360  +  "Move point to next thread header."
          361  +  (interactive "p")
          362  +  (dotimes (_ arg)
          363  +    (end-of-line)
          364  +    (catch 'found
          365  +      (while (search-forward-regexp "^#" nil t)
          366  +        (when (eq 'highlight (get-text-property (point) 'face))
          367  +          (throw 'found t)))))
          368  +  (beginning-of-line))
          369  +
          370  +(defun sbbs-read-previous (arg)
          371  +  "Move point to previous thread header."
          372  +  (interactive "p")
          373  +  (dotimes (_ arg)
          374  +    (catch 'found
          375  +      (while (search-backward-regexp "^#" nil t)
          376  +        (when (eq 'highlight (get-text-property (point) 'face))
          377  +          (throw 'found t)))))
          378  +  (beginning-of-line))
          379  +
          380  +;;;###autoload
          381  +(defun sbbs-browse (board)
          382  +  "Open thread overview for BOARD."
          383  +  (interactive (list (sbbs-read-board)))
          384  +  (let* ((name (format "*browsing /%s/*" (sbbs--board-name board)))
          385  +         (url (sbbs--board-url "list" t board)))
          386  +    (with-current-buffer (get-buffer-create name)
          387  +      (let ((buffer-read-only nil))
          388  +        (erase-buffer))
          389  +      (sbbs-view-mode)
          390  +      (setq sbbs--board board)
          391  +      (url-retrieve url #'sbbs--board-loader
          392  +                    (list (current-buffer))))))
          393  +
          394  +;;;###autoload
          395  +(defalias 'sbbs #'sbbs-browse)
          396  +
          397  + ;; MAJOR MODES
          398  +
          399  +(defvar sbbs-view-mode-map
          400  +  (let ((map (make-sparse-keymap)))
          401  +    (define-key map (kbd "RET") #'sbbs-view-open)
          402  +    (define-key map (kbd "c") #'sbbs-view-compose)
          403  +    map))
          404  +
          405  +(define-derived-mode sbbs-view-mode tabulated-list-mode "SchemeBBS Browse"
          406  +  "Major mode for browsing a SchemeBBS board."
          407  +  (buffer-disable-undo)
          408  +
          409  +  (setq tabulated-list-format [("Date" 16 t)
          410  +                               ("#" 3 t :right-align t)
          411  +                               ("Headline" 0 nil)]
          412  +        tabulated-list-sort-key '("Date" . t))
          413  +  (add-hook 'tabulated-list-revert-hook
          414  +            #'sbbs--reload-board nil t)
          415  +  (tabulated-list-init-header)
          416  +
          417  +  (hl-line-mode t))
          418  +
          419  +(defvar sbbs-read-mode-map
          420  +  (let ((map (make-sparse-keymap)))
          421  +    (suppress-keymap map)
          422  +    (define-key map (kbd "<tab>") #'forward-button)
          423  +    (define-key map (kbd "<backtab>") #'backward-button)
          424  +    (define-key map (kbd "r") #'sbbs-read-reply)
          425  +    (define-key map (kbd "n") #'sbbs-read-next)
          426  +    (define-key map (kbd "p") #'sbbs-read-previous)
          427  +    map))
          428  +
          429  +(define-derived-mode sbbs-read-mode special-mode "SchemeBBS Read"
          430  +  "Major mode for reading a thread."
          431  +  (buffer-disable-undo)
          432  +  (visual-line-mode t)
          433  +  (setq-local revert-buffer-function #'sbbs--reload-thread))
          434  +
          435  +(defvar sbbs-compose-mode-map
          436  +  (let ((map (make-sparse-keymap)))
          437  +    (define-key map (kbd "C-c C-c") #'sbbs-compose-create)
          438  +    map))
          439  +
          440  +(define-derived-mode sbbs-compose-mode text-mode "SchemeBBS Compose"
          441  +  "Major mode for composing replies and starting new threads."
          442  +  (message "Press C-c C-c to send"))
          443  +
          444  +(provide 'sbbs)
          445  +
          446  +;;; sbbs.el ends here