Yesterday while surfing the ASCII highways of IRC (yes, IRC) a URL linking to a MacOS application scrolled by my screen. Unclack is a small MacOS utility which silences the microphone of the user when they’re typing. The purpose is to prevent the noise of typing being passed through to other participants when on a Zoom / Skype / Jitsi call. Neat.
They don’t make a Linux version, and I couldn’t see anything similar, so I did what I usually do in this instance, throw the idea towards my friendly local coder, Stuart Langridge. He was, as ever, initially bemused and then dismissive.
Here’s a simulated, completly hyothetical conversation between us:
Alan: I reckon that’s an app which could exist on linux - unclack.app - “Unclack is the small but mighty Mac utility that mutes your keyboard while you type!”
Stuart: But my keyboard doesn’t make any noise?
Alan: …
Stuart: Oh haha! it mutes the mic
Stuart: I thought: the answer here is don’t be popey with the stupid noisy keyboard ;)
He has a point. Hypothetically I might have suggested:
Alan: I reckon a little python thing that sits in the corner and can be toggled on/off would be neat
Stuart: Is this an attempt to nerd-snipe me into building it? :)
Rumbled!
Stuart: Ok, if i get a chance I’ll look into it!
Stuart provided me with the first iteration, able to detect keypresses, you know, like a keylogger. We spent approximately 30 seconds brainstorming a name. I came up with automute
and unkeeb
. Stuart expressed with some vigor that unkeeb
was under no circumstances going to be the name.
A short while later, unkeeb
was born!
By late afternoon Stuart had the basics of the utility working. We tested over a Google Meet. It would detect keypresses, mute and unmute the microphone, and display the status in the indicator area. This smelled like a Minimum Viable Product!
With Stuarts veto on the unkeeb
name, as the Ideas Man, I had to think of something else. I checked a thesaurus and came up with calm-ivories
which was even dumber than unkeeb
. Then it hit me!
30 seconds of Internet due diligence later, and the name was settled. “Hushboard” it is. Who says naming things is hard? Stuart does. All the time. That’s why he relies on me for these parts of the project lifecycle. That, and packaging. We were getting close to sharing the creation with the world. We decided, with no surprise to anyone, to make a snap!
So here’s what I did to package what’s essentially a tiny bit of python, and a few dependencies as a snap, so Stuart could build and publish it in the Snap Store.
Metadata
This is straightforward. The version
tag uses git
so whenever a build is published, you can refer back to the tag which it was built from. It’s going to be strictly confined. We don’t want it doing anything narfarious with all those keypresses it’s collecting!
Note: For the humour-challenged, this is a joke. It does not collect keypresses.
We’re using core18
which means it’s using packages from Ubuntu 18.04 for stage-packages
later, which are older than the packages used when we tested on our 20.04 / 20.10 systems. Foreshadowing
name: hushboard
base: core18
version: 'git'
summary: Mute your microphone while typing
description: |
Save your friends and colleagues from the noise of your keyboard while on
audio or video calls. Hushboard detects keypresses and automatically mutes
your microphone, unmuting shortly after you finish typing.
grade: stable
confinement: strict
Parts
First is hushboard itself. As I plan to land this in the upstream repo, the source
is .
- the current directory. I’m not using the python
plugin, because the project doesn’t have all the necessary python bits like a setup.py
. We’re literally dumping the python files into the snap and running them.
We’re staging gir1.2-appindicator3-0.1
so it can place a little indicator icon in the top right of the screen, so you can see what the app is doing. We also need python3-xlib
which is needed by Hushboard to hook into X11 to detect keypresses. Yes, this uses X11, no, it hasn’t been tested on Wayland. No, it almost certainly won’t work there. Patches may be welcome, so long as it doesn’t break X11 stuff.
The prime
section is pretty neat. This means “only put these things in the snap, and nothing else”. That way we strip the snap down to the absolute minimum needed to make it run. The Xlib library is about the biggest thing in the snap, which ends up being under 200K in size.
parts:
hushboard:
plugin: dump
source: .
stage-packages:
- gir1.2-appindicator3-0.1
- python3-xlib
prime:
- usr/lib/girepository-1.0/AppIndicator3-0.1.typelib
- usr/lib/*/libappindicator3.so.*
- usr/lib/python3/dist-packages/*
- hushboard/*
- bin/*
We bundle a launcher script so we need a part to pull that in.
launcher:
plugin: dump
source: snap/local/
Here’s the launcher script. All it does is ensure python can find the dependencies we bundled (xlib) and launches the application. I could probably move the PYTHONPART
line to the apps
section as an environment
stanza, and may well have done by the time you read this.
#!/bin/bash
PYTHONPATH=$SNAP/usr/lib/python3/dist-packages:$PYTHONPATH
cd $SNAP
/usr/bin/python3 -m hushboard
Apps
Here we specify the binary we are going to expose to the outside world as hushboard
, which in fact launches $SNAP/bin/launcher
, that we just saw above. I definitely should add that PYTHONPATH
to the environment
section. Hang on. Ok, done.
We’re leveraging the GNOME extension here. It means a ton of stuff is done and bundled for us.
apps:
hushboard:
extensions: [gnome-3-34]
environment:
LD_LIBRARY_PATH: $SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/pulseaudio
command: bin/launcher
plugs:
- x11
- audio-playback
That’s it. I threw a PR over to Stuart who hooked it all up to the snapcraft build system. Within an hour or so
Challenges
Segfaults
When I first built the snap, the yaml looked pretty much as it does now, but without the prime
section. This caused a library conflict between GNOME libraries in the GNOME Platform Snap (gnome-3-34) and what was pulled in by my stage-packages
definition. Thanks to Ken VanDine on the Ubuntu Desktop Team and Chris Patterson from the Snapcraft team at work for helping me better understand this. We ndeeded to bundle the appindicator library, but didn’t want all the other dependencies, which caused the conflict. Using the prime
section we could allowlist only the things we want, which has a side-effect that the snap is nice and tight.
Architectures
When Stuart first landed the yaml and hooked up the snapcraft build service, we had build failures on the s390x and ppc64el architecture builders. That’s because the gnome-3-34-1804-sdk
component used at build time isn’t built for those architectures. So I added an architectures stanza to limit the builds we’d support. Now the build service doesn’t even try to build s390x, ppc64el. Sorry all of you doing video calls on your IBM mainframes!
Python 3.7
At one point in development, once snapped, hushboard would crash with a traceback:
$ snap run hushboard
Gtk-Message: 16:12:36.769: Failed to load module "canberra-gtk-module"
Traceback (most recent call last):
File "/usr/lib/python3.6/runpy.py", line 193, in _run_module_as_main
"__main__", mod_spec)
File "/usr/lib/python3.6/runpy.py", line 85, in _run_code
exec(code, run_globals)
File "/snap/hushboard/x19/hushboard/__main__.py", line 204, in <module>
HushboardIndicator().run()
File "/snap/hushboard/x19/hushboard/__main__.py", line 150, in __init__
self.queue = queue.SimpleQueue()
AttributeError: module 'queue' has no attribute 'SimpleQueue'
A quick search online revealed SimpleQueue which Stuart used in hushboard, was introduced in Python 3.7. We’re building on Ubuntu 18.04 due to the use of core18
as a base
(callback). I worried this might be a significant problem. Stuart pointed out it was a one line fix to use a different method to achieve the same thing, and be compliant with the features in Python 3.6. Awesomesauce!
Testing
I had a couple of meetings today in which I could test the newly minted hushboard. Hushboard has an internal timer which essential starts whenever the user presses a key and the microphone is muted. If you don’t press anything for a certain period, you’re unmuted. We initially settled on 2 seconds as the delay. But this seemed weird, as the typer would have to wait 2 seconds after typing to be able to say anything. It stilted conversations.
We tried 150ms but that was seen as too short a time. People in the meeting could hear my microphone strobing off and on as I typed, because I don’t necessarily type super fast all the time. We finally settled on 250ms but I expect Stuart is open to feedback on this topic 😃.
Conclusion
This was a fun little project. Within about 24 hours we went from idea to prototype, implementation, testing, building for multiple architectures, publishing, made a tutorial video and a promotional blog post about this simple little utility. Do feel free to install it and let us know your thoughts.