typechar 1.0.0

Type any Unicode string on macOS, regardless of keyboard layout
#[cfg(not(target_os = "macos"))]
compile_error!("typechar only works on macOS — it types characters via CoreGraphics events");

use std::thread::sleep;
use std::time::Duration;

use core_graphics::event::{CGEvent, CGEventTapLocation};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use typechar::{Action, parse_args, utf16_chunks};

/// CGEventKeyboardSetUnicodeString silently truncates long strings; 20 UTF-16
/// code units per event is the documented-by-folklore safe limit.
const MAX_UNITS_PER_EVENT: usize = 20;

// Not exposed by the core-graphics crate; resolved from the already-linked
// CoreGraphics framework.
unsafe extern "C" {
    fn CGPreflightPostEventAccess() -> bool;
    fn CGRequestPostEventAccess() -> bool;
}

const USAGE: &str = "\
typechar — type any Unicode string, regardless of keyboard layout

Usage:
  typechar [OPTIONS] <string>
  typechar [OPTIONS] --unicode <hex> [--unicode <hex>...]

Options:
  -u, --unicode <hex>  Codepoint to type, e.g. 20ac or U+20AC (repeatable)
  -d, --delay <ms>     Wait before typing (for apps that drop fast events)
  -h, --help           Show this help
  -V, --version        Show version

Examples:
  typechar €
  typechar --unicode 20ac
  typechar --delay 100 '¯\\_(ツ)_/¯'";

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    match parse_args(&args) {
        Ok(Action::Help) => println!("{USAGE}"),
        Ok(Action::Version) => println!("typechar {}", env!("CARGO_PKG_VERSION")),
        Ok(Action::Type { text, delay_ms }) => {
            if delay_ms > 0 {
                sleep(Duration::from_millis(delay_ms));
            }
            if let Err(msg) = type_text(&text) {
                eprintln!("typechar: {msg}");
                std::process::exit(1);
            }
        }
        Err(msg) => {
            eprintln!("typechar: {msg}\n\n{USAGE}");
            std::process::exit(2);
        }
    }
}

fn type_text(text: &str) -> Result<(), String> {
    if !unsafe { CGPreflightPostEventAccess() } {
        unsafe { CGRequestPostEventAccess() };
        return Err("no permission to post keyboard events.\n\
             Grant Accessibility access to the app that runs typechar (your terminal, \
             Karabiner, skhd, ...) in System Settings → Privacy & Security → Accessibility, \
             then try again."
            .into());
    }

    let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
        .map_err(|_| "failed to create event source".to_string())?;

    for chunk in utf16_chunks(text, MAX_UNITS_PER_EVENT) {
        for key_down in [true, false] {
            let event = CGEvent::new_keyboard_event(source.clone(), 0, key_down)
                .map_err(|_| "failed to create keyboard event".to_string())?;
            event.set_string_from_utf16_unchecked(&chunk);
            event.post(CGEventTapLocation::HID);
        }
        // Give the WindowServer a beat between chunks so long strings arrive in order.
        sleep(Duration::from_millis(1));
    }
    Ok(())
}