gists/gists.org

1177 lines
45 KiB
Org Mode
Raw Permalink Normal View History

#+title: Gists by bram85
#+PROPERTY: header-args :mkdirp yes
#+begin_src org :tangle README.org
Gists referred from [[https://emacs.ch/@bram85][@bram85@emacs.ch]].
2022-11-24 07:52:25 +00:00
2023-02-13 21:43:28 +00:00
Unfortunately Forgejo has no gist support and there's no mature yet lightweight gist solution suitable for self-hosting, so resorting to this poor-man's setup of sharing gists with an audience.
2023-02-14 22:04:00 +00:00
Drop me a DM on Mastodon in case of comments.
#+end_src
2022-11-25 08:06:37 +00:00
* Termux keys :termux:emacs:
2022-11-23 16:18:43 +00:00
:PROPERTIES:
:URL: https://emacs.ch/@bram85/109393570150138285
:END:
Put this in ~/.termux/termux.properties for a better Emacs experience inside Termux.
#+begin_src conf :tangle gists/termux-emacs-keys.conf
extra-keys = [[ \
{key: ESC, popup: {macro: "CTRL g", display: "C-g"}}, \
{key: CTRL, popup: {macro: "CTRL x", display: "C-x"}}, \
{key: ALT, popup: {macro: "ALT x", display: "M-x"}}, \
{key: TAB}, \
{key: DEL, popup: {macro: "CTRL k", display: C-k}}, \
{key: LEFT, popup: HOME}, \
{key: DOWN, popup: PGDN}, \
{key: UP, popup: PGUP}, \
{key: RIGHT, popup: END} \
]]
#+end_src
2022-11-23 14:17:36 +00:00
2022-11-25 08:06:37 +00:00
* Nesting with use-package :emacs:usepackage:
2022-11-23 16:18:43 +00:00
:PROPERTIES:
:URL: https://emacs.ch/@bram85/109393551314878399
:END:
2022-11-23 14:17:36 +00:00
#+begin_src elisp :tangle gists/use-package-nesting.el
2022-11-24 07:52:40 +00:00
;; Demonstrate that use-package allows for nesting configurations,
;; although it is done semantically rather than syntactically. The
;; :after keyword makes sure that the 'bar' package is only evaluated
;; after the 'foo' package was loaded.
2022-11-23 14:17:36 +00:00
(use-package foo)
;; only loads after foo has been loaded
(use-package bar
:after foo)
#+end_src
2022-11-24 07:54:39 +00:00
2022-11-25 08:06:37 +00:00
* Track usage of Emacs packages over time :emacs:org:orgbabel:
2022-11-26 07:56:47 +00:00
:PROPERTIES:
:URL: https://emacs.ch/@bram85/109403483724552863
:END:
Using the idea of evaluating code on state changes [[id:3be8333e-7a47-4251-8ee4-2cba0ec4614b][below]].
2022-11-25 08:06:37 +00:00
#+begin_src org :tangle gists/track-use-package-over-time.org
,* TODO Update Emacs package count
2023-02-15 07:13:14 +00:00
DEADLINE: <2023-03-15 Wed .+1m>
2022-11-26 06:58:45 +00:00
:PROPERTIES:
2023-02-15 07:13:14 +00:00
:LAST_REPEAT: [2023-02-15 Wed 08:12]
:ON_DONE: (org-babel-execute-subtree)
2022-11-26 06:58:45 +00:00
:END:
:LOGBOOK:
2023-02-15 07:13:14 +00:00
- State "DONE" from "TODO" [2023-02-15 Wed 08:12]
2022-11-26 06:58:45 +00:00
- State "DONE" from "TODO" [2022-11-26 Sat 07:57]
:END:
2022-12-04 10:01:35 +00:00
A small org file to keep track of some metric in a table, see [[https://orgmode.org/manual/Results-of-Evaluation.html][Results of Evaluation]] in the Org mode manual for more information. It's important that the function is supposed to return a list in order to record the result as a table row.
In this case, count the number of packages in my Emacs init file. It is executed automatically when the task is marked as done, using [[https://apps.bram85.nl/gitea/bram/gists/src/branch/main/gists/evaluate-code-on-task-state-change.org][this function]].
Press =C-c C-c= inside the code block to add a new entry to the table below.
2022-11-26 07:09:02 +00:00
,#+begin_src elisp :results table append
(with-temp-buffer
(insert-file-contents user-init-file)
2022-12-04 10:02:02 +00:00
(list (format-time-string "%F")
(count-matches (rx (seq "(" (? "elpaca-") "use-package" space)))))
2022-11-26 07:09:02 +00:00
,#+end_src
,#+RESULTS:
| 2022-11-25 | 140 |
2022-11-26 07:09:02 +00:00
| 2022-11-26 | 140 |
2023-02-15 07:13:14 +00:00
| 2023-02-15 | 138 |
2022-11-26 07:09:02 +00:00
#+end_src
2022-11-26 05:54:58 +00:00
* vertico-repeat setup :emacs:vertico:
2022-11-26 07:56:47 +00:00
:PROPERTIES:
:URL: https://emacs.ch/@bram85/109408577100294769
:END:
2022-11-26 05:54:58 +00:00
My vertico-repeat setup.
#+begin_src elisp :tangle gists/vertico-repeat.el
(use-package vertico-repeat
:straight (vertico-repeat :host github :repo "emacs-straight/vertico" :files ("extensions/vertico-repeat.el"))
:after (vertico savehist)
:bind
("M-g r" . vertico-repeat-select)
:config
(add-to-list 'savehist-additional-variables 'vertico-repeat-history)
:hook
(minibuffer-setup . vertico-repeat-save))
#+end_src
2022-11-26 20:06:16 +00:00
* Evaluate code on task state changes :emacs:org:
:PROPERTIES:
:ID: 3be8333e-7a47-4251-8ee4-2cba0ec4614b
:END:
2022-11-26 20:06:16 +00:00
#+begin_src org :tangle gists/evaluate-code-on-task-state-change.org
The function below evaluates Lisp forms stored in properties of a task, when it changes to a certain state. The function is supposed to be added to the =org-after-todo-state-change-hook=.
2022-11-26 20:06:16 +00:00
The property should be named =ON_<STATE>= where =STATE= is a state defined in =org-todo-keywords=. See also the example task below (open this file in [[https://apps.bram85.nl/gitea/bram/gists/raw/branch/main/gists/evaluate-code-on-task-state-change.org][raw mode]] to see the properties).
,#+begin_src elisp
(defun my/task-state-event-handler ()
2022-11-26 20:06:16 +00:00
"Evaluate a Lisp form attached to the task whose state is being changed.
When this function is added to org-after-todo-state-change-hook,
it looks for a Lisp form stored in the property called ON_<STATE>
where STATE is the new state of the todo item. When the state is
cleared, ON_CLEAR will be used.
Example:
,,* TODO Example task
:PROPERTIES:
:ON_PROGRESS: (message \"Busy!\")
:ON_DONE: (message \"Done!\")
:ON_CLEAR: (message \"No state.\")
:END:"
(when-let* ((state (or org-state "CLEAR"))
(event-property-name (concat "ON_" state))
2022-11-26 16:42:07 +00:00
(code-string (cdr (assoc event-property-name
(org-entry-properties))))
(code (car (read-from-string code-string))))
(org-eval code)))
2023-04-08 08:13:17 +00:00
(add-hook 'org-after-todo-state-change-hook #'my/task-state-event-handler)
,#+end_src
,* TODO Example task
:PROPERTIES:
:ON_PROGRESS: (message "Busy!")
:ON_DONE: (message "Done!")
:ON_CLEAR: (message "No state.")
:END:
#+end_src
* Emacs commenting: do what I actually mean :emacs:
#+begin_src elisp :tangle gists/emacs-comments-do-what-i-actually-mean.el
;; Replace comment-dwim (M-;) with a modified version of
;; comment-or-uncomment-region. The modification is done by the crux
2022-11-27 21:38:39 +00:00
;; [1] package, which acts on a region if active otherwise on the
;; current line.
;;
;; My personal preference goes to using straight.el-powered
;; use-package forms, even for built-in modules.
2022-11-27 21:38:39 +00:00
;;
;; [1] https://github.com/bbatsov/crux
(use-package newcomment
:straight (:type built-in)
:bind
([remap comment-dwim] . #'comment-or-uncomment-region))
(use-package crux
:config
(crux-with-region-or-line comment-or-uncomment-region))
#+end_src
2022-12-05 11:00:18 +00:00
* Use xr for more readable regular expressions :emacs:
#+begin_src org :tangle gists/xr-for-readable-regular-expressions.org
If you look for the value of =org-link-any-re=, you'll see this beauty:
,#+begin_example
"\\(\\[\\[\\(\\(?:[^][\\]\\|\\\\\\(?:\\\\\\\\\\)*[][]\\|\\\\+[^][]\\)+\\)]\\(?:\\[\\([^z-a]+?\\)]\\)?]\\)\\|\\(<\\(bibtex\\|elisp\\|f\\(?:ile\\(?:\\+\\(?:\\(?:emac\\|sy\\)s\\)\\)?\\|tp\\)\\|h\\(?:elp\\|ttps?\\)\\|id\\|mai\\(?:lto\\|rix\\)\\|news\\|org-ql-search\\|shell\\):\\([^>
]*\\(?:
[ ]*[^>
][^>
]*\\)*\\)>\\)\\|\\(\\(?:\\<\\(?:\\(bibtex\\|elisp\\|f\\(?:ile\\(?:\\+\\(?:\\(?:emac\\|sy\\)s\\)\\)?\\|tp\\)\\|h\\(?:elp\\|ttps?\\)\\|id\\|mai\\(?:lto\\|rix\\)\\|news\\|org-ql-search\\|shell\\)\\):\\(\\(?:[^][
()<>]\\|(\\(?:[^][
()<>]\\|([^][
()<>]*)\\)*)\\)+\\(?:[^[:punct:]
]\\|/\\|(\\(?:[^][
()<>]\\|([^][
()<>]*)\\)*)\\)\\)\\)\\)"
,#+end_example
[[https://github.com/mattiase/xr][xr]] to the rescue:
,#+begin_src elisp
(pp-to-string (xr org-link-any-re))
,#+end_src
,#+RESULTS:
,#+begin_example
(or
(group "[["
(group
(one-or-more
(or
(not
(any "[\\]"))
(seq "\\"
(zero-or-more "\\\\")
(any "[]"))
(seq
(one-or-more "\\")
(not
(any "[]"))))))
"]"
(opt "["
(group
(+\? anything))
"]")
"]")
(group "<"
(group
(or "bibtex" "elisp"
(seq "f"
(or
(seq "ile"
(opt "+"
(or "emac" "sy")
"s"))
"tp"))
(seq "h"
(or "elp"
(seq "ttp"
(opt "s"))))
"id"
(seq "mai"
(or "lto" "rix"))
"news" "org-ql-search" "shell"))
":"
(group
(zero-or-more
(not
(any "\n>")))
(zero-or-more "\n"
(zero-or-more
(any " "))
(not
(any " \n >"))
(zero-or-more
(not
(any "\n>")))))
">")
(group bow
(group
(or "bibtex" "elisp"
(seq "f"
(or
(seq "ile"
(opt "+"
(or "emac" "sy")
"s"))
"tp"))
(seq "h"
(or "elp"
(seq "ttp"
(opt "s"))))
"id"
(seq "mai"
(or "lto" "rix"))
"news" "org-ql-search" "shell"))
":"
(group
(one-or-more
(or
(not
(any " \n ()<>[]"))
(seq "("
(zero-or-more
(or
(not
(any " \n ()<>[]"))
(seq "("
(zero-or-more
(not
(any " \n ()<>[]")))
")")))
")")))
(or
(not
(any " \n " punct))
"/"
(seq "("
(zero-or-more
(or
(not
(any " \n ()<>[]"))
(seq "("
(zero-or-more
(not
(any " \n ()<>[]")))
")")))
")")))))
,#+end_example
2023-01-01 06:52:09 +00:00
#+end_src
2023-02-13 21:45:02 +00:00
* Define a lambda in a let expression :elisp:emacs:
#+begin_src elisp :tangle gists/let-lambda.el
2022-12-13 17:41:51 +00:00
;; https://stackoverflow.com/questions/36039840/elisp-bind-a-lambda-in-a-let-and-execute-it
(let ((f (lambda (s) (message s))))
;; f is a variable so should be treated as such.
(funcall f "This works.")
;; f is not a function so cannot be found if called like this.
(f "This does not work."))
2022-12-05 11:00:18 +00:00
#+end_src
* Use outline-minor-mode with eshell :emacs:eshell:
Found [[https://www.reddit.com/r/emacs/comments/e2u5n9/comment/f918t22/][a Reddit comment]] suggesting to assign the prompt regexp to the outline regexp. Here's the corresponding =use-package= syntax:
#+begin_src elisp :tangle gists/outline-minor-mode-eshell.el
;; use C-c @ C-t to get a foldable shell history
(use-package eshell
:config
(add-hook 'eshell-mode-hook
(lambda () (setq-local outline-regexp eshell-prompt-regexp)))
:hook
(eshell-mode . outline-minor-mode))
#+end_src
2023-02-13 21:45:16 +00:00
* Automatically reformat source blocks in Org Mode :org:orgbabel:emacs:
#+begin_src elisp :tangle gists/format-org-mode-source-blocks.el
;; https://github.com/lassik/emacs-format-all-the-code
(use-package format-all)
(use-package org
:config
(defun my/format-all-advice ()
(ignore-errors ; in case there's no language support
(call-interactively #'format-all-buffer)))
(advice-add #'org-edit-src-exit :before #'my/format-all-advice))
#+end_src
2023-02-13 21:45:02 +00:00
* Paredit inside minibuffer commands :emacs:
2023-01-05 13:07:10 +00:00
Used [[https://github.com/purcell/emacs.d/blob/master/lisp/init-paredit.el][Purcell's configation]] as a starting point.
2023-02-13 21:45:16 +00:00
It turns out it didn't work as well as I hoped, paredit steals the RET binding so a newline is inserted in the minibuffer, instead of submitting the input.
2023-01-05 13:07:10 +00:00
#+name: minibuffer-paredit
#+begin_src elisp :tangle gists/minibuffer-paredit.el
(use-package paredit
:hook
(minibuffer-setup . my/conditionally-enable-paredit-mode)
(minibuffer-exit . my/restore-paredit-key)
:config
(defvar my/paredit-minibuffer-commands '(eval-expression
pp-eval-expression
eval-expression-with-eldoc
ibuffer-do-eval
ibuffer-do-view-and-eval
org-ql-sparse-tree
org-ql-search)
"Interactive commands for which paredit should be enabled in the minibuffer.")
(defun my/conditionally-enable-paredit-mode ()
"Enable paredit during lisp-related minibuffer commands."
(when (memq this-command my/paredit-minibuffer-commands)
(enable-paredit-mode)
(unbind-key (kbd "RET") paredit-mode-map)))
(defun my/restore-paredit-key ()
"Restore the RET binding that was disabled by
my/conditionally-enable-paredit-mode."
(bind-key (kbd "RET") #'paredit-newline paredit-mode-map)))
#+end_src
2023-02-13 21:45:02 +00:00
* Store revision of Emacs configuration in a constant :emacs:
#+begin_src elisp :tangle gists/store-configuration-revision-in-constant.el
;; Store the Git revision of your Emacs configuration at the moment
;; Emacs started.
(defconst my/has-git-p (if (executable-find "git") t nil)
"Indicates if git is in your PATH.")
(defun my/configuration-revision ()
"Retrieve the current Git revision of the Emacs configuration."
(when my/has-git-p
(with-temp-buffer
(cd user-emacs-directory)
(when (eql 0 (call-process "git" nil (current-buffer) nil
"describe" "--all" "--long" "--dirty"))
(string-trim (buffer-string))))))
(defconst my/configuration-revision
(my/configuration-revision)
"Contains the Git revision of the configuration at the moment Emacs started.")
#+end_src
2023-01-05 13:07:10 +00:00
* Use Tempel to insert links in Org-Mode :emacs:tempel:org:
#+begin_src elisp :tangle gists/tempel-org-links.el
;;; Introduction
;;
;; In my Org notes I often refer to terms like JIRA tickets or some
;; queries to an (internal) search engine. Doing it natively with Org
;; mode is a bit involved:
;;
;; 1. org-insert-link
;; 2. Type link, e.g. "JIRA:MYPRJ-1234"
;; 3. Type the visible description again: "MYPRJ-1234".
;;
;; The repetition is a bit cumbersome and a template to insert the
;; link would be more convenient. And sometimes I already typed the
;; ticket number and I want to turn it into a link quickly.
;;
;; The code below adds a element which can be used in Tempel templates
;; to insert [[org:links][links]]. It works with and without active
;; region. If no region is active, a term is prompted in the
;; minibuffer.
;;
;; The following template:
;;
;; (jira (ol "JIRA"))
;;
;; results in [[JIRA:ticket][ticket]] where ticket is prompted or
;; taken from the region.
;;
;; This relies on having link types configured in your
;; `org-link-abbrev-alist', the link type accepts one parameter
;; matching a link type.
;;
;;; Requirements / why na(t)ive templates don't work
;;
;; The template has to meet the following requirements:
;;
;; - type the search term / ID only once
;; - or, use the region instead, if active
2023-02-13 21:36:55 +00:00
;; - no visible prefixes (link abbreviations)
;;
;; We need to store the region before the templating kicks
;; in, because the region can be used only once in a template. For
;; example, this doesn't work:
;;
;; (jira "[[JIRA:" r "][" r "]]")
;;
;; (and it's a region-only template, so another template would be
;; needed for a prompt).
;;
;; Also, a prompting variant doesn't work due to a bug in Org mode
;; which makes the element cache trip:
;;
;; (jira "[[JIRA:" (p "Ticket: " ticket) "][" (s ticket) "]]")
;;
;; Unfortunately, this not-so-elegant code is needed to meet the
;; requirements.
2023-02-26 21:43:09 +00:00
(defvar my/region nil
"Stores the region's content.
So it can be inserted multiple times from a template.")
(defun my/tempel-org-link (elt)
"Tempel field to insert an org link."
(when (eq (car-safe elt) 'ol)
(let ((link-type (cadr elt)))
(if (and (boundp 'my/region) my/region)
;; consume region with r and use the stored region
;; afterwards, as r can be used only once
`(l "[[" ,link-type ":" r "][" ,my/region "]]")
;; NOINSERT is used for the prompt because the org-mode
;; element cache doesn't handle simultaneous edits well. So
;; prompt in minibuffer and insert the result twice.
`(l "[[" ,link-type ":" (p "Term: " term t) term "][" term "]]")))))
(defun my/store-region (f &rest args)
"Store the region in the my/region variable."
(setq my/region (when (use-region-p)
(buffer-substring (region-beginning) (region-end))))
(apply f args)
(setq my/region nil))
(add-to-list 'tempel-user-elements #'my/tempel-org-link)
(advice-add #'tempel-insert :around #'my/store-region)
#+end_src
* dwim-shell-command to encrypt files with age :emacs:age:dwim_shell_command:
#+begin_src elisp :tangle gists/dwim-shell-command-encrypt-with-age.el
(defconst my/cygwin-p (string-equal system-type "cygwin"))
(defconst my/age-pubkey "ageXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
(defun my/dwim-shell-command/convert-path-cygwin (template unix-path)
"Convert a Unix path to a Windows path.
This can be used as a :post-process-template of a
dwim-shell-command-on-marked-files function, where <<f>>
templates expand to Unix-like paths. However, a non-Cygwin binary
does not understand Unix paths so use cygpath to convert it."
(if my/cygwin-p
(let* ((command (format "cygpath -w %s" unix-path))
(cygwin-path (string-trim (shell-command-to-string command)))
(escaped-cygwin-path (string-replace "\\" "\\\\" cygwin-path)))
(string-replace unix-path escaped-cygwin-path template))
;; just return the template as-is
template))
(defun my/dwim-shell-command/age-encrypt ()
"Encrypt marked files with the age encryption tool."
(interactive)
(dwim-shell-command-on-marked-files "Age Encypt"
(format "age -e -r %s -o <<f>>.age <<f>>" my/age-pubkey)
:utils "age"
:post-process-template #'my/dwim-shell-command/convert-path-cygwin))
#+end_src
* dwim-shell-command to upload files to 0x0 :emacs:dwim_shell_command:
2023-05-09 09:34:40 +00:00
An improved version was [[https://github.com/xenodium/dwim-shell-command/commit/1a896221cc34319582b0921b919638ea2528b0e6][added]] to dwim-shell-commands.
#+begin_src elisp :tangle gists/dwim-0x0-upload.el
(defun my/dwim-shell-command/0x0-upload ()
"Upload the marked files to 0x0.st"
(interactive)
(let ((url "https://0x0.st"))
(dwim-shell-command-on-marked-files
"0x0 upload"
(format "curl -Ffile=@<<f>> -Fsecret= %s" url)
:utils "curl"
:post-process-template
;; Insert the single quotes at the appropriate place according to
;; 0x0.st example online:
;; curl -F'file=@yourfile.png' -Fsecret= https://0x0.st
;;
;; The placement of these single quotes confuse the escaping
;; mechanisms of dwim-shell-command, as it considers @ as the
;; opening 'quote' as it appears right in front of <<f>>.
(lambda (template path)
(string-replace "-Ffile" "-F'file"
(string-replace path (concat path "'") template))))))
#+end_src
2023-05-21 11:09:02 +00:00
* Apply maybe :elisp:
#+begin_src elisp :tangle gists/apply-maybe.el
(defun my/apply-maybe (f probability &rest args)
"Apply function F with a certain PROBABILITY [0-1)."
(if (< (random 100) (* probability 100))
(apply f args)
'my/not-applied))
#+end_src
2023-05-21 11:09:02 +00:00
* Enable prism-whitespace-mode for XML and YAML :emacs:
#+begin_src elisp :tangle gists/prism-xml-yaml.el
;; Enable prism-whitespace-mode for YAML and XML automatically. Detect
2023-06-03 15:20:29 +00:00
;; the amount of whitespace from the respective mode.
(use-package prism
:config
(add-to-list 'prism-whitespace-mode-indents '(yaml-mode . yaml-indent-offset))
(add-to-list 'prism-whitespace-mode-indents '(nxml-mode . nxml-child-indent))
:hook
(yaml-mode . prism-whitespace-mode)
(nxml-mode . prism-whitespace-mode))
#+end_src
2023-06-02 19:46:17 +00:00
* Execute a command repeatedly :emacs:
#+begin_src elisp :tangle gists/execute-command-repeatedly.el
(defun my/read-list (&optional f stop-pred)
"Return a list by repeatedly requesting input using function F.
By default, F is `read-string', and should be a function that
takes a prompt as its first argument. The collection of input
continues until STOP-PRED returns t on the last input value, by
default the empty string."
(named-let read-element ((result nil)
(counter 1)
(f (or f #'read-string))
(stop-pred (or stop-pred #'string-empty-p)))
(let ((e nil))
(setq e (funcall f (format "Read element %d: " counter)))
(if (funcall stop-pred e)
result
(read-element (append result (list e)) (1+ counter) f stop-pred)))))
(defun my/execute-command-repeat (command &rest arguments)
"Execute COMMAND repeatedly on all the given ARGUMENTS.
COMMAND is an interactive function that takes a single argument.
Arguments are collected one by one with my/read-list and then
COMMAND is executed (length arguments) times, once for each
value."
(interactive (append (list (read-command "Command: "))
(my/read-list)))
(mapc (lambda (arg)
2023-07-18 11:07:15 +00:00
(with-demoted-errors
2023-06-02 19:46:17 +00:00
(funcall command arg)))
arguments))
#+end_src
2023-06-26 09:52:33 +00:00
* Get list of URLs for active packages :emacs:
#+begin_src elisp
(string-join (mapcar (lambda (pkg)
(cdr (assoc :url (package-desc-extras (cadr pkg)))))
package-alist)
"\n")
#+end_src
* Run code when opening a file with a certain name :emacs:
Based on an answer at the [[https://emacs.stackexchange.com/a/77480/34645][Emacs StackExchange]], to have some code executed for files that have no specific extension or mode set. For example, to have =yaml-mode= enabled when opening a /.clang-format/ file:
#+begin_src elisp :tangle gists/find-file-hook-for-certain-filenames.el
(setq my/setup-functions-alist '((".clang-format" . yaml-mode)))
(defun my/file-setup ()
(when-let* ((filename (file-name-nondirectory (buffer-file-name)))
(f (map-elt my/setup-functions-alist filename)))
(funcall f)))
(add-hook 'find-file-hook #'my/file-setup)
#+end_src
2023-07-25 19:33:49 +00:00
* Dabbrev case :emacs:
#+begin_src org
,* dabbrev-case-replace
- nil : Replace expansion's case pattern.
- *case-replace*: Preserve if =case-replace= is nil.
- else : Modify by applying abbreviation's case pattern.
,** case-replace
- nil: Don't preserve case in replacements.
- *t*: Preserve case in replacements.
,* dabbrev-case-distinction
- nil: Treat expansions the same if they differ in case.
2023-07-25 19:33:49 +00:00
- *case-replace*: Distinguish if =case-replace= is nil.
- else: Treat them the same.
2023-07-25 19:33:49 +00:00
,* dabbrev-case-fold-search
- nil: Case is significant.
- *case-fold-search*: Significant if =case-fold-search= is nil.
- else: Case is not significant.
,** case-fold-search
- nil: Searches and matches don't ignore case.
- *t*: Searches and matches should ignore case.
,* dabbrev-upcase-means-case-search
- *nil*: Case fold search when searching for possible expansions.
- else: Non-nil means case sensitive search.
#+end_src
* Restrict symbol / command completion to a major mode :emacs:
#+begin_src elisp :tangle gists/restrict-command-to-major-mode.el
(defun my/buffer-has-major-mode-p (major-mode _sym buffer)
"Return t if BUFFER has MAJOR-MODE set."
(eq (buffer-local-value 'major-mode buffer) major-mode))
(defun my/restrict-symbol (mode symbols)
"Restrict SYMBOLS to a certain major MODE.
Ideally, packages should restrict their own `interactive'
commands to their own mode (see the `interactive' help). However,
this is not common practice so this little facility makes it
easier to restrict symbols to a certain mode. Meaning, the
command will not appear in the M-x menu as a possible completion.
This may be handy if a command may be destructive for a major
mode it wasn't meant for.
Example:
(my/restrict-symbol 'ledger-mode '(ledger-occur))"
(dolist (sym symbols)
(put sym 'completion-predicate
(apply-partially #'my/buffer-has-major-mode-p mode))))
#+end_src
* Magit: show diff of current buffer since a given date/time :emacs:magit:
#+begin_src elisp :tangle gists/magit-diff-range-current-buffer.el
(defun my/magit-diff-since (&optional since)
"Shows the diff for the current file SINCE the given date/time."
(interactive "sSince (default 24 hours ago): ")
(let* ((revisionA (format "HEAD@{%s}" (or (and (not (string-empty-p since)) since) "24 hours ago")))
(revisionB "{worktree}")
(range (string-join (list revisionA ".." revisionB))))
(magit-diff-range range () (list (buffer-file-name)))))
#+end_src
* Narrow from/to point :emacs:
#+begin_src elisp :tangle gists/narrow-from-to-point.el
(defun my/narrow-from-point ()
"Narrow the buffer from point to end of the (narrowed) buffer."
(interactive)
(narrow-to-region (point) (point-max)))
(defun my/narrow-to-point ()
"Narrow the buffer the start of a (narrowed) buffer up to point."
(interactive)
(narrow-to-region (point-min) (point)))
#+end_src
* Create note with Denote from a URL :emacs:
#+begin_src elisp :tangle gists/denote-create-note-url.el
2024-01-15 21:14:46 +00:00
;; Watch https://asciinema.org/a/631451 for a demo
2024-01-03 18:45:22 +00:00
(defconst my/termux-p (getenv "ANDROID_ROOT"))
(defun my/clipboard-get ()
"Return the text on the system clipboard.
This function treats Termux systems differently, the clipboard is only
accessible through the termux-clipboard-get commandline interface,
part of the the termux-api package."
(if my/termux-p
(shell-command-to-string "termux-clipboard-get")
(current-kill 0)))
(defun my/get-url-title (url)
"Attempt to retrieve the title string from the given URL.
2024-01-02 14:23:45 +00:00
Assuming the URL points to an HTML source.
2024-01-02 14:23:45 +00:00
Returns nil if there is a non-200 return status or no title could
be found."
(let* ((command (format "curl --fail --silent %s" url))
(html (shell-command-to-string command))
(regexp (rx (seq "<title>"
(group (+ (not (any "<" ">"))))
"</title>")))
2024-01-02 14:23:45 +00:00
(matches (string-match regexp html)))
(match-string 1 html)))
(defun my/denote/url-clipboard ()
"Return the URL from the system clipboard, if any."
(let ((clipboard (my/clipboard-get)))
(when (org-url-p clipboard)
clipboard)))
(defvar my/denote/url-functions
'(thing-at-point-url-at-point my/denote/url-clipboard)
"List of function symbols to call to get an URL candidate.
Each function should return a string with the URL or a cons
cell (URL . TITLE), where title is either a string or a function
returning a string.")
2024-01-02 14:23:45 +00:00
(defun my/denote/url (url &optional title)
"Create a new Org-based note based on a URL.
URL can be a string or a cons cell (URL . TITLE). The TITLE, in
turn, can be a string or a function (without arguments) to
retrieve the title.
When called interactively, the candidate URLs are obtained from
the variable `my/denote/url-functions' (e.g. takes the URL from
the clipboard).
In case no TTTLE is passed to this function, or the URL wasn't
paired with a title value, the title is obtained by curl(1) by
looking at the <title> tags."
(interactive (list
(let* ((prompt (format-prompt "URL" ""))
(candidate-urls (-non-nil (mapcar #'funcall my/denote/url-functions)))
(url (if (eql 1 (length candidate-urls))
(read-string prompt (caar candidate-urls))
(completing-read prompt candidate-urls))))
;; `candidate-urls' is a mix of strings and cons
;; cells. If the selected URL comes from a cons
;; cell, (assoc) will return it. If it comes from a
;; string valuo, (assoc) will return nil. In that
;; case return the URL as is.
(or (assoc url candidate-urls #'string=) url))
2024-01-02 14:23:45 +00:00
nil))
(denote
(read-string (format-prompt "Title" "")
;; initial input. If no title was passed, see if it
;; can be obtained from the URL value (the cdr if the
;; url was a cons cell.
(or
title
(cond ((and (consp url) (stringp (cdr url))) (cdr url))
((and (consp url) (functionp (cdr url))) (funcall (cdr url)))
((stringp url) (my/get-url-title url)))))
2024-01-02 14:23:45 +00:00
(denote-keywords-prompt)
'org
(denote-subdirectory-prompt))
2024-01-03 19:33:12 +00:00
(org-set-property "URL" (if (consp url)
(car url)
2024-01-15 16:49:19 +00:00
url))
(goto-char (point-max))
;; Requires kagi.el at https://codeberg.org/bram85/kagi.el
(require 'kagi)
(when (yes-or-no-p "Insert summary?")
(kagi-summarize-url url t
(completing-read
(format-prompt "To language" "EN")
'("EN" "NL")))))
2024-01-03 18:45:22 +00:00
;;; Elfeed integration
(defun my/elfeed/entry-url ()
"Return the URL of the current elfeed entry."
(when-let ((entry (or elfeed-show-entry
(elfeed-search-selected :ignore-region))))
(cons (elfeed-entry-link entry)
(elfeed-entry-title entry))))
(add-to-list 'my/denote/url-functions #'my/elfeed/entry-url)
#+end_src
2024-02-09 18:58:48 +00:00
* Using gpg-agent inside Emacs in Termux :emacs:termux:
2023-12-14 20:39:36 +00:00
:PROPERTIES:
:URL: https://emacs.ch/@bram85/111580555195721041
:END:
Getting =gpg-agent= to work properly inside #termux and have it properly accessed from #emacs is a bit tricky.
The first issue is Android related: by default the agent will spawn as a top level process. This makes the process prone to be randomly killed by Android for memory management purposes, causing you to enter your passphrase more often than you may have configured.
Since I always run Emacs anyway, I chose to execute it from the Emacs init file, and passing a shell for the =--daemon= flag. Then it becomes a child process and won't be killed at random. It occupies a hidden buffer /*gpg-agent*/.
#+begin_src elisp
(defconst my/termux-p (getenv "ANDROID_ROOT"))
(when my/termux-p
(start-process "gpg-agent" " *gpg-agent*" "gpg-agent" "--daemon" "/bin/sh"))
#+end_src
The second issue is 'knowing' the correct TTY such that =pinentry= shows up correctly inside Emacs (using =(setq epg-pinentry-mode 'loopback)=).
Outside Emacs, =pinentry= shows up in the right place because the GPG manual dictates to have your $GPG_TTY set to the output of the =tty= command, preferably from your shell initialization.
Inside Emacs, the correct TTY may change: run =tty= inside =eshell= and it may output /dev/pts/1. Open another real terminal, go back to =eshell= and run =tty= again: /dev/pts/2. So commands inside =eshell=, such as =gpg=, =ssh= and =git= cannot rely on a fixed $GPG_TTY that was set when starting Emacs. With the wrong value, the loopback pinentry fails and no passphrase is prompted from the minibuffer. Instead, the terminal that displays Emacs gets messed up.
One could fix it with the following inside =eshell=:
#+begin_src sh
tty
(setenv "GPG_TTY" "/dev/pts/2")
gpg-connect-agent updatestartuptty /bye
#+end_src
Which needs to be executed every time you're about to run something that might trigger a =pinentry= (including remote operations with Magit or TRAMP).
These steps can be performed from various hooks such that any subprocess gets the proper $GPG_TTY to which Emacs responds.
First a function to retrieve the =tty= output /synchronously/. If we don't wait, a =ssh= subprocess may have been spawned in the meantime with an outdated/incorrect $GPG_TTY.
#+begin_src elisp
(defun my/get-pty ()
(with-temp-buffer
(let* ((process-connection-type t) ; force PTY allocation
(proc (start-process "tty" (current-buffer) "tty")))
(while (process-live-p proc)
(accept-process-output proc 0.01 nil t)) ; wait for process to terminate
(car (string-lines (buffer-string)))))) ; return process output
#+end_src
And then a function we can use for hooks to actually update $GPG_TTY and make sure that SSH uses the correct display for a possible passphrase prompt.
#+begin_src elisp
(defun my/hook/set-gpg-tty ()
(setenv "GPG_TTY" (my/get-pty))
(call-process "gpg-connect-agent" nil nil nil "updatestartuptty" "/bye"))
#+end_src
Finally, I attached this hook in three places:
#+begin_src elisp
(add-hook 'find-file-hook #'my/hook/set-gpg-tty) ; for TRAMP
(add-hook 'magit-pre-start-git-hook #'my/hook/set-gpg-tty)
(add-hook 'eshell-pre-command-hook #'my/hook/set-gpg-tty 0 t)
#+end_src
Which covers my (potential) GPG/SSH usage within Emacs. Now, anytime a I perform a GPG / SSH operation, the $GPG_TTY variable is set properly and if needed, the passhprase prompt shows up in the minibuffer.
2024-02-09 19:02:47 +00:00
* Generate and show QR for the region or minibuffer input :emacs:
#+begin_src elisp :tangle gists/generate-qr.el
(defun my/generate-qr (text &optional img)
"Generate a QR code from the region or given TEXT.
If no region is active, the TEXT defaults to the clipboard content.
When IMG is non-nil, generate an image instead of the default
UTF-8 representation.
This function relies on qrencode(1) being present in your $PATH."
(interactive
(let ((default-text (if (use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end))
(my/clipboard-get))))
(list
(read-string (format-prompt "Text" default-text) nil nil default-text)
current-prefix-arg)))
(with-current-buffer (get-buffer-create "*qr*")
;; in case the buffer still exists and image-mode is active
(fundamental-mode)
2024-02-15 20:27:23 +00:00
(read-only-mode -1)
2024-02-09 19:02:47 +00:00
(erase-buffer)
(insert text)
(let ((file-format
(if (equal current-prefix-arg '(4)) "PNG" "UTF8")))
(call-process-region nil nil "qrencode" t t nil "-o" "-" "-t" file-format)
(when (and (display-graphic-p) (string= file-format "PNG"))
(image-mode)))
2024-02-15 20:27:23 +00:00
(read-only-mode)
2024-02-09 19:02:47 +00:00
(display-buffer (current-buffer))))
#+end_src
2024-04-29 20:37:16 +00:00
* elfeed configuration :emacs:
#+begin_src elisp :tangle gists/elfeed.el
2024-04-29 20:55:43 +00:00
;; For constructing relative date strings from timestamps.
2024-04-29 20:37:16 +00:00
;; Clone from https://github.com/rougier/relative-date
(use-package relative-date
:ensure nil
:load-path "lisp/relative-date"
:config
(defun bram85-fix-relative-date-in-future (f &rest args)
"Fix future relative dates, as they are returned with a +-
prefix given the `relative-dates-formats' below. Because the
internal data is negative, it shows up as such when replacing
placeholders."
(replace-regexp-in-string "\\`+-" "+" (apply f args)))
(advice-add 'relative-date :around #'bram85-fix-relative-date-in-future)
(setq relative-date-formats
(let* ((seconds 1)
(minutes (* 60 seconds))
(hours (* 60 minutes))
(days (* 24 hours))
(months (* 30 days))
(years (* 365 days)))
`((, (* 3 minutes) . "now") ;; Less than 3 minutes (past)
(,(- (* 3 minutes)) . "soon") ;; Less than 3 minutes (future)
(, (* 1 hours) . "%(M)m") ;; Less than 1 hour
(,(- (* 1 hours)) . "+%(M)m") ;; Less than 1 hour in the future
(, (* 24 hours) . "%(H)h") ;; Less than 24 hours
(,(- (* 24 hours)) . "+%(H)h") ;; Less than 24 hours in the future
(, (* 7 days) . "%(d)d") ;; Less than 7 days
(,(- (* 7 days)) . "+%(d)d") ;; Less than 7 days in the future
(, (* 1 months) . "%(w)w") ;; Less than 30 days
(,(- (* 1 months)) . "+%(w)w") ;; Less than 30 days in the future
(, (* 365 days) . "%(m)mo") ;; Less than a year
(,(- (* 365 days)) . "+%(m)mo") ;; Less than a year in the future
(, (* 1000 years) . "%(y)y") ;; Less than 1000 years
(,(- (* 100 years)) . "+%(y)y") ;; Less than 100 years in the future (to fit in 4 chars)
))))
(use-package elfeed
:after relative-date
:init
(defun bram85-elfeed-show-eww-open (&optional use-generic-p)
"open with eww"
(interactive "P")
(let ((browse-url-browser-function #'eww-browse-url))
(elfeed-show-visit use-generic-p)))
(defun bram85-elfeed-search-eww-open (&optional use-generic-p)
"open with eww"
(interactive "P")
(let ((browse-url-browser-function #'eww-browse-url))
(elfeed-search-browse-url use-generic-p)))
;; The following functions is for easy navigation, taken from
;; Karthinks blog: https://karthinks.com/software/lazy-elfeed/
(defun bram85-elfeed-scroll-up-command (&optional arg)
2024-04-29 20:55:43 +00:00
"Scroll up or go to the next feed item in elfeed."
2024-04-29 20:37:16 +00:00
(interactive "^P")
(let ((scroll-error-top-bottom nil))
(condition-case-unless-debug nil
(scroll-up-command arg)
(error (elfeed-show-next)))))
(defun bram85-elfeed-scroll-down-command (&optional arg)
2024-04-29 20:55:43 +00:00
"Scroll down or go to the previous feed item in elfeed."
2024-04-29 20:37:16 +00:00
(interactive "^P")
(let ((scroll-error-top-bottom nil))
(condition-case-unless-debug nil
(scroll-down-command arg)
(error (elfeed-show-prev)))))
:bind
(("C-x w" . elfeed)
:map elfeed-show-mode-map
("w" . elfeed-show-visit)
("W" . bram85-elfeed-show-eww-open)
("SPC" . bram85-elfeed-scroll-up-command)
("S-SPC" . bram85-elfeed-scroll-down-command)
:map elfeed-search-mode-map
("w" . elfeed-search-browse-url)
("W" . bram85-elfeed-search-eww-open))
:config
(setq-default elfeed-search-filter "@3-days-ago +unread")
(setq elfeed-feeds '(
;; ...
))
;; enable relative dates which are max 4 chars wide
(setq elfeed-search-date-format '("rel" 4 :right))
(defvar bram85-elfeed-hidden-tags
'(unread
highvol
lowvol
linkonly)
"List of tags which do not get printed.")
(defun bram85-elfeed-tags (entry)
"Return a list of tags as strings for the given ENTRY, where
hidden tags are filtered out."
(mapcar #'symbol-name
(seq-difference (elfeed-entry-tags entry) bram85-elfeed-hidden-tags)))
(defun bram85-elfeed-search-print-entry (entry)
2024-04-29 20:55:43 +00:00
"Print the elfeed ENTRY to the buffer."
2024-04-29 20:37:16 +00:00
(let ((format-time-string-orig (symbol-function #'format-time-string)))
;; Make sure that elfeed-search-format-date calls relative-date
;; instead of format-time-string, if the format is "rel".
(cl-letf (((symbol-function #'format-time-string)
(lambda (fmt &optional time zone)
(if (string= fmt "rel")
(relative-date time)
;; For all other formats, call the builtin function.
(funcall format-time-string-orig fmt time zone)))))
(let* ((date (elfeed-search-format-date (elfeed-entry-date entry)))
(title (or (elfeed-meta entry :title) (elfeed-entry-title entry) ""))
(title-faces (elfeed-search--faces (elfeed-entry-tags entry)))
(feed (elfeed-entry-feed entry))
(feed-title
(when feed
(or (elfeed-meta feed :title) (elfeed-feed-title feed))))
(tags (bram85-elfeed-tags entry))
(tags-str (propertize (concat ":" (string-join tags ":") ":")
'face 'elfeed-search-tag-face))
(title-width (- (window-width) 10 elfeed-search-trailing-width))
(title-column (elfeed-format-column
title (elfeed-clamp
elfeed-search-title-min-width
title-width
elfeed-search-title-max-width)
:left)))
;; relative date
(insert (propertize date 'face 'elfeed-search-date-face) " ")
;; title
(insert (propertize title-column 'face title-faces 'kbd-help title) " ")
;; feed title
(when feed-title
(insert (propertize feed-title 'face 'elfeed-search-feed-face) " "))
;; tags
(when tags
(insert tags-str))))))
;; install the print function
(setq elfeed-search-print-entry-function #'bram85-elfeed-search-print-entry)
(setq elfeed-search-trailing-width 20)
;; record read date when an entry is opened
(eval-when-compile
(require 'elfeed))
(defun bram85-elfeed--mark-read (entry read-date)
"Set the READ-DATE for the given ENTRY."
(unless (elfeed-tagged-p 'unread entry)
(setf (elfeed-meta entry :read-date) read-date)))
(defun bram85-elfeed-mark-read (entries tags)
"Mark the ENTRIES as read and record the read date with some delay.
2024-04-29 20:55:43 +00:00
It is considered as marked read if the 'unread' tag is part
of the TAGS to be removed from the ENTRIES."
2024-04-29 20:37:16 +00:00
(when (member 'unread tags)
(let* ((date (format-time-string "%FT%T%z"))
(entry-unread-p (lambda (e) (elfeed-tagged-p 'unread e)))
(filtered-entries (seq-filter entry-unread-p entries)))
(dolist (entry filtered-entries)
(run-with-timer 5 nil #'bram85-elfeed--mark-read entry date)))))
(defun bram85-elfeed-mark-as-unread (entries tags)
"Reset the read date when ENTRIES are marked as read again.
It is considered marked unread if the 'unread' tag is part of
TAGS to be added to ENTRIES."
(when (member 'unread tags)
(dolist (entry entries)
(setf (elfeed-meta entry :read-date) nil))))
(add-hook 'elfeed-untag-hooks 'bram85-elfeed-mark-read)
(add-hook 'elfeed-tag-hooks 'bram85-elfeed-mark-as-unread)
;; Fix bug in Elfeed where the history variable
;; elfeed-search-filter-history remains unused.
(defun bram85-elfeed-advice-elfeed-search-live-filter (f &rest args)
(let ((read-from-minibuffer-orig (symbol-function #'read-from-minibuffer)))
(cl-letf (((symbol-function #'read-from-minibuffer)
(lambda (prompt init-value)
(funcall read-from-minibuffer-orig prompt init-value nil nil 'elfeed-search-filter-history))))
(apply f args))))
(advice-add #'elfeed-search-live-filter :around #'bram85-elfeed-advice-elfeed-search-live-filter)
(add-hook 'elfeed-update-hooks (lambda (_) (when (zerop (elfeed-queue-count-total)) (elfeed-db-save))))
;; update elfeed every two hours, only when idle
(run-with-timer nil (* 2 60 60) (lambda () (run-with-idle-timer 120 nil 'elfeed-update))))
#+end_src
2024-02-09 19:02:47 +00:00
2022-11-26 07:51:28 +00:00
* Meta
** License
#+begin_src org :tangle LICENSE.txt
MIT License
Copyright (c) 2022 Bram Schoenmakers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#+end_src
** COMMENT Local variables
2022-11-24 07:54:39 +00:00
Auto tangle this file on save.
Local variables:
eval: (add-hook 'after-save-hook #'org-babel-tangle 0 t)
End: