Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1917 lines (1393 sloc) 122 KB

zzamboni.org source file

This file is the source for all new and updated content on my website since March 2018. Content here may be in progress or incomplete. This file gets converted to Hugo files by the excellent ox-hugo.

You really should not read it here but at zzamboni.org.

Table of Contents

Pages

This section contains all the static pages.

About

Who: I am a computer scientist, consultant, programmer, sysadmin, author and overall geek, now turned project leader and engineering team manager. I am from Mexico but live in Switzerland with my awesome wife and our two beautiful daughters.

What: I work as the lead of a development team in the Cloud Platform Division of Swisscom, where I am applying my experience and background in security, configuration management, cloud computing and automation to build the next-generation cloud infrastructure for Swisscom and its customers. I am also the author of “Learning CFEngine 3”.

Where: I was born in Argentina, but have moved around all my life. When I was very young I moved to Mexico, where I lived in four different cities before moving to the U.S. to pursue my Ph.D. at Purdue University under the direction of Gene Spafford. Upon finishing my studies, my wife and I decided to go to Switzerland, where I worked at the IBM Zurich Research Lab. Eight years and two kids later, we moved to Mexico in late 2009. In 2015, we moved back to Switzerland.

Long version: If you are interested, here’s my curriculum vitae. For other useless trivia about me, see here.

My online past

About this site

This entire site is generated by Hugo and served by Netlify.

The source is stored in my zzamboni/zzamboni.org GitHub repository, which is devoted to this content. Recently I started using ox-hugo to generate the content from a single source file in org-mode format, although all the older articles and pages are still stored in their original source Markdown files (I gradually convert them whenever I update them). Some of my project pages are stored in the gh-pages branch of their own github repositories. Since GitHub Pages supports custom domains, all of them can be transparently hosted under the zzamboni.org domain.

I think it’s incredible that all of this infrastructure is so easy to use and available for free.

Image attributions:

  • C128 Code (code header background) is from the source code listing from my Commodore 128 program Supercataloguer 128.
  • Scrabble letters (blog header background) from Pexels, licensed under CC0.
  • All other header background photos were taken either by my wife or me.

If you have any concerns or questions about the images used in this site, please let me know.

Contact

If you have any questions, comments or feedback about this site, please use the form below to send me a message.

—-

@@html:{{< form-contact-netlify >}}@@

Books

Book pages

The individual pages for each book.

Learning CFEngine 3

{{< bookimglink style=”float:right” >}}

I am the author of “Learning CFEngine 3”, the best book for learning CFEngine.

The book has its own webpage at http://cf-learn.info, please visit it for more information, code samples, etc.

You can buy the book from Amazon by clicking the link on the right.

 

Learning Hammerspoon

{{< hsbookimglink style=”float:right” >}}

Automate all the things! From window manipulation to automated system settings depending on your current location, Hammerspoon makes it possible. In this book you will learn how to get started with Hammerspoon, how to use pre-made modules, and how to write your own, to achieve an unprecedented level of control over your Mac.

Learn more, read a free sample and get the book (you choose how much you pay!) at https://leanpub.com/learning-hammerspoon/, or click on the banner on the right.

Ideas

Ideas for things to write about.

Using and extending Seal

Sending things to OmniFocus

ox-hugo

CryFS

Hosting a Hugo blog in GitHub

Short guide to setting up Hugo, using /docs to host to avoid branches, and setting up your custom domain.

Posts

Blog posts.

Literate config files

I group here the posts about my documented config files, which include the live files from my current configuration.

My Emacs Configuration, With Commentary

Last update: {{{updatetime}}}

I have enjoyed slowly converting my configuration files to literate programming style style using org-mode in Emacs. I previously posted my Elvish configuration, and now it’s the turn of my Emacs configuration file. The text below is included directly from my init.org file. Please note that the text below is a snapshot as the file stands as of the date shown above, but it is always evolving. See the init.org file in GitHub for my current, live configuration, and the generated file at https://github.com/zzamboni/dot_emacs/blob/master/init.el.

My Hammerspoon Configuration, With Commentary

Last update: {{{updatetime}}}

In my ongoing series of literate config files, I present to you my Hammerspoon configuration file. You can see the generated file at https://github.com/zzamboni/dot-hammerspoon/blob/master/init.lua. As usual, this is just a snapshot at the time shown above, you can see the current version of my configuration in GitHub.

My Elvish Configuration With Commentary

Last update: {{{updatetime}}}

In this blog post I will walk you through my current Elvish configuration file, with running commentary about the different sections.

This is also my first blog post written using org-mode, which I have started using for writing and documenting my code, using literate programming. The content below is included unmodified from my rc.org file (as of the date shown above), from which the rc.elv file is directly generated.

Without further ado…

Emacs

Beautifying Org Mode in Emacs

Over the last few months, I have used org-mode more and more for writing and programming in Emacs. I love its flexibility and power, and it is the first literate programming tool which “feels right”, and I have been able to stick with it for a longer period of time than in my previous attempts.

Recently I started thinking about how I could make my editing environment more visually appealing. I am in general very happy with my Emacs’ appearance. I use the Gruvbox theme, and org-mode has very decent syntax highlighting. But as I write more and more prose in Emacs these days, I started thinking it might be nice to edit text in more visually-appealing fonts, including using a proportional font, which makes regular prose much more readable. I would like to share with you what I learned and my current Emacs configuration.

In the end, you can have an Emacs setup for editing org documents which looks very nice, with proportional fonts for text and monospaced fonts for code blocks, examples and other elements. To wet your appetite, here is what a fragment of my init.org file looks like:

images/emacs-init-propfonts.png

Step 1: Configure faces for Org headlines and lists

My first step was to make org-mode much more readable by using different fonts for headings, hiding some of the markup, and improving list bullets. I took these settings originally from Howard Abrams’ excellent Org as a Word Processor article, although I have tweaked them a bit.

First, we ask org-mode to hide the emphasis markup (e.g. /.../ for italics, *...* for bold, etc.):

(setq org-hide-emphasis-markers t)

Then, we set up a font-lock substitution for list markers (I always use ”-” for lists, but you can change this if you want) by replacing them with a centered-dot character:

(font-lock-add-keywords 'org-mode
                        '(("^ *\\([-]\\) "
                           (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) ""))))))

The org-bullets package replaces all headline markers with different Unicode bullets:

