I have an applet originally written in C, and after learning Crystal I decided to rewrite it.
Everything went fine until I reached the part that needs multiple infinite loops running in parallel.
In the original C version I had three concurrent loops:
- The GTK main loop
- An X11 key listener
- A signal listener (
SIGUSR1)
When I tried to do the same in Crystal, the program either froze, lagged, or crashed depending on which threading method I used.
Here is the X11 loop:
def keys
Xlib.call do
xInitThreads()
dpy = xOpenDisplay(nil)
unless dpy
STDERR.puts("Cannot open display")
return
end
root = xRootWindow(dpy, 0)
xGrabKey(dpy, xKeysymToKeycode(dpy, X11::XF86XK_AudioLowerVolume),
X11::AnyModifier, root, 1, X11::GrabModeAsync, X11::GrabModeAsync)
xGrabKey(dpy, xKeysymToKeycode(dpy, X11::XF86XK_AudioRaiseVolume),
X11::AnyModifier, root, 1, X11::GrabModeAsync, X11::GrabModeAsync)
xGrabKey(dpy, xKeysymToKeycode(dpy, X11::XF86XK_AudioMute),
X11::AnyModifier, root, 1, X11::GrabModeAsync, X11::GrabModeAsync)
xSelectInput(dpy, root, X11::KeyPressMask)
ev = uninitialized LRoot::XEvent
loop do
xNextEvent(dpy, pointerof(ev))
if ev.type == X11::KeyPress
keysym = xKeycodeToKeysym(dpy, ev.key.keycode, 0)
case keysym
when X11::XF86XK_AudioRaiseVolume
Volume.volume_up
Volume.update_safe(true)
when X11::XF86XK_AudioLowerVolume
Volume.volume_down
Volume.update_safe(true)
when X11::XF86XK_AudioMute
Volume.volume_mute
Volume.update_safe(false)
end
end
end
end
end
Signal handling code:
private def pid; "/tmp/volume-pulse.pid"; end
def signal_root
File.write(pid, "#{Process.pid}")
Signal::USR1.trap { Volume.update_safe(true) }
sleep
end
def signal_update
return unless File.exists?(pid)
Process.signal(Signal::USR1, File.read(pid).to_i)
end
GTK loop:
Xlib.call do
notify("Volume Pulse") if ConfigVariables.use_notifications
fcInit()
gtk2_init(nil, nil)
icon_tray = gtk2_status_new()
Volume.icon_statusX do |volume|
Xlib.call do
if Volume.get_mute
gtk2_status_icon_tooltip_text(icon_tray, "Volume: Muted")
gtk2_status_set_icon_name(icon_tray, "audio-volume-muted")
else
gtk2_status_icon_tooltip_text(icon_tray, "Volume: #{volume}")
case volume
when 0
gtk2_status_set_icon_name(icon_tray, "audio-volume-muted")
when 1..30
gtk2_status_set_icon_name(icon_tray, "audio-volume-low")
when 31..70
gtk2_status_set_icon_name(icon_tray, "audio-volume-medium")
else
gtk2_status_set_icon_name(icon_tray, "audio-volume-high")
end
end
end
end
Volume.update
g_signal_connect(icon_tray.as(LRoot::GObject*), "scroll-event", Event.scroll.as(LRoot::GCallback*), nil)
g_signal_connect(icon_tray.as(LRoot::GObject*), "button-press-event", Event.button.as(LRoot::GCallback*), nil)
g_signal_connect(icon_tray.as(LRoot::GObject*), "popup-menu", GUI.menu.as(LRoot::GCallback*), nil)
gtk2_main()
notify_uninit() if ConfigVariables.use_notifications
end
Methods I have tried:
g_thread_newandpthread_create— both cause freezing or crashesThread.newandspawn(with-Dpreview-mt) — sometimes only part of the signals are received, or it crashes after a short time, or one of the loops stops running completely
If I remove the GTK loop, everything works fine.
But as soon as I call gtk2_main, all other threads or fibers stop responding.
Is there a reliable way to run multiple native event loops (GTK, X11, signal listener) in true parallel execution in Crystal?
Or should I move one of these loops (for example the X11 listener) into a small C helper process instead?
Crystal version: Crystal 1.17.1 (2025-07-22)
OS: Artix Linux (Openrc)
LLVM: 20.1.8
Default target: x86_64-pc-linux-gnu