Skip to content
chendanielyPublic

About

No description or website provided.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

type-me

Types the contents of a local file into whatever window has focus, one simulated keystroke at a time, then tells you how to prove the file arrived byte-for-byte.

It exists for remote sessions where the clipboard does not cross the boundary -- VMware Workspace ONE and Horizon, RDP, browser-based consoles -- and you would otherwise be retyping code by hand.

Read the hazards section before you use this on anything that matters. The dangerous outcome is not a run that fails. It is a run that looks like it succeeded and quietly mangled your indentation.

Requirements

  • Python 3.10+
  • pynput, supplied per-run by uv (see below)
  • On macOS: Accessibility permission for the terminal app you launch this from

pynput is not installed system-wide, so every invocation goes through uv:

uv run --with pynput python -m type_me FILE [options]

Worth an alias in your shell profile:

alias type-me='uv run --with pynput python -m type_me'

The rest of this document writes out the full uv run form so the commands are copy-pasteable as-is.

macOS Accessibility permission

System Settings > Privacy & Security > Accessibility, and grant it to the terminal application that runs the command -- Terminal, iTerm2, Ghostty, VS Code, whichever one you actually launch from. The permission follows the app, not the script.

Without it, pynput types nothing and raises no error. The run will count down, report success, and produce an empty target window. type-me preflights for this and refuses to start, but if you ever see a silent no-op, this is why.

Quick start

Try it on the included fixture first, into a scratch file, before you point it at real work.

uv run --with pynput python -m type_me examples/sample.py --dry-run

That prints the preflight findings and a plan summary and types nothing. When it looks right, drop the --dry-run, put the cursor where you want the text, and let the countdown run:

uv run --with pynput python -m type_me examples/sample.py --mode heredoc --dest /tmp/sample.py --verify

examples/sample.py is a deliberate torture test: deep nesting, a !, a $VAR, a backtick, a line containing the heredoc delimiter, trailing whitespace, and a tab-indented block. If that file survives a round trip intact, the channel is trustworthy.

Hazards

Vim autoindent will corrupt your code and it will still look fine

This is the failure to worry about.

If the target window is vim in insert mode with autoindent (or smartindent, or a filetype plugin, or anything else that indents for you), vim adds its own leading whitespace to each new line, and then your typed leading whitespace arrives on top of it. Line 2 is indented once too far, line 3 twice, and the error compounds all the way down the file.

The result is Python that is structurally wrong -- bodies attached to the wrong block, or an IndentationError a hundred lines from where the drift began -- but which reads as plausible at a glance and diffs badly against nothing, because you have nothing to diff it against.

Use --mode vim whenever the target is vim. It sends Esc :set paste and enters insert mode before typing, and Esc :set nopaste afterwards. paste mode is exactly the switch that turns off autoindent, abbreviation expansion, and anything else that would rewrite keystrokes on the way in.

If you forget, the checksum in --verify will catch it. That is what the checksum is for.

Shell history expansion eats !

At an interactive bash prompt, ! is history expansion, and it fires inside a heredoc body. A line containing print("done!") can silently become something else entirely, or abort the command. This is not theoretical; it is the default behaviour of an interactive bash.

--mode heredoc emits set +H before the heredoc for exactly this reason.

Unquoted heredocs expand $VAR and backticks

A heredoc written <<EOF performs parameter and command substitution on its body. $HOME becomes your home directory, and a backtick pair is executed. --mode heredoc always quotes the delimiter -- <<'TYPEOUT_EOF' -- which disables both, so the body is written literally.

The delimiter is also chosen so that it does not collide with any line already present in your file. A file that happens to contain the word TYPEOUT_EOF on a line of its own would otherwise terminate the heredoc early and dump the rest of your source into the shell as commands.

Remote keyboard channels drop keystrokes

Workspace ONE, Horizon, RDP and the browser-based consoles all funnel keystrokes through a network protocol that will coalesce or drop them under load. The defaults here are slow on purpose.

Runs of identical characters are the most commonly coalesced, and the most common run of identical characters in source code is indentation. Losing two spaces out of eight is a silent, structural corruption. That is what --repeat-delay exists for: extra delay whenever a character is the same as the one before it.

--jitter breaks up the perfectly uniform keystroke stream, which some remote input paths handle poorly.

Tabs

Tabs are expanded to spaces by default, at --tab-width 4. The reason is that a literal tab at a shell prompt triggers completion rather than inserting a tab character -- you would get a list of filenames, or a mangled line, instead of indentation.

--keep-tabs sends real tab characters. Only use it when the target is a real editor, and only when you actually need tabs preserved (a Makefile, or a tab-indented codebase). It will also change the checksum relative to a tab-expanded run, which is the point.

Non-ASCII characters

