""" form_filler_ui.py Tkinter UI + Playwright browser helper that types your chosen text into the currently-focused field in a browser page at a configurable WPM. Workflow 1) Enter URL, text to type, and WPM in this app. 2) Click "Open Browser". 3) In the browser, click/focus the target input/textarea/contenteditable field. 4) Trigger typing either by: - Clicking "Type Now" in the app, OR - Pressing the in-browser hotkey: Ctrl+Shift+Enter (configurable in code) This uses Playwright to generate DOM keyboard events in the page (not OS-level keystroke injection). Intended for websites/apps you own or have permission to automate. Install: pip install playwright playwright install Run: python form_filler_ui.py """ from __future__ import annotations import random import threading import time import tkinter as tk from dataclasses import dataclass from typing import Optional # Playwright is imported lazily inside the worker thread so the UI can still open # with a helpful error message if Playwright isn't installed. HOTKEY = "Ctrl+Shift+Enter" # in-browser hotkey to start typing into focused element def delay_from_wpm(wpm: float) -> float: """Seconds per character using standard 5 chars/word.""" if wpm <= 0: raise ValueError("WPM must be > 0") chars_per_sec = (wpm * 5.0) / 60.0 return 1.0 / chars_per_sec @dataclass class Humanize: enabled: bool = True jitter_min_s: float = -0.02 jitter_max_s: float = 0.02 pause_on_space_s: float = 0.05 pause_on_punct_s: float = 0.15 punct_chars: str = ".!?," class BrowserWorker: """ Runs Playwright in a background thread. Supports: - opening a URL - arming an in-page hotkey to start typing - typing now on demand """ def __init__(self) -> None: self._thread: Optional[threading.Thread] = None self._lock = threading.Lock() self._running = False self._want_open = False self._want_type = False self._url: str = "" self._text: str = "" self._wpm: float = 90.0 self._humanize = Humanize() self._status_cb = None # set by UI # Playwright objects (owned by worker thread) self._page = None self._browser = None self._context = None def set_status_callback(self, cb): self._status_cb = cb def _status(self, msg: str) -> None: if self._status_cb: self._status_cb(msg) def start(self) -> None: if self._thread and self._thread.is_alive(): return self._running = True self._thread = threading.Thread(target=self._run, daemon=True) self._thread.start() def stop(self) -> None: with self._lock: self._running = False def request_open(self, url: str) -> None: with self._lock: self._url = url self._want_open = True def request_type(self, text: str, wpm: float, humanize: Humanize) -> None: with self._lock: self._text = text self._wpm = wpm self._humanize = humanize self._want_type = True def _compute_delay_s(self, ch: str, base_s: float, h: Humanize) -> float: d = base_s if h.enabled: d = max(0.005, d + random.uniform(h.jitter_min_s, h.jitter_max_s)) if ch == " ": d += h.pause_on_space_s elif ch in h.punct_chars: d += h.pause_on_punct_s return d def _install_hotkey_listener(self) -> None: # Sets window.__macroStart = true when Ctrl+Shift+Enter is pressed js = r""" (function () { window.__macroStart = false; window.addEventListener('keydown', (e) => { const isEnter = (e.key === 'Enter'); if (isEnter && e.ctrlKey && e.shiftKey) { window.__macroStart = true; } }, true); })(); """ self._page.evaluate(js) def _consume_hotkey_flag(self) -> bool: try: started = self._page.evaluate("(() => !!window.__macroStart)()") if started: self._page.evaluate("(() => { window.__macroStart = false; })()") return bool(started) except Exception: return False def _type_into_focused(self, text: str, wpm: float, humanize: Humanize) -> None: if not self._page: return base = delay_from_wpm(wpm) self._status(f"Typing… ({wpm:.0f} WPM) Focus a field in the browser.") for ch in text: with self._lock: if not self._running: break delay_s = self._compute_delay_s(ch, base, humanize) # Playwright delay is in ms and applies to keypress inside the page self._page.keyboard.type(ch, delay=int(delay_s * 1000)) self._status("Done typing.") def _run(self) -> None: try: from playwright.sync_api import sync_playwright # type: ignore except Exception: self._status("Playwright not installed. Run: pip install playwright && playwright install") return with sync_playwright() as p: self._status("Launching browser…") self._browser = p.chromium.launch(headless=False) self._context = self._browser.new_context() self._page = self._context.new_page() self._page.set_default_timeout(30000) self._install_hotkey_listener() self._status(f"Browser ready. Hotkey in browser: {HOTKEY}") while True: with self._lock: if not self._running: break want_open = self._want_open url = self._url if want_open: self._want_open = False want_type = self._want_type text = self._text wpm = self._wpm humanize = self._humanize if want_type: self._want_type = False if want_open and url.strip(): try: self._status(f"Opening: {url}") self._page.goto(url.strip()) self._install_hotkey_listener() self._status(f"Opened. Click a field, then press {HOTKEY} (or click Type Now).") except Exception as e: self._status(f"Failed to open URL: {e}") # If user hits hotkey in the page, start typing into focused element if self._consume_hotkey_flag(): self._type_into_focused(text, wpm, humanize) # If UI requested typing if want_type: self._type_into_focused(text, wpm, humanize) time.sleep(0.05) self._status("Closing browser…") try: if self._context: self._context.close() if self._browser: self._browser.close() except Exception: pass self._status("Stopped.") class App(tk.Tk): def __init__(self) -> None: super().__init__() self.title("Form Filler (WPM Macro via Playwright)") self.geometry("860x520") self.worker = BrowserWorker() self.worker.set_status_callback(self._set_status_threadsafe) self._build_ui() def _build_ui(self) -> None: pad = {"padx": 12, "pady": 6} tk.Label(self, text="URL to open:", font=("Segoe UI", 11, "bold")).pack(anchor="w", **pad) self.url_entry = tk.Entry(self, font=("Consolas", 12)) self.url_entry.pack(fill="x", padx=12) self.url_entry.insert(0, "https://example.com/form") tk.Label(self, text="Text to type (paste anything):", font=("Segoe UI", 11, "bold")).pack(anchor="w", **pad) self.text_box = tk.Text(self, height=8, wrap="word", font=("Consolas", 12)) self.text_box.pack(fill="both", expand=False, padx=12) self.text_box.insert("1.0", "Juan Dela Cruz\njuan@example.com\nHello! This was typed by the helper.") row = tk.Frame(self) row.pack(fill="x", padx=12, pady=10) tk.Label(row, text="WPM:", font=("Segoe UI", 10, "bold")).pack(side="left") self.wpm_spin = tk.Spinbox(row, from_=10, to=300, increment=5, width=6, font=("Consolas", 11)) self.wpm_spin.pack(side="left", padx=(6, 14)) self.wpm_spin.delete(0, "end") self.wpm_spin.insert(0, "120") self.humanize_var = tk.BooleanVar(value=True) tk.Checkbutton(row, text="Humanize cadence", variable=self.humanize_var).pack(side="left") tk.Label(row, text=f"Browser hotkey: {HOTKEY}", font=("Segoe UI", 10)).pack(side="right") btns = tk.Frame(self) btns.pack(fill="x", padx=12, pady=(0, 10)) self.open_btn = tk.Button(btns, text="Open Browser", command=self.open_browser) self.open_btn.pack(side="left") self.type_btn = tk.Button(btns, text="Type Now (into focused field)", command=self.type_now) self.type_btn.pack(side="left", padx=(8, 0)) self.stop_btn = tk.Button(btns, text="Stop", command=self.stop) self.stop_btn.pack(side="left", padx=(8, 0)) tk.Label(self, text="Status:", font=("Segoe UI", 11, "bold")).pack(anchor="w", **pad) self.status = tk.Label(self, text="Idle. Click Open Browser.", font=("Segoe UI", 10)) self.status.pack(anchor="w", padx=12) tips = ( "How to use:\n" "1) Click Open Browser.\n" "2) In the browser, click the input field you want to fill.\n" f"3) Press {HOTKEY} in the browser OR click 'Type Now' here.\n\n" "Tip: This types into the currently focused element. For multi-field forms, you can:\n" "- click field 1, trigger typing\n" "- click field 2, trigger typing\n" "- etc.\n" ) self.tips = tk.Label(self, text=tips, justify="left", font=("Segoe UI", 9)) self.tips.pack(anchor="w", padx=12, pady=(10, 0)) def _set_status_threadsafe(self, msg: str) -> None: # Playwright runs in a worker thread; update Tk on main thread self.after(0, lambda: self.status.configure(text=msg)) def _get_text(self) -> str: return self.text_box.get("1.0", "end-1c") def _get_wpm(self) -> float: try: w = float(self.wpm_spin.get()) if w <= 0: raise ValueError return w except Exception: return 120.0 def open_browser(self) -> None: self.worker.start() url = self.url_entry.get().strip() self.worker.request_open(url) self.status.configure(text="Starting browser…") def type_now(self) -> None: text = self._get_text() wpm = self._get_wpm() h = Humanize(enabled=bool(self.humanize_var.get())) self.worker.request_type(text, wpm, h) def stop(self) -> None: self.worker.stop() self.status.configure(text="Stopping…") def on_close(self) -> None: self.stop() self.destroy() def main() -> None: app = App() app.protocol("WM_DELETE_WINDOW", app.on_close) app.mainloop() if __name__ == "__main__": main()