From 3c59b27b028907f44159efa3413499671768b474 Mon Sep 17 00:00:00 2001 From: Sergio Durigan Junior Date: Mon, 8 Apr 2024 21:20:39 -0400 Subject: [PATCH] Initial version, still pre-alpha Signed-off-by: Sergio Durigan Junior --- discoursel.el | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 discoursel.el diff --git a/discoursel.el b/discoursel.el new file mode 100644 index 0000000..0baafdf --- /dev/null +++ b/discoursel.el @@ -0,0 +1,228 @@ + +;;; Code: + +(require 'json) +(require 'plz) +(require 'message) + +(defgroup discoursel nil + "Discourse TODO") + +(defcustom discoursel-instance "https://discourse.ubuntu.com" + "The Discourse instance." + :group 'discoursel + :type 'string) + +(defvar discoursel-buffer-list nil + "List of discoursel buffers.") + +(defface discoursel-topic-face + '((((class color) (background light)) (:foreground "#000")) + (((class color) (background dark)) (:foreground "#fff"))) + "Face used for topics." + :group 'discoursel) + +(defface discoursel-username-face + '((((class color) (background light)) (:foreground "#aa0")) + (((class color) (background dark)) (:foreground "#ff0"))) + "Face used for usernames." + :group 'discoursel) + +(defface discoursel-code-face + '((((class color) (background light)) (:background "#cdcecc" :extend t)) + (((class color) (background dark)) (:background "#17211c" :extend t))) + "Face used for code blocks." + :group 'discoursel) + +(defun discoursel--parse-pre (tag) + (insert + (with-temp-buffer + (let ((shr-folding-mode 'none)) + (shr-ensure-newline) + (shr-generic tag) + (shr-ensure-newline)) + (add-text-properties (point-min) (point-max) + '(pre-tag t face discoursel-code-face)) + (buffer-string)))) + +(defun discoursel--get (endpoint) + (condition-case plzerror + (plz 'get (concat discoursel-instance "/" endpoint ".json") + :as #'json-read) + (plz-error + (error "Could not query endpoint '%s': '%s'" + endpoint (plz-error-message plzerror))))) + +(defun discoursel--latest-topics () + (let* ((r (discoursel--get "latest")) + (topics (alist-get 'topics (alist-get 'topic_list r))) + (users (alist-get 'users r))) + (with-current-buffer (discoursel-get-create-buffer) + (cl-map 'vector + (lambda (topic) + (let* ((userid (alist-get 'user_id (aref (alist-get 'posters topic) 0))) + (username (seq-find + (lambda (v) + (= (alist-get 'id v) userid)) + users)) + (inhibit-read-only t)) + (insert + (propertize (format "%s (by %s)\n" + (propertize (alist-get 'title topic) + 'face 'discoursel-topic-face) + (propertize (alist-get 'name username) + 'face 'discoursel-username-face)) + 'discoursel-topic-slug (alist-get 'slug topic) + 'discoursel-topic-id (alist-get 'id topic))))) + topics)))) + +;;;###autoload +(defun discoursel-update () + (interactive) + (with-current-buffer (discoursel-get-create-buffer) + (let ((inhibit-read-only t)) + (discoursel--latest-topics)))) + +(defun discoursel--open-topic (&optional id) + (interactive) + (when (and (not (eq major-mode 'discoursel-mode)) + (not id)) + (error "You must be in a discoursel buffer")) + (let* ((topic-id (number-to-string + (or id (get-text-property (point) 'discoursel-topic-id)))) + (r (discoursel--get (concat "t/" topic-id))) + (posts (alist-get 'posts (alist-get 'post_stream r))) + (first-post (aref posts 0)) + (topic-slug (get-text-property (point) 'discoursel-topic-slug)) + (topic-title (alist-get 'title r)) + (topic-date (alist-get 'created_at r)) + (topic-date-string + (format-time-string "%c" + (encode-time + (parse-time-string topic-date)))) + (userid (alist-get 'user_id r)) + (participants (alist-get 'participants (alist-get 'details r))) + (topic-author-alist (seq-find (lambda (p) + (= (alist-get 'id p) userid)) + participants)) + (topic-author (alist-get 'username topic-author-alist)) + (buf (discoursel-get-create-buffer topic-slug))) + (switch-to-buffer buf) + (with-current-buffer buf + (discoursel-topic-mode) + (let ((inhibit-read-only t)) + (erase-buffer) + (save-excursion + (insert + (with-temp-buffer + (insert (format (propertize "Title: %s\n" 'face 'message-header-name) + (propertize topic-title 'face 'message-header-subject))) + (insert (format (propertize "Author: %s\n" 'face 'message-header-name) + (propertize topic-author 'face 'message-header-to))) + (insert (format (propertize "Date: %s\n" 'face 'message-header-name) + (propertize topic-date-string 'face 'message-header-other))) + (insert "\n") + (let ((p (point))) + (insert (alist-get 'cooked first-post)) + (let ((shr-use-fonts nil) + (shr-external-rendering-functions + '((pre . discoursel--parse-pre)))) + (shr-render-region p (point-max)))) + (buffer-substring (point-min) (point-max))))))))) + +(defun discoursel-open-topic (id) + (interactive "nTopic ID: ") + (discoursel--open-topic id)) + +(defun discoursel-get-create-buffer (&optional name) + (let* ((bufname (concat "*discoursel" + (when name (concat "-" name)) + "*")) + (buf (get-buffer-create bufname))) + (prog1 buf + (unless (memq buf discoursel-buffer-list) + (add-to-list 'discoursel-buffer-list buf))))) + +(defun discoursel-kill-buffer (&optional buf) + (interactive) + (let* ((tmpb (or buf (current-buffer))) + (b (get-buffer tmpb))) + (unless b + (error "Invalid buffer %s" b)) + (with-current-buffer b + (unless (derived-mode-p 'discoursel-mode) + (error "Cannot kill buffer %s: not a discoursel buffer" (buffer-name)))) + (kill-buffer b) + (setq discoursel-buffer-list (remove b discoursel-buffer-list)))) + +(defun discoursel-exit () + (interactive) + (dolist (buf discoursel-buffer-list) + (kill-buffer buf)) + (setq discoursel-buffer-list nil)) + +(defvar discoursel-mode-map + (let ((map (make-sparse-keymap))) + (prog1 map + (suppress-keymap map) + (define-key map "n" #'next-line) + (define-key map "p" #'previous-line) + (define-key map (kbd "RET") #'discoursel--open-topic) + (define-key map "j" #'discoursel-open-topic) + (define-key map "q" #'discoursel-exit) + (define-key map "g" #'discoursel-update))) + "Keymap for `discoursel-mode'.") + +(defvar discoursel-topic-mode-map + (let ((map (make-sparse-keymap))) + (prog1 map + (suppress-keymap map) + (define-key map "n" #'discoursel-topic-next-post) + (define-key map "p" #'discoursel-topic-previous-post) + (define-key map "q" #'discoursel-kill-buffer))) + "Keymap for `discoursel-topic-mode'.") + +(define-derived-mode discoursel-mode special-mode "Discourse client" + "Major mode for interacting with Discourse instances. +\\{discoursel-mode-map}" + (use-local-map discoursel-mode-map)) + +(define-derived-mode discoursel-topic-mode discoursel-mode "Discoursel topic" + "Major mode for interacting with Discourse topics. +\\{discoursel-topic-mode-map}" + (use-local-map discoursel-topic-mode-map)) + +(defun discoursel () + "Enter discoursel." + (interactive) + (switch-to-buffer (discoursel-get-create-buffer)) + (unless (eq major-mode 'discoursel-mode) + (discoursel-mode)) + (discoursel-update)) + + +;; TODO +;; (with-current-buffer (get-buffer-create "test-discourse") +;; (insert (plz 'get "https://discourse.ubuntu.com/raw/39765")) +;; (markdown-mode) +;; (setq buffer-read-only t)) + +;; (with-current-buffer (get-buffer-create "test-discourse4") +;; (let* ((ttt (plz 'get "https://discourse.ubuntu.com/latest.json" :as #'json-read)) +;; (topics (alist-get 'topics (alist-get 'topic_list ttt))) +;; (users (alist-get 'users ttt))) +;; (cl-map 'vector +;; (lambda (topic) +;; (let* ((uid (alist-get 'user_id (aref (alist-get 'posters topic) 0))) +;; (user (seq-find (lambda (v) (= (alist-get 'id v) uid)) +;; users))) +;; (insert +;; (propertize (format "%s [%s]\n" (alist-get 'title topic) (alist-get 'name user)) +;; 'discoursel-slug (alist-get 'slug topic))))) +;; topics))) + + + +(provide 'discoursel) + +;;; discoursel.el ends here