aboutsummaryrefslogtreecommitdiff
path: root/emacs/.emacs.d/lisp
diff options
context:
space:
mode:
Diffstat (limited to 'emacs/.emacs.d/lisp')
m---------emacs/.emacs.d/lisp/exitter0
m---------emacs/.emacs.d/lisp/hmm.el0
-rw-r--r--emacs/.emacs.d/lisp/my/fediorg.el368
-rw-r--r--emacs/.emacs.d/lisp/my/infobox.el27
-rw-r--r--emacs/.emacs.d/lisp/my/mastorg.el207
-rw-r--r--emacs/.emacs.d/lisp/my/my-buffer.el2
-rw-r--r--emacs/.emacs.d/lisp/my/my-emms.el7
-rw-r--r--emacs/.emacs.d/lisp/my/my-github.el4
-rw-r--r--emacs/.emacs.d/lisp/my/my-gitlab.el12
-rw-r--r--emacs/.emacs.d/lisp/my/my-ledger.el9
-rw-r--r--emacs/.emacs.d/lisp/my/my-magit.el25
-rw-r--r--emacs/.emacs.d/lisp/my/my-mariadb.el33
-rw-r--r--emacs/.emacs.d/lisp/my/my-net.el21
-rw-r--r--emacs/.emacs.d/lisp/my/my-package.el13
-rw-r--r--emacs/.emacs.d/lisp/my/my-prog.el68
-rw-r--r--emacs/.emacs.d/lisp/my/my-web.el46
-rw-r--r--emacs/.emacs.d/lisp/my/my-ytdl.el48
-rw-r--r--emacs/.emacs.d/lisp/my/reddio.el49
18 files changed, 668 insertions, 271 deletions
diff --git a/emacs/.emacs.d/lisp/exitter b/emacs/.emacs.d/lisp/exitter
-Subproject e0aa1eb8b5dd2696f92f90348cb9e8aedd79800
+Subproject 36551754f548954d83af723d227dc7d14fd57d6
diff --git a/emacs/.emacs.d/lisp/hmm.el b/emacs/.emacs.d/lisp/hmm.el
-Subproject a0660da71f9aef8909973e9fd44b5eb34db0386
+Subproject 318723000cad21c0134eefd33e310b953ddbbe7
diff --git a/emacs/.emacs.d/lisp/my/fediorg.el b/emacs/.emacs.d/lisp/my/fediorg.el
new file mode 100644
index 0000000..e2f21b8
--- /dev/null
+++ b/emacs/.emacs.d/lisp/my/fediorg.el
@@ -0,0 +1,368 @@
+;;; fediorg.el -- Read and archive fedi post context in org mode -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; Author: Yuchen Pei <id@ypei.org>
+;; Package-Requires: ((emacs "28.2"))
+
+;; This file is part of dotted.
+
+;; dotted is free software: you can redistribute it and/or modify it under
+;; the terms of the GNU Affero General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; dotted 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 Affero General
+;; Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public
+;; License along with dotted. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Read or archive a fedi thread context in org mode. This is a
+;; standalone library, and can be used without any other files in this
+;; project.
+
+;; Usage:
+;; M-x fediorg-open <RET> https://pleroma.instance/notice/... <RET>
+;; M-x fediorg-open <RET> https://mastodon.instance/@user/... <RET>
+;;
+;; The post, together with its ancestors and descendants, subject to
+;; the API depth limit, are displayed in an org buffer.
+
+;; TODO:
+;;
+;; To be able to refresh the org buffer at an org entry, which would
+;; re-fetch the context of the corresponding post and upsert them in
+;; the buffer.
+;;; Code:
+
+
+(require 'hierarchy)
+(require 'json)
+(require 'url-parse)
+
+(defvar fediorg-buffer "*fediorg*" "Buffer name for fediorg buffers.")
+
+;;; Fetching utilities
+(defvar fediorg-client-buffer-name "*fediorg-api*"
+ "Buffer name for logging API requests.")
+
+(defun fediorg-url-fetch-json (url &optional decompression with-header)
+ "Fetch and parse json from URL.
+
+With nonnil DECOMPRESSION, gunzip the response first.
+With nonnil WITH-HEADER, include the response headers in the return value."
+ (fediorg-url-fetch-internal
+ url
+ (lambda ()
+ (json-read-from-string (decode-coding-string (buffer-string) 'utf-8)))
+ decompression
+ with-header))
+
+(defun fediorg-url-fetch-internal (url buffer-processor decompression with-header)
+ "Fetch from URL and process the response with BUFFER-PROCESSOR.
+
+With nonnil DECOMPRESSION, gunzip the response first.
+With nonnil WITH-HEADER, include the response headers in the return value."
+ (with-current-buffer (get-buffer-create fediorg-client-buffer-name)
+ (goto-char (point-max))
+ (insert "[" (current-time-string) "] Request: " url "\n"))
+ (with-current-buffer (url-retrieve-synchronously url t)
+ (let ((header (fediorg-kill-http-header)) (status) (fields))
+ (goto-char (point-min))
+ (setq header (fediorg-parse-http-header header)
+ status (alist-get 'status header)
+ fields (alist-get 'fields header))
+ (with-current-buffer fediorg-client-buffer-name
+ (insert "[" (current-time-string) "] Response: " status "\n"))
+ (when decompression
+ (call-process-region (point) (point-max) "gunzip" t t t)
+ (goto-char (point-min)))
+ (call-interactively 'delete-trailing-whitespace)
+ (if (string= status "200")
+ (unless (= (point) (point-max))
+ (if with-header
+ (list
+ (cons 'header fields)
+ (cons 'json (funcall buffer-processor)))
+ (funcall buffer-processor)))
+ (error "HTTP error: %s" (buffer-substring (point) (point-max)))))))
+
+(defun fediorg-kill-http-header ()
+ "Kill http headers in the current buffer."
+ (fediorg-skip-http-header)
+ (let ((killed (buffer-substring-no-properties (point-min) (point))))
+ (delete-region (point-min) (point))
+ killed))
+
+(defun fediorg-skip-http-header ()
+ "Skip http headers in the current buffer."
+ (goto-char (point-min))
+ (re-search-forward "\r?\n\r?\n"))
+
+(defun fediorg-parse-http-header (text)
+ "Parse http headers from TEXT in the current buffer."
+ (let ((status) (fields))
+ (with-temp-buffer
+ (insert text)
+ (goto-char (point-min))
+ (re-search-forward "^HTTP.*\\([0-9]\\{3\\}\\).*$")
+ (setq status (match-string 1))
+ (while (re-search-forward "^\\(.*?\\): \\(.*\\)$" nil t)
+ (push (cons (intern (match-string 1)) (match-string 2)) fields)))
+ (list (cons 'status status) (cons 'fields fields))))
+
+;;; utilities
+(defun fediorg-api-search (host url)
+ (fediorg-url-fetch-json
+ (format "https://%s/api/v2/search/?q=%s&resolve=true" host url)))
+
+(defun fediorg-canonical-post-url-by-search (host url)
+ (let-alist (fediorg-api-search host url)
+ (if (seq-empty-p .statuses)
+ (error "No statuses associated with URL %s" url)
+ (fediorg-canonical-post-url (alist-get 'url (elt .statuses 0)) t))))
+
+(defun fediorg-post-url-p (url &optional no-fetch)
+ (pcase-let* ((urlobj (url-generic-parse-url url))
+ (`(,path . _) (url-path-and-query urlobj))
+ (host (url-host urlobj)))
+ (or (string-match-p "^/objects/[-a-f0-9]+$" path)
+ (string-match-p
+ "^/user[s]?/[[:alnum:]_]+/statuses/[[:digit:]]+$" path)
+ (string-match-p "^/@[^/]+/\\([0-9]+\\)$" path)
+ (string-match-p "^/notice/\\([[:alnum:]]+\\)$" path))))
+
+(defun fediorg-canonical-post-url (url &optional no-fetch)
+ (pcase-let* ((urlobj (url-generic-parse-url url))
+ (`(,path . _) (url-path-and-query urlobj))
+ (host (url-host urlobj)))
+ (cond ((or (string-match-p "^/objects/[-a-f0-9]+$" path)
+ (string-match-p
+ "^/user[s]?/[[:alnum:]_]+/statuses/[[:digit:]]+$" path))
+ (unless no-fetch (fediorg-canonical-post-url-by-search host url)))
+ ((or (string-match-p "^/@[^/]+/\\([0-9]+\\)$" path)
+ (string-match-p "^/notice/\\([[:alnum:]]+\\)$" path))
+ url)
+ (t (error "Unrecognisable URL: %s" url)))))
+
+(defun fediorg-parse-url (url)
+ "Parse fedi post URL."
+ (pcase-let* ((urlobj (url-generic-parse-url url))
+ (`(,path . _) (url-path-and-query urlobj))
+ (host (url-host urlobj)))
+ (cons host (caddr (split-string path "/")))))
+
+(defun fediorg-api-status (url)
+ "Get the status given URL."
+ (pcase-let ((`(,host . ,post-id) (fediorg-parse-url url)))
+ (fediorg-url-fetch-json
+ (format "https://%s/api/v1/statuses/%s" host post-id))))
+
+(defun fediorg-api-status-context (url)
+ "Get the status context given URL."
+ (pcase-let ((`(,host . ,post-id) (fediorg-parse-url url)))
+ (fediorg-url-fetch-json
+ (format "https://%s/api/v1/statuses/%s/context" host post-id))))
+
+(defun fediorg-get-first-ancestor (url)
+ "Given a fedi post URL, return the url of its first ancestor."
+ (let ((ancestors
+ (alist-get 'ancestors (fediorg-api-status-context url))))
+ (if (length> ancestors 0)
+ (alist-get 'url (elt ancestors 0))
+ url)))
+
+(defun fediorg-post-make-parent-fn (posts)
+ "Given a collection of POSTS, return a function that find the parent post."
+ (lambda (post)
+ (let ((id (alist-get 'in_reply_to_id post)))
+ (seq-find
+ (lambda (candidate)
+ (equal (alist-get 'id candidate) id))
+ posts))))
+
+;;; Formatting functions
+(defun fediorg-format-post-tree (url)
+ "Format a post tree of post located at URL.
+
+Including ancestors and descendants, if any."
+ (let* ((posts-hier (hierarchy-new))
+ (context-posts (fediorg-api-status-context url))
+ (posts (vconcat
+ (alist-get 'ancestors context-posts)
+ (vector (fediorg-api-status url))
+ (alist-get 'descendants context-posts))))
+ (hierarchy-add-trees
+ posts-hier
+ posts
+ (fediorg-post-make-parent-fn posts))
+ (string-join
+ (hierarchy-map 'fediorg-format-post posts-hier 1)
+ "\n")))
+
+(defun fediorg-make-org-link (link desc)
+ (format "[[%s][%s]]" link desc))
+
+(defun fediorg-format-attached (attachments host)
+ (mapconcat
+ (lambda (attachment)
+ (let-alist attachment
+ (with-temp-buffer
+ (insert
+ (fediorg-make-org-link .url .type))
+ (when .description
+ (insert ": " .description))
+ (when .preview_url
+ (let ((thumb-file-name
+ (file-name-concat
+ fediorg-dir
+ (format "%s.%s.%s" host .id
+ (file-name-extension .preview_url)))))
+ (ignore-error 'file-already-exists
+ (url-copy-file .preview_url thumb-file-name))
+ (insert "\n")
+ (when-let ((image (create-image thumb-file-name)))
+ (insert-image image))
+ ))
+ (buffer-string))))
+ attachments
+ "\n"))
+
+(defun fediorg-format-post (post level)
+ "Format a POST with indent LEVEL."
+ (let-alist post
+ (let ((host (car (fediorg-parse-url .url))))
+ (format "%s %s (@%s@%s) %s\n\n%s%s\n\n⤷%d ⇆%d ★%d\n"
+ (make-string level ?*)
+ (if (string-empty-p .account.display_name)
+ .account.username .account.display_name)
+ .account.username
+ host
+ (fediorg-make-org-link
+ .url
+ (fediorg--relative-time-description .created_at))
+ (with-temp-buffer
+ (insert .content)
+ (shr-render-region (point-min) (point-max))
+ (buffer-substring-no-properties (point-min) (point-max)))
+ (fediorg-format-attached .media_attachments host)
+ .replies_count
+ .reblogs_count
+ .favourites_count))))
+
+(defun fediorg-save-text-and-switch-to-buffer (text file-name)
+ "Save TEXT to FILE-NAME and switch to buffer."
+ (let ((buffer (find-file-noselect file-name))
+ (coding-system-for-write 'utf-8))
+ (with-current-buffer buffer
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert text))
+ (goto-char (point-min))
+ (save-buffer)
+ (revert-buffer t t))
+ (switch-to-buffer buffer)))
+
+(defvar fediorg-dir (locate-user-emacs-file "fediorg")
+ "Path to local directory of saved threads.")
+
+(defun fediorg-make-post-file-name (url)
+ (pcase-let ((`(,host . ,post-id) (fediorg-parse-url url)))
+ (format "%s.%s.org" host post-id)))
+
+;;;###autoload
+(defun fediorg-open (url)
+ "Given a fedi post URL, open an org buffer rendering the post.
+
+Including the context, i.e. ancestors and descendant posts."
+ (interactive "sPost URL: ")
+ (setq url (fediorg-canonical-post-url url))
+ (fediorg-save-text-and-switch-to-buffer
+ (fediorg-format-post-tree url)
+ (file-name-concat fediorg-dir (fediorg-make-post-file-name url))))
+
+;;; code adapted from mastodon.el
+(defun fediorg--human-duration (seconds &optional resolution)
+ "Return a string describing SECONDS in a more human-friendly way.
+The return format is (STRING . RES) where RES is the resolution of
+this string, in seconds.
+RESOLUTION is the finest resolution, in seconds, to use for the
+second part of the output (defaults to 60, so that seconds are only
+displayed when the duration is smaller than a minute)."
+ (cl-assert (>= seconds 0))
+ (unless resolution (setq resolution 60))
+ (let* ((units fediorg--time-units)
+ (n1 seconds) (unit1 (pop units)) (res1 1)
+ n2 unit2 res2
+ next)
+ (while (and units (> (truncate (setq next (/ n1 (car units)))) 0))
+ (setq unit2 unit1)
+ (setq res2 res1)
+ (setq n2 (- n1 (* (car units) (truncate n1 (car units)))))
+ (setq n1 next)
+ (setq res1 (truncate (* res1 (car units))))
+ (pop units)
+ (setq unit1 (pop units)))
+ (setq n1 (truncate n1))
+ (if n2 (setq n2 (truncate n2)))
+ (cond
+ ((null n2)
+ ;; revert to old just now style for < 1 min:
+ (cons "just now" 60))
+ ;; (cons (format "%d %s%s" n1 unit1 (if (> n1 1) "s" ""))
+ ;; (max resolution res1)))
+ ((< (* res2 n2) resolution)
+ (cons (format "%d %s%s" n1 unit1 (if (> n1 1) "s" ""))
+ (max resolution res2)))
+ ((< res2 resolution)
+ (let ((n2 (/ (* resolution (/ (* n2 res2) resolution)) res2)))
+ (cons (format "%d %s%s, %d %s%s"
+ n1 unit1 (if (> n1 1) "s" "")
+ n2 unit2 (if (> n2 1) "s" ""))
+ resolution)))
+ (t
+ (cons (format "%d %s%s, %d %s%s"
+ n1 unit1 (if (> n1 1) "s" "")
+ n2 unit2 (if (> n2 1) "s" ""))
+ (max res2 resolution))))))
+
+(defconst fediorg--time-units
+ '("sec" 60.0 ;; Use a float to convert `n' to float.
+ "min" 60
+ "hour" 24
+ "day" 7
+ "week" 4.345
+ "month" 12
+ "year"))
+
+(defun fediorg--relative-time-details (timestamp &optional current-time)
+ "Return cons of (DESCRIPTIVE STRING . NEXT-CHANGE) for the TIMESTAMP.
+Use the optional CURRENT-TIME as the current time (only used for
+reliable testing).
+The descriptive string is a human readable version relative to
+the current time while the next change timestamp give the first
+time that this description will change in the future.
+TIMESTAMP is assumed to be in the past."
+ (let* ((time-difference (time-subtract current-time timestamp))
+ (seconds-difference (float-time time-difference))
+ (tmp (fediorg--human-duration (max 0 seconds-difference))))
+ ;; revert to old just now style for < 1 min
+ (cons (concat (car tmp) (if (string= "just now" (car tmp)) "" " ago"))
+ (time-add current-time (cdr tmp)))))
+
+(defun fediorg--relative-time-description (time-string &optional current-time)
+ "Return a string with a human readable TIME-STRING relative to the current time.
+Use the optional CURRENT-TIME as the current time (only used for
+reliable testing).
+E.g. this could return something like \"1 min ago\", \"yesterday\", etc.
+TIME-STAMP is assumed to be in the past."
+ (car (fediorg--relative-time-details
+ (encode-time (parse-time-string time-string)) current-time)))
+
+(provide 'fediorg)
+;;; fediorg.el ends here
diff --git a/emacs/.emacs.d/lisp/my/infobox.el b/emacs/.emacs.d/lisp/my/infobox.el
index 518c7db..5698042 100644
--- a/emacs/.emacs.d/lisp/my/infobox.el
+++ b/emacs/.emacs.d/lisp/my/infobox.el
@@ -27,6 +27,13 @@
;;; Code:
+(defun infobox-transform-field-value (v)
+ (cond ((stringp v) v)
+ ((eq v t) "YES")
+ ((eq v :json-false) "NO")
+ ((seqp v) (mapconcat #'identity v ", "))
+ (t (format "%s" v))))
+
(defun infobox-default-specs (info)
(seq-map
(lambda (pair)
@@ -47,7 +54,7 @@ something like
(lambda (pair)
(when-let ((val (alist-get (car pair) info)))
(if (or (stringp (cdr pair)) (symbolp (cdr pair)))
- (cons (cdr pair) val)
+ (cons (cdr pair) (infobox-transform-field-value val))
(cons (cadr pair) (funcall (cddr pair) val)))))
specs))
@@ -56,6 +63,17 @@ something like
(with-help-window "*infobox*"
(with-current-buffer standard-output
(let ((n-rows 0))
+ ;; TODO: use a more standard function than
+ ;; `my-make-filename-from-url'
+ (when-let* ((thumb-url (alist-get "Thumbnail" info nil nil 'equal))
+ (file-name (file-name-concat
+ "/tmp"
+ (my-make-filename-from-url thumb-url))))
+ (url-copy-file (message thumb-url) file-name t)
+ (insert-image (create-image file-name nil nil
+ :max-width (window-width nil t)))
+ (insert "\n")
+ (setq n-rows (1+ n-rows)))
(seq-do
(lambda (pair)
(when pair
@@ -90,6 +108,13 @@ something like
(infobox-render-string
(with-temp-buffer
(call-process "exiftool" nil t nil filename)
+ (goto-char (point-min))
+ (flush-lines "ExifTool Version")
+ (end-of-line)
+ (insert " -- " (buttonize
+ "xdg-open"
+ (lambda (_)
+ (call-process "xdg-open" nil 0 nil filename))))
(buffer-string))
`(infobox-exiftool ,filename)
(called-interactively-p 'interactive)
diff --git a/emacs/.emacs.d/lisp/my/mastorg.el b/emacs/.emacs.d/lisp/my/mastorg.el
deleted file mode 100644
index 3544b2e..0000000
--- a/emacs/.emacs.d/lisp/my/mastorg.el
+++ /dev/null
@@ -1,207 +0,0 @@
-;;; mastorg.el -- Read or archive mastodon toot context in org mode -*- lexical-binding: t -*-
-
-;; Copyright (C) 2023 Free Software Foundation, Inc.
-
-;; Author: Yuchen Pei <id@ypei.org>
-;; Package-Requires: ((emacs "28.2"))
-
-;; This file is part of dotted.
-
-;; dotted is free software: you can redistribute it and/or modify it under
-;; the terms of the GNU Affero General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-
-;; dotted 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 Affero General
-;; Public License for more details.
-
-;; You should have received a copy of the GNU Affero General Public
-;; License along with dotted. If not, see <https://www.gnu.org/licenses/>.
-
-;;; Commentary:
-
-;; Read or archive mastodon toot context in org mode. This is a
-;; standalone library, and can be used without any other files in this
-;; project.
-
-;; Usage:
-;; M-x mastorg-open <RET> https://mastodon.instance/@user/12345678901234 <RET>
-;;
-;; The toot, together with its ancestors and descendants, subject to
-;; mastodon API depth limit, are displayed in an org buffer.
-
-;; TODO:
-;;
-;; To be able to refresh the org buffer at an org entry, which would
-;; re-fetch the context of the corresponding toot and upsert them in
-;; the buffer.
-;;; Code:
-
-
-(require 'hierarchy)
-(require 'json)
-(require 'url-parse)
-
-(defvar mastorg-buffer "*mastorg*" "Buffer name for mastorg buffers.")
-
-;;; Fetching utilities
-(defvar mastorg-client-buffer-name "*mastorg-api*"
- "Buffer name for logging API requests.")
-
-(defun mastorg-url-fetch-json (url &optional decompression with-header)
- "Fetch and parse json from URL.
-
-With nonnil DECOMPRESSION, gunzip the response first.
-With nonnil WITH-HEADER, include the response headers in the return value."
- (mastorg-url-fetch-internal
- url
- (lambda ()
- (json-read-from-string (decode-coding-string (buffer-string) 'utf-8)))
- decompression
- with-header))
-
-(defun mastorg-url-fetch-internal (url buffer-processor decompression with-header)
- "Fetch from URL and process the response with BUFFER-PROCESSOR.
-
-With nonnil DECOMPRESSION, gunzip the response first.
-With nonnil WITH-HEADER, include the response headers in the return value."
- (with-current-buffer (get-buffer-create mastorg-client-buffer-name)
- (goto-char (point-max))
- (insert "[" (current-time-string) "] Request: " url "\n"))
- (with-current-buffer (url-retrieve-synchronously url t)
- (let ((header (mastorg-kill-http-header)) (status) (fields))
- (goto-char (point-min))
- (setq header (mastorg-parse-http-header header)
- status (alist-get 'status header)
- fields (alist-get 'fields header))
- (with-current-buffer mastorg-client-buffer-name
- (insert "[" (current-time-string) "] Response: " status "\n"))
- (when decompression
- (call-process-region (point) (point-max) "gunzip" t t t)
- (goto-char (point-min)))
- (call-interactively 'delete-trailing-whitespace)
- (if (string= status "200")
- (unless (= (point) (point-max))
- (if with-header
- (list
- (cons 'header fields)
- (cons 'json (funcall buffer-processor)))
- (funcall buffer-processor)))
- (error "HTTP error: %s" (buffer-substring (point) (point-max)))))))
-
-(defun mastorg-kill-http-header ()
- "Kill http headers in the current buffer."
- (mastorg-skip-http-header)
- (let ((killed (buffer-substring-no-properties (point-min) (point))))
- (delete-region (point-min) (point))
- killed))
-
-(defun mastorg-skip-http-header ()
- "Skip http headers in the current buffer."
- (goto-char (point-min))
- (re-search-forward "\r?\n\r?\n"))
-
-(defun mastorg-parse-http-header (text)
- "Parse http headers from TEXT in the current buffer."
- (let ((status) (fields))
- (with-temp-buffer
- (insert text)
- (goto-char (point-min))
- (re-search-forward "^HTTP.*\\([0-9]\\{3\\}\\).*$")
- (setq status (match-string 1))
- (while (re-search-forward "^\\(.*?\\): \\(.*\\)$" nil t)
- (push (cons (intern (match-string 1)) (match-string 2)) fields)))
- (list (cons 'status status) (cons 'fields fields))))
-
-;;; mastodon utilities
-(defun mastorg-parse-url (url)
- "Parse mastodon post URL."
- (pcase-let* ((urlobj (url-generic-parse-url url))
- (`(,path . _) (url-path-and-query urlobj))
- (host (url-host urlobj)))
- (cons host (caddr (split-string path "/")))))
-
-(defun mastorg-api-status (url)
- "Get the status given URL."
- (pcase-let ((`(,host . ,post-id) (mastorg-parse-url url)))
- (mastorg-url-fetch-json
- (format "https://%s/api/v1/statuses/%s" host post-id))))
-
-(defun mastorg-api-status-context (url)
- "Get the status context given URL."
- (pcase-let ((`(,host . ,post-id) (mastorg-parse-url url)))
- (mastorg-url-fetch-json
- (format "https://%s/api/v1/statuses/%s/context" host post-id))))
-
-(defun mastorg-get-first-ancestor (url)
- "Given a mastodon URL, return the url of its first ancestor."
- (let ((ancestors
- (alist-get 'ancestors (mastorg-api-status-context url))))
- (if (length> ancestors 0)
- (alist-get 'url (elt ancestors 0))
- url)))
-
-(defun mastorg-toot-make-parent-fn (toots)
- "Given a collection of TOOTS, return a function that find the parent toot."
- (lambda (toot)
- (let ((id (alist-get 'in_reply_to_id toot)))
- (seq-find
- (lambda (candidate)
- (equal (alist-get 'id candidate) id))
- toots))))
-
-;;; Formatting functions
-(defun mastorg-format-toot-tree (url)
- "Format a toot tree of toot located at URL.
-
-Including ancestors and descendants, if any."
- (let* ((toots-hier (hierarchy-new))
- (context-toots (mastorg-api-status-context url))
- (toots (vconcat
- (alist-get 'ancestors context-toots)
- (vector (mastorg-api-status url))
- (alist-get 'descendants context-toots))))
- (hierarchy-add-trees
- toots-hier
- toots
- (mastorg-toot-make-parent-fn toots))
- (string-join
- (hierarchy-map 'mastorg-format-toot toots-hier 1)
- "\n")))
-
-(defun mastorg-format-toot (toot level)
- "Format a TOOT with indent LEVEL."
- (pcase-let* ((url (alist-get 'url toot))
- (account (alist-get 'account toot))
- (display-name (alist-get 'display_name account))
- (username (alist-get 'username account))
- (`(,host . _) (mastorg-parse-url url)))
- (format "%s %s @%s@%s %s\n%s"
- (make-string level ?*)
- (if (string-empty-p display-name) username display-name)
- username
- host
- (alist-get 'created_at toot)
- (with-temp-buffer
- (insert (alist-get 'content toot))
- (shr-render-region (point-min) (point-max))
- (buffer-substring-no-properties (point-min) (point-max))))))
-
-;;;###autoload
-(defun mastorg-open (url)
- "Given a mastodon toot URL, open an org buffer rendering the toot.
-
-Including the context, i.e. ancestors and descendant toots."
- (interactive "sToot URL: ")
- (with-current-buffer (get-buffer-create mastorg-buffer)
- (let ((inhibit-read-only t))
- (erase-buffer)
- (insert (mastorg-format-toot-tree url))
- (org-mode)
- (goto-char (point-min))))
- (switch-to-buffer mastorg-buffer))
-
-(provide 'mastorg)
-;;; mastorg.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-buffer.el b/emacs/.emacs.d/lisp/my/my-buffer.el
index f2da7f5..a8683de 100644
--- a/emacs/.emacs.d/lisp/my/my-buffer.el
+++ b/emacs/.emacs.d/lisp/my/my-buffer.el
@@ -264,8 +264,6 @@ Focus write: make the current window the only one centered with
width 80. If in org-mode, also narrow to current subtree. Make
buffers on both sides empty read-only buffers."
(interactive)
- ;; Only one window in the current frame indicates we are in focus
- ;; write mode.
(if (and (equal
(buffer-name
(window-buffer (window-left (get-buffer-window))))
diff --git a/emacs/.emacs.d/lisp/my/my-emms.el b/emacs/.emacs.d/lisp/my/my-emms.el
index fd3c73d..e6fb0e2 100644
--- a/emacs/.emacs.d/lisp/my/my-emms.el
+++ b/emacs/.emacs.d/lisp/my/my-emms.el
@@ -508,8 +508,11 @@ character."
If the last command is `emms-playlist-mode-play-smart', then set
`my-emms-score-delta' to 2."
- (when (eq last-command 'emms-playlist-mode-play-smart)
- (setq my-emms-score-delta 2)))
+ (if (not (eq last-command 'emms-playlist-mode-play-smart))
+ (setq my-emms-score-delta 1)
+ (setq my-emms-score-delta 2)
+ (setq last-command nil))
+ )
(defun my-emms-wrapped ()
"Print top 5 scored tracks."
diff --git a/emacs/.emacs.d/lisp/my/my-github.el b/emacs/.emacs.d/lisp/my/my-github.el
index 1643612..7caff57 100644
--- a/emacs/.emacs.d/lisp/my/my-github.el
+++ b/emacs/.emacs.d/lisp/my/my-github.el
@@ -75,6 +75,10 @@ License; name; description; homepage; created at"
(my-url-fetch-raw
(format "https://api.github.com/repos%s/readme" project-id))))
+(defun my-github-project-url-p (url)
+ (let ((urlobj (url-generic-parse-url url)))
+ (string-match-p "\\(www\\.\\)?github.com" (url-host urlobj))))
+
(defun my-github-project-infobox (url)
(interactive "sGithub repo url: ")
(let ((info
diff --git a/emacs/.emacs.d/lisp/my/my-gitlab.el b/emacs/.emacs.d/lisp/my/my-gitlab.el
index ad7f0ed..27f3344 100644
--- a/emacs/.emacs.d/lisp/my/my-gitlab.el
+++ b/emacs/.emacs.d/lisp/my/my-gitlab.el
@@ -73,9 +73,19 @@
(string-match-p "^/[^/]+/[^/]+$" (url-filename urlobj)))))
(require 'my-buffer)
+(require 'my-web)
+(require 'my-magit)
+(defun my-gitlab-format-url (url)
+ (concat url
+ " -- " (buttonize "clone"
+ (lambda (_)
+ (my-magit-clone url current-prefix-arg)))
+ " " (buttonize "context"
+ (lambda (_)
+ (funcall my-url-context-function url)))))
(defvar my-gitlab-project-info-specs
- `((http_url_to_repo . "Clone")
+ `((http_url_to_repo . ("URL" . my-gitlab-format-url))
(name_with_namespace . "Name")
(description . "Description")
(created_at . ("Created at" . my-gitlab-format-time-string))
diff --git a/emacs/.emacs.d/lisp/my/my-ledger.el b/emacs/.emacs.d/lisp/my/my-ledger.el
index 8c955c6..b1ad2ca 100644
--- a/emacs/.emacs.d/lisp/my/my-ledger.el
+++ b/emacs/.emacs.d/lisp/my/my-ledger.el
@@ -39,5 +39,14 @@
(call-interactively 'ledger-navigate-prev-xact-or-directive)
(call-interactively 'ledger-navigate-prev-xact-or-directive))
+;;; hledger: Error: /home/ycp/Documents/finance/huecu.ledger:1615:41:
+(defvar my-ledger-compilation-error-re
+ '(ledger "^hledger: Error: \\(.+\\):\\([0-9]+\\):\\([0-9]+\\):$" 1 2 3))
+
+(defun my-ledger-set-compile-command ()
+ (setq-local
+ compile-command
+ (format "%s bal -f %s" ledger-binary-path buffer-file-name)))
+
(provide 'my-ledger)
;;; my-ledger.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-magit.el b/emacs/.emacs.d/lisp/my/my-magit.el
index efb3c84..eabed05 100644
--- a/emacs/.emacs.d/lisp/my/my-magit.el
+++ b/emacs/.emacs.d/lisp/my/my-magit.el
@@ -32,23 +32,26 @@
(require 'my-project)
(require 'org)
-(defun my-magit-clone-org-source (arg)
- (interactive "P")
- (let* ((url (or (org-entry-get (point) "Source")
- (org-entry-get (point) "Website")))
- (default-base-dir
- (alist-get "3p" my-projects-root-dirs nil nil 'string=))
+(defun my-magit-clone (url prefix-arg)
+ (let* ((default-base-dir
+ (alist-get "3p" my-projects-root-dirs nil nil 'string=))
(default-name
- (progn (string-match "^.*/\\(.*?\\)\\(\\.git\\)?$" url)
- (match-string 1 url)))
+ (progn (string-match "^.*/\\(.*?\\)\\(\\.git\\)?$" url)
+ (match-string 1 url)))
(dir (read-file-name
- (if arg "Clone to: " "Shallow clone to: ")
+ (if prefix-arg "Clone to: " "Shallow clone to: ")
(concat default-base-dir "/")
nil nil
default-name)))
- (if arg
+ (if prefix-arg
(magit-clone-regular url dir nil)
- (magit-clone-shallow url dir nil 1))
+ (magit-clone-shallow url dir nil 1))))
+
+(defun my-magit-clone-org-source (arg)
+ (interactive "P")
+ (let* ((url (or (org-entry-get (point) "Source")
+ (org-entry-get (point) "Website"))))
+ (my-magit-clone url arg)
(org-set-property "Local-source"
(format "<file:%s>" dir))))
diff --git a/emacs/.emacs.d/lisp/my/my-mariadb.el b/emacs/.emacs.d/lisp/my/my-mariadb.el
index bdb1c60..d6c2463 100644
--- a/emacs/.emacs.d/lisp/my/my-mariadb.el
+++ b/emacs/.emacs.d/lisp/my/my-mariadb.el
@@ -33,7 +33,9 @@
(interactive)
(if (equal (file-name-extension (buffer-file-name))
"test")
- (call-interactively 'project-compile)
+ (progn
+ (my-mtr-set-compile-command)
+ (call-interactively 'compile))
(sql-send-buffer)))
(defun my-gdb-maria ()
@@ -288,5 +290,34 @@ switches to the buffer."
(file-name (format "/tmp/%s.wiki" term)))
(my-save-text-and-switch-to-buffer source file-name)))
+(defvar my-mtr-compilation-error-re
+ '(mtr "^mysqltest: At line \\([0-9]+\\)" nil 1))
+
+;; (defun my-mtr-find-test-file (test-name &optional dir)
+;; (unless dir (setq dir default-directory))
+;; ())
+
+(defun my-mtr-set-compile-command ()
+ (when (and buffer-file-name
+ (equal "test" (file-name-extension buffer-file-name)))
+ (when-let*
+ ((source-dir (expand-file-name (project-root (project-current))))
+ (build-dir (replace-regexp-in-string "/src/$" "/build/" source-dir))
+ (test-name
+ (progn
+ (when (string-match
+ "^.*/mysql-test/\\(.+?\\)/\\(t/\\)?\\([^/]+\\)\\.test$"
+ buffer-file-name)
+ (format "%s.%s"
+ (match-string 1 buffer-file-name)
+ (match-string 3 buffer-file-name))))))
+ (setq-local
+ compile-command
+ (format "%s %s %s %s"
+ "taskset -c 0-3"
+ (file-name-concat build-dir "mysql-test/mtr")
+ test-name
+ "--rr")))))
+
(provide 'my-mariadb)
;;; my-mariadb.el ends here
diff --git a/emacs/.emacs.d/lisp/my/my-net.el b/emacs/.emacs.d/lisp/my/my-net.el
index 2574789..6212b50 100644
--- a/emacs/.emacs.d/lisp/my/my-net.el
+++ b/emacs/.emacs.d/lisp/my/my-net.el
@@ -30,11 +30,22 @@
;;; net utilities
(defvar my-download-dir "~/Downloads")
-(defun my-make-file-name-from-url (url)
- (file-name-nondirectory
- (directory-file-name
- (car (url-path-and-query (url-generic-parse-url
- (url-unhex-string url)))))))
+(defmacro my-url-as-googlebot (&rest body)
+ "Run BODY while spoofing as googlebot"
+ `(let ((url-request-extra-headers '(("X-Forwarded-For" . "66.249.66.1")))
+ (url-user-agent
+ "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"))
+ ,@body))
+
+(def-edebug-spec my-url-as-googlebot t)
+
+(defun my-make-file-name-from-url (url &optional extension)
+ (format "%s%s"
+ (file-name-nondirectory
+ (directory-file-name
+ (car (url-path-and-query (url-generic-parse-url
+ (url-unhex-string url))))))
+ (if extension (concat "." extension) "")))
;; stolen from `eww-make-unique-file-name'
(defun my-make-unique-file-name (file directory)
diff --git a/emacs/.emacs.d/lisp/my/my-package.el b/emacs/.emacs.d/lisp/my/my-package.el
index b591d0f..9eefa2e 100644
--- a/emacs/.emacs.d/lisp/my/my-package.el
+++ b/emacs/.emacs.d/lisp/my/my-package.el
@@ -216,6 +216,17 @@ same name, cancel that one first."
(cancel-timer ,var-name))
(setq ,var-name (run-with-timer ,secs ,repeat ,function))))
+(defmacro my-timer (var-name secs repeat function)
+ "Create a timer.
+
+The timer has name VAR-NAME. If there is an existing time with the
+same name, cancel that one first."
+
+ `(progn
+ (when (and (boundp ',var-name) (timerp ,var-name))
+ (cancel-timer ,var-name))
+ (setq ,var-name (run-with-timer ,secs ,repeat ,function))))
+
(defun my-describe-package-from-url (url)
(interactive "sUrl: ")
(when (string-match
@@ -263,7 +274,7 @@ same name, cancel that one first."
(add-hook hook function)))
(defvar my-common-packages
- '(package windmove consult icomplete
+ '(package windmove consult icomplete isearch
my-utils my-buffer my-editing my-complete)
"Common packages to include with any profile")
diff --git a/emacs/.emacs.d/lisp/my/my-prog.el b/emacs/.emacs.d/lisp/my/my-prog.el
index a81d36d..92fcf21 100644
--- a/emacs/.emacs.d/lisp/my/my-prog.el
+++ b/emacs/.emacs.d/lisp/my/my-prog.el
@@ -365,8 +365,28 @@ left and the source buffer on the right.
(select-window (display-buffer (gdb-get-source-buffer))))
(defun my-gud-comint-set-prompt-regexp ()
- (setq comint-prompt-regexp "\\((rr)|(gdb)\\) "))
+ (setq comint-prompt-regexp "\\((rr)\\|(gdb)\\) *"))
+(defun my-gud-source-line ()
+ (with-current-buffer (gdb-get-source-buffer)
+ (buffer-substring (progn (beginning-of-line) (point))
+ (progn (end-of-line) (point)))))
+
+(defun my-gud-function-name ()
+ (with-current-buffer (gdb-get-source-buffer)
+ (which-function)))
+
+(defun my-gud-insert-source-line ()
+ (interactive)
+ (insert (my-gud-source-line)))
+
+(defun my-gud-insert-function-name ()
+ (interactive)
+ (insert (my-gud-function-name)))
+
+(defun my-gud-insert-source-line-and-function-name ()
+ (interactive)
+ (insert (format "%s IN %s" (my-gud-source-line) (my-gud-function-name))))
;;; used to override `gdb-frame-handler': do not re-display frame on
;;; completion.
@@ -422,6 +442,24 @@ overlay arrow in source buffer."
;; (accept-process-output (get-buffer-process gud-comint-buffer) .1)))
;; (gud-gdb-completions-1 gud-gdb-fetched-lines)))
+(defun my-gud-watch-expr (expr)
+ (with-current-buffer gud-comint-buffer
+ (insert "watch -l " expr)
+ (comint-send-input)))
+
+(defun my-gud-print-expr (expr)
+ (with-current-buffer gud-comint-buffer
+ (insert "p " expr)
+ (comint-send-input)))
+
+(defun my-gud-print-expr-region (b e)
+ (interactive "r")
+ (unless (eq (gdb-get-source-buffer) (current-buffer))
+ (error "Not in the source buffer"))
+ (if current-prefix-arg
+ (my-gud-watch-expr (buffer-substring b e))
+ (my-gud-print-expr (buffer-substring b e))))
+
;;; which-func
(defun my-copy-which-func ()
(interactive)
@@ -489,6 +527,34 @@ overlay arrow in source buffer."
(unless (derived-mode-p 'haskell-mode 'c-mode 'c++-mode)
(eglot-format-buffer))))
+;;; https://github.com/joaotavora/eglot/issues/88
+(defun my-eglot-ccls-inheritance-hierarchy (&optional derived)
+ "Show inheritance hierarchy for the thing at point.
+If DERIVED is non-nil (interactively, with prefix argument), show
+the children of class at point."
+ (interactive "P")
+ (if-let* ((res (jsonrpc-request
+ (eglot--current-server-or-lose)
+ :$ccls/inheritance
+ (append (eglot--TextDocumentPositionParams)
+ `(:derived ,(if derived t :json-false))
+ '(:levels 100) '(:hierarchy t))))
+ (tree (list (cons 0 res))))
+ (with-help-window "*ccls inheritance*"
+ (with-current-buffer standard-output
+ (while tree
+ (pcase-let ((`(,depth . ,node) (pop tree)))
+ (cl-destructuring-bind (&key uri range) (plist-get node :location)
+ (insert (make-string depth ?\ ) (plist-get node :name) "\n")
+ (make-text-button (+ (point-at-bol 0) depth) (point-at-eol 0)
+ 'action (lambda (_arg)
+ (interactive)
+ (find-file (eglot--uri-to-path uri))
+ (goto-char (car (eglot--range-region range)))))
+ (cl-loop for child across (plist-get node :children)
+ do (push (cons (1+ depth) child) tree)))))))
+ (eglot--error "Hierarchy unavailable")))
+
;;; lisp
(defun my-eval-defun-or-region (&optional arg)
"Call `eval-region' if region is active, otherwise call `eval-defun'"
diff --git a/emacs/.emacs.d/lisp/my/my-web.el b/emacs/.emacs.d/lisp/my/my-web.el
index f2e48ba..aeb5a6d 100644
--- a/emacs/.emacs.d/lisp/my/my-web.el
+++ b/emacs/.emacs.d/lisp/my/my-web.el
@@ -137,28 +137,32 @@
;;; webgetter
(require 'my-net)
-(defun my-open-spectator-au (url &optional no-overwrite)
- (interactive "sspectator.com.au link: ")
- (let ((url-request-extra-headers '(("X-Forwarded-For" . "66.249.66.1")))
- (url-user-agent "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"))
- (let ((file-name
- (if no-overwrite
- (my-make-unique-file-name
- (my-make-file-name-from-url url)
- my-download-dir)
- (expand-file-name
+(defun my-fetch-browse (url &optional no-overwrite)
+ "Fetch URL to a local file then browse it with firefox.
+
+Useful for bypassing \"Enable JavaScript and cookies to continue\"."
+ (interactive "sUrl to fetch and browse: ")
+ (let ((file-name
+ (if no-overwrite
+ (my-make-unique-file-name
(my-make-file-name-from-url url)
- my-download-dir))))
- (url-copy-file url file-name (not no-overwrite))
- (browse-url-firefox (format "file://%s" file-name)))))
-
-(defun my-mastodon-url-p (url)
- "Guess if a url is a mastodon post.
-e.g. https://hostux.social/@fsf/113709722998924141
-"
- (pcase-let* ((urlobj (url-generic-parse-url url))
- (`(,path . _) (url-path-and-query urlobj)))
- (string-match-p "^/@[^/]+/[0-9]\\{18\\}$" path)))
+ my-download-dir)
+ (expand-file-name
+ (my-make-file-name-from-url url "html")
+ my-download-dir))))
+ (url-copy-file url file-name (not no-overwrite))
+ (browse-url-firefox (format "file://%s" file-name))))
+
+(defun my-fetch-browse-as-googlebot (url &optional no-overwrite)
+ "Same as `my-fetch-browse', but spoofing googlebot.
+
+Useful for bypassing some paywalls."
+ (interactive "sUrl to fetch and browse as googlebot: ")
+ (my-url-as-googlebot
+ (my-fetch-browse url no-overwrite)))
+
+(require 'hmm)
+(defvar my-url-context-function 'hmm-url "Context function for urls.")
(defun my-hacker-news-url-p (url)
"Check if a url is a hacker news post.
diff --git a/emacs/.emacs.d/lisp/my/my-ytdl.el b/emacs/.emacs.d/lisp/my/my-ytdl.el
index 2811793..b3b1cf7 100644
--- a/emacs/.emacs.d/lisp/my/my-ytdl.el
+++ b/emacs/.emacs.d/lisp/my/my-ytdl.el
@@ -90,18 +90,52 @@
(defun my-ytdl-video-url-p (url)
(let ((urlobj (url-generic-parse-url url)))
- (or (and (string-match-p "^\\(www\\.\\)?youtube.com" (url-host urlobj))
+ (or (and (string-match-p
+ "^\\(www\\.\\)?\\(youtube\\.com\\|yewtu\\.be\\)"
+ (url-host urlobj))
(string-match-p "^/watch\\?v=.*" (url-filename urlobj)))
(equal "youtu.be" (url-host urlobj)))))
+(require 'hmm)
+(defvar my-ytdl-player 'hmm-external-mpv "Function to play ytdl urls.")
+
+(defun my-ytdl-video-format-seconds (secs)
+ (setq secs (floor secs))
+ (if (>= secs 3600)
+ (format "%d:%02d:%02d"
+ (/ secs 3600) (/ (% secs 3600) 60) (% secs 60))
+ (format "%d:%02d"
+ (/ secs 60) (% secs 60))))
+
+(defun my-ytdl-video-format-chapters (chapters)
+ (mapconcat
+ (lambda (chapter)
+ (let-alist chapter
+ (format "%s: %s-%s" .title (my-ytdl-video-format-seconds .start_time)
+ (my-ytdl-video-format-seconds .end_time))))
+ chapters
+ "; "))
+
+(defun my-ytdl-video-render-info (info url)
+ (setf (alist-get 'webpage_url info)
+ (concat (alist-get 'webpage_url info)
+ " -- " (buttonize "play" (lambda (_)
+ (funcall my-ytdl-player url)))
+ " " (buttonize "context"
+ (lambda (_)
+ (funcall my-url-context-function url))))
+ (alist-get 'chapters info)
+ (my-ytdl-video-format-chapters (alist-get 'chapters info)))
+ (infobox-render
+ (infobox-translate info (infobox-default-specs info))
+ `(my-ytdl-video-infobox ,url)
+ (called-interactively-p 'interactive)))
+
(defun my-ytdl-video-infobox (url)
(interactive "sytdl video url: ")
- (let* ((info (my-ytdl-video-info url))
- (specs (infobox-default-specs info)))
- (infobox-render
- (infobox-translate info specs)
- `(my-ytdl-video-infobox ,url)
- (called-interactively-p 'interactive))))
+ ;; Remove any extra queries from the URL
+ (setq url (replace-regexp-in-string "&.*" "" url))
+ (my-ytdl-video-render-info (my-ytdl-video-info url) url))
;;; fixme: autoload
(defun my-ytdl-video (urls)
diff --git a/emacs/.emacs.d/lisp/my/reddio.el b/emacs/.emacs.d/lisp/my/reddio.el
index 2198e43..f8bc77f 100644
--- a/emacs/.emacs.d/lisp/my/reddio.el
+++ b/emacs/.emacs.d/lisp/my/reddio.el
@@ -28,19 +28,46 @@
(defvar reddio-buffer "*reddio*")
-(defun reddio-open-url (url)
- (interactive "sReddit link: ")
- (when (string-match "/\\(comments/[^/]+\\)/" url)
- (with-current-buffer (get-buffer-create reddio-buffer)
+(defvar reddio-dir (locate-user-emacs-file "reddio")
+ "Path to local directory of saved threads.")
+
+(defun reddio-make-filename (url)
+ (string-match "/r/\\([^/]+\\)/comments/\\([^/]+\\)/\\([^/]+\\)" url)
+ (file-name-concat
+ reddio-dir
+ (format "%s.%s.%s.txt"
+ (match-string 1 url)
+ (match-string 3 url)
+ (match-string 2 url))))
+
+(defun reddio-save-text-and-switch-to-buffer (text file-name)
+ "Save TEXT to FILE-NAME and switch to buffer."
+ (let ((buffer (find-file-noselect file-name))
+ (coding-system-for-write 'utf-8))
+ (with-current-buffer buffer
(let ((inhibit-read-only t))
(erase-buffer)
- (when (= 0 (call-process "reddio" nil reddio-buffer nil "print"
- (match-string 1 url)))
- (goto-char (point-min)))
- (delete-trailing-whitespace))
- (text-mode)
- (view-mode))
- (display-buffer reddio-buffer)))
+ (insert text))
+ (goto-char (point-min))
+ (save-buffer)
+ (revert-buffer t t))
+ (switch-to-buffer buffer)))
+
+(defun reddio-open-url (url)
+ (interactive "sReddit link: ")
+ (let ((text
+ (when (string-match "/\\(comments/[^/]+\\)/" url)
+ (with-temp-buffer
+ (if (= 0 (call-process "reddio" nil (current-buffer) nil
+ "print" "-l" "500"
+ (match-string 1 url)))
+ (goto-char (point-min))
+ (error "reddio process failed: %s" (buffer-string)))
+ (delete-trailing-whitespace)
+ (buffer-string)))))
+ (reddio-save-text-and-switch-to-buffer
+ text
+ (reddio-make-filename url))))
(defun reddio-reddit-url-p (url)
"e.g.