Harry R. Schwartz

Code writer, sometime Internet enthusiast, attractive nuisance.

Vancouver

British Columbia

Canada

vegan


Build Your Own Snippet Manager

hrs

Published .
Tags: beards, ruby, unix.

Way back in the day, when I used a Mac for a few years, I really liked the TextExpander tool. You hit a key, pick a snippet, and it’ll paste the snippet in the currently focused text area. It can do all kinds of fancy other things, like inserting timestamps into your snippets, too, but I only ever used a subset of its features.

I found myself missing that functionality on my Linux machine a year or two ago, so I decided to write my own! I’m sure there are plenty of existing tools that’ll do what I want, but it’s not too complicated to build my own and it’s a fun process: I’m a professional engineer,1 but I’m also a “computer hobbyist,” and I take some intrinsic pleasure in building and using my own tools.2

I just found myself explaining my setup to someone and wishing I had a post to direct them toward, so here we are!

I use three external dependencies in my manager:

  • xdotool, a utility to send keyboard and mouse events to X from the command line. You can use it to programmatically move the mouse around, click, and type keys.
  • rofi, a graphical fuzzy-finder and application launcher. It’s similar to dmenu, or a subset of Alfred.
  • fuzz, a little gem I wrote a while back for integrating with fuzzy-finders like rofi through Ruby scripts.

To install those dependencies (on Debian or Ubuntu, at least):

$ sudo apt install xdotool rofi
$ gem install fuzz

Here’s how they work together.

I’ve added a keybinding in i3, my window manager, that invokes a Ruby script when I hit Super-s (for “snippet”). Here’s the line from my ~/.i3/config:

# Insert a snippet
bindsym Mod4+s exec ~/bin/snippet.rb

The snippet.rb script defines some snippets, invokes rofi through fuzz to let me choose one, and then shells out to xdotool to type the snippet.

Here’s the script:

#!/usr/bin/env ruby

require "fuzz"

class Snippet
  attr_reader :description, :text

  def initialize(text, description = nil)
    @text = text
    @description = description || text
  end

  def to_s
    description
  end
end

SNIPPETS = [
  # Personal info
  ["hello@harryrschwartz.com"],
  ["https://harryrschwartz.com"],
  ["https://github.com/hrs"],
  # ... elided snippets ...

  # Mathematical symbols and Unicode
  ["→", "→) right arrow"],
  ["∧", "∧) logical and"],
  # ... elided snippets ...
  ["¢", "¢) cent"],
  ["°", "°) degree"],
  ["§", "§) section"],

  # Malarkey
  ["ಠ_ಠ", "ಠ_ಠ) look of disapproval"],
].map { |pair| Snippet.new(*pair) }

snippet = Fuzz::Selector.new(
  SNIPPETS,
  cache: Fuzz::Cache.new("~/.cache/fuzz/snippets"),
).pick

system("xdotool type \"#{ snippet.text }\"")

A Snippet has both text (that’s what gets typed) and a description (what the user sees in the fuzzy finder). Distinguishing between the two makes it easy to fuzzy-find Unicode characters that would otherwise be hard to type.

We define a list of lists, each of which includes some text and, optionally, a description. Those are converted into Snippet objects.

Next, we invoke fuzz to prompt the user to pick one of those snippets. rofi is the default picker, so we don’t need to explicitly refer to it, but we could choose a different picker if we liked. #pick calls #to_s on each snippet before displaying it to the user, then returns the associated object.

Notice that we’re referencing a cache. This argument tells fuzz to record our selection and, in future invocations of the script, to put that selection nearer the top of the list. That makes our snippet manager learn which snippets we use most often and makes it easier to choose them in the future.

Finally, once the user has chosen a snippet, we shell out to xdotool to type the associated text. That’ll write out the characters, just as if we’d manually typed them, into the selected text area.

So, that’s how I define and type snippets! I’ve found it to be a really convenient way to easily fill out forms and enter Unicode characters. If you’re also of the “hobbyist” persuasion, and like building your own tools even when there might be a technically better solution out there, I hope you find this strategy useful!

  1. For client work, of course, this probably isn’t the approach I’d take. 

  2. See, for reference, Gerald Weinberg’s distinction between amateur and professional software. This is amateur, and totally fit for its thoroughly limited purpose. Especially since its real purpose is just my pleasure in creating it! 


You might like these textually similar articles: