Skip to content

Commit 209b317

Browse files
committed
Added 'Start Textinator on login' option
1 parent 614f362 commit 209b317

File tree

8 files changed

+99
-6
lines changed

8 files changed

+99
-6
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,27 @@ To upgrade to the latest version, download the latest installer DMG from [releas
6060
- `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`).
6161
- `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).
6262
- `Always detect English`: If checked, always attempts to detect English text in addition to the primary language selected by `Text recognition language` setting.
63+
- `Detect QR Codes`: In addition to detecting text, also detect QR codes and copy the decoded payload text to the clipboard.
6364
- `Notification`: Whether or not to show a notification when text is detected.
6465
- `Keep linebreaks`: Whether or not to keep linebreaks in the detected text; if not set, linebreaks will be stripped.
6566
- `Append to clipboard`: Append to the clipboard instead of overwriting it.
6667
- `Clear clipboard`: Clear the clipboard.
68+
- `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).
6769
- `About Textinator`: Show the about dialog.
6870
- `Quit Textinator`: Quit Textinator.
6971

72+
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.
73+
74+
![System Events permission](images/system_events_access.png)
75+
7076
## Inspiration
7177

7278
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!
7379

7480
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.
7581

82+
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!
83+
7684
## How Textinator Works
7785

7886
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.

build.sh

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,32 @@
33
# Build, sign and package Textinator as a DMG file for release
44
# this requires create-dmg: `brew install create-dmg` to install
55

6-
# --background "installer_background.png" \
7-
86
# build with py2app
97
echo "Running py2app"
108
test -d dist && rm -rf dist/
119
test -d build && rm -rf build/
1210
python setup.py py2app
1311

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

1829
# create installer DMG
30+
# to add a background image to the DMG, add the following to the create-dmg command:
31+
# --background "installer_background.png" \
1932
echo "Creating DMG"
2033
test -f Textinator-Installer.dmg && rm Textinator-Installer.dmg
2134
create-dmg \

entitlements.plist

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.automation.apple-events</key>
6+
<true/>
7+
</dict>
8+
</plist>

images/system_events_access.png

103 KB
Loading

images/textinator_settings.png

145 KB
Loading

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
py-applescript==1.0.3
12
py2app==0.28.2
23
pyobjc-core==8.5; python_version >= "3.6"
34
pyobjc-framework-cocoa==8.5; python_version >= "3.6"
@@ -6,4 +7,4 @@ pyobjc-framework-quartz==8.5; python_version >= "3.6"
67
pyobjc-framework-vision==8.5; python_version >= "3.6"
78
pyperclip==1.8.2
89
rumps==0.3.0
9-
wheel==0.37.1
10+
wheel==0.37.1

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"If you have changed the default location for screenshots, "
1818
"you will also need to grant Textinator full disk access in "
1919
"System Preferences > Security & Privacy > Privacy > Full Disk Access.",
20+
"NSAppleEventsUsageDescription": "Textinator needs permission to send AppleScript events to add itself to Login Items.",
2021
},
2122
}
2223

textinator.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55

66
import contextlib
77
import datetime
8+
import os
89
import platform
910
import plistlib
1011
from typing import List, Optional, Tuple
1112

13+
import applescript
1214
import objc
1315
import pyperclip
1416
import Quartz
1517
import rumps
1618
import Vision
1719
from Foundation import (
1820
NSURL,
21+
NSBundle,
1922
NSDesktopDirectory,
2023
NSDictionary,
2124
NSFileManager,
@@ -30,7 +33,7 @@
3033
NSUserDomainMask,
3134
)
3235

33-
__version__ = "0.7.2"
36+
__version__ = "0.8.0"
3437

3538
APP_NAME = "Textinator"
3639
APP_ICON = "icon.png"
@@ -86,6 +89,9 @@ def __init__(self, *args, **kwargs):
8689
self.clear_clipboard = rumps.MenuItem(
8790
"Clear Clipboard", self.on_clear_clipboard
8891
)
92+
self.start_on_login = rumps.MenuItem(
93+
f"Start {APP_NAME} on login", self.on_start_on_login
94+
)
8995
self.about = rumps.MenuItem(f"About {APP_NAME}", self.on_about)
9096
self.quit = rumps.MenuItem(f"Quit {APP_NAME}", self.on_quit)
9197
self.menu = [
@@ -104,6 +110,7 @@ def __init__(self, *args, **kwargs):
104110
self.append,
105111
self.clear_clipboard,
106112
None,
113+
self.start_on_login,
107114
self.about,
108115
self.quit,
109116
]
@@ -119,6 +126,8 @@ def __init__(self, *args, **kwargs):
119126
# and shown the message assigned to NSDesktopFolderUsageDescription in the Info.plist file
120127
verify_desktop_access()
121128

