gists/gists.org

36 KiB

Gists by bram85

  Gists referred from [[https://emacs.ch/@bram85][@bram85@emacs.ch]].

  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.

  Drop me a DM on Mastodon in case of comments.

Termux keys   termux emacs

Put this in ~/.termux/termux.properties for a better Emacs experience inside Termux.

  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} \
  ]]

Nesting with use-package   emacs usepackage

  ;; 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.

  (use-package foo)

  ;; only loads after foo has been loaded
  (use-package bar
    :after foo)

Track usage of Emacs packages over time   emacs org orgbabel

Using the idea of evaluating code on state changes below.

  * TODO Update Emacs package count
  DEADLINE: <2023-03-15 Wed .+1m>
  :PROPERTIES:
  :LAST_REPEAT: [2023-02-15 Wed 08:12]
  :ON_DONE: (org-babel-execute-subtree)
  :END:
  :LOGBOOK:
  - State "DONE"       from "TODO"       [2023-02-15 Wed 08:12]
  - State "DONE"       from "TODO"       [2022-11-26 Sat 07:57]
  :END:

  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.

  #+begin_src elisp :results table append
    (with-temp-buffer
      (insert-file-contents user-init-file)
      (list (format-time-string "%F")
            (count-matches (rx (seq "(" (? "elpaca-") "use-package" space)))))
  #+end_src

  #+RESULTS:
  | 2022-11-25 | 140 |
  | 2022-11-26 | 140 |
  | 2023-02-15 | 138 |

vertico-repeat setup   emacs vertico

My vertico-repeat setup.

  (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))

Evaluate code on task state changes   emacs 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=.

  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 ()
      "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))
                  (code-string (cdr (assoc event-property-name
                                           (org-entry-properties))))
                  (code (car (read-from-string code-string))))
        (org-eval code)))

    (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:

Emacs commenting: do what I actually mean   emacs

  ;; Replace comment-dwim (M-;) with a modified version of
  ;; comment-or-uncomment-region. The modification is done by the crux
  ;; [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.
  ;;
  ;; [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))

Use xr for more readable regular expressions   emacs

  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

Define a lambda in a let expression   elisp emacs

  ;; 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."))

Use outline-minor-mode with eshell   emacs eshell

Found a Reddit comment suggesting to assign the prompt regexp to the outline regexp. Here's the corresponding use-package syntax:

  ;; 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))

Automatically reformat source blocks in Org Mode   org orgbabel emacs

  ;; 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))

Paredit inside minibuffer commands   emacs

Used Purcell's configation as a starting point.

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.

  (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)))

Store revision of Emacs configuration in a constant   emacs

  ;; 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.")

Use Tempel to insert links in Org-Mode   emacs tempel org

  ;;; 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
  ;; - 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.

  (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)

dwim-shell-command to encrypt files with age   emacs age dwim_shell_command

  (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))

dwim-shell-command to upload files to 0x0   emacs dwim_shell_command

An improved version was added to dwim-shell-commands.

  (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))))))

Apply maybe   elisp

  (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))

Enable prism-whitespace-mode for XML and YAML   emacs

  ;; Enable prism-whitespace-mode for YAML and XML automatically. Detect
  ;; 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))

Execute a command repeatedly   emacs

  (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)
            (with-demoted-errors
              (funcall command arg)))
          arguments))

Get list of URLs for active packages   emacs

  (string-join (mapcar (lambda (pkg)
                         (cdr (assoc :url (package-desc-extras (cadr pkg)))))
                       package-alist)
               "\n")

Run code when opening a file with a certain name   emacs

Based on an answer at the 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:

  (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)

Dabbrev case   emacs

  * 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.
  - *case-replace*: Distinguish if =case-replace= is nil.
  - else: Treat them the same.

  * 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.

Restrict symbol / command completion to a major mode   emacs

  (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))))

Magit: show diff of current buffer since a given date/time   emacs magit

  (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)))))

Narrow from/to point   emacs

  (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)))

Create note with Denote from a URL   emacs

  ;; Watch https://asciinema.org/a/631451 for a demo

  (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.

  Assuming the URL points to an HTML source.

  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>")))
           (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.")

  (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))
                  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)))))
     (denote-keywords-prompt)
     'org
     (denote-subdirectory-prompt))

    (org-set-property "URL" (if (consp url)
                                (car url)
                              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")))))

  ;;; 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)

Using gpg-agent inside Emacs in Termux   emacs termux

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.

  (defconst my/termux-p (getenv "ANDROID_ROOT"))

  (when my/termux-p
    (start-process "gpg-agent" " *gpg-agent*" "gpg-agent" "--daemon" "/bin/sh"))

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:

  tty
  (setenv "GPG_TTY" "/dev/pts/2")
  gpg-connect-agent updatestartuptty /bye

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.

  (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

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.

  (defun my/hook/set-gpg-tty ()
    (setenv "GPG_TTY" (my/get-pty))
    (call-process "gpg-connect-agent" nil nil nil "updatestartuptty" "/bye"))

Finally, I attached this hook in three places:

  (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)

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.

Generate and show QR for the region or minibuffer input   emacs

  (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)
      (read-only-mode -1)
      (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)))
      (read-only-mode)
      (display-buffer (current-buffer))))

Meta

License

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.

COMMENT Local variables

Auto tangle this file on save.

Local variables: eval: (add-hook 'after-save-hook #'org-babel-tangle 0 t) End: