Create Your Own Text Expander with Python
I have been using text expansion as a powerful productivity tool to save countless keystrokes and hours of typing time. I was used to using a-Tex and AutoHotKey on Windows, but when I switched to Linux, I searched for an alternative. I tried AutoKey and Espanso but couldn't find a suitable option. Finally, I decided to create my own text expander.
In this post, I will walk you through the creation of Auto Text Expander for your Ubuntu 2024.04 LTS machine.
What is a Text Expander?
A text expander is a tool that automatically replaces shortcuts (triggers) with longer pieces of text (expansions). For example, by typing ";; email," the tool can automatically expand it to your full email address. This tool can be incredibly useful for frequently typed phrases, addresses, code snippets, or any text you find yourself repeating often.
How Our Text Expander Works
Our custom text expander uses Python to:
Listen for keyboard input
Match typed text against predefined triggers
Replace triggers with their corresponding expansions
The script runs in the background as a system service, ensuring it's always available when you need it.
Setting Up the Text Expander
Prerequisites
Python 3
pynput
library (install withpip install pynput
)
Step 1: Create the Python Script
Create a new file named text_
expander.py
in your project directory (e.g., ~/dev/AutoExpander/text_
expander.py
). Paste the following code:
### Step 1:
import time
import threading
import logging
import json
import os
from pynput import keyboard
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Buffer to store typed characters
buffer = ""
# Create a keyboard controller instance globally
controller = keyboard.Controller()
# Default expansions
DEFAULT_EXPANSIONS = {
";;email": "meeran@duck.com",
}
TEXT_EXPANSION_CONFIG_FILE = "~/dotfiles/local/text-expander-config.json"
# Load or create expansions JSON file
def load_or_create_expansions():
json_path = os.path.expanduser(TEXT_EXPANSION_CONFIG_FILE)
directory = os.path.dirname(json_path)
try:
# Create directory if it doesn't exist
os.makedirs(directory, exist_ok=True)
# Try to load existing file
if os.path.exists(json_path):
with open(json_path, 'r') as file:
return json.load(file)
else:
# Create new file with default expansions
with open(json_path, 'w') as file:
json.dump(DEFAULT_EXPANSIONS, file, indent=4)
logging.info(f"Created new expansions file at {json_path}")
return DEFAULT_EXPANSIONS
except Exception as e:
logging.error(f"Error loading or creating expansions: {str(e)}")
return {}
# Load expansions
expansions = load_or_create_expansions()
def type_string(string):
for line in string.split('\n'):
for char in line:
if char == '@':
controller.press(keyboard.Key.shift)
controller.press('2')
controller.release('2')
controller.release(keyboard.Key.shift)
else:
controller.press(char)
controller.release(char)
time.sleep(0.01) # Small delay between keystrokes
controller.press(keyboard.Key.enter)
controller.release(keyboard.Key.enter)
def on_press(key):
global buffer
try:
# Check if the key has a character value
char = key.char
buffer += char
# Check if the buffer ends with any of the triggers
for trigger, expansion in expansions.items():
if buffer.endswith(trigger):
# Delete the trigger characters using backspace
for _ in range(len(trigger)):
controller.press(keyboard.Key.backspace)
controller.release(keyboard.Key.backspace)
# Type the expansion
type_string(expansion)
# Clear the buffer
buffer = ""
break
# Keep buffer size reasonable (last 20 characters)
buffer = buffer[-20:]
except AttributeError:
# Handle non-character keys (shift, ctrl, etc.) without affecting buffer
pass
except Exception as e:
logging.error(f"Error in on_press: {str(e)}")
def run_listener():
try:
# Start listening for keypresses
with keyboard.Listener(on_press=on_press) as listener:
logging.info("Keyboard listener started")
listener.join()
except Exception as e:
logging.error(f"Error in run_listener: {str(e)}")
# Running the listener in a background thread
listener_thread = threading.Thread(target=run_listener, daemon=True)
listener_thread.start()
# Main thread can perform other tasks or just sleep
try:
logging.info("Auto Expander script started")
while True:
time.sleep(1) # Keep the main thread alive
except KeyboardInterrupt:
logging.info("Script interrupted by user.")
except Exception as e:
logging.error(f"Unexpected error in main thread: {str(e)}")
### Step 2: Create a System Service
To run the text expander as a system service, create a new service file:
```bash
sudo nano /etc/systemd/system/text-expander.service
Add the following content (adjust paths as necessary):
[Unit]
Description=Text Expander Python Script
After=network.target
[Service]
ExecStart=/usr/bin/python3 /home/your_username/dev/AutoExpander/text_expander.py
WorkingDirectory=/home/your_username/dev/AutoExpander
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=5
User=your_username
Environment=DISPLAY=:0
Environment=XAUTHORITY=/home/your_username/.Xauthority
[Install]
WantedBy=default.target
Replace your_username
with your actual username.
Step 3: Enable and Start the Service
Run these commands to enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable text-expander
sudo systemctl start text-expander
Configuring Your Text Expansions
The text expander uses a JSON file to store your expansions. By default, it's located at ~/dotfiles/local/text-expansion.json
. If the file doesn't exist, the script will create it with some default expansions.
To add or modify expansions, edit this JSON file:
nano ~/dev/text-expansion.json
The file structure is simple:
{
";;email": "your.email@example.com",
";;phone": "123-456-7890",
";;addr": "123 Main St, City, State, ZIP"
}
Add new triggers and expansions as needed. You can use any character as prefix instead of ";;"
Making Changes
Modify Expansions: Edit the
text-expansion.json
file and save your changes. Restart the service for changes to take effect:sudo systemctl restart text-expander
Update the Script: If you make changes to the Python script, save the file and restart the service:
sudo systemctl restart text-expander
Check Status: To check if the service is running correctly:
sudo systemctl status text-expander
View Logs: To view the logs for troubleshooting:
sudo journalctl -u text-expander -n 50 --no-pager
Conclusion
"With this custom text expander, you have a powerful and flexible tool to boost your productivity. It is easily customizable and runs smoothly as a system service. Remember, with great power comes great responsibility."