Categories: geek » emacs

2021-02-15 Emacs news

| emacs, emacs-news

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, planet.emacslife.com, YouTube, the Emacs NEWS file and emacs-devel.

View or add comments

2021-02-08 Emacs news

| emacs, emacs-news

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, planet.emacslife.com, YouTube, the Emacs NEWS file and emacs-devel.

View or add comments

Guest post: Bookmarking PDFs in Emacs with pdf-tools and registers

Posted: - Modified: | emacs

Someone wanted to share this post, so here it is!

-—

I read a lot of PDF documents on Emacs. I use the Emacs package pdf-tools for this. I also write a lot of LaTeX on Emacs (both in org-mode notes and .tex files). Frequently I find myself in the following situation: I need to jump between sections in documents to refer to definitions, before I resume my reading where I was. The natural Emacs keybindings for this are C-x r SPACE r (point-to-register) and C-x r j r (jump-to-register), which I found by searching online. I could not memorize them, so I decided to write my own keybindings that do the task. I read up on the documentation of these functions using C-h f, describing their function names and looking at the docstring. I realized that they receive an argument which is the “register name” and Emacs does the magic thing, the following two elisp lines ended up in my .emacs (after going through the usual phase where I look up online “how to set a keybinding on Emacs”, because I never remember these things):

;; Make C-f1 and C-f2 save at point & jump to region
;; useful when going back-and-forth between definitions in a file
;; subsequent code is to make this work for pdf-tools as well.
(global-set-key (kbd "<C-f1>") (lambda () (interactive) (point-to-register ?r)))
(global-set-key (kbd "<C-f2>") (lambda () (interactive) (jump-to-register ?r)))

I tested it with some text files and it worked wonderfully. However, it did not work on pdf-tools! There’s something special about the way that package treats files that is not compatible with Emacs registers. I looked around online for solutions, and found this github issue. I skimmed through with my usual impatience and ignorant attitude that only cares for the solution. Someone there mentioned the package saveplace-pdf-view, which I looked up on github. In its source code, I was able to locate the functions pdf-view-bookmark-make-record and pdf-view-bookmark-jump, which (thank goodness!) do what I need. I obtained this:

(define-key pdf-view-mode-map (kbd "<C-f1>")
  (lambda ()
    "Saves the current position on the pdf to jump to later with <C-f2>."
    (interactive)
    (setf my-bookmark (pdf-view-bookmark-make-record))))

(define-key pdf-view-mode-map (kbd "<C-f2>")
  (lambda ()
    "Loads the position saved by <C-f1>."
    (interactive)
    (pdf-view-bookmark-jump my-bookmark)))

It works wonderfully, just like the text case, thanks to the folks who designed these functions (they remember the exact position, not just page). However, they only allow me to save one point at a time. It would be nice to be able to save multiple points, so that I can jump around various places on the document, definitions I often visit, other places of interest. So what I needed was a way to save in a variable all these bookmarks generated by pdf-view-bookmark-make-record, and then be able to look them up using a name. Anticipating all this, I wrote the following:

(defvar my-bookmarks nil
  "List of bookmarks, useful for pdf-mode where I save my positions with <C-f1> etc.")

(defconst my-default-bookmark ?1
  "This is the default bookmark name")

(define-key pdf-view-mode-map (kbd "<C-f1>")
  (lambda ()
    "Saves the current position on the pdf to jump to later with <C-f2>."
    (interactive)
    (setf (alist-get my-default-bookmark my-bookmarks) (pdf-view-bookmark-make-record))))

(define-key pdf-view-mode-map (kbd "<C-f2>")
  (lambda ()
    "Loads the position saved by <C-f1>."
    (interactive)
    (pdf-view-bookmark-jump (alist-get my-default-bookmark my-bookmarks))))

That’s a typical use of an associative list, a data structure that Emacs is fond of. I looked up online ways to handle associative lists (or alists) in elisp. I wanted to emulate the behavior of point-to-register, which requires the register name (as a single character). Incidentally, single characters in elisp are denoted by ?a, ?b, ?c, etc. I found a nice Stack Exchange post that explained how to set and get values from alists. It works like this:

;;              ↓This is the key↓   ↓This is the alist↓
(setf (alist-get my-bookmark-name     my-bookmarks)
      my-value) ;; ← This is the value (bookmark data)

If you are not familiar with setf, it asks for a place to store something to, and alist-get points to the particular entry of your alist that you ask for.

The last ingredient that I needed was to figure out a way to emulate the behavior of C-x r SPACE, which shows in the minibuffer “Point to register:” and waits for input. I tried reading the documentation of point-to-register to understand how this is done, but I couldn’t figure it out. I had a vague idea it is possible with (interactive) so I looked up some examples. It wasn’t easy to figure out, but at the end I realized that the “Code characters for interactive” are just some prefix codes in the string provided to interactive that tell it how to supply its caller with input from Emacs.

To ensure I had the right idea, I tested:

(global-set-key (kbd "<C-f5>")
  (lambda (c) (interactive "cTest: ") (message "Read: %c" c)))

Pressing <C-f5> and entering "a", sure enough prints "Read: a", which is great. All I need to do is ask for the bookmark name to save to or load from. I decided to use the neighboring keybindings <C-f3> and <C-f4> for those purposes, still retaining <C-f1> and <C-f2> for a quick lookup.

I was ready to complete the functionality for pdf-tools,

(defvar my-bookmarks nil
  "List of bookmarks, useful for pdf-mode where I save my positions with <C-f1> etc.")

(defconst my-default-bookmark ?1
  "This is the default bookmark name")

(defun my-save-pdf-position (&optional b)
  "Saves the current PDF position of pdf-tools at a bookmark named B."
  (unless b (setq b my-default-bookmark))
  (setf (alist-get b my-bookmarks)
  (pdf-view-bookmark-make-record)))

(defun my-load-pdf-position (&optional b)
  "Loads the PDF position saved at the bookmark named B."
  (unless b (setq b my-default-bookmark))
  (pdf-view-bookmark-jump (alist-get b my-bookmarks)))

(define-key pdf-view-mode-map (kbd "<C-f1>")
  (lambda ()
    (interactive)
    (my-save-pdf-position)))

(define-key pdf-view-mode-map (kbd "<C-f2>")
  (lambda ()
    (interactive)
    (my-load-pdf-position)))

(define-key pdf-view-mode-map (kbd "<C-f3>")
  (lambda (b) (interactive "cSaving to bookmark name (single character): ")
    (my-save-pdf-position b)))

(define-key pdf-view-mode-map (kbd "<C-f4>")
  (lambda (b) (interactive "cLoading from bookmark name (single character): ")
    (my-load-pdf-position b)))

Now I could just complement it with the text functionality

(global-set-key (kbd "<C-f1>") (lambda () (interactive) (point-to-register my-default-bookmark)))
(global-set-key (kbd "<C-f2>") (lambda () (interactive) (jump-to-register my-default-bookmark)))
(global-set-key (kbd "<C-f3>") (lambda (r) (interactive "cSaving to register: ") (point-to-register r)))
(global-set-key (kbd "<C-f4>") (lambda (r) (interactive "cLoading from register: ") (jump-to-register r)))

I also have to thank the folks over at freenode’s #emacs channel for helping me with this (and many other things over the years), and Sacha Chua in particular for encouraging me to write this blog post. So, big thank you!

The whole code, together with the comments, is below

;; Make <C-f1> and <C-f2> save at point & jump to region.
;; Useful when going back-and-forth between definitions in a file.
;; The code below makes this work for pdf-tools as well.
;;
;; You can use <C-f3> and <C-f4> to have more save and load slots.
;; They are named by single characters, i.e. try
;; <C-f3> 5
;; to save to slot 5 (you can use a letter as well)
;; <C-f4> 5
;; to load from slot 5. The default slot name is 1.

(defvar my-bookmarks nil
  "List of bookmarks, useful for pdf-mode where I save my positions with <C-f1> etc.")

(defconst my-default-bookmark ?1
  "This is the default bookmark name")

(defun my-save-pdf-position (&optional b)
  "Saves the current PDF position of pdf-tools at a bookmark named B."
  (unless b (setq b my-default-bookmark))
  (setf (alist-get b my-bookmarks)
  (pdf-view-bookmark-make-record)))

(defun my-load-pdf-position (&optional b)
  "Loads the PDF position saved at the bookmark named B."
  (unless b (setq b my-default-bookmark))
  (pdf-view-bookmark-jump (alist-get b my-bookmarks)))

(define-key pdf-view-mode-map (kbd "<C-f1>") 
  (lambda ()
    (interactive)
    (my-save-pdf-position)))

(define-key pdf-view-mode-map (kbd "<C-f2>")
  (lambda ()
    (interactive)
    (my-load-pdf-position)))

(define-key pdf-view-mode-map (kbd "<C-f3>")
  (lambda (b) (interactive "cSaving to bookmark name (single character): ")
    (my-save-pdf-position b)))

