Search code examples
pythonpyautogui

How to simulate human keyboard input of a long text using Python?


I would like to simulate typing of huge text. I've tried to use pyautogui to do that, but I't didn't worked properly since the computer was not detecting the keys being pressed:

pyautogui.typewrite('I love python! It is such an amazing language.')

I couldn't think of another way to do that than using pynput.keyboard library, but as I have a huge text, that would not be a viable option. I don't know if there is a way to simulate the keys other than create a line for each letter.

keyboard = Controller()

keyboard.press('I')
keyboard.release('I')

keyboard.press('L')
keyboard.release('L')

keyboard.press('O')
keyboard.release('O')

keyboard.press('V')
keyboard.release('V')

Is there a way to simulate keys being pressed to write a text without having to create a single line for each letter that the script would have to type?


Solution

  • The code below demonstrates that there is a way to simulate keys being pressed to write a text without having to create a single line for each letter that the script would have to type.

    In addition to that the provided solution allows to execute code placed in the same script after the code for automated typing which feature is used to show the automated keyboard input in a typing speed test GUI:

    import pynput
    import random 
    import threading
    import time
    
    txt = "pynput is typing like a human"
    #txt = "abc" 
    
    class Sim_keyb_typing(threading.Thread):
        def __init__(self, text, 
                     strt_delay=1.5, 
                     dct_delays={('a','i'):0.15}, 
                     delayrange=(0.1, 0.3) ):
            threading.Thread.__init__(self)
    
            self.text       = text
            self.strt_delay = strt_delay
            self.last_char  = " "
            self.dct_delays = dct_delays
            self.delayrange = delayrange
    
            self.ppkbC = pynput.keyboard.Controller()
    
        def run(self):
            time.sleep(self.strt_delay)
            for char in self.text:
                delay = self.dct_delays.get( 
                    (self.last_char, char), random.uniform(*self.delayrange) )
                time.sleep(delay)
                self.ppkbC.type(char)
                self.last_char = char
    
    # Let's wait 1.5 seconds and then start typing: 
    skt = Sim_keyb_typing(txt)
    skt.start()
    # Wait for Sim_keyb_typing(txt) to finish
    #skt.join()
    
    # ======================================================================
    import tkinter
    import time
    import threading
    import random
    import string
    
    class simpleTypeSpeedGUI: # creates tkinter GUI mainloop
    
        def __init__(self, txt):
            self.root = tkinter.Tk()
            self.root.title("Type Speed Test (starts on first keypress)")
            self.root.geometry("1600x400")
    
            self.text_lines = txt.split("\n")
            self.text_line = random.choice(self.text_lines)
            self.len_text_line = len(self.text_line)
    
            # Label showing text to type:
            self.sample_label = tkinter.Label(self.root, text=self.text_line, font=("Helvetica", 14))
            self.sample_label.grid( row=0, column=1, columnspan=2, padx=15, pady=15, sticky='w')
    
            # Text box receiving typing input: 
            self.input_textbox = tkinter.Entry(self.root, width=120, font=("Helvetica", 14))
            self.input_textbox.grid(row=1, column=1, columnspan=2, padx=15, pady=15)
            # Registering callback function for text box keyboard input keypress
            self.input_textbox.bind("<KeyRelease>", self.on_key_up) 
            # The reason for          KeyRelease  is to have updated widget content in the callback function
            self.input_textbox.focus()
    
            # Label showing typing speed: 
            self.speed_label = tkinter.Label(self.root, text="Typing speed: \n---- \n---- \n---- \n---- ", font=("Helvetica", 12))
            self.speed_label.grid(row=2, column=1, columnspan=2, padx=5, pady=10)
    
            # Button for restart/reset of typing: 
            self.reset_button = tkinter.Button(self.root, text="Reset", command=self.reset, font=("Helvetica", 18))
            self.reset_button.grid(row=3, column=1, columnspan=2, padx=15, pady=15)
    
            # Standard value for average number of characters in a word 
            self.std_char_per_word = 5
            # used to calculate words per minute from typing speed in chars/min
    
            # initialisation of code flow control values: 
            self.start_time     = None
            self.end_time       = None
            self.printable = string.printable[:-5]
    
            self.root.mainloop()
    
        def on_key_up(self, event):
            # processing on key release because on key press results
            # in the widget content are not yet updated with the key char. 
            if event.keycode == 9: # pressed Escape => EXIT
                print("\nEscape-EXIT")
                self.root.destroy()
                return # <- required because root.destroy() doesn't block 
                       # execution of further code which then runs into 
                       # trouble as .destroy() makes widgets not available
    
            if self.start_time is None:
                self.start_time = time.perf_counter()
    
            input_textbox = self.input_textbox.get()
           
            # print(f'{event.keycode=} => "{event.char=}" ')
    
            if input_textbox == self.text_line:
                self.running = False
                self.input_textbox.config(fg="green")
                self.end_time = time.perf_counter()
            else: 
                if self.text_line.startswith(input_textbox):
                    self.input_textbox.config(fg="black")
                else:
                    self.input_textbox.config(fg="red")
    
            self.show_typespeed()
    
        def show_typespeed(self):
            curr_time = time.perf_counter()
                
            if   self.start_time is not None and self.end_time is not None:  
                time_diff = self.end_time - self.start_time
            elif self.start_time is not None and self.end_time is None: # and self.end_time is None  
                time_diff = curr_time - self.start_time
            elif self.start_time is None and self.end_time is None: 
                self.speed_label.config(text="Speed: \n---- CPS\n---- CPM\n---- WPS\n---- WPS")
                return
            cps = self.len_text_line / time_diff
            cpm = cps * 60.0
            wps = cps / self.std_char_per_word
            wpm = wps * 60.0
            self.speed_label.config(text=f"Speed: \n{cps:5.2f} CPS\n{cpm:5.2f} CPM\n{wps:5.2f} WPS\n{wpm:5.2f} WPM")
    
        def reset(self):
            self.start_time = None
            self.end_time   = None
            self.text_line = random.choice(self.text_lines)
            self.len_text_line = len(self.text_line)
            self.sample_label.config(text=self.text_line)
            self.input_textbox.delete(0, tkinter.END)
            self.show_typespeed()
    
    sTSG = simpleTypeSpeedGUI(txt)