This is an app to type the text symbol by symbol like on the keyboard from text copied to the keyboard.
At this moment name "tray" is not defined. Fix this and any other issue that you'd see.

import pyperclip
import time
import string
from pynput.keyboard import Key, Controller, Listener
from PyQt5 import QtWidgets, QtGui, QtCore

delay = 100
first_char_delay = 2000
SETTINGS_FILE = 'settings.txt'
shift_delay = 100

def is_key_upper_case(char):
    return char.is_upper() or char in string.punctuation

class Worker(QtCore.QThread):
    clipboard_changed = QtCore.pyqtSignal(str)

    def init(self, parent=None):
        super().init(parent)
        self.clipboard_content = pyperclip.paste()

    def run(self):
        while True:
            new_clipboard_content = pyperclip.paste()
            if new_clipboard_content != self.clipboard_content:
                self.clipboard_content = new_clipboard_content
                self.clipboard_changed.emit(new_clipboard_content)
            time.sleep(0.1)

class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
    def init(self, icon, parent=None):
        QtWidgets.QSystemTrayIcon.init(self, icon, parent)
        self.menu = QtWidgets.QMenu()
        self.settings_action = self.menu.addAction('Settings')
        self.exit_action = self.menu.addAction('Exit')
        self.setContextMenu(self.menu)
        self.activated.connect(self.icon_activated)
        self.settings_action.triggered.connect(self.open_settings)
        self.exit_action.triggered.connect(self.close_settings)
        self.settings_dialog = None
        
        self.worker = Worker()
        self.worker.start()
        self.worker.clipboard_changed.connect(self.handle_clipboard_change)
        
        self.ctrl_pressed = False
        self.alt_pressed = False
        self.shift_pressed = False
        self.keyboard_listener = Listener(on_press=self.on_press, on_release=self.on_release)
        self.keyboard_listener.start()

    def icon_activated(self, reason):
        if reason == QtWidgets.QSystemTrayIcon.Context:
            self.menu.exec_(QtGui.QCursor.pos())

    def on_press(self, key):
        if key == Key.ctrl_l:
            self.ctrl_pressed = True
        elif key == Key.alt_l:
            self.alt_pressed = True
        elif key == Key.shift:
            self.shift_pressed = True

        if self.ctrl_pressed and self.alt_pressed and self.shift_pressed:
            self.type_copied_text()

    def on_release(self, key):
        if key == Key.ctrl_l:
            self.ctrl_pressed = False
        elif key == Key.alt_l:
            self.alt_pressed = False
        elif key == Key.shift:
            self.shift_pressed = False

    def handle_clipboard_change(self, text):
        self.worker.clipboard_content = text

    def open_settings(self):
        if not self.settings_dialog:
            self.settings_dialog = SettingsDialog()
        self.settings_dialog.show()

    def close_settings(self):
        QtWidgets.QApplication.quit()

    def type_copied_text(self):
        new_clipboard_content = pyperclip.paste().strip()
        if new_clipboard_content:
            time.sleep(first_char_delay / 1000)
            keyboard = Controller()
            for char in new_clipboard_content:
                if char in string.printable:
                    if is_key_upper_case(char):
                        keyboard.press(Key.shift)
                    keyboard.press(char)
                    keyboard.release(char)
                    if is_key_upper_case(char):
                        time.sleep(shift_delay / 1000)
                        keyboard.release(Key.shift)
                    time.sleep(delay / 1000)

class SettingsDialog(QtWidgets.QDialog):
    def init(self, parent=None):
        super().init(parent)
        self.setWindowTitle('Settings')
        self.setFixedSize(400, 300)
        self.delay_label = QtWidgets.QLabel('Delay between characters (ms):')
        self.delay_spinbox = QtWidgets.QSpinBox()
        self.delay_spinbox.setMinimum(0)
        self.delay_spinbox.setMaximum(10000)
        self.delay_spinbox.setValue(delay)
        self.first_char_delay_label = QtWidgets.QLabel('Delay before first character (ms):')
        self.first_char_delay_spinbox = QtWidgets.QSpinBox()
        self.first_char_delay_spinbox.setMinimum(0)
        self.first_char_delay_spinbox.setMaximum(10000)
        self.first_char_delay_spinbox.setValue(first_char_delay)
        self.save_button = QtWidgets.QPushButton('Save')
        self.cancel_button = QtWidgets.QPushButton('Cancel')
        self.save_button.clicked.connect(self.save_settings)
        self.cancel_button.clicked.connect(self.reject)
        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(self.delay_label)
        layout.addWidget(self.delay_spinbox)
        layout.addWidget(self.first_char_delay_label)
        layout.addWidget(self.first_char_delay_spinbox)
        buttons_layout = QtWidgets.QHBoxLayout()
        buttons_layout.addWidget(self.save_button)
        buttons_layout.addWidget(self.cancel_button)
        layout.addLayout(buttons_layout)
        self.setLayout(layout)

    def save_settings(self):
        global delay, first_char_delay
        delay = self.delay_spinbox.value()
        first_char_delay = self.first_char_delay_spinbox.value()
        with open(SETTINGS_FILE, 'w') as f:
            f.write(f"{delay}\n{first_char_delay}")
        self.accept()

if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    app.setQuitOnLastWindowClosed(False)
    tray