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.