Dec 30, 2017 Tags: crystal, programming, workflow
This is a writeup for my first Crystal library (or “shard,” in Crystal parlance): x_do.cr.
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.
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 typedef
s 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
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 AND
ed or OR
ed 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.
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