diff options
Diffstat (limited to 'emacs/.emacs.d/lisp')
m--------- | emacs/.emacs.d/lisp/exitter | 0 | ||||
m--------- | emacs/.emacs.d/lisp/hmm.el | 0 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/fediorg.el | 368 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/infobox.el | 27 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/mastorg.el | 207 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-buffer.el | 2 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-emms.el | 7 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-github.el | 4 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-gitlab.el | 12 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-ledger.el | 9 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-magit.el | 25 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-mariadb.el | 33 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-net.el | 21 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-package.el | 13 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-prog.el | 68 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-web.el | 46 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/my-ytdl.el | 48 | ||||
-rw-r--r-- | emacs/.emacs.d/lisp/my/reddio.el | 49 |
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. |