I built a simple keystroke application for Windows
Tags: tauri, react, rust, tailwindcss
Published: 2023-04-30 01:50:55

## Introduction
Hi friends, it's hudy here. In this tutorial, I'm so excited to show you how I built a keystroke application for Windows that inspired by KeyCastr. You guys maybe wondering why do I have to build a keystroke when there are numerous similar applications for Windows ? That's simply because I personally find their UI/UX to be too unappealing

In this tutorial I just made a simplest version of it. So if you want to get full source code and guide, please visit the homepage below to download and access to my repo.

## Source code
Here is the homepage and github repo: [https://hudy9x.github.io/keyreader/](https://hudy9x.github.io/keyreader/)

Feel free to download, fork or contribute it

## Video version
I made a video version as well, you guys can watch it here
[https://www.youtube.com/watch?v=hhwjGIICLuA&t=10s](https://www.youtube.com/watch?v=hhwjGIICLuA&t=10s)

## Prerequisite
Well, I've used some tech for building this app
- `Yarn` or `Npm` - I use yarn 
- [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites) - a toolkit for building desktop application
- Reactjs
- [Tailwindcss](https://tailwindcss.com/)
- [rdev](https://crates.io/crates/rdev) - a rust crate for listening keystroke event
- Typescript basic

Make sure that you have `yarn` and `tauri` installed. Now, let's get started

## Implementation

Open your terminal and create new Tauri project called `keyreader`. The project use `react-ts` - means Reactjs with typescript and yarn as a package manager

```shell
$ yarn create tauri-app keyreader --template react-ts --manager yarn
$ cd keyreader
$ yarn
$ yarn tauri dev
```
After running above commands (It'll take a few minutes). The following UI shown means we created Tauri app successfully

![Tauri app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bis1w8g4b3pmdistjgey.png)

### Remove redundant files in source base
Just keep main files and remove redundant files. I just removed files inside `public` and `src` folder

![Folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/snpwdtojvdggnwirs5mm.png)

### Install tailwindcss (optional)
I'd love to use tailwindcss for styling cuz it's so useful. But you guys don't need to. Please take a look at the installation instruction [here](https://tailwindcss.com/docs/guides/vite) or follow the guide below

```shell
$ yarn add --dev tailwindcss postcss autoprefixer
$ yarn tailwindcss init -p
```
Add the paths to all of your template files in your `tailwind.config.js` file.

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
}
```
Now, add the `@tailwind` directives for each of Tailwind’s layers to your ./src/styles.css file.

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Open `src/App.tsx` and add some class
```javascript
function App() {
  return <div className="text-red-400">Hello world</div>;
}

export default App;
```

Then run the app again, if text turn to red that means it works
```
$ yarn tauri dev
```

![Install tailwindcss](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1eitkjwudhc95tjaik0n.png)


### Keystroke listener

This is the core of our app. I'll create a `keyboard_listener.rs` file for watching keystrokes that users typed. To do that, we use `rdev` crate. 

Install crate by opening `src-tauri/Cargo.toml` and add `rdev` to `[dependencies]` section

```toml
[build-dependencies]
tauri-build = { version = "1.2", features = [] }

[dependencies]
# ...
rdev = "0.5.2"
```

Time to create the listener

```rust
// ./src-tauri/src/keyboard_listener.rs
use rdev::{listen, Event, EventType};

pub fn run_listener<F>(emit: F)
where
    F: Fn(&str, &str) + 'static,
{
    if let Err(error) = listen(move |event| callback(event, &emit)) {
        println!("Error: {:?}", error)
    }
}

fn callback<F: Fn(&str, &str)>(event: Event, emit: &F) {
    match event.name {
        Some(string) => {
            println!("Some: {}", string);
            emit("Some", &string);
        }
        None => {
            match event.event_type {
                EventType::KeyPress(key) => {
                    println!("KeyPress: {:?}", key);
                    let key_str = format!("{:?}", key);
                    emit("KeyPress", &key_str);
                }
                EventType::KeyRelease(key) => {
                    let key_str = format!("{:?}", key);
                    emit("KeyRelease", &key_str);
                }
                EventType::MouseMove { .. } => {
                    // Ignore MouseMove event type
                }
                _ => {
                    // println!("None: {:?}", event.event_type);
                    // let event_type_str = format!("{:?}", event.event_type);
                    // emit(&event_type_str);
                }
            }
        }
    }
}
```
The above script's simply using `listen` method from `rdev` to watch keystrokes. All aphabet characteres return in `Some` event and the others like Ctrl, Alt, Caplocks, ... return in `None`. So we've to read it by comparing `event.event_type` with `EventType` type.

One more thing, open `src-tauri/src/main.rs` and use the listener

```rust
// ./src-tauri/src/main.rs

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod keyboard_listener;
use std::thread;
use tauri::Manager;

#[derive(Clone, serde::Serialize)]
struct Payload {
    mode: String,
    message: String,
}

// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .setup(move |app| {
            let wv = app.get_window("main").unwrap();

            thread::spawn(move || {
                keyboard_listener::run_listener(move |s: &str, s1: &str| {
                    if let Err(err) = wv.emit(
                        "keypress",
                        Payload {
                            mode: String::from(s),
                            message: String::from(s1),
                        },
                    ) {
                        eprintln!("Error while emitting event: {:?}", err);
                    }
                })
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

```

Run the app again and try to type something. If you got logs shown in terminal that means `rdev` works.


![Testing rdev](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8em6t5f2s1i4rfrbdtbh.png)



### Display keystroke on Reactjs

For now, register a event listener on the view. Open `src/App.tsx` and register a `keypress` event as follow

```javascript
import { useEffect, useState } from "react";
import { listen } from "@tauri-apps/api/event";

export default function App() {
  const [alphabeticKeys, setAlphabeticKeys] = useState<string[]>(["NONE"]);
  useEffect(() => {
    listen("keypress", ({ payload }) => {
      let { message, mode } = payload as { message: string; mode: string };
      if (mode === "Some") {
        setAlphabeticKeys((prevTickers) => {
          let max = 15;
          let newTickers = [];
          const charCode = message.charCodeAt(0);

          if (charCode === 32) {
            message = "␣";
          }

          newTickers = [...prevTickers, ...[message]];

          const currLen = newTickers.length;
          newTickers = currLen > max ? newTickers.slice(1) : newTickers;

          return newTickers;
        });
        console.log(message);
      }
    });
  }, []);
  return (
    <div data-tauri-drag-region className="bg-black">
      <div className=" px-4 py-3 text-3xl text-white flex items-center gap-1 pointer-events-none">
        {alphabeticKeys.map((key, index) => {
          return <div key={index}>{key}</div>;
        })}
      </div>
    </div>
  );
}


```

I use Inter font for fixing UX bug that make Esc character shorter than the others

```css
/*.........*/

@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-Regular.ttf") format("truetype");
  font-weight: normal;
  font-style: normal;
}

@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-Bold.ttf") format("truetype");
  font-weight: bold;
  font-style: normal;
}

:root {
  font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 24px;
  font-weight: 400;

  color: #0f0f0f;
  background-color: transparent;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
}

body {
  @apply flex items-center justify-center h-screen overflow-hidden;
}

```

Re-run the app one more time. It's testing time guys

### Make background transparent and hide titlebar

Last thing to do for better UI is hiding titlebar and make the background transparent. It's so simple, just open `src-tauri/tauri.conf.json` and add some config fields

```json
{
  // ...
  "tauri": {
    "allowlist": {
      // .... For custom titlebar to drag, close, ... window
      "window": {
        "all": false,
        "close": true,
        "hide": true,
        "show": true,
        "maximize": true,
        "minimize": true,
        "unmaximize": true,
        "unminimize": true,
        "startDragging": true
      }
    },
    "bundle": {
      // ..... Rename to build app
      "identifier": "com.keyreader.app",
    },
    // ....
    "windows": [
      {
        "fullscreen": false,
        "resizable": true,
        "title": "KeyReader",
        "width": 400,
        "height": 200,
        "decorations": false,  // turn of titlebar
        "transparent": true // make background transparency
      }
    ]
  }
}

```

Wait for a few minutes to rebuild the app. And here is our result

![result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kn1wbae1r6j0fxop6fow.png)

## Modifier keys

Just now, I show you how to display alphabet keys. So what if modifier keys like Ctrl, Enter, Space, Alt, etc... ?

You guys just use `mode` variable for checking these keys. For examples.

```javascript
listen("keypress", ({ payload }) => {
  let { message, mode } = payload as { message: string; mode: string };
  let charCode = message.charCodeAt(0)

  if (mode === "Some") {/*...*/}
  if (mode === "KeyPress") {
    if (message=== "ControlRight") {
      message = "⌃"
    }

    // update state
  }
}
```

Still don't get it? Scroll to top and refer to my video or repo

## Conclution

So far, I just show you how to listen keystrokes event from keyboard using `rdev`. And send it to Tauri view by registering an event. Hope you guys learn something news about building desktop application using Reacjts and Tauri.