- Create a project folder, e.g.
clipboard-fixer/ - Create and activate a Python virtual environment inside it:
python -m venv .venv && source .venv/bin/activate - Install dependencies:
pip install PyQt6 - Create the main entry point file:
main.py - Create a
rules.pyfile where link correction logic will live - Create a
config.pyfile to hold user preferences (e.g. which rules are enabled) - Create a
README.mddescribing what the app does and how to run it
- In
main.py, importQApplication,QSystemTrayIcon,QMenu, andQClipboardfrom PyQt6 - Instantiate
QApplicationwithsys.argvand setapp.setQuitOnLastWindowClosed(False)so the app stays alive with no visible windows - Create a
ClipboardWatcherclass that inherits fromQObject - In
ClipboardWatcher.__init__, grab the clipboard viaQApplication.clipboard() - Connect
clipboard.dataChangedsignal to a handler method (e.g.on_clipboard_changed) - Verify the event loop runs and the
dataChangedsignal fires by printing to console when you copy anything
- Create a
TrayIconclass that inherits fromQSystemTrayIcon - Supply a 16x16 or 32x32 PNG icon file in the project folder and load it with
QIcon - Build a right-click
QMenuwith at minimum: "Pause / Resume", "Settings", "Quit" - Wire "Quit" to
QApplication.quit() - Wire "Pause / Resume" to a boolean flag on
ClipboardWatcherthat skips processing when paused - Call
tray.show()to make it visible in the system tray - Confirm the tray icon appears and the menu opens correctly
- In
on_clipboard_changed, callclipboard.text()to get the current clipboard string - Write a helper function
is_link(text: str) -> boolusing a regex orurllib.parse.urlparseto detect URLs - Guard the handler: only proceed if
is_link(text)returnsTrue - Log detected links to the console for now to confirm detection works
- In
rules.py, define aRuledataclass or namedtuple with fields:name,enabled,apply(url: str) -> str - Implement the rules you want — common ones to start with:
- Strip UTM tracking parameters (
utm_source,utm_medium,utm_campaign, etc.) - Remove AMP suffixes (
/amp,?amp=1) - Replace
x.comwithvxtwitter.comin any copied link — match the full domain only (i.e.x.com/orwww.x.com/) so substrings likebox.comare not affected; preserve the entire path, query string, and fragment exactly as-is so the link stays functional - Strip Facebook redirect wrappers (
l.facebook.com/l.php?u=...) - Unwrap Google redirect URLs (
google.com/url?q=...) - Force HTTPS on HTTP links
- Strip UTM tracking parameters (
- Write a
clean_url(url: str, rules: list[Rule]) -> strfunction that runs the URL through each enabled rule in sequence - Write unit tests in
test_rules.pyusingunittestorpytestcovering each rule with a before/after URL pair
- In
on_clipboard_changed, after detecting a link, pass it throughclean_url() - Compare the result to the original — if they differ, call
clipboard.setText(cleaned_url)to replace it - Guard against infinite loops: the
setTextcall will itself firedataChanged. Set a flag (e.g.self._setting = True) before writing, check it at the top of the handler, and clear it immediately after - Show a tray notification via
QSystemTrayIcon.showMessage()when a correction is made, displaying the original vs cleaned URL
- In
ClipboardWatcher, add acollections.deque(maxlen=10)to store recent corrections; each entry is a(original_url, cleaned_url)tuple - After a correction is applied in
on_clipboard_changed, append the pair to the deque - In
TrayIcon, add a "Recent Corrections" submenu to the context menu - Connect the submenu's
aboutToShowsignal to a method that rebuilds its entries from the deque each time it opens - Each entry should display a truncated form of the cleaned URL; clicking it copies that cleaned URL back to the clipboard
- When the deque is empty, show a single disabled "No corrections yet" placeholder item
- Create a
settings_dialog.pywith aQDialogsubclass - Add a
QListWidgetor table of checkboxes, one per rule, so the user can toggle rules on/off - Persist settings to a JSON file in the user's config directory (use
QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)) - Load settings on startup in
config.pyand apply them when building the rules list - Wire the "Settings" tray menu item to open this dialog
- macOS: Create a
LaunchAgentplist at~/Library/LaunchAgents/com.yourname.clipboardfixer.plistpointing to the Python executable andmain.py - Windows: Add a registry entry under
HKCU\Software\Microsoft\Windows\CurrentVersion\Runor create a shortcut in the Startup folder - Linux (systemd): Create a
~/.config/systemd/user/clipboard-fixer.serviceunit file and enable it withsystemctl --user enable --now clipboard-fixer - Test that the app starts automatically after a reboot and appears in the tray
- Add a
requirements.txtby runningpip freeze > requirements.txt - Install
pyinstaller:pip install pyinstaller - Run
pyinstaller --onefile --windowed --icon=icon.png main.pyto produce a standalone binary - Test the binary on a clean machine (or clean virtualenv) to confirm no missing dependencies
- Write a short install section in
README.mdcovering both "run from source" and "use the binary" paths