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/
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
Prerequisite
Well, I've used some tech for building this app
-
Yarn
orNpm
- I use yarn - Tauri - a toolkit for building desktop application
- Reactjs
- Tailwindcss
- 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
$ 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
Remove redundant files in source base
Just keep main files and remove redundant files. I just removed files inside public
and src
folder
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 or follow the guide below
$ 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.
/** @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.
@tailwind base;
@tailwind components;
@tailwind utilities;
Open src/App.tsx
and add some class
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
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
[build-dependencies]
tauri-build = { version = "1.2", features = [] }
[dependencies]
# ...
rdev = "0.5.2"
Time to create the listener
// ./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
// ./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.
Display keystroke on Reactjs
For now, register a event listener on the view. Open src/App.tsx
and register a keypress
event as follow
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
/*.........*/
@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
{
// ...
"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
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.
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.
Top comments (0)