From e1b017d6e66df3f555ed8502079a3d56f34307c3 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Fri, 30 Apr 2021 21:22:57 -0600 Subject: [PATCH 01/17] Adding new jxa-based app title script for macos --- aw_watcher_window/printAppStatus.jxa | 59 +++++++++++++++++++++++++++ aw_watcher_window/printAppTitle.scpt | Bin 3278 -> 0 bytes 2 files changed, 59 insertions(+) create mode 100755 aw_watcher_window/printAppStatus.jxa delete mode 100644 aw_watcher_window/printAppTitle.scpt diff --git a/aw_watcher_window/printAppStatus.jxa b/aw_watcher_window/printAppStatus.jxa new file mode 100755 index 00000000..49b82e07 --- /dev/null +++ b/aw_watcher_window/printAppStatus.jxa @@ -0,0 +1,59 @@ +#!/usr/bin/osascript -l JavaScript + +// adapted from: +// https://gist.github.com/EvanLovely/cb01eafb0d61515c835ecd56f6ac199a + +// new to jxa? +// - https://apple-dev.groups.io/g/jxa/wiki/3202 +// - interactive repl: `osascript -il JavaScript` +// - API reference: Script Editor -> File -> Open Dictionary + +var seApp = Application("System Events"); +var oProcess = seApp.processes.whose({frontmost: true})[0]; +var appName = oProcess.displayedName(); + +// as of 05/01/21 incognio & url are not actively used in +var url, incognito, title; + +// it's not possible to get the URL from firefox +// https://stackoverflow.com/questions/17846948/does-firefox-offer-applescript-support-to-get-url-of-windows + +switch(appName) { + case "Safari": + // incognito is not available via safari applescript + url = Application(appName).documents[0].url(); + title = Application(appName).documents[0].name(); + break; + case "Google Chrome": + case "Google Chrome Canary": + case "Chromium": + case "Brave Browser": + const activeWindow = Application(appName).windows[0]; + const activeTab = activeWindow.activeTab(); + + url = activeTab.url(); + title = activeTab.name(); + incognito = activeWindow.mode() === 'incognito'; + break; + default: + mainWindow = oProcess. + windows(). + find(w => w.attributes.byName("AXMain").value() === true) + + // in some cases, the primary window of an application may not be found + // this occurs rarely and seems to be triggered by switching to a different application + if(mainWindow) { + title = mainWindow. + attributes. + byName("AXTitle"). + value() + } +} + +// key names must match expected names in lib.py +JSON.stringify({ + appname: appName, + url, + title, + incognito +}); \ No newline at end of file diff --git a/aw_watcher_window/printAppTitle.scpt b/aw_watcher_window/printAppTitle.scpt deleted file mode 100644 index d004b036791ed371a0841c31edf9af5670c9fb1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3278 zcmb_e`*##Y7XG?tgwUPpq>~^4SsbEKc7Qx2vXE?m0Kuq0kR&ES9(IO?3`}NnX2!7m z=AW=<|BXHSgCe`W7i2{QAH)Zst5FeDp5g;VRIXojPlV%|WIe~4Q{U9>x>fg4_ui^i zUT$kwOGm7}roK*h2SBt$1O`k*3=W_;Hg@v6Zc!6a&fJ7aieLy8Bu?aKlP&~?#_6Ga{MYgr1OiUr`5AwY&KVU{3>|M$9 z*37PaGN1BCiB3(J@;#`|Bz6n7B1BRgv&mKYPOcbo6)@yG4OcX$&0Y+p2nEgv9Qjth zQE*^p19KuML8&4b0|9Nob>PBy3}r6Lv_nc^WP&NkWm_(ftD+&_YU$r->59D^6^al@ zkD?t@fQE8%*3#Vg-A_PhnVH$41Ohqsk8lY#V;t!am z2xbG%s|dyNC2q9{QEM>a4JzCknnO3q7ZLdahQchE&4^H|3dAs55sZ53;Gk44S#n7) zP3E>K%np~jD3{MI`J9K`m&pv}vyb=+Gq1biqAeH4Jo_26pbEDsLSR89RlOb6ieRQG zw34RWfjbo;P+~f6kqeew&`K~s6z&WKRbslFx8?j8f)uK+D|F75a}!3|ZB%TIoK=_; zp1>@6{Evt$LgWD|R*f~7i+OTJ@h>=|IarDLs8NK#lqRe|E$S2@Fr^7CsK;H35SX$Q zE%KQqpV5>dFST0XE`3Nq6PC)SSYQ#NmY2_G6&8e^ZpIQcIN(V>hPz$d%_C_+4?2-V z9x2f6b=R%@#FkISjX;GO9d0+#!bMmpr{$Ew!Z2?1mp@^VBA5*Z9YI4s#$t;QwGzxa zg~g#tamFY7^SP=KO^OiMT|@Q%jAlg$?5@WG`N)!wezv=EOoF{+r7-iGG?i^dks8VPF(E+XXiXb*3-Rg zG9vHGdzOr#vc}6LvVJxji?fBZe}x_!@;*~j-qXv^=;iTH%UP(B|DwerFa)!SoT4s4 z+A~+=U0dFrD756P-fmi`bCmvHUV`PurJ_*!?}<5oCvJ6eAAO50p=?)!U_+p#;B_XlFFLw-7+tIe`6a z$-bAcWuP4nBuU+xah!cF~m`tbdJg4VUWKP%wW9fSWD<=<6MTb z{p4o)ZgqQv5tn_i~AIzKyShQU_H8jl-@-1*D69`zL&SrIz=eZ-FN`&6~WLq<2pLE z!yzzRJ6%>Mi#|=zV;RI0)`v_Tco1C+E{RE~!bVgnE)TM)8$I%dEpJSaO^RhX zklM@D-443-hCME;gQd4)(g&Na4maI|%`$Au@C0@7zHrMe=tHlAUcKc87aORf-m=P+ zH}!EG)4>jw!Ad1y|x@2UJ~w$aF9 zY&RLk`VrJ`GEf-dvd@%#+8GD5GhWaJZ`I)Q1?0-Vv0R=wHHE`GFb6*7fH1acXgwvho!BwzpJa= zgX^KJ`cSop^6LG>is~K3m|Eg6-Wd8jT Date: Fri, 30 Apr 2021 21:24:00 -0600 Subject: [PATCH 02/17] Use new jxa implementation in macos python branch --- aw_watcher_window/lib.py | 7 ++----- aw_watcher_window/macos.py | 25 ++++++++++++------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/aw_watcher_window/lib.py b/aw_watcher_window/lib.py index 010fb802..e8943b42 100644 --- a/aw_watcher_window/lib.py +++ b/aw_watcher_window/lib.py @@ -17,12 +17,9 @@ def get_current_window_linux() -> Optional[dict]: def get_current_window_macos() -> Optional[dict]: + # TODO should we use unknown when the title is blank like the other platforms? from . import macos - info = macos.getInfo() - app = macos.getApp(info) - title = macos.getTitle(info) - - return {"title": title, "appname": app} + return macos.getInfo() def get_current_window_windows() -> Optional[dict]: diff --git a/aw_watcher_window/macos.py b/aw_watcher_window/macos.py index 968d4920..398169ad 100644 --- a/aw_watcher_window/macos.py +++ b/aw_watcher_window/macos.py @@ -1,21 +1,20 @@ import subprocess -from subprocess import PIPE import os +import json +import logging +logger = logging.getLogger(__name__) def getInfo() -> str: - cmd = ["osascript", os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppTitle.scpt")] - p = subprocess.run(cmd, stdout=PIPE) - return str(p.stdout, "utf8").strip() - - -def getApp(info: str) -> str: - return info.split('","')[0][1:] - - -def getTitle(info: str) -> str: - return info.split('","')[1][:-1] - + cmd = [os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppStatus.jxa")] + p = subprocess.run(cmd, stdout=subprocess.PIPE) + result = str(p.stdout, "utf8").strip() + + try: + return json.loads(result) + except json.JSONDecodeError as e: + logger.warn(f"invalid JSON encountered {result}") + return {} def background_ensure_permissions() -> None: from multiprocessing import Process From ee8445d6f66b71de934041276c3b575515638651 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Tue, 4 May 2021 16:22:35 -0600 Subject: [PATCH 03/17] Add url to window even if it exists --- aw_watcher_window/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aw_watcher_window/main.py b/aw_watcher_window/main.py index af365e6f..f77ab947 100644 --- a/aw_watcher_window/main.py +++ b/aw_watcher_window/main.py @@ -76,11 +76,15 @@ def heartbeat_loop(client, bucket_id, poll_time, exclude_title=False): if current_window is None: logger.debug('Unable to fetch window, trying again on next poll') else: - # Create current_window event data = { "app": current_window["appname"], "title": current_window["title"] if not exclude_title else "excluded" } + + # url is populated on macos when a browser is the frontmost application + if current_window.get("url", None): + data["url"] = current_window["url"] + current_window_event = Event(timestamp=now, data=data) # Set pulsetime to 1 second more than the poll_time From 749c1e19f343070a778c0dd2195a362c091f4fd8 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Tue, 4 May 2021 16:33:06 -0600 Subject: [PATCH 04/17] Use app instead of appname everywhere --- aw_watcher_window/lib.py | 4 ++-- aw_watcher_window/main.py | 16 ++++++---------- aw_watcher_window/printAppStatus.jxa | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/aw_watcher_window/lib.py b/aw_watcher_window/lib.py index e8943b42..e310a742 100644 --- a/aw_watcher_window/lib.py +++ b/aw_watcher_window/lib.py @@ -13,7 +13,7 @@ def get_current_window_linux() -> Optional[dict]: cls = xlib.get_window_class(window) name = xlib.get_window_name(window) - return {"appname": cls, "title": name} + return {"app": cls, "title": name} def get_current_window_macos() -> Optional[dict]: @@ -33,7 +33,7 @@ def get_current_window_windows() -> Optional[dict]: if title is None: title = "unknown" - return {"appname": app, "title": title} + return {"app": app, "title": title} def get_current_window() -> Optional[dict]: diff --git a/aw_watcher_window/main.py b/aw_watcher_window/main.py index f77ab947..4a31eb9e 100644 --- a/aw_watcher_window/main.py +++ b/aw_watcher_window/main.py @@ -15,6 +15,8 @@ logger = logging.getLogger(__name__) +# enable this line for easier debugging +# logger.setLevel(logging.DEBUG) def main(): # Read settings from config @@ -70,22 +72,16 @@ def heartbeat_loop(client, bucket_id, poll_time, exclude_title=False): except Exception as e: logger.error("Exception thrown while trying to get active window: {}".format(e)) traceback.print_exc() - current_window = {"appname": "unknown", "title": "unknown"} + current_window = {"app": "unknown", "title": "unknown"} now = datetime.now(timezone.utc) if current_window is None: logger.debug('Unable to fetch window, trying again on next poll') else: - data = { - "app": current_window["appname"], - "title": current_window["title"] if not exclude_title else "excluded" - } + if exclude_title: + current_window["title"] = "excluded" - # url is populated on macos when a browser is the frontmost application - if current_window.get("url", None): - data["url"] = current_window["url"] - - current_window_event = Event(timestamp=now, data=data) + current_window_event = Event(timestamp=now, data=current_window) # Set pulsetime to 1 second more than the poll_time # This since the loop takes more time than poll_time diff --git a/aw_watcher_window/printAppStatus.jxa b/aw_watcher_window/printAppStatus.jxa index 49b82e07..3f79e01d 100755 --- a/aw_watcher_window/printAppStatus.jxa +++ b/aw_watcher_window/printAppStatus.jxa @@ -52,7 +52,7 @@ switch(appName) { // key names must match expected names in lib.py JSON.stringify({ - appname: appName, + app: appName, url, title, incognito From a090c6e89cbe11e7e22321be55f10b825eab29db Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Wed, 5 May 2021 11:16:46 -0600 Subject: [PATCH 05/17] Adding osakit --- poetry.lock | 146 ++++++++++++++++++++++++++++--------------------- pyproject.toml | 6 +- 2 files changed, 86 insertions(+), 66 deletions(-) diff --git a/poetry.lock b/poetry.lock index 148b0679..14ff2e5f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -24,29 +24,29 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "20.3.0" +version = "21.2.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] name = "aw-client" -version = "0.5.0" +version = "0.5.1" description = "Client library for ActivityWatch" category = "main" optional = false -python-versions = "^3.6" +python-versions = "^3.7" develop = false [package.dependencies] -aw-core = "^0.5" +aw-core = "^0.5.1" click = "^7.1.1" persist-queue = "^0.6.0" requests = "^2.22.0" @@ -55,15 +55,15 @@ requests = "^2.22.0" type = "git" url = "https://github.com/ActivityWatch/aw-client.git" reference = "master" -resolved_reference = "6ba8eb0f09ca2da2d6fd4e99ceb0b02c481a051b" +resolved_reference = "6b08d438c5143149d1e60cc789a4da426771126c" [[package]] name = "aw-core" -version = "0.5.0" +version = "0.5.1" description = "Core library for ActivityWatch" category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.7,<4.0" [package.dependencies] appdirs = ">=1.4.3,<2.0.0" @@ -257,7 +257,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyobjc-core" -version = "7.1" +version = "7.2" description = "Python<->ObjC Interoperability Module" category = "main" optional = false @@ -265,52 +265,64 @@ python-versions = ">=3.6" [[package]] name = "pyobjc-framework-applicationservices" -version = "6.2.2" +version = "7.2" description = "Wrappers for the framework ApplicationServices on macOS" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyobjc-core = ">=6.2.2" -pyobjc-framework-Cocoa = ">=6.2.2" -pyobjc-framework-Quartz = ">=6.2.2" +pyobjc-core = ">=7.2" +pyobjc-framework-Cocoa = ">=7.2" +pyobjc-framework-Quartz = ">=7.2" [[package]] name = "pyobjc-framework-cocoa" -version = "7.1" +version = "7.2" description = "Wrappers for the Cocoa frameworks on macOS" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyobjc-core = ">=7.1" +pyobjc-core = ">=7.2" [[package]] name = "pyobjc-framework-coretext" -version = "6.2.2" +version = "7.2" description = "Wrappers for the framework CoreText on macOS" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyobjc-core = ">=6.2.2" -pyobjc-framework-Cocoa = ">=6.2.2" -pyobjc-framework-Quartz = ">=6.2.2" +pyobjc-core = ">=7.2" +pyobjc-framework-Cocoa = ">=7.2" +pyobjc-framework-Quartz = ">=7.2" + +[[package]] +name = "pyobjc-framework-osakit" +version = "7.2" +description = "Wrappers for the framework OSAKit on macOS" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyobjc-core = ">=7.2" +pyobjc-framework-Cocoa = ">=7.2" [[package]] name = "pyobjc-framework-quartz" -version = "7.1" +version = "7.2" description = "Wrappers for the Quartz frameworks on macOS" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyobjc-core = ">=7.1" -pyobjc-framework-Cocoa = ">=7.1" +pyobjc-core = ">=7.2" +pyobjc-framework-Cocoa = ">=7.2" [[package]] name = "pyparsing" @@ -341,7 +353,7 @@ python-versions = ">=3.5" [[package]] name = "pytest" -version = "6.2.3" +version = "6.2.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -408,7 +420,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] name = "six" -version = "1.15.0" +version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "main" optional = false @@ -456,7 +468,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.7.4.3" +version = "3.10.0.0" description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false @@ -508,7 +520,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "cb68b2a767d7ac4442d2d3024d543053a3318ea5a4dca06a1661c0d6bce5d0ee" +content-hash = "93c4037f6b3763d989be6e204b699fb2e3338a9429a62a5c4ffc859eb335566e" [metadata.files] altgraph = [ @@ -524,13 +536,13 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, - {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] aw-client = [] aw-core = [ - {file = "aw-core-0.5.0.tar.gz", hash = "sha256:d74c22b4b338a20e05a3e8a345a1bc427a2579be669c5b9ea1fd04bb6fd18f9e"}, - {file = "aw_core-0.5.0-py3-none-any.whl", hash = "sha256:d72a56a532b447fee48cad2ba0e602713d93b803dff3283256273ca5859007e5"}, + {file = "aw-core-0.5.1.tar.gz", hash = "sha256:ec6ba1d140f76f832f8e69e4ff32ed498d994d438073bfa59ea588613ad2cff6"}, + {file = "aw_core-0.5.1-py3-none-any.whl", hash = "sha256:2ce56a9365fde822e44c1cd24c9de7a6ca52ff1f5cbad794cc39c228d9a3a345"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, @@ -620,36 +632,44 @@ py = [ {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pyobjc-core = [ - {file = "pyobjc-core-7.1.tar.gz", hash = "sha256:a0616d5d816b4471f8f782c3a9a8923d2cc85014d88ad4f7fec694be9e6ea349"}, - {file = "pyobjc_core-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9fb45c9916f2a03ecd6b9ecde4c35d1d0f1a590ae2ea2372f9d9a360226ac1d"}, - {file = "pyobjc_core-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff8e87358c6195a2937004f279050cce3d4c02cd77acd73c5ad367307def855"}, - {file = "pyobjc_core-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:afb38efd3f2960eb49eb78552d465cfd025a9d6efa06cd4cd8694dafbe7c6e06"}, - {file = "pyobjc_core-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cb329c4119044fe83bcb3c5d4794d636c706ff0cb7c1c77d36ef5c373100082"}, - {file = "pyobjc_core-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7913d7b20217c294900537faf58e5cc15942ed7af277bf05db25667d18255114"}, + {file = "pyobjc-core-7.2.tar.gz", hash = "sha256:9e9ec482d80ea030cdb1613d05a247f31eedabe6666d884d42dd890cc5fb0e05"}, + {file = "pyobjc_core-7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:94b4d9de9d228db52dd35012096d63bdf8c1ace58ea3be1d5f6f39313cd502f2"}, + {file = "pyobjc_core-7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:971cbd7189ae1aa03ef0d16124aa5bcd053779e0e6b6011a41c3dbd5b4ea7e88"}, + {file = "pyobjc_core-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9d93b20394008373d6d2856d49aaff26f4b97ff42d924a14516c8a82313ec8c0"}, + {file = "pyobjc_core-7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:860183540d1be792c26426018139ac8ba75e85f675c59ba080ccdc52d8e74c7a"}, + {file = "pyobjc_core-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ffe61d3c2a404354daf2d895e34e38c5044453353581b3c396bf5365de26250c"}, ] pyobjc-framework-applicationservices = [ - {file = "pyobjc-framework-ApplicationServices-6.2.2.tar.gz", hash = "sha256:f9d74f1d72713180638fc441c072eaaa8e59ccabb04bac18b21d137e9c0cb5e6"}, - {file = "pyobjc_framework_ApplicationServices-6.2.2-py2.py3-none-any.whl", hash = "sha256:9018463ad698e6bb4f4dfb01cd3157b521b68ace447df8cceff17c2b99d0ebc9"}, + {file = "pyobjc-framework-ApplicationServices-7.2.tar.gz", hash = "sha256:938fcebda774b772b7681b5ff4d2b3c91c4bde29d6ad8b4e7cdd87d4df1f42ec"}, + {file = "pyobjc_framework_ApplicationServices-7.2-py2.py3-none-any.whl", hash = "sha256:8a941282072e8f6801b01c13648514044515ad306336498f62740a48eff383df"}, ] pyobjc-framework-cocoa = [ - {file = "pyobjc-framework-Cocoa-7.1.tar.gz", hash = "sha256:67966152b3d38a0225176fceca2e9f56d849c8e7445548da09a00cb13155ec3e"}, - {file = "pyobjc_framework_Cocoa-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bef77eafaac5eaf1d91d479d5483fd02216caa3edc27e8f5adc9af0b3fecdac3"}, - {file = "pyobjc_framework_Cocoa-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2ea3582c456827dc20e648c905fdbcf8d3dfae89434f981e9b761cd07262049"}, - {file = "pyobjc_framework_Cocoa-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a4050f2d776f40c2409a151c6f7896420e936934b3bdbfabedf91509637ed9b"}, - {file = "pyobjc_framework_Cocoa-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f68f022f1f6d5985c418e10c6608c562fcf4bfe3714ec64fd10ce3dc6221bd4"}, - {file = "pyobjc_framework_Cocoa-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ecfefd4c48dae42275c18679c69f6f2fff970e711097515a0a8732fc10194018"}, + {file = "pyobjc-framework-Cocoa-7.2.tar.gz", hash = "sha256:c8b23f03dc3f4436d36c0fd006a8a084835c4f6015187df7c3aa5de8ecd5c653"}, + {file = "pyobjc_framework_Cocoa-7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8e5dd5daa0096755937ec24c345a4b07c3fa131a457f99e0fdeeb01979178ec7"}, + {file = "pyobjc_framework_Cocoa-7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:828d183947fc7746953fd0c9b1092cc423745ba0b49719e7b7d1e1614aaa20ec"}, + {file = "pyobjc_framework_Cocoa-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e4c6d7baa0c2ab5ea5efb8836ad0b3b3976cffcfc6195c1f195e826c6eb5744"}, + {file = "pyobjc_framework_Cocoa-7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9a9d1d49cc5a810773c88d6de821e60c8cc41d01113cf1b9e7662938f5f7d66"}, + {file = "pyobjc_framework_Cocoa-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:506c2cd09f421eac92b9008a0142174c3d1d70ecd4b0e3fa2b924767995fd14e"}, ] pyobjc-framework-coretext = [ - {file = "pyobjc-framework-CoreText-6.2.2.tar.gz", hash = "sha256:944fda1bd9c2827e36907216a930fcaf429788132616bed3d687ba0e80405d34"}, - {file = "pyobjc_framework_CoreText-6.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0a5f2288b1e29d93d00d708d0e10d8ca49af1116772d48d0623cc62ffad12918"}, + {file = "pyobjc-framework-CoreText-7.2.tar.gz", hash = "sha256:8e3e52298073bf75c33fdc0c9f19c5d5c03b32cc507a4138386ea0e9886fd5d1"}, + {file = "pyobjc_framework_CoreText-7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:13ef2c684396d7d29fc12164bbf801c33879c9639edc2202a0e7b2202f4be6c5"}, + {file = "pyobjc_framework_CoreText-7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6bfc35769b9f9cbfc50dc0e2d3bf50fac78c5f5b66d51130daece6817a2a6d26"}, + {file = "pyobjc_framework_CoreText-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3013c2c006df857adcf2a3fdf1c4f185f8a75d513fa9d766922a3659940e7b0c"}, + {file = "pyobjc_framework_CoreText-7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:047c0ac8cf2075a6ee11f039df4ea53f7b87003f37a91ee2fd731a7656df9ccf"}, + {file = "pyobjc_framework_CoreText-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b519e462db1747c1b6702d7abc2bf036c8570dc634d59a5f02324876826b516c"}, +] +pyobjc-framework-osakit = [ + {file = "pyobjc-framework-OSAKit-7.2.tar.gz", hash = "sha256:e553c2689ffd897040f6a1e24bdef6a0b3904e09f9a37bc65e377f1f016cd86a"}, + {file = "pyobjc_framework_OSAKit-7.2-py2.py3-none-any.whl", hash = "sha256:e7d873c2cd252027b07281cd65fb14b16938e2d0ee17cca6264e23df1cfca8f8"}, ] pyobjc-framework-quartz = [ - {file = "pyobjc-framework-Quartz-7.1.tar.gz", hash = "sha256:73102c9f4dbfa13275621014785ab3b684cf03ce93a4b0b270500c795349bea9"}, - {file = "pyobjc_framework_Quartz-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7207a26244f02d4534ebb007fa55a9dc7c1b7fbb490d1e89e0d62cfd175e20f3"}, - {file = "pyobjc_framework_Quartz-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5bc7a4fb3ea80b5af6910cc27729a0774a96327a69583fcf28057cb2ffce33ac"}, - {file = "pyobjc_framework_Quartz-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c0469d60d4a79fc252f74adaa8177d2c680621d858c1b8ef19c411e903e2c892"}, - {file = "pyobjc_framework_Quartz-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:04953c031fc35020682bd4613b9b5a9688bdb9eab7ed76fd8dcf028783568b4f"}, - {file = "pyobjc_framework_Quartz-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d8e0c086faf649f86386d0ed99194c6d0704b602576e2b258532b635b510b790"}, + {file = "pyobjc-framework-Quartz-7.2.tar.gz", hash = "sha256:ea554e5697bc6747a4ce793c0b0036da16622b44ff75196d6124603008922afa"}, + {file = "pyobjc_framework_Quartz-7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dc61fe61d26f797e4335f3ffc891bcef64624c728c2603e3307b3910580b2cb8"}, + {file = "pyobjc_framework_Quartz-7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ad8103cc38923f2708904db11a0992ea960125ce6adf7b4c7a77d8fdafd412c4"}, + {file = "pyobjc_framework_Quartz-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4549d17ca41f0bf62792d5bc4b4293ba9a6cc560014b3e18ba22c65e4a5030d2"}, + {file = "pyobjc_framework_Quartz-7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:da16e4f1e13cb7b02e30fa538cbb3a356e4a694bbc2bb26d2bd100ca12a54ff6"}, + {file = "pyobjc_framework_Quartz-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1f6471177a39535cd0358ae29b8f3d31fe778a21deb74105c448c4e726619d7"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -663,8 +683,8 @@ pyrsistent = [ {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, ] pytest = [ - {file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"}, - {file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"}, + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, ] python-json-logger = [ {file = "python-json-logger-0.1.11.tar.gz", hash = "sha256:b7a31162f2a01965a5efb94453ce69230ed208468b0bbc7fdfc56e6d8df2e281"}, @@ -690,8 +710,8 @@ requests = [ {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] strict-rfc3339 = [ {file = "strict-rfc3339-0.7.tar.gz", hash = "sha256:5cad17bedfc3af57b399db0fed32771f18fc54bbd917e85546088607ac5e1277"}, @@ -740,9 +760,9 @@ typed-ast = [ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ - {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, - {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, - {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] urllib3 = [ {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, diff --git a/pyproject.toml b/pyproject.toml index 7d8c463f..658ea92e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,9 @@ aw-client = {git = "https://github.com/ActivityWatch/aw-client.git"} python-xlib = {version = "^0.28", platform = "linux"} pypiwin32 = {version = "223", platform = "win32"} wmi = {version = "^1.4.9", platform = "win32"} -pyobjc-framework-ApplicationServices = { version = "^6.2", platform="darwin"} -pyobjc-framework-CoreText = {version = "^6.2", platform="darwin"} - +pyobjc-framework-ApplicationServices = { version = "^7.2", platform="darwin"} +pyobjc-framework-CoreText = {version = "^7.2", platform="darwin"} +pyobjc-framework-OSAKit = {version = "^7.2", platform="darwin"} [tool.poetry.dev-dependencies] pytest = "^6.0" From 4b38c4aeea71b59dbaed1674e17572ad6ff9925d Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Wed, 5 May 2021 11:34:02 -0600 Subject: [PATCH 06/17] reset vars on each run --- aw_watcher_window/printAppStatus.jxa | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aw_watcher_window/printAppStatus.jxa b/aw_watcher_window/printAppStatus.jxa index 3f79e01d..e1d3dc1f 100755 --- a/aw_watcher_window/printAppStatus.jxa +++ b/aw_watcher_window/printAppStatus.jxa @@ -9,11 +9,13 @@ // - API reference: Script Editor -> File -> Open Dictionary var seApp = Application("System Events"); -var oProcess = seApp.processes.whose({frontmost: true})[0]; +var oProcess = seApp.processes.whose({frontmost: true})[0]; var appName = oProcess.displayedName(); -// as of 05/01/21 incognio & url are not actively used in -var url, incognito, title; +// as of 05/01/21 incognio & url are not actively used in AW +// variables must be set to `undefined` since this script is re-run via osascript +// and the previously set values will be cached otherwise +var url = undefined, incognito = undefined, title = undefined; // it's not possible to get the URL from firefox // https://stackoverflow.com/questions/17846948/does-firefox-offer-applescript-support-to-get-url-of-windows From 66cbfc628615ff2273da2d2371b18f67b9d9f856 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Wed, 5 May 2021 11:34:30 -0600 Subject: [PATCH 07/17] Run script directly instead of using a shell --- aw_watcher_window/macos.py | 55 +++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/aw_watcher_window/macos.py b/aw_watcher_window/macos.py index 398169ad..41e664a3 100644 --- a/aw_watcher_window/macos.py +++ b/aw_watcher_window/macos.py @@ -1,20 +1,50 @@ -import subprocess import os import json import logging logger = logging.getLogger(__name__) +script = None + +def compileScript(): + # https://stackoverflow.com/questions/44209057/how-can-i-run-jxa-from-swift + # https://stackoverflow.com/questions/16065162/calling-applescript-from-python-without-using-osascript-or-appscript + from OSAKit import OSAScript, OSALanguage + + scriptPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppStatus.jxa") + scriptContents = open(scriptPath, mode="r").read() + javascriptLanguage = OSALanguage.languageForName_("JavaScript") + + script = OSAScript.alloc().initWithSource_language_(scriptContents, javascriptLanguage) + (success, err) = script.compileAndReturnError_(None) + + # should only occur if jxa was modified incorrectly + if not success: + raise Exception("error compiling jxa script") + + return script def getInfo() -> str: - cmd = [os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppStatus.jxa")] - p = subprocess.run(cmd, stdout=subprocess.PIPE) - result = str(p.stdout, "utf8").strip() + # use a global variable to cache the compiled script for performance + global script + if not script: + script = compileScript() - try: - return json.loads(result) - except json.JSONDecodeError as e: - logger.warn(f"invalid JSON encountered {result}") - return {} + (result, err) = script.executeAndReturnError_(None) + + if err: + # error structure: + # { + # NSLocalizedDescription = "Error: Error: Can't get object."; + # NSLocalizedFailureReason = "Error: Error: Can't get object."; + # OSAScriptErrorBriefMessageKey = "Error: Error: Can't get object."; + # OSAScriptErrorMessageKey = "Error: Error: Can't get object."; + # OSAScriptErrorNumberKey = "-1728"; + # OSAScriptErrorRangeKey = "NSRange: {0, 0}"; + # } + + raise Exception("jxa error: {}".format(err["NSLocalizedDescription"])) + + return json.loads(result.stringValue()) def background_ensure_permissions() -> None: from multiprocessing import Process @@ -42,3 +72,10 @@ def ensure_permissions() -> None: print(choice) if choice == NSAlertFirstButtonReturn: NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")) + +if __name__ == "__main__": + print(getInfo()) + print("Waiting 5 seconds...") + import time + time.sleep(5) + print(getInfo()) \ No newline at end of file From 9bcafc270c51484b3200f2b4a452b86cb92ffa7c Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Wed, 5 May 2021 11:37:46 -0600 Subject: [PATCH 08/17] Minor note about the executable name --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8cfd46d9..b495e2e4 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ To install the latest git version directly from github without cloning, run `pip install git+https://github.com/ActivityWatch/aw-watcher-window.git` To install from a cloned version, cd into the directory and run -`poetry install` to install inside an virtualenv. If you want to install it -system-wide it can be installed with `pip install .`, but that has the issue -that it might not get the exact version of the dependencies due to not reading -the poetry.lock file. +`poetry install` to install inside an virtualenv. You can run the binary via `aw-watcher-window`. + +If you want to install it system-wide it can be installed with `pip install .`, but that has the issue +that it might not get the exact version of the dependencies due to not reading the poetry.lock file. ## Note to macOS users From 5002b3216c65c81fa37f127af92515afd966725d Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Mon, 17 May 2021 20:28:01 -0600 Subject: [PATCH 09/17] Adding config option for macos strategy --- aw_watcher_window/config.py | 26 +++++++++++++++++++------- aw_watcher_window/main.py | 30 +++++++----------------------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/aw_watcher_window/config.py b/aw_watcher_window/config.py index 372a680f..f5b2b7bf 100644 --- a/aw_watcher_window/config.py +++ b/aw_watcher_window/config.py @@ -1,18 +1,30 @@ from configparser import ConfigParser +import argparse from aw_core.config import load_config as _load_config - def load_config(): default_client_config = ConfigParser() - default_client_config["aw-watcher-window"] = { - "exclude_title": False, - "poll_time": "1.0" - } - default_client_config["aw-watcher-window-testing"] = { + default_client_config["aw-watcher-window"] = default_client_config["aw-watcher-window-testing"] = { "exclude_title": False, - "poll_time": "1.0" + "poll_time": "1.0", + "macos_strategy": "jxa" } # TODO: Handle so aw-watcher-window testing gets loaded instead of testing is on return _load_config("aw-watcher-window", default_client_config)["aw-watcher-window"] + +def parse_args(): + config = load_config() + + default_poll_time = config.getfloat("poll_time") + default_exclude_title = config.getboolean("exclude_title") + default_macos_strategy = config.get("macos_strategy") + + parser = argparse.ArgumentParser("A cross platform window watcher for Activitywatch.\nSupported on: Linux (X11), macOS and Windows.") + parser.add_argument("--testing", dest="testing", action="store_true") + parser.add_argument("--exclude-title", dest="exclude_title", action="store_true", default=default_exclude_title) + parser.add_argument("--verbose", dest="verbose", action="store_true") + parser.add_argument("--poll-time", dest="poll_time", type=float, default=default_poll_time) + parser.add_argument("--macos-strategy", dest="macos_strategy", default=default_macos_strategy, choices=["jxa", "applescript"]) + return parser.parse_args() diff --git a/aw_watcher_window/main.py b/aw_watcher_window/main.py index 4a31eb9e..9f3608ff 100644 --- a/aw_watcher_window/main.py +++ b/aw_watcher_window/main.py @@ -1,4 +1,3 @@ -import argparse import logging import traceback import sys @@ -11,7 +10,7 @@ from aw_client import ActivityWatchClient from .lib import get_current_window -from .config import load_config +from .config import parse_args logger = logging.getLogger(__name__) @@ -19,16 +18,11 @@ # logger.setLevel(logging.DEBUG) def main(): - # Read settings from config - config = load_config() - args = parse_args( - default_poll_time=config.getfloat("poll_time"), - default_exclude_title=config.getboolean("exclude_title"), - ) - if sys.platform.startswith("linux") and ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]): raise Exception("DISPLAY environment variable not set") + args = parse_args() + setup_logging(name="aw-watcher-window", testing=args.testing, verbose=args.verbose, log_stderr=True, log_file=True) @@ -47,27 +41,17 @@ def main(): sleep(1) # wait for server to start with client: - heartbeat_loop(client, bucket_id, poll_time=args.poll_time, exclude_title=args.exclude_title) - - -def parse_args(default_poll_time: float, default_exclude_title: bool): - """config contains defaults loaded from the config file""" - parser = argparse.ArgumentParser("A cross platform window watcher for Activitywatch.\nSupported on: Linux (X11), macOS and Windows.") - parser.add_argument("--testing", dest="testing", action="store_true") - parser.add_argument("--exclude-title", dest="exclude_title", action="store_true", default=default_exclude_title) - parser.add_argument("--verbose", dest="verbose", action="store_true") - parser.add_argument("--poll-time", dest="poll_time", type=float, default=default_poll_time) - return parser.parse_args() - + heartbeat_loop(client, bucket_id, + poll_time=args.poll_time, exclude_title=args.exclude_title, macos_strategy=args.macos_strategy) -def heartbeat_loop(client, bucket_id, poll_time, exclude_title=False): +def heartbeat_loop(client, bucket_id, poll_time, macos_strategy, exclude_title=False): while True: if os.getppid() == 1: logger.info("window-watcher stopped because parent process died") break try: - current_window = get_current_window() + current_window = get_current_window(macos_strategy=macos_strategy) logger.debug(current_window) except Exception as e: logger.error("Exception thrown while trying to get active window: {}".format(e)) From ed7b9eac775d59aa02c89a5c0300cd356965f282 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Mon, 17 May 2021 20:30:01 -0600 Subject: [PATCH 10/17] Adding back old macos method --- aw_watcher_window/lib.py | 17 +++++++++++------ aw_watcher_window/macos_applescript.py | 17 +++++++++++++++++ aw_watcher_window/printAppTitle.scpt | Bin 0 -> 3194 bytes 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 aw_watcher_window/macos_applescript.py create mode 100644 aw_watcher_window/printAppTitle.scpt diff --git a/aw_watcher_window/lib.py b/aw_watcher_window/lib.py index e310a742..4e280054 100644 --- a/aw_watcher_window/lib.py +++ b/aw_watcher_window/lib.py @@ -1,7 +1,6 @@ import sys from typing import Optional - def get_current_window_linux() -> Optional[dict]: from . import xlib window = xlib.get_current_window() @@ -16,10 +15,16 @@ def get_current_window_linux() -> Optional[dict]: return {"app": cls, "title": name} -def get_current_window_macos() -> Optional[dict]: +def get_current_window_macos(strategy) -> Optional[dict]: # TODO should we use unknown when the title is blank like the other platforms? - from . import macos - return macos.getInfo() + + # `jxa` is the default & preferred strategy. It includes the url + incognito status + if strategy == 'jxa': + from . import macos + return macos.getInfo() + else: + from . import macos_applescript + return macos_applescript.getInfo() def get_current_window_windows() -> Optional[dict]: @@ -36,11 +41,11 @@ def get_current_window_windows() -> Optional[dict]: return {"app": app, "title": title} -def get_current_window() -> Optional[dict]: +def get_current_window(macos_strategy) -> Optional[dict]: if sys.platform.startswith("linux"): return get_current_window_linux() elif sys.platform == "darwin": - return get_current_window_macos() + return get_current_window_macos(macos_strategy) elif sys.platform in ["win32", "cygwin"]: return get_current_window_windows() else: diff --git a/aw_watcher_window/macos_applescript.py b/aw_watcher_window/macos_applescript.py new file mode 100644 index 00000000..d7815998 --- /dev/null +++ b/aw_watcher_window/macos_applescript.py @@ -0,0 +1,17 @@ +import subprocess +from subprocess import PIPE +import os + + +def getInfo() -> str: + cmd = ["osascript", os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppTitle.scpt")] + p = subprocess.run(cmd, stdout=PIPE) + return str(p.stdout, "utf8").strip() + + +def getApp(info: str) -> str: + return info.split('","')[0][1:] + + +def getTitle(info: str) -> str: + return info.split('","')[1][:-1] diff --git a/aw_watcher_window/printAppTitle.scpt b/aw_watcher_window/printAppTitle.scpt new file mode 100644 index 0000000000000000000000000000000000000000..89b87f18b501584b046a321d00acd0327228f38c GIT binary patch literal 3194 zcmb_e`*&2;75?s>5#qf$liVaoK#Cz6r6bHF5-E}b1`sl?W`@h*%uJaBLmmcYUHZqMW%3a}2>64ZmWpjk&Pp->7g2bQ zTgc~1snOB$sC{(QMg>{^g{xh{qGnW1vgA@DewiZMR#G;uChO1glPf=CVKdzEOl~MY zUdoiR{v^?(ge^aT>dfKmz%_(OmSaA0S$>osEV&F=@}t5J%IWg0gfWCNX9O|%UcNJk z!7c>mL{NolL$F2y+JYB@2kQaUc&Jf_RKv;#Q;=_6`F2JXE%{!hf2Yz7-&)ieLLfbg zF3gi}rFWmGVzq=MbXKXgW1&a9wB)-6UT{RLhr+e5p$_ z#k~eg!lfQ+N(F-zp{LhiEm~t>Bp<-79&Tkw+R=|5WKcpD zbbIp^D?fGR(-||+Aff4Y6)jwam2yT-8>|fTM!)+zwde>M`U!4x2vH}^sx!DP zR4I;|E={_nBI5CXefutGj|=zSm!Rxf zEApNz@6DE4a!$9KAL<;X|JPSxy>+M}l>Yz3qCZxIZ6TG-6m?%~CzIN$+>%OXa;1E6 zxM5SruZUaUL6iA--4n%Cru=E(mF-A$@S_S5$7u7yNF&FqTRC z)E3Rj)uG_kn=^xjRAFNEREF~06s3}^f*atW8xtt94t$KTV|8MTqa5iX(>#Sy{;IH$ znWI^g*1X|dp0mT`-o(Fz_|_kaEBb1w+-$<72AdX?#3kHj1_Vka66mj`vxhkkj} zl{aUp3&RE*NFCtnz8Ly+!+wu-?&x+b-(b_t;ikK=TaLMMY?cmpSGeUK3}PUL0o`(k zhaJ>Ww`{cKEe*$Ug|{^(8~NUcordsB^xlxy!LBQuugJ^t zk|nPw)5{7kkyBpPqm!49s%jppbL2(r#va_yEF82*@Y1jwQ8_9v$Pszok)zs^{IOD= z^_DxB%>o+euqV0l!i@I1a%7tG&z0wAaes_T&8?QdAuZ3zvlxOehaHmBYh#!SHVk|*T}IV6wE zWAZ2uz(>$356eT$yuDjXi;mOQL>J_I=O;0#6U$zcqC)OoDyR1G%8KRJ** zeczd7J9qf2S~`z+9r literal 0 HcmV?d00001 From 520e008ff25c91dd443d5bc5e62ec6824ed11001 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Tue, 18 May 2021 10:25:13 -0600 Subject: [PATCH 11/17] Fixing invalid applescript payload --- aw_watcher_window/config.py | 3 ++- aw_watcher_window/macos_applescript.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/aw_watcher_window/config.py b/aw_watcher_window/config.py index f5b2b7bf..ff320ae4 100644 --- a/aw_watcher_window/config.py +++ b/aw_watcher_window/config.py @@ -27,4 +27,5 @@ def parse_args(): parser.add_argument("--verbose", dest="verbose", action="store_true") parser.add_argument("--poll-time", dest="poll_time", type=float, default=default_poll_time) parser.add_argument("--macos-strategy", dest="macos_strategy", default=default_macos_strategy, choices=["jxa", "applescript"]) - return parser.parse_args() + parsed_args = parser.parse_args() + return parsed_args diff --git a/aw_watcher_window/macos_applescript.py b/aw_watcher_window/macos_applescript.py index d7815998..40de88b6 100644 --- a/aw_watcher_window/macos_applescript.py +++ b/aw_watcher_window/macos_applescript.py @@ -6,7 +6,12 @@ def getInfo() -> str: cmd = ["osascript", os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppTitle.scpt")] p = subprocess.run(cmd, stdout=PIPE) - return str(p.stdout, "utf8").strip() + info = str(p.stdout, "utf8").strip() + + app = getApp(info) + title = getTitle(info) + + return {"app": app, "title": title} def getApp(info: str) -> str: From 9c88ae92bac1cf15d6ad0333b43ffda030a86192 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Tue, 18 May 2021 10:26:14 -0600 Subject: [PATCH 12/17] Allow log level to be set via environment --- aw_watcher_window/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aw_watcher_window/main.py b/aw_watcher_window/main.py index 9f3608ff..c2a90cee 100644 --- a/aw_watcher_window/main.py +++ b/aw_watcher_window/main.py @@ -14,8 +14,10 @@ logger = logging.getLogger(__name__) -# enable this line for easier debugging -# logger.setLevel(logging.DEBUG) +# run with LOG_LEVEL=DEBUG +log_level = os.environ.get('LOG_LEVEL') +if log_level: + logger.setLevel(logging.__getattribute__(log_level.upper())) def main(): if sys.platform.startswith("linux") and ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]): From cd767dfb9266a3f644fb20421362b5ca1358650d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sun, 23 May 2021 12:16:34 +0200 Subject: [PATCH 13/17] fix: minor fixes to new macOS strategy logic --- aw_watcher_window/config.py | 6 +++--- aw_watcher_window/lib.py | 8 +++++--- aw_watcher_window/main.py | 7 +++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/aw_watcher_window/config.py b/aw_watcher_window/config.py index ff320ae4..332d1ff7 100644 --- a/aw_watcher_window/config.py +++ b/aw_watcher_window/config.py @@ -8,7 +8,7 @@ def load_config(): default_client_config["aw-watcher-window"] = default_client_config["aw-watcher-window-testing"] = { "exclude_title": False, "poll_time": "1.0", - "macos_strategy": "jxa" + "strategy_macos": "jxa" } # TODO: Handle so aw-watcher-window testing gets loaded instead of testing is on @@ -19,13 +19,13 @@ def parse_args(): default_poll_time = config.getfloat("poll_time") default_exclude_title = config.getboolean("exclude_title") - default_macos_strategy = config.get("macos_strategy") + default_strategy_macos = config.get("strategy_macos") parser = argparse.ArgumentParser("A cross platform window watcher for Activitywatch.\nSupported on: Linux (X11), macOS and Windows.") parser.add_argument("--testing", dest="testing", action="store_true") parser.add_argument("--exclude-title", dest="exclude_title", action="store_true", default=default_exclude_title) parser.add_argument("--verbose", dest="verbose", action="store_true") parser.add_argument("--poll-time", dest="poll_time", type=float, default=default_poll_time) - parser.add_argument("--macos-strategy", dest="macos_strategy", default=default_macos_strategy, choices=["jxa", "applescript"]) + parser.add_argument("--strategy", dest="strategy", default=default_strategy_macos, choices=["jxa", "applescript"], help="(macOS only) strategy to use for retrieving the active window") parsed_args = parser.parse_args() return parsed_args diff --git a/aw_watcher_window/lib.py b/aw_watcher_window/lib.py index 4e280054..dbc983db 100644 --- a/aw_watcher_window/lib.py +++ b/aw_watcher_window/lib.py @@ -22,9 +22,11 @@ def get_current_window_macos(strategy) -> Optional[dict]: if strategy == 'jxa': from . import macos return macos.getInfo() - else: + elif strategy == 'applescript': from . import macos_applescript return macos_applescript.getInfo() + else: + return ValueError(f"invalid strategy '{strategy}'") def get_current_window_windows() -> Optional[dict]: @@ -41,11 +43,11 @@ def get_current_window_windows() -> Optional[dict]: return {"app": app, "title": title} -def get_current_window(macos_strategy) -> Optional[dict]: +def get_current_window(strategy: str = None) -> Optional[dict]: if sys.platform.startswith("linux"): return get_current_window_linux() elif sys.platform == "darwin": - return get_current_window_macos(macos_strategy) + return get_current_window_macos(strategy) elif sys.platform in ["win32", "cygwin"]: return get_current_window_windows() else: diff --git a/aw_watcher_window/main.py b/aw_watcher_window/main.py index c2a90cee..284c349f 100644 --- a/aw_watcher_window/main.py +++ b/aw_watcher_window/main.py @@ -43,17 +43,16 @@ def main(): sleep(1) # wait for server to start with client: - heartbeat_loop(client, bucket_id, - poll_time=args.poll_time, exclude_title=args.exclude_title, macos_strategy=args.macos_strategy) + heartbeat_loop(client, bucket_id, poll_time=args.poll_time, strategy=args.strategy, exclude_title=args.exclude_title) -def heartbeat_loop(client, bucket_id, poll_time, macos_strategy, exclude_title=False): +def heartbeat_loop(client, bucket_id, poll_time, strategy, exclude_title=False): while True: if os.getppid() == 1: logger.info("window-watcher stopped because parent process died") break try: - current_window = get_current_window(macos_strategy=macos_strategy) + current_window = get_current_window(strategy) logger.debug(current_window) except Exception as e: logger.error("Exception thrown while trying to get active window: {}".format(e)) From cbda3edf46c97b46d1697a15d8c918662363cb0b Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Tue, 25 May 2021 15:02:35 -0600 Subject: [PATCH 14/17] parse_args first before checking for linux display --- aw_watcher_window/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aw_watcher_window/main.py b/aw_watcher_window/main.py index 284c349f..ead2e0f6 100644 --- a/aw_watcher_window/main.py +++ b/aw_watcher_window/main.py @@ -20,11 +20,11 @@ logger.setLevel(logging.__getattribute__(log_level.upper())) def main(): + args = parse_args() + if sys.platform.startswith("linux") and ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]): raise Exception("DISPLAY environment variable not set") - args = parse_args() - setup_logging(name="aw-watcher-window", testing=args.testing, verbose=args.verbose, log_stderr=True, log_file=True) From b8858f0d6bb70a9442757bc32b0185f0207aedd3 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Tue, 25 May 2021 15:03:39 -0600 Subject: [PATCH 15/17] restoring older applescript file --- aw_watcher_window/printAppTitle.scpt | Bin 3194 -> 3278 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/aw_watcher_window/printAppTitle.scpt b/aw_watcher_window/printAppTitle.scpt index 89b87f18b501584b046a321d00acd0327228f38c..d004b036791ed371a0841c31edf9af5670c9fb1f 100644 GIT binary patch delta 314 zcmew*aZYkW5i8RNj>(Ozu9GLQ@=iX+D#$o#@(or6o(saiet2jzFfbliZ9SQV%|VLs z@V=sp|NebtfP()(z`($%z`!s$j!imKfPsU7;eznJ8$fA65c}f4M^}MtMg|^`5kPDO z#Fh+947R?B$(ea2`NbIu9xhhFmBl5gxmFB5nMp;7MU_?z&iO^D!Kp=MnaQceRtykn z1=q6Fypm$Q#DW5b&1=~7SrsD~5*f-Fbb&aLp@bosA%h{6p$N#&1@e;_@)?RJzvGsb zhVvK{6hs&l7z`QA84MV%Gng_MPZs0Ru?PX{%4Nu8$OG!j;bTZ*VBE-%CkW&UFrY~$ O7AF^heDmwxy|nn`AcF$5i8R!j>(Ozu9GLQ@=iX+D##c!`39>3kjKK| Date: Tue, 25 May 2021 15:05:17 -0600 Subject: [PATCH 16/17] Note about why the applescript version is lying around --- aw_watcher_window/macos_applescript.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aw_watcher_window/macos_applescript.py b/aw_watcher_window/macos_applescript.py index 40de88b6..e1e1bcf8 100644 --- a/aw_watcher_window/macos_applescript.py +++ b/aw_watcher_window/macos_applescript.py @@ -3,6 +3,10 @@ import os +# the applescript version of the macos strategy is kept here until the jxa +# approach is proven out in production environments +# https://github.com/ActivityWatch/aw-watcher-window/pull/52 + def getInfo() -> str: cmd = ["osascript", os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppTitle.scpt")] p = subprocess.run(cmd, stdout=PIPE) From 2ef5bb52b37147f7710e8fd908e0dbeff1a34448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 26 May 2021 13:04:29 +0200 Subject: [PATCH 17/17] fix: fixed typing issues --- aw_watcher_window/lib.py | 17 ++++++++----- aw_watcher_window/macos_applescript.py | 11 ++++++--- aw_watcher_window/{macos.py => macos_jxa.py} | 26 ++++++++++++++++---- 3 files changed, 40 insertions(+), 14 deletions(-) rename aw_watcher_window/{macos.py => macos_jxa.py} (83%) diff --git a/aw_watcher_window/lib.py b/aw_watcher_window/lib.py index dbc983db..f2743b07 100644 --- a/aw_watcher_window/lib.py +++ b/aw_watcher_window/lib.py @@ -1,8 +1,10 @@ import sys from typing import Optional + def get_current_window_linux() -> Optional[dict]: from . import xlib + window = xlib.get_current_window() if window is None: @@ -15,22 +17,25 @@ def get_current_window_linux() -> Optional[dict]: return {"app": cls, "title": name} -def get_current_window_macos(strategy) -> Optional[dict]: +def get_current_window_macos(strategy: str) -> Optional[dict]: # TODO should we use unknown when the title is blank like the other platforms? # `jxa` is the default & preferred strategy. It includes the url + incognito status - if strategy == 'jxa': - from . import macos - return macos.getInfo() - elif strategy == 'applescript': + if strategy == "jxa": + from . import macos_jxa + + return macos_jxa.getInfo() + elif strategy == "applescript": from . import macos_applescript + return macos_applescript.getInfo() else: - return ValueError(f"invalid strategy '{strategy}'") + raise ValueError(f"invalid strategy '{strategy}'") def get_current_window_windows() -> Optional[dict]: from . import windows + window_handle = windows.get_active_window_handle() app = windows.get_app_name(window_handle) title = windows.get_window_title(window_handle) diff --git a/aw_watcher_window/macos_applescript.py b/aw_watcher_window/macos_applescript.py index e1e1bcf8..bc94b646 100644 --- a/aw_watcher_window/macos_applescript.py +++ b/aw_watcher_window/macos_applescript.py @@ -1,14 +1,19 @@ +import os import subprocess from subprocess import PIPE -import os +from typing import Dict # the applescript version of the macos strategy is kept here until the jxa # approach is proven out in production environments # https://github.com/ActivityWatch/aw-watcher-window/pull/52 -def getInfo() -> str: - cmd = ["osascript", os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppTitle.scpt")] + +def getInfo() -> Dict[str, str]: + cmd = [ + "osascript", + os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppTitle.scpt"), + ] p = subprocess.run(cmd, stdout=PIPE) info = str(p.stdout, "utf8").strip() diff --git a/aw_watcher_window/macos.py b/aw_watcher_window/macos_jxa.py similarity index 83% rename from aw_watcher_window/macos.py rename to aw_watcher_window/macos_jxa.py index 41e664a3..a5eb2f56 100644 --- a/aw_watcher_window/macos.py +++ b/aw_watcher_window/macos_jxa.py @@ -1,20 +1,26 @@ import os import json import logging +from typing import Dict logger = logging.getLogger(__name__) script = None + def compileScript(): # https://stackoverflow.com/questions/44209057/how-can-i-run-jxa-from-swift # https://stackoverflow.com/questions/16065162/calling-applescript-from-python-without-using-osascript-or-appscript from OSAKit import OSAScript, OSALanguage - scriptPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "printAppStatus.jxa") + scriptPath = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "printAppStatus.jxa" + ) scriptContents = open(scriptPath, mode="r").read() javascriptLanguage = OSALanguage.languageForName_("JavaScript") - script = OSAScript.alloc().initWithSource_language_(scriptContents, javascriptLanguage) + script = OSAScript.alloc().initWithSource_language_( + scriptContents, javascriptLanguage + ) (success, err) = script.compileAndReturnError_(None) # should only occur if jxa was modified incorrectly @@ -23,7 +29,8 @@ def compileScript(): return script -def getInfo() -> str: + +def getInfo() -> Dict[str, str]: # use a global variable to cache the compiled script for performance global script if not script: @@ -46,8 +53,10 @@ def getInfo() -> str: return json.loads(result.stringValue()) + def background_ensure_permissions() -> None: from multiprocessing import Process + permission_process = Process(target=ensure_permissions, args=(())) permission_process.start() return @@ -56,6 +65,7 @@ def background_ensure_permissions() -> None: def ensure_permissions() -> None: from ApplicationServices import AXIsProcessTrusted from AppKit import NSAlert, NSAlertFirstButtonReturn, NSWorkspace, NSURL + accessibility_permissions = AXIsProcessTrusted() if not accessibility_permissions: title = "Missing accessibility permissions" @@ -71,11 +81,17 @@ def ensure_permissions() -> None: choice = alert.runModal() print(choice) if choice == NSAlertFirstButtonReturn: - NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")) + NSWorkspace.sharedWorkspace().openURL_( + NSURL.URLWithString_( + "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" + ) + ) + if __name__ == "__main__": print(getInfo()) print("Waiting 5 seconds...") import time + time.sleep(5) - print(getInfo()) \ No newline at end of file + print(getInfo())