Showing 397 to 400

Organizing Emacs initfiles

I haven't done an #emacs bankruptcy for half a decade now. Think I settled into a good system (famous last words…).

Briefly tried the literate "init.org" some people like. Quickly bailed out because the code changes too often and then any lengthy explanations go stale. Short crisp code-comments survive more refactors.

(If y'ain't gonna write full paragraphs, there's no need for Org-mode.)

On the other extreme, the 1-file-per-package some people like, does not seem nice to my mind either. I have 10-20 elisp files total (for 8,000 lines of code).

Tips to past-me

  • collect all your defuns into a single "my-lib.el", then all the other files are cleaner
    • That's certainly going to become your single largest initfile, yet it cannot break init since new defuns don't affect emacs state!
  • set most builtin variables in one file
  • bind most keys in one file. load it early, so when half of init breaks, you still have your keys
  • load most low-config packages in one file
    • what needs more extensive conf get a dedicated file—for me that's 1 file for org-mode, 1 file for dired, 1 for shell/term, 1 for completion systems (helm/vertico/company), 1 for exwm… that's about it.
  • have an "experiment zone" which is the file that's loaded last. then if it breaks init, the rest still works
Created (7 months ago)

cl-loop beats map-keymap

Did you ever need to make many lookups or changes in #emacs key bindings, but balked at the complexity of programming with map-keymap?

Me too. Good news, you can skip that mess! There's a keyword in cl-loop that is much more human-friendly, and it's still blisteringly fast. It's been around since 1993, but I've found no blog posts about it.

There's a gotcha I want to tell you about, I'll get to that in a moment.

First, typical usage goes like this example:

(Behold! Lisp gone ugly!)

(cl-loop
 for seq being the key-seqs of KEYMAP
 using (key-bindings cmd)
 as key = (key-description seq)
 do ...)

In the above loop, you'll get access to KEY and its bound command, CMD. And that brings me to the gotcha.

Here's what happened to me: if KEYMAP has many adjacent keys bound to the same command (which is typically either nil or self-insert-command), then (keymap-lookup KEYMAP key) will error out saying that this key is invalid. Some example values of key are SPC..~, ESC i..j, C-x (..*, \200..\377.

Yes, a string "SPC..~" is being provided as key. Can you guess what's going on?

I can't, but these seem to be ranges of keys. I think it might only happen in global-map, actually.

Filter them out with a simple (not (string-search ".." key)).

And it turns out there's a couple more more sanitizations you'll probably want.

  1. Command bindings that are actually nil.
  2. Command bindings that are actually lists of the form (menu-item ... ... ...). At least, I assume you have no interest in menu bindings.

Thus, sanitized form:

(cl-loop
 for seq being the key-seqs of KEYMAP
 using (key-bindings cmd)
 as key = (key-description seq)
 when cmd
 when (not (string-search ".." key))
 when (or (not (listp cmd))
          (member (car cmd) '(closure lambda)))
 do ...)

If you're like me and never bind a key to a lambda, the third when can be simplified to just

when (not (listp cmd))

Warning for cleverness. You might think this could be

when (commandp cmd)

but many autoloaded commands won't satisfy commandp nor functionp until called the first time. At init time, they only positively satisfy symbolp.

Anyway, that's it! No more keeping mental track of subkeymaps nor the finer details of recursing into them!

Bonus: get all keys for the current buffer

OK great, we can unnest a single keymap variable (such as org-mode-map or global-map) and its children into a flat list of key seqs. But what about listing all the current buffer's keys, not just one mode-map's keys?

You know that any given key can usually be found several times in multiple keymaps, where it's bound to different commands. The current buffer has a sort of keymap composite calculated by merging all relevant keymaps. That composite is not stored as a variable anywhere.

Fortunately we can know which binding is correct for the current buffer thanks to the fact that (current-active-maps) seems to return maps in the order in which Emacs selects them. So what comes earlier in the list is what would in fact be used. Then we can run (delete-dups) to declutter the list, which likewise keeps the first instance of each set of duplicates.

Presto – list current buffer keys:

(delete-dups
 (cl-loop
  for map in (current-active-maps)
  append (cl-loop
          for seq being the key-seqs of map
          using (key-bindings cmd)
          as key = (key-description seq)
          when cmd
          when (not (string-search ".." key))
          when (or (not (listp cmd))
                   (member (car cmd) '(closure lambda)))
          collect key)))

Bonus: get all keys and bindings for the current buffer

To list the keys together with their command bindings, we need a bit more complexity as delete-dups will not work. While we're at it, I'll throw in a key-valid-p check to represent a somewhat compute-heavy filter:

(cl-loop
 for map in (current-active-maps)
 append (cl-loop
         for seq being the key-seqs of map
         using (key-bindings cmd)
         as key = (key-description seq)
         when cmd
         when (not (string-search ".." key))
         when (or (not (listp cmd))
                  (member (car cmd) '(closure lambda)))
         unless (assoc key map-bindings)
         unless (assoc key buffer-bindings)
         when (ignore-errors (key-valid-p key))
         collect (cons key cmd)
         into map-bindings
         finally return map-bindings)
 into buffer-bindings
 finally return buffer-bindings)

Uncompiled, this is somewhat slow (0.5 seconds on a Surface Pro 7 @ 1.1GHz). Compiled is faster, but we can also speed it up somewhat with caching. That trick really comes into its own when you want to filter the result with all sorts of inefficient checks; add your custom checks inside the defun seq-to-key-if-legal in the following snippet.

Final version!

(defvar legal-key-cache (make-hash-table :size 2000 :test #'equal))
(defun seq-to-key-if-legal (seq)
  (let ((cached-value (gethash seq legal-key-cache 'not-found)))
    (if (eq 'not-found cached-value)
        (puthash seq
                 (let ((key (key-description seq)))
                   (when (and
                          ;; Ignore menu-bar/tool-bar/tab-bar
                          (not (string-search "-bar>" key))
                          ;; All anomalies with ".." like "ESC 3..9" are bound
                          ;; to self-insert-command or nil, ok to filter out.
                          (not (string-search ".." key))
                          (ignore-errors (key-valid-p key)))
                     key))
                 legal-key-cache)
      cached-value)))

(cl-loop
 for map in (current-active-maps)
 append (cl-loop
         for seq being the key-seqs of map
         using (key-bindings cmd)
         as key = (seq-to-key-if-legal seq)
         when (and key cmd)
         when (or (not (listp cmd))
                  (member (car cmd) '(closure lambda)))
         unless (assoc key map-bindings)
         unless (assoc key buffer-bindings)
         collect (cons key cmd)
         into map-bindings
         finally return map-bindings)
 into buffer-bindings
 finally return buffer-bindings)

Alternatives

Keymap-utils ships kmu-map-keymap which seems easier to use than the builtin map-keymap. Although it doesn't ship an explicit utility for buffer bindings. For that, you may be able to borrow which-key's which-key--get-current-bindings.

What links here

  • 2023-11-06
Created (11 months ago)
Updated (7 months ago)

TOML as default, YAML when you know you need it

Takeaways from: stackoverflow.com/questions/65283208/toml-vs-yaml-vs-strictyaml

In summary, YAML isn't actually sloppy #tech, but it's full of features that most people don't use. For example, you can write a value 1d6 and have that parsed as an object {number: 1, sides: 6}!

When you do have use for stuff like that, you'll know.

However… most uses of YAML/TOML/JSON files just have a program read in the data in a simplistic way to transform into a table or map object, end of story. You're not looking for any sophistications.

In that case, TOML serves better because fewer possibilities in syntax means it's able to give more specific error messages when the file is misformatted.

Created (7 months ago)

Design of this site

Looking at gwern.net/design, I realized I have things to say too.

URLs shouldn't be too short

  • Look at the link sive.rs/su – what is this monstrosity of a web address? No contextual information. If it had been named https://sive.rs/short-urls, I could recognize it as a page I've read before: "ah yeah, that's that page Sivers wrote about short URLs". With the ultra-short variant, I have to visit it before I can recognize that I've already been there and go back to wherever I was.
    • Sivers would respect my time better if the URL gave enough info for me to avoid clicking it! There's a design criterion.
    • Even qntm.org/destroy is not a link I can recognize on sight. Single-word slugs are usually a bad idea, though gwern.net/design is OK. It strains the limits of my pattern-matcher, but it matches.
      • For that particular page, the top domain gwern.net/... luckily contributes to the meaning, as the page is literally about the design of gwern.net, not about design in general.
      • A year from now, I might not recognize it, but it's night and day compared with qntm.org/destroy, which takes me only an hour to forget. In the time I've been drafting this article I've been largely unaware of just what article that links to – I visit a few times for curiosity, but the association between the generic verb "destroy" and that article fades so quickly. "Qntm destroy what?" It doesn't complete to anything like a sentence.
      • The ideal slug is seen in URLs like www.greaterwrong.com/posts/x4dG4GhpZH2hgz59x/joy-in-the-merely-real, if we ignore for a moment the superfluous bits and bobs like the /posts/x4dG4GhpZH2hgz59x/ and the www. I prefer to handle links such as this. It's many years ago I visited this link, yet even after all that time, it's impossible to mistake. That feature, recognizability, trumps any length aesthetic.
        • Takeaway: slugs should be long and specific, maximizing recognizability.
    • You should have to click links as little as possible (my site is a rat's nest of hyperlinks only as far as it must be). The same theory underlies people like Gwern's efforts to contextualize links and generate preview pop-ups. I say, it helps a lot just having a descriptive URL in the first place.

Permanent page ID🔗

  • My URLs get a unique random ID before the descriptive slug, i.e. domain.com/ID/slug, giving me full freedom to rename the slug part and split and merge pages. I couldn't live without that; almost all my pages have been renamed at least once, and some have been through a dozen renames.
    • When writing the notes for yourself only, it is possible to rely on a mass-renaming toolkit such as what orgrr provides. However, that does not keep alive your blog visitors' bookmarks.
      • To keep them alive, the toolkit would have to keep a record of all renames ever done, that you can translate into URL redirects…
    • My page ID is five alphabetic characters, sans vowels, all lowercase.
      • Example: edstrom.dev/lfqtr/design-of-this-site
      • This implies a 21-character alphabet, or "base-21". Out of that alphabet, there are 4,084,101 ways to compose a five-char string, plenty for one person's homepage (How long page-IDs?).
        • The probability of collision is low but not effectively zero like with a proper UUID, but here's an advantage of a one-person one-machine system – when a collision occurs, I can just renew the ID.
      • Excluding vowels prevents accidentally generating words (and obscenities). It means I can have custom routes such as domain.com/login without worrying that there already exists an autogenerated ID "login".
      • Old mistake: Instead of alphabetic, it used to be base-62 i.e. the ID could include numbers and capital letters. I had links like edstrom.dev/m99K and edstrom.dev/0dSJ.

        Use lowercase alphabetic! It nods to the fact that URLs are a user interface!

        Sometimes I want to send myself a link between devices, but I have to stop and think how to do that. (Do I email myself? Do I use the Share button?) If the URL is short, it's almost always faster and more comfortable to just type it. Right?

        Here base-62 comes into the picture and tramples my lawn, because phone keyboards—

Created (11 months ago)
Updated (7 months ago)
Showing 397 to 400