emacs.d/lisp/init-utils.el

562 lines
21 KiB
EmacsLisp

;;; init-utils.el --- Elisp helper functions and commands -*- lexical-binding: t -*-
;;; Commentary:
;;; Code:
;; the my/xxx utils config - yet to be placed in dedicated init-xxx.el files
(defun my/highlight-selected-text (start end &optional color)
"Highlight the selected region temporarily with the specified color.
If color is not provided, the default color is #5F87FF.
Version 2023-10-18"
(interactive "r\nsEnter color (e.g., 'red', press ENTER for default #5F87FF): ")
(let* ((overlay (make-overlay start end))
(color (if (string= color "") "#5F87FF" color))
(text-color (if (or (string= color "black") (string= color "#5F87FF"))
"white"
"black")))
(overlay-put overlay 'face `((:background ,color :foreground ,text-color)))
(add-hook 'before-revert-hook (lambda () (delete-overlay overlay)))))
(defun my/random-org-item ()
"Go to a random org heading from all org files in `org-directory`."
(interactive)
(let* ((org-files (directory-files-recursively org-directory "\\.org$"))
(random-file (nth (random (length org-files)) org-files)))
(find-file random-file) ; open the random org file
(org-mode)
(org-overview)
(goto-char (point-min))
(org-next-visible-heading 1) ; move to the first visible heading
;; parse the buffer and collect all headline elements into `headings` list, see,
(let* ((headings (org-element-map (org-element-parse-buffer) 'headline 'identity)))
(if headings
(progn
;; generate a random index within the length of `headings`, see,
(let* ((random-index (random (length headings)))
;; select a random heading from `headings` using the random index, see,
(random-heading (nth random-index headings)))
;; move the cursor to the beginning of the randomly selected heading. see,
(goto-char (org-element-property :begin random-heading))
(org-show-subtree))) ; expand the selected heading
(goto-char (point-min))))) ; if no headings are found, move the cursor to the beginning of the buffer
)
(defun my/eww-open-local-file ()
"Open the local file at point in EWW."
(interactive)
(let ((file (thing-at-point 'filename)))
(eww-open-file file)))
(defun my/org-repeated-deadline-schedule (type &optional arg time repeater)
"Create repeated deadlines or scheduled tasks.
TYPE is either 'deadline or 'schedule.
With prefix argument(C-u), the existing deadline/schedule will
be removed.
Repeater string should be in the format of `+1m' (exactly 1 month),
`.+1m' (1 month from last completion) or `++1m' (at least 1 month from
last completion, and keep it on the same day of the week, moving the
due date into the future by increments of month)."
;; This function is improved by ChatGPT :)
(interactive
(list (intern (completing-read "Type: "
'(("deadline") ("schedule"))))
current-prefix-arg nil))
(unless arg
(setq repeater (read-string "Input the repeater: ")))
(let ((org-time-stamp-formats
(if repeater
`(,(concat "<%Y-%m-%d %a " repeater ">") .
,(concat "<%Y-%m-%d %a %H:%M " repeater ">"))
org-time-stamp-formats)))
(cl-case type
(deadline (org-deadline arg time))
(schedule (org-schedule arg time)))))
;; https://stackoverflow.com/a/10628109/4274775
;; keybinding: =C-k= -> [[./init-keybindings.el::my-dpap]]
;; <2023-03-23 Thu 10:22> In Emacs 25, you can do what you'd expect: in the
;; process list, hit d(M-x process-menu-delete-process) to "delete" the process
;; under point(D is binded in evil-mode). See
;; https://stackoverflow.com/a/31538514/4274775
(defun my/delete-process-at-point ()
(interactive)
(let ((process (get-text-property (point) 'tabulated-list-id)))
(cond ((and process
(processp process))
(delete-process process)
(revert-buffer))
(t
(error "no process at point!")))))
;; https://stackoverflow.com/a/7043786
(defun my/sudo-find-file (file-name)
"like find file, but opens the file as root."
(interactive "FSudo Find File: ")
(let ((tramp-file-name (concat "/sudo::" (expand-file-name file-name))))
(find-file tramp-file-name)))
;; { START: undo fill-paragraph
;; https://stackoverflow.com/a/2478549
(defun my/unfill-paragraph ()
(interactive)
(let ((fill-column (point-max)))
(fill-paragraph nil)))
(defun my/unfill-region ()
(interactive)
(let ((fill-column (point-max)))
(fill-region (region-beginning) (region-end) nil)))
;; END: undo fill-paragraph
(defun my/org-insert-src-block (src-code-type)
"insert a `SRC-CODE-TYPE' type source code block in org-mode."
(interactive
(let ((src-code-types
'("emacs-lisp" "python" "C" "sh" "java" "js" "clojure" "C++" "css"
"calc" "asymptote" "dot" "gnuplot" "ledger" "lilypond" "mscgen"
"octave" "oz" "plantuml" "R" "sass" "screen" "sql" "awk" "ditaa"
"haskell" "latex" "lisp" "matlab" "ocaml" "org" "perl" "ruby"
"scheme" "sqlite")))
(list (ido-completing-read "Source code type: " src-code-types))))
(progn
(newline-and-indent)
(insert (format "#+BEGIN_SRC %s\n" src-code-type))
(newline-and-indent)
(insert "#+END_SRC\n")
(previous-line 2)
(org-edit-src-code))
)
(defun my/hide-dos-eol ()
"do not show ^M in files containing mixed UNIX and DOS line endings."
(interactive)
(unless buffer-display-table
(setq buffer-display-table (make-display-table)))
(aset buffer-display-table ?\^M []))
(defun my/remove-dos-eol ()
"replace DOS eolns CR LF with Unix eolns CR."
(interactive)
(goto-char (point-min))
(while (search-forward "\r" nil t) (replace-match "")))
(defun my/generate-current-time-string (&optional universal-arg silent)
"Generate a string representing the current date and time in specific format.
(e.g., 230725192607 for July 25th, 2023 at 19:26:07).
When UNIVERSAL-ARG (C-u) is provided, copy the time string to the kill ring.
Usage:
M-x my/generate-current-time-string
C-u M-x my/generate-current-time-string
Version 2023-07-25"
(interactive "P")
(let* ((now (current-time))
(time-string (concat (substring (format-time-string "%Y" now) -2)
(format-time-string "%m%d%H%M%S" now))))
(unless silent
(insert time-string))
(when universal-arg
(kill-new time-string)
(message "%s is copied." time-string))
(message "Current time string generated: %s" time-string)
time-string))
(defun my/review-random-function ()
"Review a random function defined in my Emacs configuration."
(interactive)
(let* ((config-functions '())
(config-files (directory-files-recursively user-emacs-directory "\\.el$")))
(dolist (file config-files)
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(while (re-search-forward "(defun \\([^ ]+\\)" nil t)
(push (match-string 1) config-functions))))
(let* ((command (nth (random (length config-functions)) config-functions)))
(describe-function (intern command)))))
(defun my/review-random-my-function ()
"Review a random function that starts with 'my/' in my Emacs configuration."
(interactive)
(let* ((config-functions '())
(config-files (directory-files-recursively user-emacs-directory "\\.el$")))
(dolist (file config-files)
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(while (re-search-forward "(defun my/\\([^ ]+\\)" nil t)
(push (match-string 1) config-functions))))
(let* ((command (nth (random (length config-functions)) config-functions)))
(describe-function (intern (concat "my/" command))))))
;; {{ START: my/open-link-at-point-as-gpg
(defun my/securely-delete-file (&optional filename)
"Securely delete the specified file interactively or by providing FILENAME.
If secure deletion failed, then continue with the normal deletion."
(interactive (list (when current-prefix-arg
(read-file-name "Choose file to securely delete: "))))
(if filename
(progn
(message "Securely deleting %s..." (shell-quote-argument filename))
(cond
((eq system-type 'windows-nt)
;; https://learn.microsoft.com/en-us/sysinternals/downloads/sdelete
(my-check-for-executable "SDelete" "sdelete")
(shell-command (concat "sdelete -p 3 " (shell-quote-argument filename))))
((eq system-type 'gnu/linux)
(my-check-for-executable "shred" "shred")
(shell-command (concat "shred -v -z -u -n 10 " (shell-quote-argument filename))))
((eq system-type 'darwin)
(my-check-for-executable "shred" "gshred")
(shell-command (concat "gshred -v -z -u -n 10 " (shell-quote-argument filename)))))
(when (file-exists-p (shell-quote-argument filename))
(message "Securely deleting %s failed, and continue with the normal deletion." (shell-quote-argument filename))
(delete-file filename)))
(user-error "No file specified for secure deletion.")))
(defun my/open-link-at-point-as-gpg ()
"Open the link at point using Emacs epa in a temporary buffer,
and the decrypted file will be securely deleted after opening in buffer."
(interactive)
(require 'epa)
(let* ((link-info (org-element-context))
(path (org-element-property :path link-info))
(abs-path (if (string-prefix-p "file:" path)
(file-truename (replace-regexp-in-string ":" "" path))
(file-truename path)))
(decrypted-file (concat abs-path ".clear")))
(if (file-exists-p abs-path)
(progn
(epa-decrypt-file abs-path decrypted-file)
(find-file decrypted-file)
(when (file-exists-p decrypted-file)
(my/securely-delete-file decrypted-file)))
(message "File does not exist: %s" abs-path))))
;; END: my/open-link-at-point-as-gpg }}
;; {{ START: my/check-orphaned-org-ids-in-directory
(defun my-org-id-link-pre ()
"The precondition config to my org id link settings"
(my-require 'org-element) ; this should be here before `org-add-link-type'
(my-require 'cl-lib)
;; From ChatGPT,
;; The message "Created id link." is printed by the `org-add-link-type` function
;; each time it is called.
;; Since you have the line `(org-add-link-type "id" #'my-org-id-link-follow)` in
;; your code, this function is called every time you load or reload your Emacs
;; configuration. It registers a new link type called `"id"` that is handled by
;; the `my-org-id-link-follow` function.
;; register new link type called "id"
(org-add-link-type "id" #'my-org-id-link-follow))
(defun my-org-id-link-follow (id)
"Follow an `id' link."
(message "Link ID: %s" id))
(defun my-org-id-links-in-buffer ()
"Return a list of Org ID links in the current buffer."
(my-org-id-link-pre)
(let (org-id-links) ; creates a local variable called `org-id-links` with an
; initial value of `nil` that is only visible within the
; `let` block
(org-element-map (org-element-parse-buffer) 'link
(lambda (link)
(when (string= (org-element-property :type link) "id")
(push (org-element-property :path link) org-id-links)
)))
org-id-links))
(defun my-list-org-id-links-in-directory (directory)
"Search all .org files in DIRECTORY for Org ID links, and return a list of unique IDs found."
(interactive "DDirectory: ")
(let (org-ids)
(dolist (file (directory-files-recursively directory "\\.org$") org-ids)
(with-temp-buffer
(insert-file-contents file)
(setq org-ids (append org-ids (my-org-id-links-in-buffer)))
))
(delete-dups org-ids)
))
(defun my-list-org-ids-in-directory (directory)
"List all org-ids in org-files in the given DIRECTORY and return them as a list."
(interactive "DDirectory: ")
(my-org-id-link-pre)
(let ((org-files (directory-files-recursively directory "\\.org$"))
(org-ids '()))
(dolist (file org-files)
(with-temp-buffer
(insert-file-contents file)
(org-mode)
(org-element-map (org-element-parse-buffer) 'headline
(lambda (headline)
(when-let ((id (org-element-property :ID headline)))
(push id org-ids))))
(goto-char (point-min))
(while (re-search-forward "^:ID:\\s-+\\(\\S-+\\)" nil t)
(push (match-string 1) org-ids))))
org-ids))
(defun my/check-orphaned-org-ids-in-directory (dir)
"Find the difference between org-ids obtained by `my-list-org-ids-in-directory'
and org-ids obtained by `my-list-org-id-links-in-directory'.
DIRECTORY is the directory where the org files are located."
(interactive "DDirectory: ")
(let ((org-ids (my-list-org-ids-in-directory dir))
(id-links (my-list-org-id-links-in-directory dir)))
(let ((not-linked (cl-set-difference org-ids id-links :test #'string=))
(invalid-links (cl-set-difference id-links org-ids :test #'string=)))
(message "%d not-linked org-ids: %s"
(length not-linked)
(format "%s" not-linked))
(message "%d invalid org-id links: %s"
(length invalid-links)
(format "%s" invalid-links)))))
;; END: my/check-orphaned-org-ids-in-directory }}
(defun my/org-list-entries-without-id-property ()
"List all entries in the current buffer that don't have an ID property."
(interactive)
(with-output-to-temp-buffer "*Org Entries Without ID*"
(let ((results nil))
(org-map-entries
(lambda ()
(unless (org-id-get)
(push (format "** LINE #%d:\n%s"
(line-number-at-pos)
(buffer-substring-no-properties
(line-beginning-position)
(line-end-position)))
results)))
nil nil t)
(princ (concat "#+TITLE: Org Entries Without ID\n\n"))
(princ (concat "#+OPTIONS: toc:nil\n\n"))
(princ (concat "* Entries without ID\n\n"))
(dolist (result (nreverse results))
(princ (concat result "\n\n")))))
(with-current-buffer "*Org Entries Without ID*"
(org-mode)))
(defun my/list-packages-and-versions ()
(interactive)
(package-initialize)
(let ((pkgs (mapcar 'car package-alist)))
(dolist (pkg pkgs)
(message "%s - %s"
pkg (package-desc-version (cadr (assq pkg package-alist)))))))
(defun my/copy-org-id-at-point ()
"Copy the ID property of the heading at point to the kill-ring."
(interactive)
(let ((id (org-entry-get nil "ID")))
(when id
(kill-new id)
(message "Copied ID: %s" id))))
(defun my-get-heading-from-org-id-db (org-id)
"Retrieve the heading title associated with an Org ID from the
current buffer's Org mode database."
(org-with-point-at (org-id-find org-id 'marker)
(org-get-heading)))
(defun my/insert-org-id-from-kill-ring ()
"Insert a link to an Org ID from the kill-ring with a user-defined description.
The user is prompted to enter a description for the link.
If description is empty, retrieve the heading from the org-id
database using `my-get-heading-from-org-id-db` function."
(interactive)
(let ((id (current-kill 0)))
(when id
(let* ((org-id (replace-regexp-in-string "^id:" "" id))
(description (read-string "Description: " nil 'my-history)))
(if (string-empty-p description)
(setq description (my-get-heading-from-org-id-db org-id)))
(org-insert-link nil (concat "id:" org-id) description)))))
(defun my/link-selected-text-with-org-id-from-kill-ring ()
"Create an Org-mode link using the selected text and an Org ID from the kill ring.
Version 2023-04-28
The selected text is replaced with,
[[id:<Org ID unique identifier>][<selected text>]].
Usage: Select the text that you want to link to an Org ID, then
run `M-x my/link-selected-text-with-org-id-from-kill-ring`. The
function will take the Org ID from the kill ring, and create an
Org-mode link with the selected text and the Org ID. The link
will be inserted at the cursor position, replacing the selected
text."
(interactive)
(let* ((org-id (substring-no-properties (current-kill 0)))
(text (buffer-substring-no-properties (region-beginning) (region-end)))
(link (concat "[[id:" org-id "][" text "]]")))
(delete-region (region-beginning) (region-end))
(insert link)))
(defun my-parse-link-id (link)
"Parse the ID from an org-mode link of the form `id:xxxxxxxxxxxx'."
(when (string-match "id:\\(.+\\)" link)
(match-string 1 link)))
(defun my/org-link-goto-at-point ()
"Check if link at point is a file link or an ID link, and jump to
the appropriate location."
(interactive)
(if-let ((link (org-element-property :raw-link (org-element-context))))
(cond ((string-prefix-p "file:" link)
(org-open-at-point))
((string-prefix-p "id:" link)
(org-id-goto (my-parse-link-id link))))
(message "No link at point.")))
(defun my/switch-opened-org-files-to-org-mode ()
"Switch all open buffers that end with .org to org-mode,
skipping buffers that are already in org-mode.
Version 2023-05-06"
;; See, https://stackoverflow.com/a/76187210/4274775
(interactive)
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (and (buffer-file-name)
(string= (file-name-extension (buffer-file-name)) "org")
(not (eq major-mode 'org-mode)))
(org-mode)
(message "Switched %s to org-mode." (buffer-name))))))
(defun my/strikethrough-current-line ()
"Strikethrough the current line using +<striked text>+"
(interactive)
(back-to-indentation)
(insert "+")
(move-end-of-line nil)
;; skips over any consecutive space or tab characters immediately before the
;; end of the line, effectively moving the cursor to the last non-blank
;; character on the line, rather than after any trailing whitespace. see,
(skip-chars-backward " \t")
(insert "+"))
(defun my/readonly-files ()
"Check for a '.readonly' file in the directory of the current
buffer, and set the read-only status of any listed buffers. The
'.readonly' file should contain a list of buffer names, one per
line, that should be set to read-only. Any buffers not listed in
the file will remain unaffected.
Version 2023-05-04
This function is intended to be used as a hook to automatically
set the read-only status of buffers when they are opened or
saved, based on the contents of the '.readonly' file. To use this
function as a hook, add it to the appropriate hook list, such as
'find-file-hook', 'after-save-hook' or 'switch-buffer-hook'."
;; (add-hook 'find-file-hook 'my/readonly-files)
;; (add-hook 'after-save-hook 'my/readonly-files)
;; (add-hook 'switch-buffer-hook 'my/readonly-files)
(interactive)
(let ((readonly-file (concat (file-name-directory (buffer-file-name)) ".readonly")))
(when (file-exists-p readonly-file)
(let ((readonly-bufs (split-string (with-temp-buffer
(insert-file-contents readonly-file)
(buffer-string))
"\n" t)))
(message "read-only files list: %s" readonly-bufs)
(dolist (buf readonly-bufs)
(message "%s is read-only now" buf)
(let ((buf (find-buffer-visiting buf)))
(when buf
(with-current-buffer buf
(toggle-read-only t)))))))))
(defun my/revert-all-file-buffers ()
"Refresh all open file buffers without confirmation.
Buffers in modified (not yet saved) state in emacs will not be reverted. They
will be reverted though if they were modified outside emacs.
Buffers visiting files which do not exist any more or are no longer readable
will be killed."
;; via https://emacs.stackexchange.com/a/24461/29715
(interactive)
(dolist (buf (buffer-list))
(let ((filename (buffer-file-name buf)))
;; Revert only buffers containing files, which are not modified;
;; do not try to revert non-file buffers like *Messages*.
(when (and filename
(not (buffer-modified-p buf)))
(if (file-readable-p filename)
;; If the file exists and is readable, revert the buffer.
(with-current-buffer buf
(revert-buffer :ignore-auto :noconfirm :preserve-modes))
;; Otherwise, kill the buffer.
(let (kill-buffer-query-functions) ; No query done when killing buffer
(kill-buffer buf)
(message "Killed non-existing/unreadable file buffer: %s" filename))))))
(message "Finished reverting buffers containing unmodified files."))
(defun my/copy-current-buffer-to-another-buffer (target-buffer)
"Copy the content of the current buffer to another buffer.
If the target buffer does not exist, it will be created.
If the target buffer exists, the content will be appended.
Version: 2023-08-31"
(interactive "BTarget Buffer: ")
(let ((source-buffer (current-buffer))
(existing-buffer (get-buffer-create target-buffer)))
(with-current-buffer existing-buffer
(goto-char (point-max)) ; move to the end of the existing buffer
(insert-buffer-substring source-buffer)
(pop-to-buffer existing-buffer))))
(defun my/kill-buffers-by-pattern (pattern)
"Kill buffers whose names match the specified pattern.
This function interactively prompts the user for a pattern and then searches
through the list of all buffers. Buffers whose names match the given pattern
are killed, effectively closing them. The pattern is a regular expression that
is compared against buffer names using 'string-match-p'.
Version: 2023-08-16"
(interactive "sEnter a pattern: ")
(dolist (buffer (buffer-list))
(let ((buffer-name (buffer-name buffer)))
(message "Processing buffer: %s" buffer-name)
(when (string-match-p pattern buffer-name)
(kill-buffer buffer)
(message "Killed buffer '%s'" buffer-name)))))
(provide 'init-utils)
;; Local Variables:
;; coding: utf-8
;; End:
;;; init-utils.el ends here