From aac4f88bb54fa39c41f2b33db61bdaa8cbb8cc56 Mon Sep 17 00:00:00 2001 From: Ian Obermiller Date: Sat, 10 Feb 2018 21:42:44 -0800 Subject: [PATCH 1/7] Update MacOS title logic - Switch from the opaque scpt file to a normal JS file using JavaScript for Automation. - Grab the first window (which is the active one) and get its name as the title. This worked across all apps I tried, including Sublime, Atom, Chrome, Safari, and iTerm - Clean up the api to the macos.py file (have it return the dict directly) - Fix a typo in README --- .gitignore | 1 + README.md | 6 ++--- aw_watcher_window/lib.py | 6 +---- aw_watcher_window/macos.py | 35 ++++++++++++++++++--------- aw_watcher_window/printAppTitle.scpt | Bin 3278 -> 0 bytes 5 files changed, 27 insertions(+), 21 deletions(-) delete mode 100644 aw_watcher_window/printAppTitle.scpt diff --git a/.gitignore b/.gitignore index f13f69ce..99a8446f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ build dist *.swp +.mypy_cache diff --git a/README.md b/README.md index 2a420a49..27e813dc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -aw-watcher-window -================= +# aw-watcher-window Cross-platform window-Watcher for Linux (X11), macOS, Windows. @@ -7,6 +6,5 @@ Cross-platform window-Watcher for Linux (X11), macOS, Windows. ### Note to macOS users -To log current window title the teminal needs access to macOS accessibility API. +To log current window title the terminal needs access to macOS accessibility API. This can be enabled in System Preferences > Security & Privacy > Accessibility, then add the Terminal to this list. If this is not enabled the watcher can only log current application, and not window title. - diff --git a/aw_watcher_window/lib.py b/aw_watcher_window/lib.py index 010fb802..b0c42736 100644 --- a/aw_watcher_window/lib.py +++ b/aw_watcher_window/lib.py @@ -18,11 +18,7 @@ def get_current_window_linux() -> Optional[dict]: def get_current_window_macos() -> Optional[dict]: 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 9cc04d9a..46dda4d2 100644 --- a/aw_watcher_window/macos.py +++ b/aw_watcher_window/macos.py @@ -1,17 +1,28 @@ -import subprocess -from subprocess import PIPE -import os +from typing import Optional +from AppKit import NSWorkspace +from Quartz import ( + CGWindowListCopyWindowInfo, + kCGWindowListOptionOnScreenOnly, + kCGNullWindowID +) +def getInfo() -> Optional[dict]: + app_name = '' + title = '' -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() + app = NSWorkspace.sharedWorkspace().activeApplication() + if app: + pid = app['NSApplicationProcessIdentifier'] -def getApp(info) -> str: - return info.split('","')[0][1:] + options = kCGWindowListOptionOnScreenOnly + window_list = CGWindowListCopyWindowInfo(options, kCGNullWindowID) + for window in window_list: + if pid == window['kCGWindowOwnerPID']: + # We could use app['NSApplicationName'], but this value is more + # accurate and matches other methods (like applescript) + app_name = window['kCGWindowOwnerName'] + title = window.get('kCGWindowName', u'') + break - -def getTitle(info) -> str: - return info.split('","')[1][:-1] + return {"appname": app_name, "title": title} 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: Tue, 27 Mar 2018 14:45:49 +0200 Subject: [PATCH 2/7] Removed .scpt from PyInstaller datas argument --- aw-watcher-window.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aw-watcher-window.spec b/aw-watcher-window.spec index be81152e..8793f0af 100644 --- a/aw-watcher-window.spec +++ b/aw-watcher-window.spec @@ -6,7 +6,7 @@ block_cipher = None a = Analysis(['aw_watcher_window/__main__.py'], pathex=[], binaries=None, - datas=[("aw_watcher_window/printAppTitle.scpt", "aw_watcher_window")], + datas=[], hiddenimports=[], hookspath=[], runtime_hooks=[], From 4e88cec3ab04a5defdaee2730c79af9ce12ee716 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Sun, 22 Mar 2020 11:49:37 +0200 Subject: [PATCH 3/7] Build on PR --- aw_watcher_window/macos.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/aw_watcher_window/macos.py b/aw_watcher_window/macos.py index 46dda4d2..6e3c8b44 100644 --- a/aw_watcher_window/macos.py +++ b/aw_watcher_window/macos.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Dict, Optional from AppKit import NSWorkspace from Quartz import ( CGWindowListCopyWindowInfo, @@ -6,23 +6,25 @@ kCGNullWindowID ) -def getInfo() -> Optional[dict]: - app_name = '' - title = '' +def getInfo() -> Optional[Dict[str, str]]: + app = NSWorkspace.sharedWorkspace().frontmostApplication() + if app: + app_name = app.localizedName() + title = getTitle(app.processIdentifier()) - app = NSWorkspace.sharedWorkspace().activeApplication() + print("appname: " + app_name + ", title: "+ title) + return {"appname": app_name, "title": title} - if app: - pid = app['NSApplicationProcessIdentifier'] + else: + return None + +def getTitle(pid: int) -> str: + options = kCGWindowListOptionOnScreenOnly + windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) - options = kCGWindowListOptionOnScreenOnly - window_list = CGWindowListCopyWindowInfo(options, kCGNullWindowID) - for window in window_list: - if pid == window['kCGWindowOwnerPID']: - # We could use app['NSApplicationName'], but this value is more - # accurate and matches other methods (like applescript) - app_name = window['kCGWindowOwnerName'] - title = window.get('kCGWindowName', u'') - break + for window in windowList: + lookupPid = window['kCGWindowOwnerPID'] + if (pid == lookupPid): + return str(window.get('kCGWindowName', 'Non-detected window title')) + return "" - return {"appname": app_name, "title": title} From 38591c560cfd1dd6132a4f5d65214a05a756dd3c Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Tue, 24 Mar 2020 15:31:44 +0200 Subject: [PATCH 4/7] Standardize macos window lib with other OS window libs --- aw_watcher_window/lib.py | 5 ++++- aw_watcher_window/macos.py | 23 ++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/aw_watcher_window/lib.py b/aw_watcher_window/lib.py index b0c42736..b002a6a1 100644 --- a/aw_watcher_window/lib.py +++ b/aw_watcher_window/lib.py @@ -18,7 +18,10 @@ def get_current_window_linux() -> Optional[dict]: def get_current_window_macos() -> Optional[dict]: from . import macos - return macos.getInfo() + + app = macos.get_current_app() + print ("appname" + macos.get_app_name(app) + ", title" + macos.get_app_title(app)) + return {"appname": macos.get_app_name(app), "title": macos.get_app_title(app)} def get_current_window_windows() -> Optional[dict]: diff --git a/aw_watcher_window/macos.py b/aw_watcher_window/macos.py index 6e3c8b44..4326d161 100644 --- a/aw_watcher_window/macos.py +++ b/aw_watcher_window/macos.py @@ -1,30 +1,27 @@ from typing import Dict, Optional -from AppKit import NSWorkspace +from AppKit import NSWorkspace, NSRunningApplication from Quartz import ( CGWindowListCopyWindowInfo, kCGWindowListOptionOnScreenOnly, kCGNullWindowID ) -def getInfo() -> Optional[Dict[str, str]]: - app = NSWorkspace.sharedWorkspace().frontmostApplication() - if app: - app_name = app.localizedName() - title = getTitle(app.processIdentifier()) +def get_current_app() -> Optional[NSRunningApplication]: + return NSWorkspace.sharedWorkspace().frontmostApplication() - print("appname: " + app_name + ", title: "+ title) - return {"appname": app_name, "title": title} - else: - return None +def get_app_name(app: NSRunningApplication) -> str: + return app.localizedName() -def getTitle(pid: int) -> str: + +def get_app_title(app: NSRunningApplication) -> str: + pid = app.processIdentifier() options = kCGWindowListOptionOnScreenOnly windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) for window in windowList: lookupPid = window['kCGWindowOwnerPID'] - if (pid == lookupPid): - return str(window.get('kCGWindowName', 'Non-detected window title')) + if (lookupPid == pid): + return window.get('kCGWindowName', 'Non-detected window title') return "" From 01a5f18aede761751a6afa59ea74ccafbed2886b Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Tue, 24 Mar 2020 16:49:51 +0200 Subject: [PATCH 5/7] try using a notification observer on macos. still not working --- aw_watcher_window/macos.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/aw_watcher_window/macos.py b/aw_watcher_window/macos.py index 4326d161..5a27b707 100644 --- a/aw_watcher_window/macos.py +++ b/aw_watcher_window/macos.py @@ -1,13 +1,32 @@ from typing import Dict, Optional -from AppKit import NSWorkspace, NSRunningApplication +from AppKit import NSObject, NSWorkspace, NSRunningApplication, NSWorkspaceDidActivateApplicationNotification from Quartz import ( CGWindowListCopyWindowInfo, kCGWindowListOptionOnScreenOnly, kCGNullWindowID ) +from PyObjCTools import AppHelper -def get_current_app() -> Optional[NSRunningApplication]: - return NSWorkspace.sharedWorkspace().frontmostApplication() + +class Observer(NSObject): + app = NSWorkspace.sharedWorkspace().frontmostApplication() + + def get_front_app(self) -> NSRunningApplication: + return self.app + def set_front_app_(self) -> NSRunningApplication: + self.app = NSWorkspace.sharedWorkspace().frontmostApplication() + +observer = Observer.new() +NSWorkspace.sharedWorkspace().notificationCenter().addObserver_selector_name_object_( + observer, + "set_front_app:", + NSWorkspaceDidActivateApplicationNotification, + None) + +AppHelper.runConsoleEventLoop() + +def get_current_app() -> NSRunningApplication: + return observer.get_front_app() def get_app_name(app: NSRunningApplication) -> str: @@ -22,6 +41,8 @@ def get_app_title(app: NSRunningApplication) -> str: for window in windowList: lookupPid = window['kCGWindowOwnerPID'] if (lookupPid == pid): + print(window) return window.get('kCGWindowName', 'Non-detected window title') - return "" + + return "Couldn't find title by pid" From 7ffb3fc229376c8399b68faa72a4452501ebbc5e Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Wed, 25 Mar 2020 20:26:50 +0200 Subject: [PATCH 6/7] Try a side-effect-ful import approach to initializing macOS event loop --- aw_watcher_window/lib.py | 2 +- aw_watcher_window/macos.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/aw_watcher_window/lib.py b/aw_watcher_window/lib.py index b002a6a1..526716d1 100644 --- a/aw_watcher_window/lib.py +++ b/aw_watcher_window/lib.py @@ -18,7 +18,7 @@ def get_current_window_linux() -> Optional[dict]: def get_current_window_macos() -> Optional[dict]: from . import macos - + # The side effectful import breaks thigns here app = macos.get_current_app() print ("appname" + macos.get_app_name(app) + ", title" + macos.get_app_title(app)) return {"appname": macos.get_app_name(app), "title": macos.get_app_title(app)} diff --git a/aw_watcher_window/macos.py b/aw_watcher_window/macos.py index 5a27b707..e1406ae1 100644 --- a/aw_watcher_window/macos.py +++ b/aw_watcher_window/macos.py @@ -1,5 +1,6 @@ -from typing import Dict, Optional -from AppKit import NSObject, NSWorkspace, NSRunningApplication, NSWorkspaceDidActivateApplicationNotification +from threading import Thread +from typing import Dict, Optional, NoReturn +from AppKit import NSObject, NSNotification, NSWorkspace, NSRunningApplication, NSWorkspaceDidActivateApplicationNotification from Quartz import ( CGWindowListCopyWindowInfo, kCGWindowListOptionOnScreenOnly, @@ -13,16 +14,18 @@ class Observer(NSObject): def get_front_app(self) -> NSRunningApplication: return self.app - def set_front_app_(self) -> NSRunningApplication: + + def handle_(self, noti: NSNotification) -> None: + self._set_front_app() + def _set_front_app(self) -> None: self.app = NSWorkspace.sharedWorkspace().frontmostApplication() observer = Observer.new() NSWorkspace.sharedWorkspace().notificationCenter().addObserver_selector_name_object_( - observer, - "set_front_app:", - NSWorkspaceDidActivateApplicationNotification, - None) - + observer, + "handle:", + NSWorkspaceDidActivateApplicationNotification, + None) AppHelper.runConsoleEventLoop() def get_current_app() -> NSRunningApplication: @@ -41,7 +44,6 @@ def get_app_title(app: NSRunningApplication) -> str: for window in windowList: lookupPid = window['kCGWindowOwnerPID'] if (lookupPid == pid): - print(window) return window.get('kCGWindowName', 'Non-detected window title') return "Couldn't find title by pid" From 52852aea64443106e4b311989b3d860af468881a Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Sat, 28 Mar 2020 15:44:47 +0200 Subject: [PATCH 7/7] Try to use a higher order function to initialize macos spesific event stuff --- aw_watcher_window/lib.py | 26 +++++++++++++------------- aw_watcher_window/main.py | 3 ++- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/aw_watcher_window/lib.py b/aw_watcher_window/lib.py index 526716d1..ceaf4a68 100644 --- a/aw_watcher_window/lib.py +++ b/aw_watcher_window/lib.py @@ -1,8 +1,8 @@ import sys -from typing import Optional +from typing import Callable, Dict, Optional -def get_current_window_linux() -> Optional[dict]: +def get_current_window_linux() -> Dict[str, str]: from . import xlib window = xlib.get_current_window() @@ -16,15 +16,15 @@ def get_current_window_linux() -> Optional[dict]: return {"appname": cls, "title": name} -def get_current_window_macos() -> Optional[dict]: +def initialize_get_macos_window() -> Callable[[], Dict[str, str]]: from . import macos - # The side effectful import breaks thigns here - app = macos.get_current_app() - print ("appname" + macos.get_app_name(app) + ", title" + macos.get_app_title(app)) - return {"appname": macos.get_app_name(app), "title": macos.get_app_title(app)} + def get_current_window_macos() -> Dict[str, str]: + app = macos.get_current_app() + print ("appname" + macos.get_app_name(app) + ", title" + macos.get_app_title(app)) + return {"appname": macos.get_app_name(app), "title": macos.get_app_title(app)} + return get_current_window_macos - -def get_current_window_windows() -> Optional[dict]: +def get_current_window_windows() -> Dict[str, str]: from . import windows window_handle = windows.get_active_window_handle() app = windows.get_app_name(window_handle) @@ -38,12 +38,12 @@ def get_current_window_windows() -> Optional[dict]: return {"appname": app, "title": title} -def get_current_window() -> Optional[dict]: +def get_current_window() -> Optional[Callable[[], Dict[str, str]]]: if sys.platform.startswith("linux"): - return get_current_window_linux() + return get_current_window_linux elif sys.platform == "darwin": - return get_current_window_macos() + return initialize_get_macos_window() elif sys.platform in ["win32", "cygwin"]: - return get_current_window_windows() + return get_current_window_windows else: raise Exception("Unknown platform: {}".format(sys.platform)) diff --git a/aw_watcher_window/main.py b/aw_watcher_window/main.py index f22ce497..10839f1e 100644 --- a/aw_watcher_window/main.py +++ b/aw_watcher_window/main.py @@ -54,13 +54,14 @@ def parse_args(default_poll_time: float, default_exclude_title: bool): def heartbeat_loop(client, bucket_id, poll_time, exclude_title=False): + current_window_fn = get_current_window() while True: if os.getppid() == 1: logger.info("window-watcher stopped because parent process died") break try: - current_window = get_current_window() + current_window = current_window_fn() logger.debug(current_window) except Exception as e: logger.error("Exception thrown while trying to get active window: {}".format(e))