Reads a text file and "types" its contents into whatever window currently has focus — nvim in a terminal, VSCode, anything — using real simulated keystrokes (X11 XTEST extension). Built for recording clean coding-demo footage without having to actually type on camera.
It does not paste text or fake a terminal. It sends real key events
system-wide via X11, the same way xdotool type does. That means:
- It works with any focused window (nvim, VSCode, a browser, whatever).
- You must manually click/focus the target window before it starts typing — there's a countdown for that.
- It only works on X11, not Wayland. Linux Mint Cinnamon uses X11 by
default, so you should be fine. Run
echo $XDG_SESSION_TYPEto confirm — it should printx11.
Install a Rust toolchain (skip if you already have one):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"Install the X11 dev headers enigo links against:
sudo apt update
sudo apt install libxdo-devBuild:
cd type-sim
cargo build --releaseThe binary will be at target/release/type-sim. Optionally install it
somewhere on your $PATH:
cp target/release/type-sim ~/.local/bin/type-sim path/to/script.txtYou'll get a 5-second countdown — click into nvim (make sure you're in INSERT mode) or VSCode before it ends. Then it types.
type-sim <FILE> [OPTIONS]
--min-delay <ms> min pause between keystrokes (default 25)
--max-delay <ms> max pause between keystrokes (default 90)
--newline-pause <ms> extra pause after each Enter (default 250)
--start-delay <secs> countdown before typing begins (default 5)
--hesitate-chance <0-1> chance of a random long pause (default 0.02)
--hesitate-ms <ms> length of that long pause (default 600)
Example — faster, twitchier typing with no hesitation:
type-sim demo.rs --min-delay 10 --max-delay 40 --hesitate-chance 0- Open nvim, press
ito enter INSERT mode, then runtype-simand switch focus back during the countdown. - If your file has leading whitespace/indentation you want nvim to
reproduce literally rather than re-indent, run nvim with
:set noautoindent noexpandtabfirst, or the auto-indent will double up whitespace as it retypes each\n.
- Click into the editor pane during the countdown.
- Turn off VSCode's "Format on Type" / auto-closing brackets if you want an exact character-for-character replay, since VSCode will otherwise insert matching brackets/quotes itself as it "sees" you type them.
- This sends genuine input events, so anything with focus receives them — don't let the countdown end while some other window (like a password field) is focused.
- Unicode characters are supported via
Key::Unicode.
Short answer: yes, the code itself should work as-is on both — enigo is cross-platform and picks the right backend automatically based on target OS. You don't need to change main.rs or Cargo.toml. What changes is the setup step, not the code.
# install Rust: https://rustup.rs (just run the installer)
cd type-sim
cargo build --release
.\target\release\type-sim.exe demo.rs- No
libxdo-devneeded — that was Linux/X11-only. On Windows,enigouses the nativeSendInputAPI. - Should work out of the box with VSCode. For a Windows terminal + nvim, same deal — focus it during the countdown.
- Watch out for elevated (Run as Administrator) target apps — synthetic input from a non-elevated process generally can't reach an elevated window (Windows' UIPI blocks it). Run your terminal and VSCode/nvim at the same privilege level.
# install Rust: https://rustup.rs
cd type-sim
cargo build --release
./target/release/type-sim demo.rs- No
libxdo-devequivalent needed —enigouses Core Graphics events (CGEvent) on macOS. - You will need to grant Accessibility permission the first time: System Settings → Privacy & Security → Accessibility → enable it for your terminal app (Terminal.app, iTerm2, etc.) or the compiled binary itself. Without this, keystrokes silently won't send.
- Some sandboxed/hardened apps ignore synthetic events even with permission granted — VSCode and terminal-based nvim should be fine.
Cargo.toml, main.rs, all the CLI flags, the whole workflow (countdown → focus window → it types) — identical on every platform. The only platform-specific bit is the one-time OS permission/dependency dance above.
This isn't actually a keybinding problem — it's an editor autoindent/autocomment problem. Here's what's happening:
The cause:
type-sim sends real, individual keystrokes (Key::Return, Key::Unicode(ch), etc.) — not a single paste event. Your target editor (looks like Neovim, based on the NORMAL/libversion statusline) doesn't know this is "pasted" text. So every feature that normally helps a human typing kicks in and fights your file's own formatting:
-
Autoindent: when you press Enter, Neovim auto-inserts indentation matching the previous line. Then
type-simalso types the leading spaces/tabs that are already in your source file. Those two sources of indentation stack, so each line gets progressively more indented than the last — exactly the staircase effect you're seeing between lines 6→5→4→3→2→1 in image 2. -
Comment continuation (
formatoptionscontainingr/o): after a line starting with///or//!, Neovim auto-inserts a new//!(or///) prefix on the next line for you. Thentype-simtypes the file's own//!////prefix right after that — giving you the doubled/mangled comment markers and words getting jammed together (the//! decision-making).
The fix — put Neovim in "dumb" mode before the demo runs:
In Neovim, run this before starting type-sim (or add it to a scratch config you source just for demos):
:set pastepaste mode disables autoindent, smartindent, cindent, and the comment-continuation formatting all at once — so raw keystrokes land exactly as sent, no side effects. Turn it back off after with :set nopaste.
If you want it scriptable rather than manual, you could have countdown() in type-sim also send the necessary keys to toggle paste mode itself, e.g. type :set paste<CR>i before typing the body and <Esc>:set nopaste<CR> after — but doing it manually once per demo is simpler and less fragile.
If you also demo in VSCode, the equivalent culprits are editor.autoIndent and editor.autoClosingBrackets/editor.formatOnType — worth turning those off for the same reason.