Rapid prototyping in Rust
On my business PC I'm using KeyboardCleanTool every time I need to clean the keyboard. It's a simple tool which allows to click the button to disable the keyboard, clean everything and click the button again to enable it.
I was looking for something similar for my personal PC (I'm using Linux), but didn't find anything.
In this blog post I'll describe quick and dirty prototyping process of such tool using Rust.
Context
Ok, so a bit of a context. When I'm working with code, my hands are sweating. The result of that is that I need to clean my keyboard pretty often. Hibernating the PC is a bit annoying. Especially that often, modern OSes allow to wake the PC just by pressing any button.
So I was looking for a tool which can disable the keyboard for the time of cleaning. I found KeyboardCleanTool. It works pretty fine. Simple software, no issues at all. The problem is that I couldn't find anything similar for Linux.
Of course, I can use terminal to disable my keyboard. First you need to find the
ID of your keyboard device. Run xinput list
and locate AT Translated Set 2
keyboard. Take note of two numbers:
- The ID number — this is the ID of your device — it will be used to disable the keyboard.
- The number at the end — this is the ID of a master device — it will be used to reattach the keyboard.
The example output of xinput list
looks like following:
⎡ Virtual core pointer id=2 [master pointer (3)]
⎜ ↳ Virtual core XTEST pointer id=4 [slave pointer (2)]
⎜ ↳ DLL07BE:01 06CB:7A13 Mouse id=12 [slave pointer (2)]
⎜ ↳ DLL07BE:01 06CB:7A13 Touchpad id=13 [slave pointer (2)]
⎣ Virtual core keyboard id=3 [master keyboard (2)]
↳ Virtual core XTEST keyboard id=5 [slave keyboard (3)]
↳ Power Button id=6 [slave keyboard (3)]
↳ Video Bus id=7 [slave keyboard (3)]
↳ Video Bus id=8 [slave keyboard (3)]
↳ Power Button id=9 [slave keyboard (3)]
↳ Sleep Button id=10 [slave keyboard (3)]
↳ Integrated_Webcam_HD: Integrate id=11 [slave keyboard (3)]
↳ Intel HID events id=14 [slave keyboard (3)]
↳ Intel HID 5 button array id=15 [slave keyboard (3)]
↳ Dell WMI hotkeys id=16 [slave keyboard (3)]
↳ AT Translated Set 2 keyboard id=17 [slave keyboard (3)]
Based on that we have:
- device ID: 17
- master ID: 3
Next, to disable the keyboard I just need to run: xinput float 17
(don't try it
yet :)). Now, the keyboard is disabled. To reattach the keyboard all I need to do
is to execute xinput reattach 17 3
— attach the keyboard to master device.
Perfect. The problem is that we have the keyboard disabled, so how can we execute
command to reattach it.
For some time I was doing something like this:
xinput float 17 && sleep 120 && xinput reattach 17 3
— disable keyboard, give me
2 min to clean it, and reattach at the end. It's working pretty fine, it's just
not that comfortable as KeyboardCleanTool.
I decided to mimic the functionality of KeyboardCleanTool in Rust.
Prototype
The idea is to write quick and dirty code just to proof that our assumptions are correct and the solution born in our heads will work. This is called Proof of Concept, and it's often used technique in projects when we want to have precise plan but don't know the details which can have big impact on our plan.
So let's start our prototype. Because KeyboardCleanTool is a GUI app I'll go this way as well. I'm not a fan of GUI apps which you can read more about in this blog, but here the GUI is pretty important to not block ourselves.
For the GUI I'll use druid library. In Rust, the GUI world is still emerging and changing rapidly. So far I used iced in one of my projects, now I wanted to try something different, so decided to go with druid.
As a mechanism to disable the keyboard I'll use exactly the same steps as
described earlier — I'll just use xinput
and
std::process::Command
to spawn child process.
Code
I'll name the project Cleaboard. Let's create new binary:
cargo new cleaboard
. Now let's add druid as a dependency. I'm using
cargo-edit to manage dependencies via
command line. All I need to do is cargo add druid
(I can also just add
druid = "0.7.0"
to Cargo.toml
).
Let's start with druid's example from the README:
use druid::widget::{Button, Flex, Label};
use druid::{AppLauncher, LocalizedString, PlatformError, Widget, WidgetExt, WindowDesc};
fn main() -> Result<(), PlatformError> {
let main_window = WindowDesc::new(ui_builder());
let data = 0_u32;
AppLauncher::with_window(main_window)
.log_to_console()
.launch(data)
}
fn ui_builder() -> impl Widget<u32> {
// The label text will be computed dynamically based on the current locale and count
let text =
LocalizedString::new("hello-counter").with_arg("count", |data: &u32, _env| (*data).into());
let label = Label::new(text).padding(5.0).center();
let button = Button::new("increment")
.on_click(|_ctx, data, _env| *data += 1)
.padding(5.0);
Flex::column().with_child(label).with_child(button)
}
It's a simple counter app — clicking on the button updates the state and displays it in the label.
What we'll need is only a button. We'll reflect the state of the app by changing
button label. Let's do some cleanup of the ui_builder
:
fn ui_builder() -> impl Widget<u32> {
let button = Button::new("Turn off the keyboard").padding(5.0);
Flex::column().with_child(button)
}
Ok, now we need to change the label of the button after clicking it. To do that we
need to introduce the state to our app. It actually has a state which is u32
value, but we need to track if the keyboard is turned off or on.
use druid::widget::{Button, Flex};
use druid::{AppLauncher, Data, PlatformError, Widget, WidgetExt, WindowDesc};
#[derive(Clone, Data)] // Data is required to be implemented by our state
pub(crate) struct State {
pub(crate) enabled: bool, // our state
}
impl Default for State {
fn default() -> Self {
Self { enabled: true } // the keyboard is enabled by default
}
}
fn main() -> Result<(), PlatformError> {
let main_window = WindowDesc::new(ui_builder());
AppLauncher::with_window(main_window)
.log_to_console()
.launch(State::default()) // pass the initial state
}
fn ui_builder() -> impl Widget<State> { // remember to change the Widget type
let button = Button::new("Turn off the keyboard").padding(5.0);
Flex::column().with_child(button)
}
Now let's add the on_click handler:
fn ui_builder() -> impl Widget<State> {
let button = Button::new("Turn off the keyboard")
.on_click(|_ctx, state: &mut State, _env| state.enabled = !state.enabled) // toggle the state
.padding(5.0);
Flex::column().with_child(button)
}
Perfect, we can now toggle our state. But the label of the button still doesn't depend on the state. To change that we need to use different approach. Fortunately button can be created by passing a closure which will be converted to a label. So let's do that:
use druid::{AppLauncher, Data, Env, PlatformError, Widget, WidgetExt, WindowDesc}; // add Env here
fn ui_builder() -> impl Widget<State> {
let button = Button::new(|state: &State, _env: &Env| {
if state.enabled {
"Turn off the keyboard".into()
} else {
"Turn on the keyboard".into()
}
})
.on_click(|_ctx, state: &mut State, _env| state.enabled = !state.enabled)
.padding(5.0);
Flex::column().with_child(button)
}
Great, now our button changes the state when clicked, which then changes the label of the button.
Now it's time for the last part — disabling and enabling the keyboard on click:
fn ui_builder() -> impl Widget<State> {
let button = Button::new(|state: &State, _env: &Env| {
if state.enabled {
"Turn off the keyboard".into()
} else {
"Turn on the keyboard".into()
}
})
.on_click(|_ctx, state: &mut State, _env| {
state.enabled = !state.enabled; // change the state
if state.enabled { // if enabled, reattach keyboard by `xinput reattach 17 3`
Command::new("xinput")
.arg("reattach")
.arg("17")
.arg("3")
.spawn()
.expect("failed to reattach the keyboard");
} else { // otherwise detach by `xinput float 17`
Command::new("xinput")
.arg("float")
.arg("17")
.spawn()
.expect("failed to disable the keyboard");
}
})
.padding(5.0);
Flex::column().with_child(button)
}
summary
And that's pretty much it. In ~50 lines of code we have working GUI app which
makes cleaning the keyboard easier. Of course, it's not even close to the point of
publishing. First, we are assuming that xinput
is on the system. In case it's
not we should display information to the user and disable the button.
Additionally, we are using IDs which are working on my system, but won't work
everywhere. We should either use lower level API to communicate with X server
(that's what xinput
uses) or we should at least execute xinput list
and parse
the output to get what we need. We also don't have any tests and logging which
makes debugging easier.
At the end it's just a prototype which makes my life easier. That's the power of programming — you can make your life easier by using code.