(use-package org-bullets
  :config
  (add-hook 'org-mode-hook (lambda () (org-bullets-mode 1))))

Finally, we set up a nice proportional font, in different sizes, for the headlines. The fonts listed will be tried in sequence, and the first one found will be used. Feel free to add your own favorite font:

(let* ((variable-tuple
        (cond ((x-list-fonts "Source Sans Pro") '(:font "Source Sans Pro"))
              ((x-list-fonts "Lucida Grande")   '(:font "Lucida Grande"))
              ((x-list-fonts "Verdana")         '(:font "Verdana"))
              ((x-family-fonts "Sans Serif")    '(:family "Sans Serif"))
              (nil (warn "Cannot find a Sans Serif Font.  Install Source Sans Pro."))))
       (base-font-color     (face-foreground 'default nil 'default))
       (headline           `(:inherit default :weight bold :foreground ,base-font-color)))

  (custom-theme-set-faces
   'user
   `(org-level-8 ((t (,@headline ,@variable-tuple))))
   `(org-level-7 ((t (,@headline ,@variable-tuple))))
   `(org-level-6 ((t (,@headline ,@variable-tuple))))
   `(org-level-5 ((t (,@headline ,@variable-tuple))))
   `(org-level-4 ((t (,@headline ,@variable-tuple :height 1.1))))
   `(org-level-3 ((t (,@headline ,@variable-tuple :height 1.25))))
   `(org-level-2 ((t (,@headline ,@variable-tuple :height 1.5))))
   `(org-level-1 ((t (,@headline ,@variable-tuple :height 1.75))))
   `(org-document-title ((t (,@headline ,@variable-tuple :height 2.0 :underline nil))))))

Step 2: Setting up variable-pitch and fixed-pitch faces

My next realization was that Emacs already includes support for displaying proportional fonts with the variable-pitch-mode command. You can try it right now: type M-x variable-pitch-mode and your current buffer will be shown in a proportional font (you can disable it by running variable-pitch-mode again). On my Mac the default variable-pitch font is Helvetica. You can change the font used by configuring the variable-pitch face. You can do this interactively through the customize interface by typing M-x customize-face variable-pitch. At the moment I like Source Sans Pro.

As a counterpart to variable-pitch, you need to configure the fixed-pitch face for the text that needs to be shown in a monospaced font. My first instinct was to inherit this from my default face (I use Inconsolata), but it seems that this gets remapped when variable-pitch-mode is active, so I had to configure it by hand with the same font as my default face.

What I would suggest is that you customize the fonts interactively, as you can see live how it looks on your text. You can make the configuration permanent from the customize screen as well. If you want to explicitly set them in your configuration file, you can do it with the custom-theme-set-faces function, like this:

(custom-theme-set-faces
 'user
 '(variable-pitch ((t (:family "Source Sans Pro" :height 180 :weight light))))
 '(fixed-pitch ((t ( :family "Inconsolata" :slant normal :weight normal :height 1.0 :width normal)))))

Tip: you can get the LISP expression for your chosen font (the part that looks like ((t (:family ... ))) from the customize-face screen - open the “State” button and choose the “Show Lisp Expression” menu item.

You can enable variable-pitch-mode automatically for org buffers by setting up a hook like this:

(add-hook 'org-mode-hook 'variable-pitch-mode)

Step 2: Use long lines and visual-line-mode

One thing you will notice right away with proportional fonts is that filling paragraphs no longer makes sense. This is because fill-paragraph works based on the number of characters in a line, but with a proportional font, characters have different widths, so a filled paragraph looks strange:

images/emacs-filled-paragraph.png

Of course, you can still do it, but there’s a better way. With visual-line-mode enabled, long lines will flow and adjust to the width of the window. This is great for writing prose, because you can choose how wide your lines are by just resizing your window.

images/emacs-narrow-window.png

images/emacs-wide-window.png

There is one habit you have to change for this to work: the instinct (at least for me) of pressing M-q every once in a while to readjust the current paragraph. I personally think it’s worth it.

You can enable visual-line-mode automatically for org buffers by setting up another hook:

(add-hook 'org-mode-hook 'visual-line-mode)

Step 4: Configure faces for specific Org elements

After all the changes above, you will have nice, proportional fonts in your Org buffers. However, there are some things for which you still want monospace fonts! Things like source blocks, examples, tags and some other markup elements still look better in a fixed-spacing font, in my opinion. Fortunately, org-mode has an extremely granular face selection, so you can easily customize them to have different elements shown in the correct font, color, and size.

Tip: you can use C-u C-x = (which runs the command what-cursor-position with a prefix argument) to show information about the character under the cursor, including the face which is being used for it. If you find a markup element which is not correctly configured, you can use this to know which face you have to customize.

You can configure specific faces any way you want, but if you simply want them to be rendered in monospace font, you can set them to inherit from the fixed-pitch face we configured before. You can also inherit from multiple faces to combine their attributes.

Here are the faces I have configured so far (there are probably many more to do, but I don’t use org-mode to its full capacity yet). I’m showing here the LISP expressions, but you can just as well configure them using customize-face.

(custom-theme-set-faces
 'user
 '(org-block                 ((t (:inherit fixed-pitch))))
 '(org-document-info         ((t (:foreground "dark orange"))))
 '(org-document-info-keyword ((t (:inherit (shadow fixed-pitch)))))
 '(org-link                  ((t (:foreground "royal blue" :underline t))))
 '(org-meta-line             ((t (:inherit (font-lock-comment-face fixed-pitch)))))
 '(org-property-value        ((t (:inherit fixed-pitch))) t)
 '(org-special-keyword       ((t (:inherit (font-lock-comment-face fixed-pitch)))))
 '(org-tag                   ((t (:inherit (shadow fixed-pitch) :weight bold :height 0.8))))
 '(org-verbatim              ((t (:inherit (shadow fixed-pitch))))))

One minor issue I have noticed is that, in variable-pitch-mode, the fixed-pitch blocks have a slight increase in inter-line spacing. This is not a deal breaker for me, but it is a noticeable difference. This can be observed in the following screenshot, which shows the block of code above embedded in the org-mode buffer and in the block-editing buffer, which uses the fixed-width font. If you know a way in which this could be fixed, please let me know!

images/emacs-differing-heights.png

Conclusion

The setup described above has considerably improved my enjoyment of writing in Emacs. I hope you find it useful. If you have any feedback, suggestions or questions, please let me know in the comments.

Hammerspoon

Getting Started With Hammerspoon

This is the first installment of a series of posts about Hammerspoon, a staggeringly powerful automation utility which gives you an amazing degree of control over your Mac, allowing you to automate and control almost anything. In the word of Hammerspoon’s motto: Making the runtime, funtime.

Why Hammerspoon?

Hammerspoon is a Mac application that allows you to achieve an unprecedented level of control over your Mac. Hammerspoon enables interaction with the system at multiple layers–from low-level file system or network access, mouse or keyboard event capture and generation, all the way to manipulating applications or windows, processing URLs and drawing on the screen. It also allows interfacing with AppleScript, Unix commands and scripts, and other applications. Hammerspoon configuration is written in Lua, a popular embedded programming language.

Using Hammerspoon, you can replace many stand-alone Mac utilities for controlling or customizing specific aspects of your Mac (the kind that tends to overcrowd the menubar). For example, the following are doable using Hammerspoon (these are all things I do with it on my machine - each paragraph links to the corresponding sections in my config file):

Hammerspoon is the most powerful Mac automation utility I have ever used. If you are a programmer, it can make using your Mac vastly more fun and productive.

How does Hammerspoon work?

Hammerspoon acts as a thin layer between the operating system and a Lua-based configuration language. It includes extensions for querying and controlling many aspects of the system. Some of the lower-level extensions are written in Objective-C, but all of them expose a Lua API, and it is trivial to write your own extensions or modules to extend its functionality.

From the Hammerspoon configuration you can also execute external commands, run AppleScript or JavaScript code using the OSA scripting framework, establish network connections and even run network servers; you can capture and generate keyboard events, detect network changes, USB or audio devices being plugged in or out, changes in screen or keyboard language configuration; you can draw directly on the screen to display whatever you want; and many other things. Take a quick look at the Hammerspoon API index page to get a feeling of its extensive capabilities. And that is only the libraries that are built into Hammerspoon. There is an extensive and growing collection of Spoons, modules written in pure Lua that provide additional functionality and integration. And of course, the configuration is simply Lua code, so you can write your own code to do whatever you want.

Interested? Let’s get started!

Installing Hammerspoon

Hammerspoon is a regular Mac application. To install it by hand, you just need to download it from https://github.com/Hammerspoon/hammerspoon/releases/latest, unzip the downloaded file and drag it to your /Applications folder (or anywhere else you want).

If you are automation-minded like me, you probably use Homebrew and its plugin Cask to manage your applications. In this case, you can use Cask to install Hammerspoon:

brew cask install hammerspoon

When you run Hammerspoon for the first time, you will see its icon appear in the menubar, and a notification telling you that it couldn’t find a configuration file. Let’s fix that!

images/hammerspoon-startup.png

{{% tip %}} If you click on the initial notification, your web browser will open to the excellent Getting Started with Hammerspoon page, which I highly recommend you read for more examples. {{% /tip %}}

Your first Hammerspoon configuration

Let us start with a few simple examples. As tradition mandates, we will start with a “Hello World” example. Open $HOME/.hammerspoon/init.lua (Hammerspoon will create the directory upon first startup, but you need to create the file) in your favorite editor, and type the following:

hs.hotkey.bindSpec({ { "ctrl", "cmd", "alt" }, "h" },
  function()
    hs.notify.show("Hello World!", "Welcome to Hammerspoon", "")
  end
)

Save the file, and from the Hammerspoon icon in the menubar, select “Reload config”. Apparently nothing will happen, but if you then press {{{keys(Ctrl ⌘ Alt h)}}} on your keyboard, you will see a notification on your screen welcoming you to the world of Hammerspoon.

images/hammerspoon-hello-world.png

Although it should be fairly self-explanatory, let us dissect this example to give you a clearer understanding of its components:

  • All Hammerspoon built-in extensions start with hs. In this case, {{{hsapi(hs.hotkey)}}} is the extension that handles keyboard bindings. It allows us to easily define which functions will be called in response to different keyboard combinations. You can even differentiate between the keys being pressed, released or held down if you need to. The other extension used in this example is {{{hsapi(hs.notify)}}}, which allows us to interact with the macOS Notification Center to display, react and interact with notifications.
  • Within hs.hotkey, the {{{hsapi(hs.hotkey,bindSpec)}}} function allows you to bind a function to a pressed key. Its first argument is a key specification which consists of a list (Lua lists and table literals are represented using curly braces) with two elements: a list of the key modifiers, and the key itself. In this example, { { "ctrl", "cmd", "alt" }, "h" } represents pressing {{{keys(Ctrl ⌘ Alt h)}}}.
  • The second argument to bindSpec is the function to call when the key is pressed. Here we are defining an inline anonymous function using function() ... end.
  • The callback function uses {{{hsapi(hs.notify,show)}}} to display the message. Take a quick look at the {{{hsapi(hs.notify)}}} documentation to get an idea of its extensive capabilities, including configuration of all aspects of a notification’s appearance and buttons, and the functions to call upon different user actions.

Try changing the configuration to display a different message or use a different key. After every change, you need to instruct Hammerspoon to reload its configuration, which you can do through its menubar item.

Debugging tools and the Hammerspoon console

As you start modifying your configuration, errors will happen, as they always do when coding. To help in development and debugging, Hammerspoon offers a console window where you can see any errors and messages printed by your Lua code as it executes, and also type code to be evaluated. It is a very useful tool while developing your Hammerspoon configuration.

To invoke the console, you normally choose “Console…” from the Hammerspoon menubar item. However, this is such a common operation, that you might find it useful to also set a key combination for showing the console. Most of Hammerspoon’s internal functionality is also accessible through its API. In this case, looking at the {{{hsapi(hs,,documentation for the main hs module)}}} reveals that there is an {{{hsapi(hs,toggleConsole)}}} function. Using the knowledge you have acquired so far, you can easily configure a hotkey for opening and hiding the console:

hs.hotkey.bindSpec({ { "ctrl", "cmd", "alt" }, "y" }, hs.toggleConsole)

Once you reload your configuration, you should be able to use {{{keys(Ctrl ⌘ Alt y)}}} to open and close the console. Any Lua code you type in the Console will be evaluated in the main Hammerspoon context, so you can add to your configuration directly from there. This is a good way to incrementally develop your code before committing it to the init.lua file.

You may have noticed by now another common operation while developing Hammerspoon code: reloading the configuration, which you normally have to do from the Hammerspoon menu. So why not set up a hotkey to do that as well? Again, the {{{hsapi(hs)}}} module comes to our help with the {{{hsapi(hs,reload)}}} method:

hs.hotkey.bindSpec({ { "ctrl", "cmd", "alt" }, "r" }, hs.reload)

Another useful development tool is the hs command, which you can run from your terminal to get a Hammerspoon console. To install it, you can use the {{{hsapi(hs.ipc”,cliInstall)}}} function, which you can just add to your init.lua file to check and install the command every time Hammerspoon runs.

{{% warning %}} {{{hsapi(hs.ipc,cliInstall)}}} creates symlinks under /usr/local/ to the hs command and its manual page file, located inside the Hammerspoon application bundle. Under some circumstances (particularly if you build Hammerspoon from source, or if you install different versions of it), you may end up with broken symlinks. If the hs command stops working and hs.ipc.cliInstall() doesn’t fix it, look for broken symlinks left behind from old versions of Hammerspoon. Remove them and things should work again. {{% /warning %}}

Now you have all the tools for developing your Hammerspoon configuration. In the next installment we will look at how you can save yourself a lot of coding by using pre-made modules. In the meantime, feel free to look through my Hammerspoon configuration file for ideas, and please let me know your thoughts in the comments!

Using Spoons in Hammerspoon

In this second article about Hammerspoon, we look into Spoons, modules written in Lua which can be easily installed and loaded into Hammerspoon to provide ready-to-use functionality. Spoons provide a predefined API to configure and use them. They are also a good way to share your own work with other users.

See also the first article in this series.

Using a Spoon to locate your mouse

As a first example, we will use the MouseCircle spoon, which allows us to set up a hotkey that displays a color circle around the current location of the mouse pointer for a few seconds, to help you locate it.

To install the spoon, download its zip file from https://github.com/Hammerspoon/Spoons/raw/master/Spoons/MouseCircle.spoon.zip, unpack it, and double-click on the resulting MouseCircle.spoon file. Hammerspoon will install the Spoon under ~/.hammerspoon/Spoons/.

images/mousecircle.png

Once a Spoon is installed, you need to use the hs.loadSpoon() function to load it. Type the following in the Hammerspoon console, or add it to your init.lua file and reload the configuration:

hs.loadSpoon("MouseCircle")

After a spoon is loaded, and depending on what it does, you may need to configure it, assign hotkeys, and start it. A spoon’s API is available through the spoon.<SpoonName> namespace. To learn the API you need to look at the spoon documentation page. In the case of MouseCircle, a look at http://www.hammerspoon.org/Spoons/MouseCircle.html reveals that it has two methods (bindHotkeys() and show()) and one configuration variable (color) available under spoon.MouseCircle.

The first API call is spoon.MouseCircle:bindHotkeys(), which allows us to set up a hotkey that shows the mouse locator circle around the location of the mouse pointer. Let’s say we wanted to bind the mouse circle to {{{keys(Ctrl ⌘ Alt d)}}}. According to the MouseCircle documentation, the name for this action is show, so we can do the following:

spoon.MouseCircle:bindHotkeys({
  show = { { "ctrl", "cmd", "alt" }, "d" }
})

Once you do this, press the hotkey and you should see a red circle appear around the mouse cursor, and fade away after 3 seconds.

{{% tip %}} All spoons which offer the possibility of binding hotkeys have to expose it through the same API:

spoon.SpoonName:bindHotkeys({ action1 = keySpec1,
                              action2 = keySpec2, ... })

Each actionX is a name defined by the spoon, which refers to something that can be bound to a hotkey, and each keySpecX is a table with two elements: a list of modifiers and the key itself, such as { { "ctrl", "cmd", "alt" }, "d" }. {{% /tip %}}

The second API call in the MouseCircle spoon is show(), which triggers the functionality of showing the locator circle directly. Let’s try it – type the following in the console:

spoon.MouseCircle:show()

Most spoons are structured like this: you can set up hotkeys to trigger the main functionality, but you can also trigger it via method calls. Normally you won’t use these methods, but their availability makes it possible for you to use spoon functionality from our own configuration, or from other spoons, to create further automation.

spoon.MouseCircle.color is a public configuration variable exposed by the spoon, which specifies the color that will be used to draw the circle. Colors are defined according to the documentation for the {{{hsapi(hs.drawing.color)}}} module. Several color collections are supported, including the OS X system collections and a few defined by Hammerspoon itself. Color definitions are stored in Lua tables indexed by their name. For example, you can view the {{{hsapi(hs.drawing.color,hammerspoon)}}} table, including the color definitions, by using the convenient {{{hsapi(hs.inspect)}}} method on the console:

> hs.inspect(hs.drawing.color.hammerspoon)
{
  black = {
    alpha = 1,
    blue = 0.0,
    green = 0.0,
    red = 0.0
  },
  green = {
    alpha = 1,
    blue = 0.0,
    green = 1.0,
    red = 0.0
  },
  osx_red = {
    alpha = 1,
    blue = 0.302,
    green = 0.329,
    red = 0.996
  },
  osx_green = {
...

{{% tip %}}

Lua does not include a function to easily get the keys of a table so you have to use the {{{luafun(pairs)}}} function to loop over the key/value pairs of the table. The {{{hsapi(hs.inspect)}}} function is convenient, but to get just the list of tables and the color names, without the color definitions themselves, you can use the following code (if you type this in the console you have to type it all in a single line – and beware, the output is a long list):

for listname,colors in pairs(hs.drawing.color.lists()) do
  print(listname)
  for color,def in pairs(colors) do
    print("  " .. color)
  end
end

{{% /tip %}}

If we wanted to make the circle green, we can assign the configuration value like this:

spoon.MouseCircle.color = hs.drawing.color.hammerspoon.green

The next time you invoke the show() method, either directly or through the hotkey, you will see the circle in the new color.

{{% tip %}}

(We will look at this in more detail in a future installment about Lua, but in case you were wondering…)

You may have noticed that the configuration variable was accessed with a dot (spoon.MouseCircle.color), and we also used it for some function calls earlier (e.g. {{{hsapi(hs.notify,show)}}}, whereas for the show() method we used a colon (spoon.MouseCircle:show()). The latter is Lua’s object-method-call notation, and its effect is to pass the object on which the method is being called as an implicit first argument called self. This is simply a syntactic shortcut, i.e. the following two are equivalent:

spoon.MouseCircle:show()
spoon.MouseCircle.show(spoon.MouseCircle)

Note that in the second statement, we are calling the method using the dot notation, and explicitly passing the object as the first argument. Normally you would use colon notation, but the alternative can be useful when constructing function pointers. For example, if you wanted to manually bind a second key to show the mouse circle, you might initially try to use the following:

hs.hotkey.bindSpec({ {"ctrl", "alt", "cmd" }, "p" },
                   spoon.MouseCircle:show)

But this results in an error. The correct way is to wrap the call in an anonymous function:

hs.hotkey.bindSpec({ {"ctrl", "alt", "cmd" }, "p" },
                   function() spoon.MouseCircle:show() end)

Alternatively, you can use the {{{hsapi(hs.fnutils,partial)}}} function to construct a function pointer that includes the correct first argument:

hs.hotkey.bindSpec({ {"ctrl", "alt", "cmd" }, "p" },
                   hs.fnutils.partial(spoon.MouseCircle.show,
                                      spoon.MouseCircle))

This is more verbose than the previous example, but the technique can be useful sometimes. Although Lua is not a full functional language, it supports using functions as first-class values, and the {{{hsapi(hs.fnutils)}}} extension includes a number of functions that make it easy to use them.

{{% /tip %}}

By now you know enough to use spoons with Hammerspoon’s native capabilities: look for the ones you want, download and install them by hand, and configure them in your init.lua using their configuration variables and API. In the next sections you will learn more about the minimum API of spoons, and how to install and configure spoons in a more automated way.

The Spoon API

The advantage of using spoons is that you can count on them to adhere to a defined API, which makes it easier to automate their use. Although each spoon is free to define additional variable and methods, the following are standard:

  • SPOON:init() is called automatically (if it exists) by {{{hsapi(hs,loadSpoon)}}} after loading the spoon, and can be used to initialize variables or anything else needed by the Spoon.
  • SPOON:start() should exist if the spoon requires any ongoing or background processes such as timers or watchers of any kind.
  • SPOON:stop() should exist if start() does, to stop any background processes that were started by start().
  • SPOON:bindHotkeys(map) is exposed by spoons which allow binding hotkeys to certain actions. Its map argument is a Lua table with key/value entries of the following form: ACTION = { MODS, KEY }, where ACTION is a string defined by the spoon (multiple such actions can be defined), MODS is a list of key modifiers (valid values are =”cmd”=, =”alt”=, =”ctrl”= and =”shift”=), and KEY is the key to be bound, as shown in our previous example. All available actions for a spoon should be listed in its documentation.

Automated Spoon installation and configuration

Once you develop a complex Hammerspoon configuration using spoons, you may start wondering if there is an easy way to manage them. There are no built-in mechanisms for automatically installing spoons, but you can use a spoon called SpoonInstall that implements this functionality. You can download it from http://www.hammerspoon.org/Spoons/SpoonInstall.html. Once installed, you can use it to declaratively install, configure and run spoons. For example, with SpoonInstall you can use the MouseCircle spoon as follows:

hs.loadSpoon("SpoonInstall")
spoon.SpoonInstall:andUse("MouseCircle", {
                  config = {
                     color = hs.drawing.color.osx_red,
                  },
                  hotkeys = {
                     show = { { "ctrl", "cmd", "alt"}, "d" }
                  }})

If the MouseCircle spoon is not yet installed, spoon.SpoonInstall:andUse() will automatically download and install it, and set its configuration variables and hotkeys according to the declaration.

If there is nothing to configure in the spoon, spoon.SpoonInstall:andUse("SomeSpoon") does exactly the same as hs.loadSpoon("SomeSpoon"). But if you want to set configuration variables, hotkey bindings or other parameters, the following keys are recognized in the map provided as a second parameter:

  • config is a Lua table containing keys corresponding to configuration variables in the spoon. In the example above, config = { color = hs.drawing.color.osx_red } has the same effect as setting spoon.MouseCircle.color = hs.drawing.color.osx_red
  • hotkeys is a Lua table with the same structure as the mapping parameter passed to the bindHotkeys method of the spoon. In our example above, hotkeys = { show = { { "ctrl", "cmd", "alt"}, "d" } } automatically triggers a call to spoon.MouseCircle:bindHotkeys({ show = { { "ctrl", "cmd", "alt"}, "d" } }).
  • loglevel sets the log level of the logger attribute within the spoon, if it exists. The valid values for this attribute are ‘nothing’, ‘error’, ‘warning’, ‘info’, ‘debug’, or ‘verbose’.
  • start is a boolean value which indicates whether to call the Spoon’s start() method (if it has one) after configuring everything else.
  • fn specifies a function which will be called with the freshly-loaded Spoon object as its first argument. This can be used to execute other startup or configuration actions that are not covered by the other attributes. For example, if you use the {{{spoon(Seal)}}} spoon (a configurable launcher), you need to call its loadPlugins() method to specify which Seal plugins to use. You can achieve this with something like this:
    spoon.SpoonInstall:andUse("Seal",
      { hotkeys = { show = { {"cmd"}, "space" } },
        fn = function(s)
                 s:loadPlugins({"apps", "calc", "safari_bookmarks"})
             end,
             start = true,
      })
        
  • repo indicates the repository from where the Spoon should be installed if needed. Defaults to =”default”=, which indicates the official Spoon repository at http://www.hammerspoon.org/Spoons/. I keep a repository of unofficial Spoons at http://zzamboni.org/zzSpoons/, and others may be available by the time you read this.
  • disable can be set to true to disable the Spoon (easier than commenting it out when you want to temporarily disable a spoon) in your configuration.

{{% tip %}}

You can assign functions and modules to variables to improve readability of your code. For example, in my init.lua file I make the following assignment:

Install=spoon.SpoonInstall

Which allows me to write Install:andUse("MouseCircle", …​ ), which is shorter and easier to read.

{{% /tip %}}

Managing repositories and spoons using SpoonInstall

Apart from the andUse() “all-in-one” method, SpoonInstall has methods for specific repository- and spoon-maintenance operations. As of this writing, there are two Spoon repositories: the official one at http://www.hammerspoon.org/Spoons/, and my own at http://zzamboni.org/zzSpoons/, where I host some unofficial and in-progress Spoons.

The configuration variable used to specify repositories is SpoonInstall.repos. Its default value is the following, which configures the official repository identified as “default”:

{
  default = {
      url = "https://github.com/Hammerspoon/Spoons",
      desc = "Main Hammerspoon Spoon repository",
  }
}

To configure a new repository, you can define an extra entry in this variable. The following code creates an entry named “zzspoons” for my Spoon repository:

spoon.SpoonInstall.repos.zzspoons = {
   url = "https://github.com/zzamboni/zzSpoons",
   desc = "zzamboni's spoon repository",
}

After this, both “zzspoons” and “default” can be used as values to the repo attribute in the andUse() method, and in any of the other methods that take a repository identifier as a parameter. You can find the full API documentation at http://www.hammerspoon.org/Spoons/SpoonInstall.html.

Conclusion

Spoons are a great mechanism for structuring your Hammerspoon configuration. If you want an example of a working configuration based almost exclusively on Spoons, you can view my own Hammerspoon configuration at https://github.com/zzamboni/dot-hammerspoon.

Just Enough Lua to Be Productive in Hammerspoon, Part 1

Hammerspoon’s configuration files are written in Lua, so a basic knowledge of the language is very useful to be an effective user of Hammerspoon. In this 2-part article I will show you the basics of Lua so you can read and write Hammerspoon configuration. Along the way you will discover that Lua is a surprisingly powerful language.

Lua is a scripting language created in 1993, and focused from the beginning in being an embedded language for extending other applications. It is easy to learn and use while having pretty powerful features, and is frequently used in games, but also in many other applications including, of course, Hammerspoon.

The purpose of this section is to give you a quick overview of the Lua features and peculiarities you may find most useful for developing Hammerspoon policies. I assume you are a programmer who knows some other C-like language–if you already know C, Java, Ruby, Python, Perl, Javascript or some similar language, picking up Lua should be pretty easy. Instead of detailing every structure, I will focus on the aspects that are most different or that are most likely to trip you up as you learn it.

Flow control

Lua includes all the common flow-control structures you might expect. Some examples:

local info = "No package selected"
if pkg and pkg ~= "" then
   info, st = hs.execute("/usr/local/bin/brew info " .. pkg)
   if st == nil then
      info = "No information found about formula '" .. pkg .. "'!"
   end
end

In this example, in addition to the {{{luadoc(if,3.3.4,)}}} statement, you can see in the line that runs {{{hsapi(hs,execute)}}} that Lua functions can return multiple values (which is not the same as returning an array, which counts as a single value). Within the function, this is implemented simply by separating the values with commas in the return statement, like this: return val1, val2. You can also see in action the following operators:

  • ==== for equality;
  • ~= for inequality (in this respect it differs from most C-like languages, which use !=);
  • .. for string concatenation;
  • and for the logical AND operation (by extension, you can deduct that or and not are also available).
local doReload = false
for _,file in pairs(files) do
   if file:sub(-4) == ".lua" and (not string.match(file, '/[.]#')) then
      doReload = true
   end
end

In this example we see the {{{luadoc(for,3.3.5)}}} statement in its so-called generic form:

for <var> in <expression> do <block> end

This statement loops the variables over the values returned by the expressions, executing the block with each the consecutive value until it becomes nil.

{{% tip %}} Strictly speaking, expression is executed once and its value must be an iterator function, which returns one new value from the sequence every time it is called, returning nil at the end. {{% /tip %}}

The for statement also has a numeric form:

for <var> = <first>,<last>,<inc> do <block> end

This form loops the variable from the first to the last value, incrementing it by the given increment (defaults to 1) at each iteration.

Going back to our example, we can also learn the following:

  • The {{{luafun(pairs)}}} function, which loops over a table. We will learn more about Lua tables below, but they can be used to represent both regular and associative arrays. pairs() treats the files variable as an associative array, and returns in each iteration a key/value pair of its contents.
  • The _ variable, while not special per se, is used by convention in Lua for “throwaway values”. In this case we are not interested in the key in each iteration, just the value, so we assign the key to _, never to be used again.
  • Our first glimpse into the Lua {{{luadoc(string library,6.4)}}}, and the two ways in which it can be used:
    • In file:sub(-4), the colon indicates the object-oriented notation (see “Lua dot-vs-colon method access” below). This invokes the {{{luafun(string.sub)}}} function, automatically passing the file variable as its first argument. This statement is equivalent to string.sub(file, -4).
    • In string.match(file, '/'), we see the function notation used to call {{{luafun(string.match)}}}. Since the file variable is being passed as the first argument, you could rewrite this statement as file:match('/[.]'). In practice, I’ve found myself using both notations somewhat exchangeably - feel free to use whichever you find most comfortable.

Dot-vs-colon method access in Lua

You will notice that sometimes, functions contained within a module are called with a dot, and others with a colon. The latter is Lua’s object-method-call notation, and its effect is to pass the object on which the method is being called as an implicit first argument called self. This is simply a syntactic shortcut, i.e. the following two are equivalent:

file:match('/[.]')
string.match(file, '/')

Note that in the second statement, we are calling the method using the dot notation, and explicitly passing the object as the first argument. Normally you would use colon notation, but when you need a function pointer, you need to use the dot notation.

Functions

Functions are defined using the function keyword.

function leftDoubleClick(modifiers)
   local pos=hs.mouse.getAbsolutePosition() -- <1>
   hs.eventtap.event.newMouseEvent(
     hs.eventtap.event.types.leftMouseDown, pos, modifiers) -- <2>
      :setProperty(hs.eventtap.event.properties.mouseEventClickState, 2)
      :post() -- <3>
   hs.eventtap.event.newMouseEvent( -- <4>
     hs.eventtap.event.types.leftMouseUp, pos, modifiers):post()
end

In this example we can also see some examples of the Hammerspoon library in action, in particular two extremely powerful libraries: {{{hsapi(hs.mouse)}}} for interacting with the mouse pointer, and {{{hsapi(hs.eventtap)}}}, which allows you to both intercept and generate arbitrary system events, including key pressed and mouse clicks. This function simulates a double click on the current pointer position:

  1. We first get the current position of the mouse pointer using {{{hsapi(hs.mouse,getAbsolutePosition)}}}.
  2. We create a new mouse event of type {{{hsapi(hs.eventtap.event,types,=leftMouseDown=)}}} in the obtained coordinates and with the given modifiers.
  3. By convention, most Hammerspoon API methods return the same object on which they operate. This allows us to chain the calls as shown: setProperty() is called on the hs.eventtap object returned by newMouseEvent to set its type to a double click, and post() is called on the result to issue the event.
  4. Since we are generating system events directly, we also need to take care of generating a “mouse up” event at the end.

Function parameters are always optional, and those not passed will default to nil, so you need to do proper validation. In this example, the function can be called as leftDoubleClick(), without any parameters, which means the modifiers parameter might have a nil value. Looking at the {{{hsapi(hs.eventtap.event,newMouseEvent,documentation for newMouseEvent())}}}, we see that the parameter is optional, so for this particular function our use is OK.

You should try this function to see that it works. Adding it to you ~/.hammerspoon/init.lua function will make Hammerspoon define it the next time you reload your configuration. You could then try calling it from the console, but the easiest is to bind a hotkey that will generate a double click. For example:

hs.hotkey.bindSpec({ { "cmd", "ctrl", "alt" }, "p" },
                   leftDoubleClick)

Once you reload your config, you can generate a double click by moving the cursor where you want it and pressing {{{keys(Ctrl ⌘ Alt p)}}}. While this is a contrived example, the ability to generate events like this is immensely powerful in automating your system.

{{% tip %}}

By now you have seen that we are using {{{keys(Ctrl ⌘ Alt)}}} very frequently in our keybindings. To avoid having to type this every time, and since the modifiers are defined as an array, you can define them as variable. For example, I have the following at the top of my init.lua:

hyper = {"cmd","alt","ctrl"}
shift_hyper = {"cmd","alt","ctrl","shift"}

Then I simply use hyper or shift_hyper in my key binding declarations:

hs.hotkey.bindSpec({ hyper, "p" }, leftDoubleClick)

{{% /tip %}}

Until next time!

In the next installment, we will dive into Lua’s types and data structures. In the meantime, feel free to explore and learn on your own. If you need more information, I can recommend the following resources, which I have found useful:

Just Enough Lua to Be Productive in Hammerspoon, Part 2

In this second article of the “Just Enough Lua” series, we dive into Lua’s types and data structures.

{{% tip %}} If you haven’t already, be sure to read the first installment of this series to learn about the basic Lua concepts. {{% /tip %}}

Tables

Table are the only compound data type in Lua, and are used to implement arrays, associative arrays (commonly called “maps” or “hashes” in other languages), modules, objects and namespaces. As you can see, it is very important to understand them!

A table in Lua is a collection of values, which can be indexed either by numbers or by arbitrary strings (the two types of indices can coexist within the same table). Let’s go through a few examples that will give you an overview (you can type these in the Hammerspoon console as we go, or at the prompt of the hs command - keep in mind that some of the statements are broken across multiple lines here for formatting, but each statement should be type in a single line in the console).

Table literals are declared using curly braces:

> unicorns = {}  -- empty table
> people = { "Chris", "Aaron", "Diego" }  -- array
> handles = { Diego = "zzamboni",
              Chris = "cmsj",
              Aaron = "asmagill" } -- associative array

Indices are indicated using square brackets. Numeric indices start at 1 (not 0 as in most other languages). For identifier-like string indices, you can use the dot shortcut. Requesting a non-existent index returns nil:

> unicorns[1]
nil
> people[0]
nil
> people[1]
Chris
> handles['Diego']
zzamboni
> handles.Diego
zzamboni
> handles.Michael
nil

Within the curly-brace notation, indices that are not identifier-like (letters, numbers, underscores) need to be enclosed in quotes and square brackets. Values can be tables as well:

colors = { ["U.S."] = { "red", "white", "blue" },
           Mexico = { "green", "white", "red" },
           Germany = { "black", "red", "yellow" } }

With non-identifier indices, you cannot use the dot-notation. Also, to see a table within the Hammerspoon console, use {{{hsapi(hs.inspect)}}}:

> colors["U.S."]
table: 0x618000470400
> hs.inspect(colors.Mexico)
{ "green", "white", "red" }
> hs.inspect(colors)
{
  Germany = { "black", "red", "yellow" },
  Mexico = { "green", "white", "red" },
  ["U.S."] = { "red", "white", "blue" }
}

Iteration through an array is commonly done using the {{{luafun(ipairs)}}} functions. Note that it will only iterate through contiguous numeric indices starting at 1, so that it does not work well with “sparse” tables.

> for i,v in ipairs(people) do print(i, v) end
1   Chris
2   Aaron
3   Diego
> people[4]='John'
> for i,v in ipairs(people) do print(i, v) end
1   Chris
2   Aaron
3   Diego
4   John
> people[7]='Mike'
> for i,v in ipairs(people) do print(i, v) end
1   Chris
2   Aaron
3   Diego
4   John
> hs.inspect(people)
{ "Chris", "Aaron", "Diego", "John",
  [7] = "Mike"
}

The {{{luafun(pairs)}}} function, on the other hand, will iterate through all the elements in the table (both numeric and string indices), but does not guarantee their order. Both numeric and string indices can be mixed in a single table (although this gets confusing quickly unless you access everything using {{{luafun(pairs)}}}).

> for i,v in pairs(people) do print(i,v) end
1   Chris
2   Aaron
3   Diego
4   John
7   Mike
> for i,v in ipairs(handles) do print(i,v) end
<no output>
> for i,v in pairs(handles) do print(i,v) end
Aaron   asmagill
Diego   zzamboni
Chris   cmsj
> handles[1]='whoa'  -- assign the first numeric index
> hs.inspect(handles)
{ "whoa",
  Aaron = "asmagill",
  Chris = "cmsj",
  Diego = "zzamboni"
}
> for i,v in ipairs(handles) do print(i,v) end
1   whoa

The built-in {{{luadoc(table,6.6)}}} module includes a number of useful table-manipulation functions, including the following:

  • {{{luafun(table.concat)}}} for joining the values of a list in a single string (equivalent to join in other languages). This only joins the elements that would be returned by {{{luafun(ipairs)}}}.
    > table.concat(people, ", ")
    Chris, Aaron, Diego, John
        
  • {{{luafun(table.insert)}}} adds an element to a list, by default adding it to the end.
    > hs.inspect(people)
    { "Chris", "Aaron", "Diego", "John", "Bill",
      [7] = "Mike"
    }
    > table.insert(people, "George")
    > hs.inspect(people)
    { "Chris", "Aaron", "Diego", "John", "Bill", "George", "Mike" }
        

    Note how in the last example, the contiguous indices have finally caught up to 7, so the last element is no longer shown separately (and will now be included by {{{luafun(ipairs)}}}, {{{luafun(table.concat)}}}, etc.

  • {{{luafun(table.remove)}}} removes an element from a list, by default the last one. It returns the removed element.
    > for i=1,4 do print(table.remove(people)) end
    Mike
    George
    Bill
    John
    > hs.inspect(people)
    { "Chris", "Aaron", "Diego" }
        

Notable omissions from the language and the {{{luadoc(table,6.6)}}} module are “get keys” and “get values” functions, common in other languages. This may be explained by the flexible nature of Lua tables, so that those functions would need to behave differently depending on the contents of the table. If you need them, you can easily build your own. For example, if you want to get a sorted list of the keys in a table, you can use this function:

function sortedkeys(tab)
  local keys={}
  for k,v in pairs(tab) do table.insert(keys, k) end
  table.sort(keys)
  return keys
end

Tables as namespaces

Functions in Lua are first-class objects, which means they can be used like any other value. This means that functions can be stored in tables, and this is how namespaces (or “modules”) are implemented in Lua. We can inspect an manipulate them like any other table. Let us look at the {{{luadoc(table,6.6)}}} library itself. First, the module itself is a table:

> table
table: 0x61800046f740

Second, we can inspect its contents using the functions we know:

> hs.inspect(table)
{
  concat = <function 1>,
  insert = <function 2>,
  move = <function 3>,
  pack = <function 4>,
  remove = <function 5>,
  sort = <function 6>,
  sortedkeys = <function 7>,
  unpack = <function 8>
}

The function values themselves are opaque (we cannot see their code), but we can easily extend the module. For example, we could add our sortedkeys() function above to the table module for consistency. Lua allows us to specify the namespace of a function in its declaration:

function table.sortedkeys(tab)
  local keys={}
  for k,v in pairs(tab) do table.insert(keys, k) end
  table.sort(keys)
  return keys
end

All the Hammerspoon modules are implemented the same way:

> type(hs)
table
> type(hs.mouse)
table
> hs.inspect(hs.mouse)
{
  get = <function 1>,
  getAbsolutePosition = <function 2>,
  getButtons = <function 3>,
  getCurrentScreen = <function 4>,
  getRelativePosition = <function 5>,
  set = <function 6>,
  setAbsolutePosition = <function 7>,
  setRelativePosition = <function 8>,
  trackingSpeed = <function 9>
}

The common way of defining a new module in Lua is to create an empty table, and populate it with functions or variables as needed. For example, let’s put our double-click generator in a module. Create the file ~/.hammerspoon/doubleclick.lua with the following contents:

local mod={}

mod.default_modifiers={}

function mod.leftDoubleClick(modifiers)
  modifiers = modifiers or mod.default_modifiers
  local pos=hs.mouse.getAbsolutePosition()
  hs.eventtap.event.newMouseEvent(
    hs.eventtap.event.types.leftMouseDown, pos, modifiers)
    :setProperty(hs.eventtap.event.properties.mouseEventClickState, 2)
    :post()
  hs.eventtap.event.newMouseEvent(
    hs.eventtap.event.types.leftMouseUp, pos, modifiers):post()
end

function mod.bindto(keyspec)
  hs.hotkey.bindSpec(keyspec, mod.leftDoubleClick)
end

return mod

You can then, from the console, do the following:

> doubleclick=require('doubleclick')
> doubleclick.bindto({ {"ctrl", "alt", "cmd"}, "p" })
19:53:53     hotkey: Disabled previous hotkey ⌘⌃⌥P
             hotkey: Enabled hotkey ⌘⌃⌥P

You have written and loaded your first Lua module. Let’s try it out! Press {{{keys(Ctrl ⌘ Alt p)}}} while your cursor is over a word in your terminal or web browser, to select it as if you had double-clicked it. You can also change the modifiers used with it. For example, did you know that Cmd-double-click can be used to open URLs from the macOS Terminal application?

> doubleclick.default_modifiers={cmd=true}

Now try pressing {{{keys(Ctrl ⌘ Alt p)}}} while your pointer is over a URL displayed on your Terminal (you can just type one yourself to test), and it will open in your browser.

Note that the name doubleclick does not have any special meaning—it is a regular variable to which you assigned the value returned by require('doubleclick'), which is the value of the mod variable created within the module file (note that within the module file you use the local variable name to refer to functions and variables within itself). You could assign it to any name you want:

> a=require('doubleclick')
> a.leftDoubleClick()

The argument of the {{{luafun(require)}}} function is the name of the file to load, without the .lua extension. Hammerspoon by default adds your ~/.hammerspoon/ directory to its load path, along with any other default directories in your system. You can view the places where Hammerspoon will look for files by examining the package.path variable. On my machine I get the following:

> package.path
/Users/zzamboni/.hammerspoon/?.lua;/Users/zzamboni/.hammerspoon/?/
init.lua;/Users/zzamboni/.hammerspoon/Spoons/?.spoon/init.lua;/usr/
local/share/lua/5.3/?.lua;/usr/local/share/lua/5.3/?/init.lua;/usr/
local/lib/lua/5.3/?.lua;/usr/local/lib/lua/5.3/?/init.lua;./?.lua;
./?/init.lua;/Users/zzamboni/Dropbox/Personal/devel/hammerspoon/
hammerspoon/build/Hammerspoon.app/Contents/Resources/extensions/?.lua;
/Users/zzamboni/Dropbox/Personal/devel/hammerspoon/hammerspoon/build/
Hammerspoon.app/Contents/Resources/extensions/?/init.lua

{{% tip %}}

Hammerspoon automatically loads any modules under the hs namespace the first time you use them. For example, when you use {{{hsapi(hs.application)}}} for the first time, you will see a message in the console:

> hs.application.get("Terminal")
2017-10-31 06:47:15: -- Loading extension: application
hs.application: Terminal (0x61000044dfd8)

If you want to avoid these messages, you need to explicitly load the modules and assign them to variables, as follows:

> app=require('hs.application')
> app.get("Terminal")
hs.application: Terminal (0x610000e49118)

This avoids the console message and has the additional benefit of allowing you to use app (you can use whatever variable you want) instead of typing hs.application in your code. This is a matter of taste—I usually prefer to have the full descriptive names (makes the code easier to read), but when dealing with some of the longer module names (e.g. {{{hsapi(hs.distributednotifications)}}}), this technique can be useful.

{{% /tip %}}

Patterns

If you are familiar with regular expressions, you know how powerful they are for examining and manipulating strings in any programming language. Lua has {{{luadoc(patterns,6.4.1)}}}, which fulfill many of the same functions but have a different syntax and some limitations. They are used by many functions in the string library like {{{luafun(string.find)}}} and {{{luafun(string.match)}}}.

The following are some differences and similarities you need to be aware of when using patterns:

  • The dot (.) represents any character, just like in regexes.
  • The asterisk (*), plus sign (+) and question mark (?) represent “zero or more”, “one or more” and “one or none” of the previous character, just like in regexes. Unlike regexes, these characters can only be applied to a single character and not to a whole capture group (i.e. the regex (foo)+ is not possible).
  • Alternations, represented by the vertical bar (|) in regexes, are not supported.
  • The caret (^) and dollar sign ($) represent “beginning of string” and “end of string”, just like in regexes.
  • The dash (-) represents a non-greedy “zero or more” (i.e. match the shortest possible string instead of the longest one) of the previous character, unlike in regexes, in which it’s commonly indicate by a question mark following the corresponding * or + The regex .*? is equivalent to the Lua pattern .-.
  • The escape character is the ampersand (%) instead of the backslash (\).
  • Most character classes are represented by the same characters, but preceded by ampersand. For example %d for digits, %s for spaces, %w for alphanumeric characters.

For most common use cases, Lua patterns are enough, you just have to be aware of their differences. If you encounter something that really cannot be done, you can always resort to libraries like Lrexlib, which provide interfaces to real regex libraries. Unfortunately these are not included in Lua, so you would need to install them on your own.

Patterns, just like regular expressions, are commonly used for string manipulation, using primarily functions from the {{{luadoc(string,6.4)}}} library.

String manipulation

Lua includes the {{{luadoc(string,6.4)}}} library to implement common string manipulation functions, including pattern matching. All of these functions can be called either as regular functions, with the string as the first argument, or as method calls on the string itself, using the colon syntax (which, as we saw before, gets converted to the same call). For example, the following two are equivalent:

string.find(a, "^foo")
a:find("^foo")

You can find the full documentation in the Lua reference manual and many other examples in the Lua-users wiki String Library Tutorial. The following is a partial list of some of the functions I have found most useful:

  • {{{luafun(string.find,=string.find(str\, pat\, pos\, plain)=)}}} finds the pattern within the string. By default the search starts at the beginning of the string, but can be modified with the pos argument (index starts at 1, as with the tables). By default pat is intepreted as a Lua pattern, but this can be disabled by passing plain as a true value. If the pattern is not found, returns nil. If the pattern is found, the function returns the start and end position of the pattern within the string. Furthermore, if the pattern contains parenthesis capture groups, all groups are returned as well. For example:
    > string.find("bah", "ah")
    2   3
    > string.find("bah", "foo")
    nil
    > string.find("bah", "(ah)")
    2   3   ah
    > p1, p2, g1, g2 = string.find("bah", "(b)(ah)")
    > p1,p2,g1,g2
    1   3   b   ah
        

    Note that the return value is not a table, but rather multiple values, as shown in the last example.

    {{% tip %}}

It can sometimes be convenient to handle multiple values as a table or as separate entities, depending on the circumstances. For example, you may have a programmatically-constructed pattern with a variable number of capture groups, so you cannot know to how many variables you need to assign the result. In this case, the {{{luafun(table.pack)}}} and {{{luafun(table.unpack)}}} functions can be useful.

{{{luafun(table.pack)}}} takes a variable number of arguments and returns them in a table which contains an array component containing the values, plus an index n containing the total number of elements:

> res = table.pack(string.find("bah", "(b)(ah)"))
> res
table: 0x608000c76e80
> hs.inspect(res)
{ 1, 3, "b", "ah",
  n = 4
}

{{{luafun(table.unpack)}}} does the opposite, expanding an array into separate values which can be assigned to separate values as needed, or passed as arguments to a function:

> args={"bah", "(b)(ah)"}
> string.find(args)
[string "return string.find(args)"]:1:
  bad argument #1 to 'find' (string expected, got table)
> string.find(table.unpack(args))
1   3   b   ah

{{% /tip %}}

  • {{{luafun(string.match,=string.match(str\, pat\, pos)=)}}} is similar to string.find, but it does not return the positions, rather it returns the part of the string matched by the pattern, or if the pattern contains capture groups, returns the captured segments:
    > string.match("bah", "ah")
    ah
    > string.match("bah", "foo")
    nil
    > string.match("bah", "(b)(ah)")
    b   ah
        
  • {{{luafun(string.gmatch,=string.gmatch(str\, pat)=)}}} returns a function that returns the next match of pat within str every time it is called, returning nil when there are no more matches. If pat contains capture groups, they are returned on each iteration.
    > a="Hammerspoon is awesome!"
    > f=string.gmatch(a, "(%w+)")
    > f()
    Hammerspoon
    > f()
    is
    > f()
    awesome
    > f()
        

    Most commonly, this is used inside a loop:

    > for cap in string.gmatch(a, "%w+") do print(cap) end
    Hammerspoon
    is
    awesome
        
  • {{{luafun(string.format,=string.format(formatstring\, …​)=)}}} formats a sequence of values according to the given format string, following the same formatting rules as the ISO C sprintf() function. It additionally supports a new format character %q, which formats a string value in a way that can be read back by Lua, escaping or quoting characters as needed (for example quotes, newlines, etc.).
  • {{{luafun(string.len,=string.len(str)=)}}} returns the length of the string.
  • {{{luafun(string.lower,=string.lower(str)=)}}} and {{{luafun(string.upper,=string.upper(str)=)}}} convert the string to lower and uppercase, respectively.
  • {{{luafun(string.gsub,=string.gsub(str\, pat\, rep\, n)=)}}} is a very powerful string-replacement function which hides considerably more power than its simple syntax would lead you to believe. In general, it replaces all (or the first n) occurrences of pat in str with the replacement rep. However, rep can take any of the following values:
    • A string which is used for the replacement. If the string contains %m, where m is a number, the it is replaced by the m-th captured group (or the whole match if m is zero).
    • A table which is consulted for the replacement values, using the first capture group as a key (or the whole match if there are no captures). For example:
      > a="Event type codes: leftMouseDown=$leftMouseDown, rightMouseDown=$rightMouseDown, mouseMoved=$mouseMoved"
      > a:gsub("%$(%w+)", hs.eventtap.event.types)
      Event type codes: leftMouseDown=1, rightMouseDown=3, mouseMoved=5   3
              
    • A function which is executed with the captured groups (or the whole match) as an argument, and whose return value is used as the replacement. For example, using the os.getenv function, we can easily replace environment variables by their values in a string:
      > a="Hello $USER, your home directory is $HOME"
      > a:gsub("%$(%w+)", os.getenv)
      Hello zzamboni, your home directory is /Users/zzamboni  2
              

    Note that gsub returns the modified string as its first return value, and the number of replacements it made as the second (2 in the example above). If you don’t need the number, you can simply ignore it (you don’t even need to assign it). Also note that gsub does not modify the original string, only returns a copy with the changes:

    > b = a:gsub("%$(%w+)", os.getenv)
    > b
    Hello zzamboni, your home directory is /Users/zzamboni
    > a
    Hello $USER, your home directory is $HOME
        

Keep learning!

You know now enough Lua to start being productive with Hammerspoon. You’ll pick up more details as you play with it. If you need more information, I can recommend the following resources, which I have found useful:

Have fun!

First release of “Learning Hammerspoon”

I am happy to announce the first release of my new book “Learning Hammerspoon”, a book devoted to using Hammerspoon to make using your Mac easier, faster and more fun.

You can find more information, read a free sample, and purchase it at LeanPub.

{{< hsbookimglink >}}

This book is based partly on blog posts published in my blog, but with a lot of new material. In this first release, the following chapters are finished:

  • Getting started with Hammerspoon
  • Using Spoons in Hammerspoon
  • Just enough Lua to be productive with Hammerspoon
  • Using and extending Seal

It is not complete yet, but already includes a lot of useful information. I will be updating it frequently, and of course you get all updates for free.

I look forward to your feedback! Feel free to message me through the /Email the author/ page at LeanPub.

Elvish

Bang-Bang (!!, !$) Shell Shortcuts in Elvish

(Updated on March 19th, 2018 to use the new Elvish Package Manager)

The bash shortcuts (maybe older? I’m not sure in which shell these originated) for “last command” (!!) and “last argument of last command” (!$) are, for me at least, among the most strongly imprinted in my muscle memory, and I use them all the time. Although these shortcuts are not available in Elvish by default, they are easy to implement. I have written a module called bang-bang which you can readily use as follows:

  • Use epm to install my elvish-modules package (you can also add this to your rc.elv file to have the package installed automatically if needed):
    use epm
    epm:install github.com/zzamboni/elvish-modules
        
  • In your rc.elv (see mine as an example), add the following to load the bang-bang module and to set up the appropriate keybindings:
    use github.com/zzamboni/elvish-modules/bang-bang
        

That’s it! Start a new shell window, and test how command-history mode can be invoked by the ! key. Assuming your last command was ls -l ~/.elvish/rc.elv, when you press ! you will see the following:

bang-lastcmd [A C] _
! ls -l .elvish/rc.elv
0 ls
1 -l
2/$ .elvish/rc.elv
Alt-! !

If you press ! again, the whole last command will be inserted. If you press $ (or 2), only the last argument will be inserted. You can insert any other component of the previous command using its corresponding number. If you want to insert an exclamation sign, you can press Alt-!.

Note that by default, Alt-! will also be bound to trigger this mode, so you can fully replace the default “last command” mode in Elvish.

Have fun with Elvish!

Using and writing completions in Elvish

Like other Unix shells, Elvish has advanced command-argument completion capabilities. In this article I will explore the existing completions, and show you how you can create your own (and contribute them back to the community).

Using existing completions

There is a growing body of shell completions that you can simply load and use.

Elvish has a still-small but growing collection of completions that have been created by its users. These are a few that I know of (let me know if you know others!):

  • My own zzamboni/elvish-completions package, which contains completions for git (providing automatically-generated completions for all commands and their options, plus hand-crafted argument completions for many of them), ssh, vcsh, cd, and a few of Elvish’s built-in functions and modules. It also contains comp, a framework for building completers, which we will explore in more detail below. To use any of these modules, you just need to install the elvish-completions package, and then load the modules you want. For example:
    epm:install &silent-if-installed github.com/zzamboni/elvish-completions
    use github.com/zzamboni/elvish-completions/vcsh
    use github.com/zzamboni/elvish-completions/cd
    use github.com/zzamboni/elvish-completions/ssh
    use github.com/zzamboni/elvish-completions/builtins
    use github.com/zzamboni/elvish-completions/git
        
  • xiaq’s edit.elv/compl/go.elv, which provides extensive hand-crafted completions for go. You can also install this one as an Elvish package:
    epm:install &silent-if-installed github.com/xiaq/edit.elv
    use github.com/xiaq/edit.elv/compl/go
    go:apply
        
  • occivink’s completers.elv file, which contains completers for kak, ssh, systemctl, ffmpeg and a few other commands.
  • Tw’s completer/ files, which contains completions for adb, git and ssh.
  • SolitudeSF’s completers.elv file, which contains completers for cd, kak, kitty, git, man, pkill and quite a few other commands.

As of this writing, there is no “official” collection of Elvish completions, so feel free to look at the existing ones and choose/use the ones that work best for you.

Since the collection is not yet very big, it’s likely you will want to build your own completions. This is what the next section is about.

Creating your own completions

Elvish has a simple but powerful argument-completion mechanism. You can find the full documentation in the Elvish reference, but let’s take a quick look here.

Basic (built-in) argument completion mechanisms

Command argument completion in Elvish is implemented by functions stored inside $edit:completion:arg-completer. This variable is a map in which the indices are command names, and the values are functions which must receive a variable number of arguments. When the user types cat Space Tab, the function stored in $edit:completion:arg-completer[cat] (if any) is called, as follows:

$edit:completion:arg-completer[cat] cat ''

The function receives the full current command line as arguments, including the current argument, which might be empty as in the example above, or be a partially typed string. For example, if the user types cat f Tab, the completer function will be called like this:

$edit:completion:arg-completer[cat] cat 'f'

The completion function must use its arguments to determine the appropriate completions at that point, and return them by one of the following methods (which can be combined):

  • Output the completions to stdout, one per line;
  • Output the completions to the data stream (using put);
  • Output the completions using the edit:complex-candidate command, which can additionally specify a suffix to append to the completion in the completion menu or in the returned value, and a style to use (as accepted by edit:styled). The full syntax of edit:complex-candidate is as follows:
    edit:complex-candidate &code-suffix='' &display-suffix='' &style='' $string
        

    $string is the option to display; &code-suffix indicates a suffix to be appended to the completion string when the user selects it; &display-suffix indicates a suffix to be shown in the completion menu (but which is not returned as part of the completion); and &style indicates a text style to use in the completion menu.

Keep in mind that the options returned by the completion function are additionally filtered by what the user has typed so far. This means that the last argument can usually be ignored, since Elvish will automatically do the filtering. An exception to this is if you want to return different types of things depending on what the user has typed already. For example, if the last argument start with -, you may want to return the possible command-line options, and return regular argument completions otherwise.

Example #1: A very simple completer for the brew command:

edit:completion:arg-completer[brew] = [@cmd]{
  len = (count $cmd)
  if (eq $len 2) {
    if (has-prefix $cmd[-1] -) {
      put '--version' '--verbose'
    } else {
      put install uninstall
    }
  } elif (eq $len 3) {
    brew search | eawk [l @f]{ put $@f }
  }
}

If the function receives two arguments, we check to see if the last argument begins with a dash. If so, we return the possible command-line options, otherwise we return the two commands install and uninstall. If we receive three arguments (i.e. we are past the initial command), we return the list of possible packages to install or uninstall.

You may noticed that there are many cases that this simple function does not handle correctly. For example, if you type brew --verbose Space Tab, you get the list of packages as completion, which does not make sense at that point. We will look at more complex and complete completion functions next.

The first step to more complex completions is the edit:complete-getopt command, which allows us to specify a sequence of positional completion functions. The general syntax of the command is:

edit:complete-getopt $args $opts $handlers

Please see its documentation for a full description of the arguments.

Example #2: The completer for brew shown before can be specified like this:

edit:completion:arg-completer[brew] = [@cmd]{
  edit:complete-getopt $cmd[1:] \
  [[&long=version] [&long=verbose]] \
  [
    [_]{ put install uninstall }
    [_]{ brew search | eawk [_ @f]{ put $@f } }
    ...
  ]
}

This new completer overcomes a few of the limitations in our previous attempt. For one, the install and uninstall commands are now properly completed even if you specify options before. Furthermore, the ... at the end of the handler list indicates that the previous one (the package names) will be repeated for all further arguments - this makes it possible to complete multiple package names to install or uninstall. However, it still has some limitations! For example, it will give you all existing packages as possible arguments to uninstall, which only accepts already installed packages.

In addition to complete-getopt, Elvish includes a few other functions to help build completers:

  • edit:complete-filename produces a listing of all the files and directories in the directory of its argument, and is the default completion function when no other completer is specified. See its documentation for full details.
  • edit:complete-sudo provides completions for commands like sudo which take a command as their first argument. It is the default completer for the sudo command, so that if you type sudo Space Tab, you get a list of all the commands on your execution path. It can be reused for other commands, for example time:
    edit:completion:arg-completer[time] = $edit:complete-sudo~
        

Finally, note that if $edit:completion:arg-completer[''] exists, it will be called as a fall-back completer if no command-specific argument completer exists. You can see that the default completer is edit:complete-filename, as mentioned before:

~> put $edit:completion:arg-completer['']
▶ $edit:complete-filename~

With the tools you know so far, you can already create fairly complex completers. In the next section, we will explore comp, an external library I wrote to make it easier to specify complex completion trees.

Complex completions using the comp framework

The built-in completion functions make it possible to build any completer you want. However, you might realize that for more complex cases, the specifications can be quite complex. For this reason, I wrote the comp library as a framework to more easily specify completion functions. The basic Elvish mechanisms and functions are still used in the backend, so you can rest assured about their compatibility with the basic mechanisms.

As a first step, if you haven’t done so already, you should install the elvish-completions package using epm:

use epm
epm:install github.com/zzamboni/elvish-completions

From the file where you will define your completions (or from your interactive session if you just want to play with it), load the comp module:

use github.com/zzamboni/elvish-completions/comp

The main entry points for this module are comp:item, comp:sequence and comp:subcommands. Each one receives a single argument containing a “completion definition”, which indicates how the completions will be produced. Each one receives a different kind of completion structure, and returns a ready-to-use completion function, which can be assigned directly to an element of $edit:completion:arg-completer. A simple example:

edit:completion:arg-completer[foo] = (comp:item [ bar baz ])

If you type this in your terminal, and then type foo<space> and press Tab, you will see the appropriate completions:

> foo <Tab>
 COMPLETING argument _
 bar  baz

To create completions for new commands, your main task is to define the corresponding completion definition. The different types of definitions and functions are explained below, with examples of the different available structures and features.

Note: the main entry points return a ready-to-use argument handler function. If you ever need to expand a completion definition directly (maybe for some advanced usage), you can call comp:-expand-item, comp:-expand-sequence and comp:-expand-subcommands, respectively. These functions all take the definition structure and the current command line, and return the appropriate completions at that point.

We now look at the different types of completion definitions understood by comp.

Items

The base building block is the “item”, can be one of the following:

  • An array containing all the potential completions (it can be empty, in which case no completions are provided). This is useful for providing a static list of completions.
  • A function which returns the potential completions (it can return nothing, in which case no completions are provided). The function should have one of the following arities, which affect which arguments will be passed to it (other arities are not valid, and in that case the item will not be executed):
    • If it takes no arguments, no arguments are passed to it.
    • If it takes a single argument, it gets the current (last) component of the command line @cmd; this is just like the handler functions understood by the edit:complete-getopt command.
    • If it takes a rest argument, it gets the full current command line (the contents of @cmd); this is just like the functions assigned to $edit:completion:arg-completer.

Example #3: a simple completer for cd

In this case, we define a function which receives the current “stem” (the part of the filename the user has typed so far) and offers all the relevant files, then filters those which are directories, and returns them as completion possibilities. We pass the function directly as a completion item to comp:-expand.

fn complete-dirs [arg]{ put {$arg}* | each [x]{ if (-is-dir $x) { put $x } } }
edit:completion:arg-completer[cd] = (comp:item $complete-dirs~)

For file and directory completion, you can use the utility function comp:files instead of defining your own function (see Utility functions). comp:files uses edit:complete-filename in the backend but offers a few additional filtering options:

edit:completion:arg-completer[cd] = (comp:item [arg]{ comp:files $arg &dirs-only })
Sequences and command-line options

Completion items can be aggregated in a sequence of items and used with the comp:sequence function when you need to provide different completions for different positional arguments of a command, including support for command-line options at the beginning of the command (comp:sequence uses edit:complete-getopt in the backend, but provides a few additional convenient features). The definition structure in this case has to be an array of items, which will be applied depending on their position within the command parameter sequence. If the the last element of the list is the string ... (three periods), the next-to-last element of the list is repeated for all later arguments. If no completions should be provided past the last argument, simply omit the periods. If a sequence should produce no completions at all, you can use an empty list []. If any specific elements of the sequence should have no completions, you can specify { comp:empty } or [] as its value.

If the &opts option is passed to the comp:sequence function, it must contain a single definition item which produces a list of command-line options that are allowed at the beginning of the command, when no other arguments have been provided. Options can be specified in either of the following formats:

  • As a string which gets converted to a long-style option; e.g. all to specify the --all option. The string must not contain the dashes at the beginning.
  • As a map in the style of complete-getopt, which may contain the following keys:
    • short for the short one-letter option;
    • long for the long-option string;
    • desc for a descriptive string which gets shown in the completion menu;
    • arg-mandatory or arg-optional: either one but not both can be set to $true to indicate whether the option takes a mandatory or optional argument;
    • arg-completer can be specified to produce completions for the option argument. If specified, it must contain completion item as described in Items, and which will be expanded to provide completions for that argument’s values.

Simple example of a completion data structure for option -t (long form --type), which has a mandatory argument which can be elv, org or txt:

[ &short=t
  &long=type
  &desc="Type of file to show"
  &arg-mandatory=$true
  &arg-completer= [ elv org txt ]
]

Note: options are only offered as completions when the use has typed a dash as the first character. Otherwise the argument completers are used.

Example #4: we can improve on the previous completer for cd by preventing more than one argument from being completed (only the first argument will be completed using complete-dirs, since the list does not end with ...):

edit:completion:arg-completer[cd] = (comp:sequence [ [arg]{ comp:files $arg &dirs-only }])

Example #5: a simple completer for ls with a subset of its options. Note that -l and -R are only provided as completions when you have not typed any filenames yet. Also note that we are using comp:files to provide the file completions, and the ... at the end of the sequence to use the same completer for all further elements.

ls-opts = [
  [ &short=l                 &desc='use a long listing format' ]
  [ &short=R &long=recursive &desc='list subdirectories recursively' ]
]
edit:completion:arg-completer[ls] = (comp:sequence &opts=$ls-opts [ $comp:files~ ... ])

Example #6: See the ssh completer for a real-world example of using sequences.

Subcommands

Finally, completion sequences can be aggregated into subcommand structures using the comp:subcommands function, to provide completion for commands such as git, which accept multiple subcommands, each with their own options and completions. In this case, the definition is a map indexed by subcommand names. The value of each element can be a comp:item, a comp:sequence or another comp:subcommands (to provide completion for sub-sub-commands, see the example below for vagrant). The comp:subcommands function can also receive the &opts option to generate any available top-level options.

Example #7: let us reimplement our completer for the brew package manager, but now with support for the install, uninstall and cat commands. install and cat gets as completions all available packages (the output of the brew search command), while uninstall only completes installed packages (the output of brew list). Note that for install and uninstall we automatically extract command-line options from their help messages using the comp:extract-opts function (wrapped into the -brew-opts function), and pass them as the &opts option in the corresponding sequence functions. Also note that all &opts elements get initialized at definition time (they are arrays), whereas the sequence completions get evaluated at runtime (they are lambdas), to automatically update according to the current packages. The cat command sequence allows only one option. The load-time initialization of the options incurs a small delay, and you could replace these with lambdas as well so that the options are computed at runtime. Note also the usage of the comp:decorate function to colorize the package names in different colors for each command.

fn -brew-opts [cmd]{
  brew $cmd -h | take 1 | \
  comp:extract-opts &regex='--(\w[\w-]*)' &regex-map=[&long= 1]
}
brew-completions = [
  &install= (comp:sequence &opts= [ (-brew-opts install) ] \
    [ { brew search | comp:decorate &style=green } ... ]
  )
  &uninstall= (comp:sequence &opts= [ (-brew-opts uninstall) ] \
    [ { brew list | comp:decorate &style=red }   ... ]
  )
  &cat= (comp:sequence [{ brew search | comp:decorate &style=blue }])
]
edit:completion:arg-completer[brew] = (comp:subcommands \
  &opts= [ version verbose ] $brew-completions
)

Note that in contrast to our previous brew completer, this definition is much more expressive, accurate, and much easier to extend.

Example #8: a simple completer for a subset of vagrant, which receives commands which may have subcommands and options of their own. Note that the value of &up is a comp:sequence, but the value of &box is another comp:subcommands which includes the completions for box add and box remove. Also note the use of the comp:extract-opts function to extract the command-line arguments automatically from the help messages. The output of the vagrant help messages matches the default format expected by comp:extract-opts, so we don’t even have to specify a regular expression like for brew.

Tip: note that the values of &opts are functions (e.g. { vagrant-up -h | comp:extract-opts }) instead of arrays (e.g. ( vagrant up -h | comp:extract-opts )). As mentioned in the previous example, both are valid, but in the latter case they are all initialized at load time (when the data structure is defined), which might introduce a delay, particularly with more command definitions. By using functions the options are only extracted at runtime when the completion is requested. For further optimization, vagrant-opts could be made to memoize the values so that the delay only occurs the first time.

vagrant-completions = [
  &up= (comp:sequence [] \
    &opts= { vagrant up -h | comp:extract-opts }
  )
  &box= (comp:subcommands [
      &add= (comp:sequence [] \
        &opts= { vagrant box add -h | comp:extract-opts }
      )
      &remove= (comp:sequence [ { \
            vagrant box list | eawk [_ @f]{ put $f[0] } \
        } ... ] \
        &opts= { vagrant box remove -h | comp:extract-opts }
      )
])]

edit:completion:arg-completer[vagrant] = (comp:subcommands \
  &opts= [ version help ] $vagrant-completions
)

Example #9: See the git completer for a real-world subcommand completion example, which also shows how extensively auto-population of subcommands and options can be done by extracting information from help messages.

Utility functions

The comp module includes a few utility functions, some of which you have seen already in the examples.

comp:decorate maps its input through edit:complex-candidate with the given options. Can be passed the same options as edit:complex-candidate. In addition, if &suffix is specified, it is used to set both &display-suffix and &code-suffix. Input can be given either as arguments or through the pipeline:

> comp:decorate &suffix=":" foo bar
▶ (edit:complex-candidate foo &code-suffix=: &display-suffix=: &style='')
▶ (edit:complex-candidate bar &code-suffix=: &display-suffix=: &style='')
> put foo bar | comp:decorate &style="red"
▶ (edit:complex-candidate foo &code-suffix='' &display-suffix='' &style=31)
▶ (edit:complex-candidate bar &code-suffix='' &display-suffix='' &style=31)

comp:extract-opts takes input from the pipeline and extracts command-line option data structures from its output. By default it understand the following common formats:

-o, --option                Option description
-p, --print[=<what>]        Option with an optional argument
    --select <type>         Option with a mandatory argument

Typical use would be to populate an &opts element with something like this:

comp:sequence &opts= { vagrant -h | comp:extract-opts } [ ... ]

The regular expression used to extract the options can be specified with the &regex option. Its default value (which parses the common formats shown above) is:

&regex='^\s*(?:-(\w),?\s*)?(?:--?([\w-]+))?(?:\[=(\S+)\]|[ =](\S+))?\s*?\s\s(\w.*)$'

The mapping of capture groups from the regex to option components is defined by the &regex-map option. Its default value (which also shows the available fields) is:

&regex-map=[&short=1 &long=2 &arg-optional=3 &arg-mandatory=4 &desc=5]

At least one of short or long must be present in regex-map. The arg-optional and arg-mandatory groups, if present, are handled specially: if any of them is not empty, then its contents is stored as arg-desc in the output, and the corresponding arg-mandatory / arg-optional is set to $true.

If &fold is $true, then the input is preprocessed to join option descriptions which span more than one line (the heuristic is not perfect and may not work in all cases, also for now it only joins one line after the option).

Contributing your completions

So you have created a brand-new completion function and would like to share it with the Elvish community. Nothing could be easier! You have two main options:

  • Publish them on your own. For example, if you put your .elv files into their own repository in GitHub or Gitlab, they are ready to be installed and used using epm.
  • Contribute it to an existing repository (for example elvish-completions). Just add your files, submit a pull request, and you are done.

I hope you have found this tutorial useful. Please let me know in the comments if you have any questions, feedback or if you find something that is incorrect.

Now, go have fun with Elvish!

Other

My blogging workflow with Emacs, org-mode+ox-hugo, Hugo and GitHub

My blogging has seen multiple iterations over the years, and with it, the tools I use have changed. At the moment I use a set of tools and workflows which make it very easy to keep my blog updated, and I will describe them in this post. In short, they are:

  • Writing: Emacs, org-mode
  • Exporting: ox-hugo
  • Publishing: Hugo
  • Hosting: GitHub Pages

Let’s take a closer look at each of the stages.

Writing using Emacs and org-mode

I have been using Emacs for almost 30 years, so its use for me is second nature. However, I only recently started using org-mode for writing, blogging, coding, presentations and more, thanks to the hearty recommendations and information from Nick and many others. I am duly impressed. I have been a fan of the idea of literate programming for many years, and I have tried other tools before (most notably noweb, which I used during grad school for many of my homeworks and projects), but org-mode is the first tool I have encountered which seems to make it practical. Here are some of the resources I have found useful in learning it:

You can see some examples in my “literate config files” series, and all recent posts in this blog are written using org-mode (you can find the source file in GitHub).

Over time I have tweaked my Emacs configuration to make writing with org-mode more pleasant. You can see my tutorial on Beautifying org-mode in Emacs, and also see the full org-mode section of my Emacs config for reference.

So, I write posts using Emacs, in org-mode markup. What’s next?

Exporting

When I first started writing my blog posts in org-mode, I relied on Hugo’s built-in support for it, which allows you to simply create posts in .org files instead of .md and have them parse in org-mode format. Unfortunately, the support is not perfect. Hugo relies on the goorgeous library which, while quite powerful, does not support the full org-mode markup capabilities, so many elements are not rendered or processed properly.

Happily, I discovered ox-hugo, an org-mode exporter which produces Hugo-ready Markdown files from the org-mode source, from which Hugo can produce the final HTML output. This is a much better arrangement, because each component handles only its native format: ox-hugo processes the org-mode source with the full support of org-mode and Emacs, and Hugo processes Markdown files, which are its native input format. You can use the full range of org-mode markup in your posts, and they will be correctly converted to their equivalents in Markdown. Furthermore, your source files remain output-agnostic, as you can still use all other org-mode exporters if you need to produce other formats.

Ox-hugo supports two ways of organizing your posts: one post per org file, and one post per org subtree. In the first one, you write a separate org file for each post. In the second, you keep all your posts in a single org file, and specify (through org-mode properties) which subtrees should be exported as posts. The latter is the recommended way to organize posts. At first I was skeptical - who wants to keep everything in a single file? However, as I have worked more with it, I have come to realize its advantages. For one, it makes it easier to specify post metadata - for example, I have defined sections in my org-mode source file for certain frequent topics, and those are tagged accordingly in the org source. When I create posts as subtrees of those sections, they inherit the top-level tags automatically, as well as any other properties, which I used to define (for example) the header images used in the posts. Having all posts in a single file also makes it easier to share other content, such as org macro definitions, ox-hugo configuration options, etc.

Note that ox-hugo is not limited to exporting blog posts, but any content processed by Hugo. For example, my org source file also includes all the static pages in my web site - they are differentiated from blog posts simply by the Hugo section to which they belong, which is defined using the HUGO_SECTION property, which is interpreted accordingly by ox-hugo.

Since the full power of org markup is available when using ox-hugo, you can do very interesting things. For example, all posts in my Literate Config Files category are automatically updated every time I export them with the actual, real content of the corresponding config files, which I also keep in org format. There is a lot of hidden power in org-mode and ox-hugo. My recommendation is to go through the source files for some of the websites listed in ox-hugo’s Real World Examples section. For example, I have learned a lot by reading through the source files for the ox-hugo website itself.

Publishing

Using Netlify. Caveats:

Once ox-hugo has generated the Markdown files for my posts, it is Hugo’s turn to convert them into HTML files that can be published as a website. Ox-hugo knows the default structure expected by Hugo (a top-level content/ directory in which you have directories for each section), so there’s usually not much to do other than point ox-hugo to where your top-level Hugo directory is, using the HUGO_BASE_DIR property. Of course, this presumes you already have a Hugo site. If you have never used Hugo before, I would suggest you go through the Quick Start guide, which shows how to set up a basic website using the Ananke theme. This is the same theme I use for my website.

One particular piece of configuration I use to make publishing with GitHub pages easier: I change Hugo’s publishDir parameter from its default value of public to docs, to make it easier to publish my final website from within the same repository (more below in the Hosting section). This is done by specifying the parameter in Hugo’s =config.toml= file:

publishDir = "docs"

Hugo has extensive capabilities and it is beyond the scope of this article to show you how to use it, but it has very good documentation which I would urge you to peruse to learn more about it. Feel free to peruse my setup for ideas.

Hosting

  • Publishing from docs/
  • Setting up SSL certificate

What’s next?

  • Automatic publishing using GitHub CI
  • Maybe migrate to GitLab pages?

Footnotes