Skip to content

Commit

Permalink
Added 'Start Textinator on login' option
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Sep 28, 2022
1 parent 614f362 commit 209b317
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 6 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,27 @@ To upgrade to the latest version, download the latest installer DMG from [releas
- `Text detection threshold confidence`: The confidence threshold for text detection. The higher the value, the more accurate the text detection will be but a higher setting may result in some text not being detected (because the detected text was below the specified threshold). The default value is 'Low' which is equivalent to a [VNRecognizeTextRequest](https://developer.apple.com/documentation/vision/vnrecognizetextrequest?language=objc) confidence threshold of `0.3` (Medium = `0.5`, Migh = `0.8`).
- `Text recognition language`: Select language for text recognition (languages listed by [ISO code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and are limited to those which your version of macOS supports).
- `Always detect English`: If checked, always attempts to detect English text in addition to the primary language selected by `Text recognition language` setting.
- `Detect QR Codes`: In addition to detecting text, also detect QR codes and copy the decoded payload text to the clipboard.
- `Notification`: Whether or not to show a notification when text is detected.
- `Keep linebreaks`: Whether or not to keep linebreaks in the detected text; if not set, linebreaks will be stripped.
- `Append to clipboard`: Append to the clipboard instead of overwriting it.
- `Clear clipboard`: Clear the clipboard.
- `Start Textinator on login`: Add Textinator to the Login Items list so it will launch automatically when you login. This will cause Textinator to prompt for permission to send AppleScript events to the System Events app (see screnshot below).
- `About Textinator`: Show the about dialog.
- `Quit Textinator`: Quit Textinator.

When you first select `Start Textinator on login`, you will be prompted to allow Textinator to send AppleScript events to the System Events app. This is required to add Textinator to the Login Items list. The screenshot below shows the prompt you will see.

![System Events permission](images/system_events_access.png)

## Inspiration

I heard [mikeckennedy](https://github.com/mikeckennedy) mention [Text Sniper](https://textsniper.app/) on [Python Bytes](https://pythonbytes.fm/) podcast [#284](https://pythonbytes.fm/episodes/show/284/spicy-git-for-engineers) and thought "That's neat! I bet I could make a clone in Python!" and here it is. You should listen to Python Bytes if you don't already and you should go buy Text Sniper!

This project took a few hours and the whole thing is a few hundred lines of Python. It was fun to show that you can build a really useful macOS native app in just a little bit of Python.

Textinator was featured on [Talk Python to Me](https://www.youtube.com/watch?v=ndFFgJhrUhQ&t=810s)! Thanks [Michael Kennedy](https://twitter.com/mkennedy) for hosting me!

## How Textinator Works

Textinator is built with [rumps (Ridiculously Uncomplicated macOS Python Statusbar apps)](https://github.com/jaredks/rumps) which is a python package for creating simple macOS Statusbar apps.
Expand Down
21 changes: 17 additions & 4 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,32 @@
# Build, sign and package Textinator as a DMG file for release
# this requires create-dmg: `brew install create-dmg` to install

# --background "installer_background.png" \

# build with py2app
echo "Running py2app"
test -d dist && rm -rf dist/
test -d build && rm -rf build/
python setup.py py2app

# sign with adhoc certificate
# sign with ad-hoc certificate (if you have an Apple Developer ID, you can use your developer certificate instead)
# for the app to send AppleEvents to other apps, it needs to be signed and include the
# com.apple.security.automation.apple-events entitlement in the entitlements file
# --force: force signing even if the app is already signed
# --deep: recursively sign all embedded frameworks and plugins
# --options=runtime: Preserve the hardened runtime version
# --entitlements: use specified the entitlements file
# -s -: sign the code at the path(s) given using this identity; "-" means use the ad-hoc certificate
echo "Signing with codesign"
codesign --force --deep -s - dist/Textinator.app
codesign \
--force \
--deep \
--options=runtime \
--entitlements=script.entitlements entitlements.plist \
-s - \
dist/Textinator.app

# create installer DMG
# to add a background image to the DMG, add the following to the create-dmg command:
# --background "installer_background.png" \
echo "Creating DMG"
test -f Textinator-Installer.dmg && rm Textinator-Installer.dmg
create-dmg \
Expand Down
8 changes: 8 additions & 0 deletions entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.automation.apple-events</key>
<true/>
</dict>
</plist>
Binary file added images/system_events_access.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/textinator_settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
py-applescript==1.0.3
py2app==0.28.2
pyobjc-core==8.5; python_version >= "3.6"
pyobjc-framework-cocoa==8.5; python_version >= "3.6"
Expand All @@ -6,4 +7,4 @@ pyobjc-framework-quartz==8.5; python_version >= "3.6"
pyobjc-framework-vision==8.5; python_version >= "3.6"
pyperclip==1.8.2
rumps==0.3.0
wheel==0.37.1
wheel==0.37.1
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"If you have changed the default location for screenshots, "
"you will also need to grant Textinator full disk access in "
"System Preferences > Security & Privacy > Privacy > Full Disk Access.",
"NSAppleEventsUsageDescription": "Textinator needs permission to send AppleScript events to add itself to Login Items.",
},
}

Expand Down
64 changes: 63 additions & 1 deletion textinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@

import contextlib
import datetime
import os
import platform
import plistlib
from typing import List, Optional, Tuple

import applescript
import objc
import pyperclip
import Quartz
import rumps
import Vision
from Foundation import (
NSURL,
NSBundle,
NSDesktopDirectory,
NSDictionary,
NSFileManager,
Expand All @@ -30,7 +33,7 @@
NSUserDomainMask,
)

__version__ = "0.7.2"
__version__ = "0.8.0"

APP_NAME = "Textinator"
APP_ICON = "icon.png"
Expand Down Expand Up @@ -86,6 +89,9 @@ def __init__(self, *args, **kwargs):
self.clear_clipboard = rumps.MenuItem(
"Clear Clipboard", self.on_clear_clipboard
)
self.start_on_login = rumps.MenuItem(
f"Start {APP_NAME} on login", self.on_start_on_login
)
self.about = rumps.MenuItem(f"About {APP_NAME}", self.on_about)
self.quit = rumps.MenuItem(f"Quit {APP_NAME}", self.on_quit)
self.menu = [
Expand All @@ -104,6 +110,7 @@ def __init__(self, *args, **kwargs):
self.append,
self.clear_clipboard,
None,
self.start_on_login,
self.about,
self.quit,
]
Expand All @@ -119,6 +126,8 @@ def __init__(self, *args, **kwargs):
# and shown the message assigned to NSDesktopFolderUsageDescription in the Info.plist file
verify_desktop_access()

self.log(__file__)

# start the spotlight query
self.start_query()

Expand Down Expand Up @@ -151,6 +160,7 @@ def load_config(self):
"language": self.recognition_language,
"always_detect_english": True,
"detect_qrcodes": False,
"start_on_login": False,
}
self.log(f"loaded config: {self.config}")
self.append.state = self.config.get("append", False)
Expand All @@ -164,6 +174,7 @@ def load_config(self):
self.language_english.state = self.config.get("always_detect_english", True)
self.qrcodes.state = self.config.get("detect_qrcodes", False)
self._debug = self.config.get("debug", False)
self.start_on_login.state = self.config.get("start_on_login", False)
self.save_config()

def save_config(self):
Expand All @@ -176,6 +187,7 @@ def save_config(self):
self.config["always_detect_english"] = self.language_english.state
self.config["detect_qrcodes"] = self.qrcodes.state
self.config["debug"] = self._debug
self.config["start_on_login"] = self.start_on_login.state
with self.open(CONFIG_FILE, "wb+") as f:
plistlib.dump(self.config, f)
self.log(f"saved config: {self.config}")
Expand Down Expand Up @@ -257,6 +269,20 @@ def start_query(self):
self.query.setDelegate_(self)
self.query.startQuery()

def on_start_on_login(self, sender):
"""Configure app to start on login or toggle this setting."""
self.start_on_login.state = not self.start_on_login.state
if self.start_on_login.state:
app_path = get_app_path()
self.log(f"adding app to login items with path {app_path}")
if APP_NAME not in list_login_items():
add_login_item(APP_NAME, app_path, hidden=False)
else:
self.log("removing app from login items")
if APP_NAME in list_login_items():
remove_login_item(APP_NAME)
self.save_config()

def on_about(self, sender):
"""Display about dialog."""
rumps.alert(
Expand Down Expand Up @@ -534,5 +560,41 @@ def detect_qrcodes(filepath: str) -> List[str]:
return results


def get_app_path() -> str:
"""Return path to the bundle containing this script"""
# Note: This must be called from an app bundle built with py2app or you'll get
# the path of the python interpreter instead of the actual app
return NSBundle.mainBundle().bundlePath()


# The following functions are used to manipulate the Login Items list in System Preferences
# To use these, your app must include the com.apple.security.automation.apple-events entitlement
# in its entitlements file during signing and must have the NSAppleEventsUsageDescription key in
# its Info.plist file
# These functions use AppleScript to interact with System Preferences. I know of no other way to
# do this programmatically from Python. If you know of a better way, please let me know!


def add_login_item(app_name: str, app_path: str, hidden: bool = False):
"""Add app to login items"""
scpt = (
'tell application "System Events" to make login item at end with properties '
+ f'{{name:"{app_name}", path:"{app_path}", hidden:{"true" if hidden else "false"}}}'
)
applescript.AppleScript(scpt).run()


def remove_login_item(app_name: str):
"""Remove app from login items"""
scpt = f'tell application "System Events" to delete login item "{app_name}"'
applescript.AppleScript(scpt).run()


def list_login_items() -> List[str]:
"""Return list of login items"""
scpt = 'tell application "System Events" to get the name of every login item'
return applescript.AppleScript(scpt).run()


if __name__ == "__main__":
Textinator(name=APP_NAME, quit_button=None).run()

0 comments on commit 209b317

Please sign in to comment.