Preflight reports every non-ASCII character it finds, with line numbers. These are the characters most likely to map wrong across a remote keyboard channel: smart quotes pasted in from a document, en-dashes, non-breaking spaces, accented letters, emoji. They may arrive as the wrong character, or as nothing at all.

The report is informational -- the run proceeds -- but if your file is meant to be plain ASCII and preflight finds a U+00A0 where you expected a space, fix the source file rather than the transmission.

Aborting a run

Press F9. It stops immediately.

F9 was chosen because type-me never injects that key. Every other candidate key is one the tool might itself type as part of your file's contents, and the abort listener cannot distinguish a synthetic keystroke from a real one. F9 is never in the output stream, so a press is unambiguously you.

Ctrl-C will not work. While typing is in progress the target window has focus, not the terminal you launched from, so the interrupt goes to the wrong process. Do not go hunting for the source terminal mid-run -- clicking around changes focus and the remaining keystrokes land wherever you clicked. Press F9.

If a run is aborted, the far side holds a partial file. In heredoc mode the heredoc is left unterminated, so the shell is sitting at a continuation prompt; type the delimiter to close it, or Ctrl-C in the remote shell to discard. In every case, verify or delete the partial result. Never assume an aborted run left nothing behind.

Modes

raw (default)

Types the file exactly as-is, nothing added, nothing wrapped. Use it anywhere: an editor in paste mode, a text field, a web form, a chat box.

uv run --with pynput python -m type_me notes.txt

raw into a shell prompt means each line is executed as a command. That is sometimes what you want, and it is why raw is the default -- but if what you want is a file on the far side, use heredoc.

raw carries the vim autoindent hazard in full. If the target is vim, use vim mode.

heredoc --dest PATH

For a shell prompt on the far side. Wraps your file so that the remote shell writes it to disk:

set +H
cat > /path/on/remote <<'TYPEOUT_EOF'
...your file, verbatim...
TYPEOUT_EOF

Three protections, all described above: set +H disables history expansion so ! survives, the quoted delimiter disables $VAR and backtick expansion, and the delimiter itself is chosen not to collide with a line in your file.

This is the safest mode. It is also the only one that can verify itself, because there is a file on the far side to checksum -- so it types the checksum command for you once the heredoc closes.

uv run --with pynput python -m type_me deploy.py --mode heredoc --dest /srv/app/deploy.py --verify

--dest is required here. Without a destination there is nothing to redirect into.

Prefer this mode even when your eventual target is an editor: land the file on disk at a shell, checksum it, then open it. It removes the editor from the trusted path entirely.

vim

For typing into vim. Sends Esc, :set paste, i before the content and Esc, :set nopaste after.

uv run --with pynput python -m type_me model.py --mode vim

Put the cursor where you want the text and leave vim in normal or insert mode -- the leading Esc normalises either one. The file is typed into the buffer; saving it is yours to do.

This mode prevents the compounding-indentation failure described above. It does not prevent everything: if your vim has mappings or an InsertLeave autocmd that rewrites the buffer, paste mode will not save you. When in doubt, use heredoc at a shell and open the file afterwards.

Verifying the result

A run that reports success has proved only that keystrokes were sent. It has not proved they arrived. For anything you care about, verify.

The recommended workflow, end to end:

uv run --with pynput python -m type_me deploy.py --mode heredoc --dest /srv/app/deploy.py --verify
  1. The tool preflights the file, prints the plan, and asks you to confirm.
  2. You confirm, then have --start-delay seconds (5 by default) to click into the remote window and put the cursor at a shell prompt.
  3. It types the heredoc, then types the checksum command on the far side and presses Enter. The remote shell prints a hash.
  4. Back in your local terminal, the tool is waiting at a prompt. Type the hash the remote side printed.
  5. It compares against the digest of what was sent, and prints MATCH or MISMATCH.
Expected sha256  3f0a1c9b8e2d47a5c6109fbb4e7d2a83c5f1e09d7b4a6c28e35f81d0a9c47b6e
Expected md5     8e2d47a5c6109fbb4e7d2a83c5f1e09d
CHECKSUM MATCH (sha256)  3f0a1c9b8e2d47a5c6109fbb4e7d2a83c5f1e09d7b4a6c28e35f81d0a9c47b6e

Two digests, and why they differ

The preflight prints src sha256 / src md5. The postflight prints Expected sha256 / Expected md5. Compare the far side against the Expected pair, never the src pair.

They are not always the same number, because what lands on the far side is not always what is on your local disk:

  • tabs are expanded to spaces (unless --keep-tabs)
  • CRLF and lone-CR line endings become LF
  • heredoc mode forces a trailing newline, because a heredoc whose terminator is glued to the last body line never closes

When any of these apply, the tool says so under the expected digests:

Expected sha256  b203770cae481081c939e97190fe0899...
Expected md5     b203770cae481081c939e97190fe0899
  (the far-side file is not byte-identical to your local copy, which is expected:)
    - 2 tabs expanded to spaces (tab width 4)

