This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ScreenKey is a cross-platform desktop application built with Tauri 2.4 + React 19 + Rust that displays keyboard inputs on screen in real-time. The application uses platform-specific keyboard capture implementations: evdev for Linux (global /dev/input access) and rdev for macOS/Windows.
All commands must be run from the screenkey-app/ directory.
cd screenkey-app
# Development mode (requires sudo on Linux for keyboard capture)
sudo npm run tauri:dev
# Frontend-only development (without Tauri)
npm run dev
# TypeScript type checking
npx tsc --noEmitcd screenkey-app
# Build frontend only
npm run build
# Build complete Tauri application (frontend + backend)
npm run tauri:build
# Build output locations:
# - Linux: src-tauri/target/release/bundle/{appimage,deb}/
# - macOS: src-tauri/target/release/bundle/macos/
# - Windows: src-tauri/target/release/bundle/msi/cd screenkey-app/src-tauri
# Format Rust code
cargo fmt --all
# Linting with Clippy
cargo clippy --all-targets -- -D warnings
# Check without building
cargo check# Automated version bump and release (from project root)
cd screenkey-app
npm run release # Patch version (0.0.x)
npm run release:minor # Minor version (0.x.0)
npm run release:major # Major version (x.0.0)
# This script:
# 1. Checks for uncommitted changes (fails if dirty)
# 2. Bumps version in package.json, tauri.conf.json, Cargo.toml
# 3. Updates CHANGELOG.md with new version and date
# 4. Commits changes with "chore: bump version to X.X.X"
# 5. Creates git tag (vX.X.X)
# 6. Pushes to origin (triggers GitHub Actions release builds)Rust → React: Keyboard events are emitted from Rust backend via Tauri's event system:
// src-tauri/src/main.rs
app_handle.emit("key-press", KeyEvent {
key: String,
modifiers: Vec<String>
})// src/App.tsx
import { listen } from '@tauri-apps/api/event'
listen<KeyEvent>('key-press', (event) => {
setKeys(prev => [...prev, { ...event.payload, timestamp: Date.now() }])
})React → Rust: Window control operations use Tauri window API:
import { getCurrentWindow } from '@tauri-apps/api/window'
const appWindow = getCurrentWindow()
await appWindow.minimize()
await appWindow.close()
await appWindow.startDragging()The backend uses conditional compilation for different platforms:
Linux (src-tauri/src/main.rs:279-352):
- Uses
evdevcrate to read from/dev/input/event*devices - Requires root privileges or user in
inputgroup find_keyboard_devices()scans for devices with keyboard capabilities- Tracks modifier state globally in
AppState::modifiersMutex - Adaptive polling: 1ms when events detected, 10ms when idle (CPU optimization)
macOS/Windows (src-tauri/src/main.rs:355-402):
- Uses
rdevcrate for cross-platform keyboard hooks - Requires Accessibility permissions (macOS) or Administrator (Windows)
- Same modifier state tracking mechanism
Frontend State (React hooks):
keys: KeyEvent[]- Keypress history with timestampslayoutDirection- 'vertical' | 'horizontal' | 'wrapped'settings- Persisted to localStorage, contains:displayDuration- Auto-hide timer (0 = never hide)opacity- Window opacity (0.1-1.0)fontSize- Key display size (12-32px)theme- Active theme namecustomTheme?- Custom theme colors
Backend State (Rust Mutex):
AppState::modifiers: Mutex<Vec<String>>- Currently pressed modifier keys- Thread-safe access from keyboard capture thread
src/App.tsx (401 lines):
- Event listener setup and cleanup
- Settings panel rendering and persistence
- Auto-hide timer (
useEffectwithdisplayDuration) - Theme management (6 presets + custom)
- Window control handlers (drag, minimize, close)
src/components/KeyDisplay.tsx (84 lines):
- Smart auto-scroll: only scrolls when user is at bottom (prevents forced scrolling during manual navigation)
- Layout-aware scrolling (horizontal:
scrollLeft, vertical/wrapped:scrollTop) - Per-key rendering with modifier support
src-tauri/src/main.rs (409 lines):
- Platform detection via
#[cfg(target_os = "...")] - Key mapping functions:
key_to_string(),rdev_key_to_string() - Always-on-top enforcement: re-asserts every 2 seconds in background thread
- Modifier tracking on press/release
The window uses periodic re-assertion to stay on top:
// main.rs:273-276
std::thread::spawn(move || loop {
std::thread::sleep(std::time::Duration::from_secs(2));
let _ = window_clone.set_always_on_top(true);
});Auto-scroll only triggers when user is near bottom/end (50px threshold):
// KeyDisplay.tsx:34-48
const threshold = 50
const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold
if (isAtBottom) {
container.scrollTop = container.scrollHeight
}Buttons in the header must stop propagation to prevent drag interference:
<button onMouseDown={(e) => e.stopPropagation()}>Settings are automatically saved to localStorage on every change:
useEffect(() => {
localStorage.setItem('screenkey-settings', JSON.stringify(settings))
}, [settings]).github/workflows/ci.yml:
- Runs on push/PR to main/master
- Steps: TypeScript type check → Frontend build → Rust fmt check → Clippy → Tauri build
.github/workflows/release.yml:
- Triggered by version tags (
v*) or manual dispatch - Builds for 4 targets: Linux x64, macOS Intel/ARM, Windows x64
- Creates draft release, uploads binaries, auto-publishes
All publishing workflows are triggered on release publication or can be manually dispatched.
.github/workflows/publish-apt.yml - APT Repository (Debian/Ubuntu):
- Downloads
.debfiles from release - Generates APT repository metadata (Packages, Release files)
- Signs repository with GPG (requires
APT_GPG_PRIVATE_KEYandAPT_GPG_KEY_IDsecrets) - Deploys to GitHub Pages branch
gh-pages-apt - Users can add repo:
https://rusmanplatd.github.io/screenkey
.github/workflows/publish-snap.yml - Snap Store:
- Builds snap package with classic confinement (required for keyboard capture)
- Uploads to Snap Store (requires
SNAPCRAFT_TOKENsecret) - Publishes to stable channel
- Users install:
sudo snap install screenkey --classic
.github/workflows/publish-flatpak.yml - Flatpak/Flathub:
- Builds Flatpak bundle with org.freedesktop.Platform runtime
- Creates AppData metadata and desktop integration files
- Uploads
.flatpakbundle to release assets - Note: Flathub publishing requires separate PR to flathub/flathub repository
.github/workflows/publish-rpm.yml - RPM Repository (Fedora/RHEL/CentOS):
- Builds RPM package from Tauri binary
- Creates RPM repository with
createrepo_c - Deploys to GitHub Pages branch
gh-pages-rpm - Users can add repo config from:
https://rusmanplatd.github.io/screenkey-rpm/
.github/workflows/publish-aur.yml - AUR (Arch Linux):
- Generates PKGBUILD and .SRCINFO files
- Pushes to AUR repository
ssh://aur@aur.archlinux.org/screenkey-app.git - Requires
AUR_SSH_PRIVATE_KEYsecret - Users install:
yay -S screenkey-apporparu -S screenkey-app
.github/workflows/publish-appimage.yml - AppImage:
- Builds portable AppImage with all dependencies bundled
- Uses Ubuntu 20.04 for maximum compatibility
- Generates zsync file for delta updates
- Uploads to release assets
- Users download and run:
chmod +x ScreenKey-*.AppImage && sudo ./ScreenKey-*.AppImage
To enable all publishing workflows, configure these secrets in repository settings:
APT_GPG_PRIVATE_KEY- GPG private key for signing APT repositoryAPT_GPG_KEY_ID- GPG key ID for APT signingSNAPCRAFT_TOKEN- Snapcraft authentication token (get viasnapcraft export-login)AUR_SSH_PRIVATE_KEY- SSH private key for AUR publishing
All publishing workflows support manual dispatch for testing:
# Via GitHub CLI
gh workflow run publish-apt.yml
gh workflow run publish-snap.yml
gh workflow run publish-flatpak.yml
gh workflow run publish-rpm.yml
gh workflow run publish-aur.yml
gh workflow run publish-appimage.ymlRequired for building on Ubuntu/Debian:
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
build-essential \
curl wget file \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libx11-devThe app requires elevated permissions for global keyboard capture:
# Option 1: Run with sudo
sudo npm run tauri:dev
sudo ./src-tauri/target/release/screenkey-app
# Option 2: Add user to input group (requires logout)
sudo usermod -a -G input $USER
# Then run without sudo after logging out/inscreenkey-app/- Main application directory (all npm commands run here)screenkey-app/src/- React frontend (TypeScript + CSS)screenkey-app/src-tauri/- Rust backend (Tauri application)screenkey-app/src-tauri/Cargo.toml- Platform-specific dependencies with[target.'cfg(...)']- Root directory contains project-level files (CHANGELOG.md, RELEASE.md)
tauri = "2.4"- Desktop app frameworkevdev = "0.12"- Linux keyboard capture (Linux only)rdev = "0.5"- Cross-platform keyboard hooks (macOS/Windows only)x11 = "2.21"- X11 display server access (Linux only)serde+serde_json- Serialization for IPC
Development server runs on port 1420 (strict port mode enabled). Environment variables with VITE_ or TAURI_ prefix are exposed to frontend.