129+
self.log(__file__)
130+
122131
# start the spotlight query
123132
self.start_query()
124133

@@ -151,6 +160,7 @@ def load_config(self):
151160
"language": self.recognition_language,
152161
"always_detect_english": True,
153162
"detect_qrcodes": False,
163+
"start_on_login": False,
154164
}
155165
self.log(f"loaded config: {self.config}")
156166
self.append.state = self.config.get("append", False)
@@ -164,6 +174,7 @@ def load_config(self):
164174
self.language_english.state = self.config.get("always_detect_english", True)
165175
self.qrcodes.state = self.config.get("detect_qrcodes", False)
166176
self._debug = self.config.get("debug", False)
177+
self.start_on_login.state = self.config.get("start_on_login", False)
167178
self.save_config()
168179

169180
def save_config(self):
@@ -176,6 +187,7 @@ def save_config(self):
176187
self.config["always_detect_english"] = self.language_english.state
177188
self.config["detect_qrcodes"] = self.qrcodes.state
178189
self.config["debug"] = self._debug
190+
self.config["start_on_login"] = self.start_on_login.state
179191
with self.open(CONFIG_FILE, "wb+") as f:
180192
plistlib.dump(self.config, f)
181193
self.log(f"saved config: {self.config}")
@@ -257,6 +269,20 @@ def start_query(self):
257269
self.query.setDelegate_(self)
258270
self.query.startQuery()
259271

272+
def on_start_on_login(self, sender):
273+
"""Configure app to start on login or toggle this setting."""
274+
self.start_on_login.state = not self.start_on_login.state
275+
if self.start_on_login.state:
276+
app_path = get_app_path()
277+
self.log(f"adding app to login items with path {app_path}")
278+
if APP_NAME not in list_login_items():
279+
add_login_item(APP_NAME, app_path, hidden=False)
280+
else:
281+
self.log("removing app from login items")
282+
if APP_NAME in list_login_items():
283+
remove_login_item(APP_NAME)
284+
self.save_config()
285+
260286
def on_about(self, sender):
261287
"""Display about dialog."""
262288
rumps.alert(
@@ -534,5 +560,41 @@ def detect_qrcodes(filepath: str) -> List[str]:
534560
return results
535561

536562

563+
def get_app_path() -> str:
564+
"""Return path to the bundle containing this script"""
565+
# Note: This must be called from an app bundle built with py2app or you'll get
566+
# the path of the python interpreter instead of the actual app
567+
return NSBundle.mainBundle().bundlePath()
568+
569+
570+
# The following functions are used to manipulate the Login Items list in System Preferences
571+
# To use these, your app must include the com.apple.security.automation.apple-events entitlement
572+
# in its entitlements file during signing and must have the NSAppleEventsUsageDescription key in
573+
# its Info.plist file
574+
# These functions use AppleScript to interact with System Preferences. I know of no other way to
575+
# do this programmatically from Python. If you know of a better way, please let me know!
576+
577+
578+
def add_login_item(app_name: str, app_path: str, hidden: bool = False):
579+
"""Add app to login items"""
580+
scpt = (
581+
'tell application "System Events" to make login item at end with properties '
582+
+ f'{{name:"{app_name}", path:"{app_path}", hidden:{"true" if hidden else "false"}}}'
583+
)
584+
applescript.AppleScript(scpt).run()
585+
586+
587+
def remove_login_item(app_name: str):
588+
"""Remove app from login items"""
589+
scpt = f'tell application "System Events" to delete login item "{app_name}"'
590+
applescript.AppleScript(scpt).run()
591+
592+
593+
def list_login_items() -> List[str]:
594+
"""Return list of login items"""
595+
scpt = 'tell application "System Events" to get the name of every login item'
596+
return applescript.AppleScript(scpt).run()
597+
598+
537599
if __name__ == "__main__":
538600
Textinator(name=APP_NAME, quit_button=None).run()

0 commit comments

Comments
 (0)