(define-key pdf-view-mode-map (kbd "<C-f4>")
  (lambda (b) (interactive "cLoading from bookmark name (single character): ")
    (my-load-pdf-position b)))

(global-set-key (kbd "<C-f1>") (lambda () (interactive) (point-to-register my-default-bookmark)))
(global-set-key (kbd "<C-f2>") (lambda () (interactive) (jump-to-register my-default-bookmark)))
(global-set-key (kbd "<C-f3>") (lambda (r) (interactive "cSaving to register: ") (point-to-register r)))
(global-set-key (kbd "<C-f4>") (lambda (r) (interactive "cLoading from register: ") (jump-to-register r)))

-–— Note from Sacha:

You can bind interactive functions without using a lambda, like this:

(global-set-key (kbd "<C-f3>") #'point-to-register)

For interactive functions that sometimes prompt for arguments and sometimes don’t, I’ve seen people use the prefix argument like this:

(interactive (list (if current-prefix-arg ... "default argument")))

You can distinguish multiple uses of the prefix argument by checking the value of current-prefix-arg.

Alternatively, sometimes people define an interactive function that doesn’t have arguments, and they have that function call the other interactive function that has arguments.

Also, if you’re trying the keyboard shortcuts in this and you’re wondering why Ctrl F1 doesn’t seem to work for you (like it didn’t work for me), check if your window manager is using the shortcuts for something else. I’m on KDE, so I needed to use Global Shortcuts to remove the KWin keyboard shortcuts for changing to desktop 1, 2, 3, and 4.

Let me know if you have comments or feedback and I can pass them along. If you want to share a tip about Emacs and don’t have a place to put it, feel free to send it to me too. Enjoy!

View or add comments

Controlling my stream audio from Emacs: background music, typing sounds, and push to talk

Posted: - Modified: | emacs

Update: 2021-02-11: Parsed pacmd list-sources so that I can mute/unmute devices by regular expression. Update: 2021-02-07: Made it work with my USB microphone.

I was experimenting with streaming Emacs geeking around on twitch.tv. Someone asked me to have soft background music and typing sounds. Since I’m a little clueless about music and don’t want to bother with hunting down nice royalty-free music, I figured I could just use the Mozart dice game to programmatically generate music.

I installed the mozart-dice-game NPM package and used this bit of Javascript to generate a hundred MIDI files.

const x = require('mozart-dice-game')
for (let i = 0; i < 100; i++) { x.saveMinuet('minuet' + String(i).padStart('3', '0') + '.mid'); }

Then I wrote this Emacs Lisp function to turn it on and off.

(defvar my/background-music-process nil "Process for playing background music")
(defun my/stream-toggle-background-music (&optional enable)
  (interactive)
  (if (or my/background-music-process
          (and (numberp enable) (< enable 0)))
      (progn
        (when (process-live-p my/background-music-process)
          (kill-process my/background-music-process))
        (setq my/background-music-process nil))
    (let ((files (directory-files "~/code/music" t "mid\\'")))
      (setq my/background-music-process
            (apply
             'start-process
             "*Music*"
             nil
             (append (list "timidity" "-idlr" "--volume=10") files))))))

People also suggested typing sounds. I guess that’s a good way to get a sense of activity. The default selectric sound was a little too loud for me, so we’ll use the move sound for now. It would be nice to make this more random-sounding someday.

(defun my/selectric-type-sound ()
  "Make the sound of typing."
  ;; Someday, randomize this or something
  (selectric-make-sound (expand-file-name "selectric-move.wav" selectric-files-path)))

(use-package selectric-mode
  :diminish ""
  :config
  (fset #'selectric-type-sound #'my/selectric-type-sound))

I was having a hard time remembering to go back on mute during meetings, since the LED on the mute button wasn’t working at the time and the system tray icon was a little hard to notice. The LED has mysteriously decided to start working again, but push-to-talk is handy anyway. I want to be able to tap a key to toggle my microphone on and off, and hold it down in order to make it push-to-talk. It looks like my key repeat is less than 0.5 seconds, so I can set a timer that will turn things off after a little while. This code doesn’t pick up any changes that happen outside Emacs, but it’ll do for now. I used pacmd list-sources to list the sources and get the IDs.

(defun my/pacmd-set-device (regexp status)
  (with-current-buffer (get-buffer-create "*pacmd*")
    (erase-buffer)
    (shell-command "pacmd list-sources" (current-buffer))
    (goto-char (point-max))
    (let (results)
      (while (re-search-backward regexp nil t)
        (when (re-search-backward "index: \\([[:digit:]]+\\)" nil t)
          (setq results (cons (match-string 1) results))
          (shell-command-to-string (format "pacmd set-source-mute %s %d"
                                           (match-string 1)
                                           (if (equal status 'on) 0 1)))))
      results)))

(defvar my/mic-p nil "Non-nil means microphone is on")
(add-to-list 'mode-line-front-space '(:eval (if my/mic-p "*MIC*" "")))

(defun my/mic-off ()
  (interactive)
  (my/pacmd-set-device "Yeti" 'off)
  (my/pacmd-set-device "Internal Microphone" 'off)
  (setq my/mic-p nil))
(defun my/mic-on ()
  (interactive)
  (my/pacmd-set-device "Yeti" 'on)
  (my/pacmd-set-device "Internal Microphone" 'on)
  (setq my/mic-p t))
(defun my/mic-toggle ()
  (interactive)
  (if my/mic-p (my/mic-off) (my/mic-on)))

(defvar my/push-to-talk-mute-timer nil "Timer to mute things again.")
(defvar my/push-to-talk-last-time nil "Last time my/push-to-talk was run")
(defvar my/push-to-talk-threshold 0.5 "Number of seconds")

(defun my/push-to-talk-mute ()
  (interactive)
  (message "Muting.")
  (my/mic-off)
  (force-mode-line-update)
  (my/obs-websocket-add-subtitle (my/obs-websocket-stream-time-msecs) "[Microphone off]"))

(defun my/push-to-talk ()
  "Tap to toggle microphone on and off, or repeat the command to make it push to talk."
  (interactive)
  (cond
   ((null my/mic-p) ;; It's off, so turn it on
    (when (timerp my/push-to-talk-mute-timer)
      (cancel-timer my/push-to-talk-mute-timer))
    (my/mic-on)
    (my/obs-websocket-add-subtitle (my/obs-websocket-stream-time-msecs) "[Microphone on]")
    (setq my/push-to-talk-last-time (current-time)))
   ((timerp my/push-to-talk-mute-timer) ;; Push-to-talk mode
    (cancel-timer my/push-to-talk-mute-timer)
    (setq my/push-to-talk-mute-timer
          (run-at-time my/push-to-talk-threshold nil #'my/push-to-talk-mute)))
   ;; Might be push to talk, if we're within the key repeating time
   ((< (- (time-to-seconds (current-time)) (time-to-seconds my/push-to-talk-last-time)) 
       my/push-to-talk-threshold)
    (setq my/push-to-talk-mute-timer
          (run-at-time my/push-to-talk-threshold nil #'my/push-to-talk-mute)))
   ;; It's been a while since I turned the mic on.
   (t (my/push-to-talk-mute))))

(global-set-key (kbd "<f12>") #'my/push-to-talk)
View or add comments

Marginalia and hiding the value of password-ish variables

Posted: - Modified: | emacs

I like the way Marginalia adds annotations to minibuffer completion. I’m experimenting with streaming, so I’m trying to not leak passwords while playing around with marginalia. (I’ll probably mess up at some point. Please be nice! =) )

This is the annotator function:

(defun my/marginalia-annotate-variable (cand)
  "Annotate variable CAND with its documentation string."
  (when-let (sym (intern-soft cand))
    (marginalia--fields
     ((marginalia--symbol-class sym) :face 'marginalia-modified)
     ((let ((print-escape-newlines t)
            (print-escape-control-characters t)
            (print-escape-multibyte t))
        (prin1-to-string
         (cond
          ((string-match "pass" cand) "*******")
          ((boundp sym) (symbol-value sym))
          (t 'unbound))))
      :truncate (/ marginalia-truncate-width 3) :face 'marginalia-variable)
     ((documentation-property sym 'variable-documentation)
      :truncate marginalia-truncate-width :face 'marginalia-documentation))))

Something like the following code adds it to my annotator functions. The actual code I evaluate is in my use-package marginalia declaration in my Emacs config.

(setq marginalia-annotators
      '(marginalia-annotators-heavy
        marginalia-annotators-light))
(add-to-list 'marginalia-annotators-heavy
             '(symbol . my/marginalia-annotate-function-with-args))
View or add comments

2021-02-01 Emacs news

Posted: - Modified: | emacs, emacs-news

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, planet.emacslife.com, YouTube, the Emacs NEWS file and emacs-devel.

View or add comments

2021-01-25 Emacs news

| emacs, emacs-news

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, planet.emacslife.com, YouTube, the Emacs NEWS file and emacs-devel.

View or add comments