ENOSUCHBLOG

Programming, philosophy, pedaling.


Introducing x_do.cr

Dec 30, 2017     Tags: crystal, programming, workflow    

This post is at least a year old.

This is a writeup for my first Crystal library (or “shard,” in Crystal parlance): x_do.cr.

Background

x_do.cr is a wrapper for libxdo, the C library that powers Jordan Sissel’s xdotool.

xdotool and libxdo are both awesome tools for automating interactions with X11 windows, so I thought I would bring a bit of their awesomeness to the Crystal world.

Implementation

This was my first attempt at writing a Crystal library, and also my first time using C bindings in Crystal.

Despite these personal disadvantages, I found the experience of programming x_do.cr to be extremely pleasant. Crystal is still an unfinished language, but the maturity of its tooling made the development process surprisingly easy.

First was the bindings themselves. I began by writing these out manually (following the docs) but this was tedious and error-prone, especially when dealing with nested structures and opaque structures referenced from Xlib.

Luckily, the Crystal community has a tool for exactly this: crystal_lib. I ran it on a file that looked like this:

1
2
3
4
5
6
@[Include(
  "xdo.h",
  prefix: %w(xdo_ XDO_))]
@[Link("xdo")]
lib LibXDo
end

…and it spat out autogenerated bindings for every struct, typedef, and function exposed by xdo.h.

That alone reduced an hour-plus manual task into a few minutes of cleanup and type simplification (crystal_lib is a little conservative, and won’t attempt to reduce a bunch of typedefs down into a single Crystal alias or type). Nice!

Once libxdo’s functions were exposed to Crystal, the majority of the work was just encapsulation and translation to Crystal semantics (like adding blocks where relevant).

Calling a C fun from Crystal is very straightforward, as can be seen from Window#[]=:

1
2
3
4
def []=(property : String, value : String)
  # `property` and `value` are converted to `Pointer(UInt8)` implicitly!
  LibXDo.set_window_property(xdo_p, window, property, value)
end

Installation and usage

The easiest way to install x_do.cr is via Crystal’s shards dependency manager. You can find the relevant steps in the repository README.

Once installed, most interaction can be done within the block provided by XDo.act:

1
2
3
4
5
require "x_do"

XDo.act do
 # actions here...
end

For example, if you’d like to get the window that the mouse is currently over:

1
2
3
4
XDo.act do
  win = mouse_window
  # window actions here...
end

Or in block form:

1
2
3
4
5
XDo.act do
  mouse_window do |win|
    # window actions here...
  end
end

(Also available: focused_window, active_window, and select_window (which is interactive!))

Once a window is selected, you can perform a variety of actions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# type some text into the window
win.type "hello from Crystal!"

# send a keysequence to the window
win.keys "Ctrl+s"

# move the mouse around, relative to the window
win.move_mouse 20, 20

# click on the window
win.click

# wait for the window to change resolution
win.on_size_change do
  puts "window size changed!"
end

# change a window property, like WM_NAME
win["WM_NAME"] = "x_do was here"

# or just kill the window entirely
win.kill!

Many actions can also be done without a window, in just the scope of XDo.act:

1
2
3
4
5
6
7
8
9
10
XDo.act do
  # move the mouse relative to its current position
  move_mouse 100, 100

  # get a list of active modifiers (Ctrl, Alt, Mod1, etc)
  mods = active_modifiers

  # get the number of desktops
  puts "# of desktops: #{desktops}"
end

Perhaps most powerfully of all, x_do.cr exposes libxdo’s window searching features. This allows you to search for windows based on multiple conditions (either ANDed or ORed together):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
XDo.act do
  # find all windows that:
  #  1. are visible
  #  2. have "urxvt" as their name
  wins = search do
    # makes the requirements conjunctive, rather than disjunctive
    require_all
    only_visible
    window_name "urxvt"
  end

  wins.each do |win|
    # window actions here...
  end
end

The snippets above are only brief examples of what x_do.cr can do. For more featured examples, check out the examples directory in the repository.

Full (or nearly full) API documentation is also available here. I’ll try to keep it synced with the latest released version.

Summary

Writing this library was a fun exercise, and improved my understanding of Crystal’s syntax and semantics (namely, where they differentiate from those of Ruby).

x_do.cr is not 100% complete, but it covers most of the libxdo API and is ready for most usecases. I’ll probably fill in the remaining parts in the coming weeks.

Let me know if you experience any problems/find any bugs!

- William