This distinction is the difference between a check that works and one you learn to ignore. Comparing a tab-indented file against its source digest reports MISMATCH on a perfectly successful transfer; do that twice and you stop reading the output, which is worse than having no check at all.

On MISMATCH, do not try to patch the file by hand. Delete it on the far side and run again more slowly -- raise --delay and --repeat-delay first, since dropped keystrokes are the usual cause.

In raw and vim modes there is no remote file to hash automatically, so --verify prints the command for you to run yourself once you have saved the buffer:

sha256sum /path/on/remote

On a BSD or macOS far side that is:

shasum -a 256 /path/on/remote

A checksum over a file that has not been saved yet proves nothing. Write the buffer first.

Pacing

Defaults target a remote desktop channel. On a local terminal they are needlessly slow; in a browser-based console they are too fast.

Target --delay --newline-delay --repeat-delay
Local terminal, same machine 2 20 0
Workspace ONE / Horizon / RDP 15 (default) 80 (default) 10 (default)
Browser console (Guacamole, vSphere, iDRAC) 40 150 20

Browser-based consoles are the slowest of the three because keystrokes pass through a JavaScript keyboard handler before they reach the protocol. Start at 40 ms and go up, not down.

uv run --with pynput python -m type_me script.sh --delay 2 --repeat-delay 0
uv run --with pynput python -m type_me script.sh --delay 40 --newline-delay 150 --repeat-delay 20

At the defaults, expect roughly a minute per 3 KB. --dry-run reports the estimate before you commit to it. If a file is large enough that the estimate makes you wince, that is a signal to find a real file-transfer path rather than to turn the delays down.

Reference

type-me FILE [options]

  --mode {raw,heredoc,vim}   what to wrap the content in (default: raw)
  --dest PATH                destination path on the far side;
                             REQUIRED when --mode heredoc
  --delay MS                 per-character delay, milliseconds (default: 15)
  --newline-delay MS         extra settle time after Enter (default: 80)
  --repeat-delay MS          extra delay when a character repeats (default: 10)
  --jitter FRAC              randomize delay by +/- this fraction (default: 0.30)
  --start-delay SEC          countdown before typing begins (default: 5)
  --keep-tabs                do not expand tabs to spaces
  --tab-width N              spaces per tab when expanding (default: 4)
  --dry-run                  preflight and plan summary; type nothing
  --verify                   after typing, prompt for the far-side checksum
                             and compare
  --yes                      skip the interactive confirmation

Notes on a few of these:

  • --delay, --newline-delay and --repeat-delay are in milliseconds on the command line. --start-delay is in seconds.
  • --jitter is a fraction of --delay, so the default 0.30 with a 15 ms delay gives a range of roughly 10.5 to 19.5 ms per character.
  • --repeat-delay is additive on top of --delay, and applies only when a character is identical to the one before it.
  • --yes skips the confirmation prompt but not the --start-delay countdown. You still need those seconds to change focus.
  • --dry-run is the right first move on any file you have not typed before.

How it is put together

Four modules, split along one line: the code that decides what to type is pure and fully testable, and the code that touches the keyboard is as thin as it can possibly be.

  • type_me/actions.py -- the frozen contract. Text, Key, Pause, the Options dataclass that holds every default, and the Backend protocol. Imports nothing, touches nothing. Both sides depend on this and not on each other.
  • type_me/core.py -- pure. Takes file text plus Options and returns a list of actions. Tab expansion, heredoc wrapping, delimiter selection, vim's set paste bracketing, pacing decisions, preflight findings: all of it happens here, and none of it requires a keyboard.
  • type_me/executor.py -- impure, and deliberately dull. Walks the action list and presses keys via pynput. It makes no decisions.
  • type_me/cli.py -- argument parsing, preflight reporting, the confirmation prompt, the countdown, checksum comparison.

The point of the split is that the risky logic -- delimiter collision, tab expansion, indentation handling -- is exercised in tests against a list of actions, with no simulator, no focused window and no flakiness. If you change how a file is turned into keystrokes, core.py is where it goes and where the test belongs.

uv run --with pytest python -m pytest -q

The fixture

examples/sample.py is 36 lines of valid, runnable Python containing every hazard on this page at once:

  • nesting six levels deep, so vim autoindent drift is visible immediately
  • a string with ! in it, for history expansion
  • a $VAR and a ${HOME}, for parameter expansion
  • a backtick pair in a comment, for command substitution
  • a string containing TYPEOUT_EOF, for delimiter collision
  • one line ending in trailing whitespace, which you cannot see and the checksum can
  • a tab-indented block at the bottom

It runs, so a round trip can be checked two ways:

python3 examples/sample.py
sha256sum examples/sample.py

Type it, run both on the far side, compare. If the output matches and the hash matches, the channel is good.

About

No description or website provided.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages