From 3531508bbd0f4b70b081b51ac876eb13385fa082 Mon Sep 17 00:00:00 2001 From: tecosaur <20903656+tecosaur@users.noreply.github.com> Date: Sun, 17 May 2020 00:15:58 +0800 Subject: [PATCH] Make xkcd-mode work with evil + over-engineering --- config.org | 479 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 274 insertions(+), 205 deletions(-) diff --git a/config.org b/config.org index a65a5ca..e510aaf 100644 --- a/config.org +++ b/config.org @@ -988,7 +988,279 @@ done perfectly acceptable, so let's make that happen. We wan't to set this up so it loads nicely in [[*Extra links][Extra links]]. #+BEGIN_SRC emacs-lisp (use-package! xkcd - :commands (xkcd-get-json xkcd-download)) + :commands (xkcd-get-json xkcd-download xkcd-get + ;; now for funcs from my extension of this pkg + +xkcd-find-and-copy +xkcd-find-and-view + +xkcd-fetch-info +xkcd-select) + :config + (add-to-list 'evil-snipe-disabled-modes 'xkcd-mode) + :general (:states 'normal + :keymaps 'xkcd-mode-map + "" #'xkcd-next + "n" #'xkcd-next ; evil-ish + "" #'xkcd-prev + "N" #'xkcd-prev ; evil-ish + "r" #'xkcd-rand + "a" #'xkcd-rand ; because image-rotate can interfere + "t" #'xkcd-alt-text + "q" #'xkcd-kill-buffer + "o" #'xkcd-open-browser + "e" #'xkcd-open-explanation-browser + ;; extras + "s" #'+xkcd-find-and-view + "/" #'+xkcd-find-and-view + "y" #'+xkcd-copy)) +#+END_SRC + +Let's also extend the functionality a whole bunch. +#+BEGIN_SRC emacs-lisp +(after! xkcd + (require 'emacsql-sqlite) + + (defun +xkcd-select () + "Prompt the user for an xkcd using `ivy-read' and `+xkcd-select-format'. Return the xkcd number or nil" + (let* (prompt-lines + (-dummy (maphash (lambda (key xkcd-info) + (push (+xkcd-select-format xkcd-info) prompt-lines)) + +xkcd-stored-info)) + (num (ivy-read (format "xkcd (%s): " xkcd-latest) prompt-lines))) + (if (equal "" num) xkcd-latest + (string-to-number (replace-regexp-in-string "\\([0-9]+\\).*" "\\1" num))))) + + (defun +xkcd-select-format (xkcd-info) + "Creates each ivy-read line from an xkcd info plist. Must start with the xkcd number" + (format "%-4s %-30s %s" + (propertize (number-to-string (plist-get xkcd-info :num)) + 'face 'counsel-key-binding) + (plist-get xkcd-info :title) + (propertize (plist-get xkcd-info :alt) + 'face '(variable-pitch font-lock-comment-face)))) + + (defun +xkcd-fetch-info (&optional num) + "Fetch the parsed json info for comic NUM. Fetches latest when omitted or 0" + (require 'xkcd) + (when (or (not num) (= num 0)) + (+xkcd-check-latest) + (setq num xkcd-latest)) + (let ((res (or (gethash num +xkcd-stored-info) + (puthash num (+xkcd-db-read num) +xkcd-stored-info)))) + (unless res + (+xkcd-db-write + (let* ((url (format "http://xkcd.com/%d/info.0.json" num)) + (json-assoc + (if (assoc num +xkcd-stored-info) + (assoc num +xkcd-stored-info) + (json-read-from-string (xkcd-get-json url num))))) + json-assoc)) + (setq res (+xkcd-db-read num))) + res)) + + ;; since we've done this, we may as well go one little step further + (defun +xkcd-find-and-copy () + "Prompt for an xkcd using `+xkcd-select' and copy url to clipboard" + (interactive) + (+xkcd-copy (+xkcd-select))) + + (defun +xkcd-copy (&optional num) + "Copy a url to xkcd NUM to the clipboard" + (interactive "i") + (let ((num (or num xkcd-cur))) + (gui-select-text (format "https://xkcd.com/%d" num)) + (message "xkcd.com/%d copied to clipboard" num))) + + (defun +xkcd-find-and-view () + "Prompt for an xkcd using `+xkcd-select' and view it" + (interactive) + (xkcd-get (+xkcd-select)) + (switch-to-buffer "*xkcd*")) + + (defvar +xkcd-latest-max-age (* 60 60) ; 1 hour + "Time after which xkcd-latest should be refreshed, in seconds") + + ;; initialise `xkcd-latest' and `+xkcd-stored-info' with latest xkcd + (add-transient-hook! '+xkcd-select + (require 'xkcd) + (+xkcd-fetch-info xkcd-latest) + (setq +xkcd-stored-info (+xkcd-db-read-all))) + + (add-transient-hook! '+xkcd-fetch-info + (xkcd-update-latest)) + + (defun +xkcd-check-latest () + "Use value in `xkcd-cache-latest' as long as it isn't older thabn `+xkcd-latest-max-age'" + (unless (and (file-exists-p xkcd-cache-latest) + (< (- (time-to-seconds (current-time)) + (time-to-seconds (file-attribute-modification-time (file-attributes xkcd-cache-latest)))) + +xkcd-latest-max-age)) + (let* ((out (xkcd-get-json "http://xkcd.com/info.0.json" 0)) + (json-assoc (json-read-from-string out)) + (latest (cdr (assoc 'num json-assoc)))) + (when (/= xkcd-latest latest) + (+xkcd-db-write json-assoc) + (with-current-buffer (find-file xkcd-cache-latest) + (setq xkcd-latest latest) + (erase-buffer) + (insert (number-to-string latest)) + (save-buffer) + (kill-buffer (current-buffer))))) + (shell-command (format "touch %s" xkcd-cache-latest)))) + + (defvar +xkcd-stored-info (make-hash-table :test 'eql) + "Basic info on downloaded xkcds, in the form of a hashtable") + + (defadvice! xkcd-get-json--and-cache (url &optional num) + "Fetch the Json coming from URL. +If the file NUM.json exists, use it instead. +If NUM is 0, always download from URL. +The return value is a string." + :override #'xkcd-get-json + (let* ((file (format "%s%d.json" xkcd-cache-dir num)) + (cached (and (file-exists-p file) (not (eq num 0)))) + (out (with-current-buffer (if cached + (find-file file) + (url-retrieve-synchronously url)) + (goto-char (point-min)) + (unless cached (re-search-forward "^$")) + (prog1 + (buffer-substring-no-properties (point) (point-max)) + (kill-buffer (current-buffer)))))) + (unless (or cached (eq num 0)) + (xkcd-cache-json num out)) + out)) + + (defadvice! +xkcd-get (num) + "Get the xkcd number NUM." + :override 'xkcd-get + (interactive "nEnter comic number: ") + (xkcd-update-latest) + (get-buffer-create "*xkcd*") + (switch-to-buffer "*xkcd*") + (xkcd-mode) + (let (buffer-read-only) + (erase-buffer) + (setq xkcd-cur num) + (let* ((xkcd-data (+xkcd-fetch-info num)) + (num (plist-get xkcd-data :num)) + (img (plist-get xkcd-data :img)) + (safe-title (plist-get xkcd-data :safe-title)) + (alt (plist-get xkcd-data :alt)) + title file) + (message "Getting comic...") + (setq file (xkcd-download img num)) + (setq title (format "%d: %s" num safe-title)) + (insert (propertize title + 'face 'outline-1)) + (center-line) + (insert "\n") + (xkcd-insert-image file num) + (if (eq xkcd-cur 0) + (setq xkcd-cur num)) + (setq xkcd-alt alt) + (message "%s" title)))) + + (defconst +xkcd-db--sqlite-available-p + (with-demoted-errors "+org-xkcd initialization: %S" + (emacsql-sqlite-ensure-binary) + t)) + + (defvar +xkcd-db--connection (make-hash-table :test #'equal) + "Database connection to +org-xkcd database.") + + (defun +xkcd-db--get () + "Return the sqlite db file." + (expand-file-name "xkcd.db" xkcd-cache-dir)) + + (defun +xkcd-db--get-connection () + "Return the database connection, if any." + (gethash (file-truename xkcd-cache-dir) + +xkcd-db--connection)) + + (defconst +xkcd-db--table-schema + '((xkcds + [(num integer :unique :primary-key) + (year :not-null) + (month :not-null) + (link :not-null) + (news :not-null) + (safe_title :not-null) + (title :not-null) + (transcript :not-null) + (alt :not-null) + (img :not-null)]))) + + (defun +xkcd-db--init (db) + "Initialize database DB with the correct schema and user version." + (emacsql-with-transaction db + (pcase-dolist (`(,table . ,schema) +xkcd-db--table-schema) + (emacsql db [:create-table $i1 $S2] table schema)))) + + (defun +xkcd-db () + "Entrypoint to the +org-xkcd sqlite database. +Initializes and stores the database, and the database connection. +Performs a database upgrade when required." + (unless (and (+xkcd-db--get-connection) + (emacsql-live-p (+xkcd-db--get-connection))) + (let* ((db-file (+xkcd-db--get)) + (init-db (not (file-exists-p db-file)))) + (make-directory (file-name-directory db-file) t) + (let ((conn (emacsql-sqlite db-file))) + (set-process-query-on-exit-flag (emacsql-process conn) nil) + (puthash (file-truename xkcd-cache-dir) + conn + +xkcd-db--connection) + (when init-db + (+xkcd-db--init conn))))) + (+xkcd-db--get-connection)) + + (defun +xkcd-db-query (sql &rest args) + "Run SQL query on +org-xkcd database with ARGS. +SQL can be either the emacsql vector representation, or a string." + (if (stringp sql) + (emacsql (+xkcd-db) (apply #'format sql args)) + (apply #'emacsql (+xkcd-db) sql args))) + + (defun +xkcd-db-read (num) + (when-let ((res + (car (+xkcd-db-query [:select * :from xkcds + :where (= num $s1)] + num + :limit 1)))) + (+xkcd-db-list-to-plist res))) + + (defun +xkcd-db-read-all () + (let ((xkcd-table (make-hash-table :test 'eql :size 4000))) + (mapcar (lambda (xkcd-info-list) + (puthash (car xkcd-info-list) (+xkcd-db-list-to-plist xkcd-info-list) xkcd-table)) + (+xkcd-db-query [:select * :from xkcds])) + xkcd-table)) + + (defun +xkcd-db-list-to-plist (xkcd-datalist) + `(:num ,(nth 0 xkcd-datalist) + :year ,(nth 1 xkcd-datalist) + :month ,(nth 2 xkcd-datalist) + :link ,(nth 3 xkcd-datalist) + :news ,(nth 4 xkcd-datalist) + :safe-title ,(nth 5 xkcd-datalist) + :title ,(nth 6 xkcd-datalist) + :transcript ,(nth 7 xkcd-datalist) + :alt ,(nth 8 xkcd-datalist) + :img ,(nth 9 xkcd-datalist))) + + (defun +xkcd-db-write (data) + (+xkcd-db-query [:insert-into xkcds + :values $v1] + (list (vector + (cdr (assoc 'num data)) + (cdr (assoc 'year data)) + (cdr (assoc 'month data)) + (cdr (assoc 'link data)) + (cdr (assoc 'news data)) + (cdr (assoc 'safe_title data)) + (cdr (assoc 'title data)) + (cdr (assoc 'transcript data)) + (cdr (assoc 'alt data)) + (cdr (assoc 'img data)) + ))))) #+END_SRC * Language configuration *** File Templates @@ -1985,210 +2257,7 @@ Saving seconds adds up after all! (but only so much) (defun +org-xkcd-complete (&optional arg) "Complete xkcd using `+xkcd-stored-info'" - (format "xkcd:%d" (+xkcd-select))) - - (defun +xkcd-select () - "Prompt the user for an xkcd using `ivy-read' and `+xkcd-select-format'. Return the xkcd number or nil" - (let ((num - (ivy-read (format "xkcd (%s): " xkcd-latest) - (mapcar #'+xkcd-select-format - +xkcd-stored-info)))) - (if (equal "" num) xkcd-latest - (string-to-number (replace-regexp-in-string "\\([0-9]+\\).*" "\\1" num))))) - - (defun +xkcd-select-format (xkcd-info) - "Creates each ivy-read line from an xkcd info plist. Must start with the xkcd number" - (format "%-4s %-30s %s" - (propertize (number-to-string (plist-get xkcd-info :num)) - 'face 'counsel-key-binding) - (plist-get xkcd-info :title) - (propertize (plist-get xkcd-info :alt) - 'face '(variable-pitch font-lock-comment-face)))) - - (defun +xkcd-fetch-info (num) - "Fetch the parsed json info for comic NUM" - (require 'xkcd) - (let ((res (+xkcd-db-read num))) - (unless res - (+xkcd-db-write - (let* ((url (format "http://xkcd.com/%d/info.0.json" num)) - (json-assoc - (if (assoc num +xkcd-stored-info) - (assoc num +xkcd-stored-info) - (json-read-from-string (xkcd-get-json url num))))) - json-assoc)) - (setq res (+xkcd-db-read num))) - res)) - - ;; since we've done this, we may as well go one little step further - (defun +xkcd-find-and-copy () - "Prompt the user for an xkcd using `+xkcd-select' and copy url to clipboard" - (interactive) - (let ((num (+xkcd-select))) - (gui-select-text (format "https://xkcd.com/%d" num)) - (message "xkcd.com/%d copied to clipboard" num))) - - (defun +xkcd-find-and-view () - "Prompt the user for an xkcd using `+xkcd-select' and copy url to clipboard" - (interactive) - (xkcd-get (+xkcd-select)) - (switch-to-buffer "*xkcd*")) - - (defvar +xkcd-latest-max-age (* 60 60 12) - "Time after which xkcd-latest should be refreshed, in seconds") - - ;; initialise `xkcd-latest' and `+xkcd-stored-info' with latest xkcd - (add-transient-hook! '+xkcd-select - (require 'xkcd) - (xkcd-update-latest) - (unless (and (file-exists-p xkcd-cache-latest) - (< (- (time-to-seconds (current-time)) - (time-to-seconds (file-attribute-modification-time (file-attributes xkcd-cache-latest)))) - +xkcd-latest-max-age)) - (let* ((out (xkcd-get-json "http://xkcd.com/info.0.json" 0)) - (json-assoc (json-read-from-string out)) - (latest (cdr (assoc 'num json-assoc)))) - (when (/= xkcd-latest latest) - (+xkcd-db-write json-assoc) - (with-current-buffer (find-file xkcd-cache-latest) - (setq xkcd-latest latest) - (erase-buffer) - (insert (number-to-string latest)) - (save-buffer) - (kill-buffer (current-buffer)))))) - - (setq +xkcd-stored-info `(,(+xkcd-fetch-info xkcd-latest))) - (+xkcd-update-stored-info)) - - (defvar +xkcd-stored-info nil - "Basic info on downloaded xkcds, in the form ((num . title) ...)") - - (defun +xkcd-update-stored-info () - "Compare the json files in `xkcd-cache-dir' to the info in `+xkcd-stored-info' -and ensure that `+xkcd-stored-info' has info for every file" - (let* ((file-nums (mapcar (lambda (f) (string-to-number (replace-regexp-in-string "\\.json" "" f))) - (directory-files xkcd-cache-dir nil "\\.json"))) - (stored-nums (mapcar #'car +xkcd-stored-info)) - (new-nums (set-difference file-nums stored-nums))) - (dolist (num new-nums) - (push (+xkcd-fetch-info num) +xkcd-stored-info)))) - - (defadvice! xkcd-get-json--and-cache (url &optional num) - "Fetch the Json coming from URL. -If the file NUM.json exists, use it instead. -If NUM is 0, always download from URL. -The return value is a string." - :override #'xkcd-get-json - (let* ((file (format "%s%d.json" xkcd-cache-dir num)) - (cached (and (file-exists-p file) (not (eq num 0)))) - (out (with-current-buffer (if cached - (find-file file) - (url-retrieve-synchronously url)) - (goto-char (point-min)) - (unless cached (re-search-forward "^$")) - (prog1 - (buffer-substring-no-properties (point) (point-max)) - (kill-buffer (current-buffer)))))) - (unless (or cached (eq num 0)) - (xkcd-cache-json num out)) - out)) - - (defconst +xkcd-db--sqlite-available-p - (with-demoted-errors "+org-xkcd initialization: %S" - (emacsql-sqlite-ensure-binary) - t)) - - (defvar +xkcd-db--connection (make-hash-table :test #'equal) - "Database connection to +org-xkcd database.") - - (defun +xkcd-db--get () - "Return the sqlite db file." - (expand-file-name "xkcd.db" xkcd-cache-dir)) - - (defun +xkcd-db--get-connection () - "Return the database connection, if any." - (gethash (file-truename xkcd-cache-dir) - +xkcd-db--connection)) - - (defconst +xkcd-db--table-schema - '((xkcds - [(num integer :unique :primary-key) - (year :not-null) - (month :not-null) - (link :not-null) - (news :not-null) - (safe_title :not-null) - (title :not-null) - (transcript :not-null) - (alt :not-null) - (img :not-null)]))) - - (defun +xkcd-db--init (db) - "Initialize database DB with the correct schema and user version." - (emacsql-with-transaction db - (pcase-dolist (`(,table . ,schema) +xkcd-db--table-schema) - (emacsql db [:create-table $i1 $S2] table schema)))) - - (defun +xkcd-db () - "Entrypoint to the +org-xkcd sqlite database. -Initializes and stores the database, and the database connection. -Performs a database upgrade when required." - (unless (and (+xkcd-db--get-connection) - (emacsql-live-p (+xkcd-db--get-connection))) - (let* ((db-file (+xkcd-db--get)) - (init-db (not (file-exists-p db-file)))) - (make-directory (file-name-directory db-file) t) - (let ((conn (emacsql-sqlite db-file))) - (set-process-query-on-exit-flag (emacsql-process conn) nil) - (puthash (file-truename xkcd-cache-dir) - conn - +xkcd-db--connection) - (when init-db - (+xkcd-db--init conn))))) - (+xkcd-db--get-connection)) - - (defun +xkcd-db-query (sql &rest args) - "Run SQL query on +org-xkcd database with ARGS. -SQL can be either the emacsql vector representation, or a string." - (if (stringp sql) - (emacsql (+xkcd-db) (apply #'format sql args)) - (apply #'emacsql (+xkcd-db) sql args))) - - (defun +xkcd-db-read (num) - (when-let ((res - (car (+xkcd-db-query [:select * :from xkcds - :where (= num $s1)] - num - :limit 1)))) - (+xkcd-db-list-to-plist res))) - - (defun +xkcd-db-list-to-plist (xkcd-datalist) - `(:num ,(nth 0 xkcd-datalist) - :year ,(nth 1 xkcd-datalist) - :month ,(nth 2 xkcd-datalist) - :link ,(nth 3 xkcd-datalist) - :news ,(nth 4 xkcd-datalist) - :safe-title ,(nth 5 xkcd-datalist) - :title ,(nth 6 xkcd-datalist) - :transcript ,(nth 7 xkcd-datalist) - :alt ,(nth 8 xkcd-datalist) - :img ,(nth 9 xkcd-datalist))) - - (defun +xkcd-db-write (data) - (+xkcd-db-query [:insert-into xkcds - :values $v1] - (list (vector - (cdr (assoc 'num data)) - (cdr (assoc 'year data)) - (cdr (assoc 'month data)) - (cdr (assoc 'link data)) - (cdr (assoc 'news data)) - (cdr (assoc 'safe_title data)) - (cdr (assoc 'title data)) - (cdr (assoc 'transcript data)) - (cdr (assoc 'alt data)) - (cdr (assoc 'img data)) - ))))) + (format "xkcd:%d" (+xkcd-select)))) #+END_SRC ***** YouTube The ~[[yt:...]]~ links preview nicely, but don't export nicely. Thankfully, we can