Closed macdeport closed 2 years ago
Hi there!
Unfortunately, Apple Script is slow. The execution times on my system are a little better (though tested on virtualbox), but not good at all. I managed to reduce getAllTitles() execution time, but it is not possible for other functions like getAllWindows():
['Proyectos', 'pywinctl — osascript ◂ Python _pywinctl_macos.py — 80×24', 'Abrir', 'Abrir', 'Favoritos', 'Contacts', 'Calendario']
getAllTitles() duration=1.177137069
getAllWindows() duration=2.181389136
Can you please try this on your system? If possible, open a number of random apps/windows.
import subprocess
import timeit
def getAllTitles():
"""Returns a list of strings of window titles for all visible windows."""
cmd = """osascript -e 'tell application "System Events"
set winNames to {}
repeat with p in every process whose background only is false
repeat with w in every window of p
set end of winNames to name of w
end repeat
end repeat
return winNames
end tell'"""
ret = subprocess.check_output(cmd, shell=True).decode(encoding="utf-8").strip().split(", ")
return ret
start_time = timeit.default_timer()
windows = getAllTitles()
duration = timeit.default_timer() - start_time
print(f'getAllTitles() {duration=}\n')
Thank you!
Hi again!
I managed to improve performance (by some "non-intuitive" changes and workarounds). On my system, new measures are as follows:
['Proyectos', 'pywinctl — osascript ◂ Python _pywinctl_macos.py — 80×24', 'Favoritos', 'Contacts', 'Notas', 'Abrir', 'Abrir']
getAllTitles() duration=0.17727856399999997
getAllWindows() duration=0.462323936
getWindowsWithTitle() duration=0.4617865079999999
getMenu() duration=2.2305079800000005
They are high in absolute terms, but I don't think it's possible to improve them much more when using AppleScript (will keep on investigating anyway). getMenu() is specially hard to be improved.
If you can test on your system what I suggested in my previous comment, as well as this new version, I would really appreciate it!!!
Thanks!
from pywinctl import *
import AppKit
import subprocess
import timeit
#def getAllTitles(app: AppKit.NSApplication = None) -> List[str]:
def getAllTitles(app: AppKit.NSApplication = None):
"""Returns a list of strings of window titles for all visible windows."""
cmd = ("""'tell application "System Events"
set winNames to {}
repeat with p in every process whose background only is false
repeat with w in every window of p
set end of winNames to name of w
end repeat
end repeat
return winNames
end tell'""")
cmd='osascript -e '+cmd #;print(cmd);1/0
ret = subprocess.check_output(cmd,
shell=True).decode(encoding="utf-8").strip().split(", ")
return ret
start_time = timeit.default_timer()
windows = getAllTitles()
duration = timeit.default_timer() - start_time
print(f'number of windows = {len(windows)}\ngetAllTitles() {duration = }')
$python3 pywinctl_timeit.py
windows number = 20
getAllTitles() duration = 1.532654942
Have you try *.scpt AppleScript
file (pre-processed AppleScript
code)?
To test your speedy code could you build the wheel (?) as I used pip
update ;-)
Cheers
Thank you!!!
the wheel with new scripts and other modifications should be located at "dist" folder (version 0.0.18). If you get the time to test it, I would really appreciate it a lot!
Besides, I will dig into *scpt files (I had not hear from it before). If I am not wrong, it seems to be separate, pre-compiled script files. Perhaps it makes it much more difficult to pack and disttibute the library? We'll see!
Thanks for all your help.
How to used the 0.0.18 wheel (which I have already downloaded)?
Use pip for that:
pip uninstall pywinctl
pip install PyWinCtl-0.0.18-py3-none-any.whl
Found and tested about scptd!
import subprocess
import timeit
def getAllTitlesB():
"""Returns a list of strings of window titles for all visible windows."""
cmd = """osascript Test.scptd"""
ret = subprocess.check_output(cmd, shell=True).decode(encoding="utf-8").strip().split(", ")
return ret
def getAllTitles() :
"""Returns a list of strings of window titles for all visible windows."""
cmd = """osascript -e 'tell application "System Events"
try
set winNames to name of (every window of (every process whose background only is false))
end try
end tell
return winNames'"""
ret = subprocess.check_output(cmd, shell=True).decode(encoding="utf-8").strip().split(", ")
return ret
start_time = timeit.default_timer()
windows = getAllTitles()
duration = timeit.default_timer() - start_time
print(f'number of windows = {len(windows)}\ngetAllTitles() {duration = }')
start_time = timeit.default_timer()
windows = getAllTitlesB()
duration = timeit.default_timer() - start_time
print(f'number of windows = {len(windows)}\ngetAllTitlesB() {duration = }')
The result is not very encouraging though (dammit!):
number of windows = 7
getAllTitles() duration = 0.186754525
number of windows = 7
getAllTitlesB() duration = 0.20994049099999978
Inside Test.scptd there is exactly the same script code. I can provide you with it if you want to test it on you own system too.
Thank you so much again!
from pywinctl import *
import AppKit
import subprocess
import sysconfig
import timeit
from pywinctl import __version__ as version
print(f'{sysconfig.get_platform()}\nPyWinctl v{version}')
#def getAllTitles(app: AppKit.NSApplication = None) -> List[str]:
def getAllTitles2(app: AppKit.NSApplication = None):
"""Returns a list of strings of window titles for all visible windows."""
cmd = ("""'tell application "System Events"
set winNames to {}
repeat with p in every process whose background only is false
repeat with w in every window of p
set end of winNames to name of w
end repeat
end repeat
return winNames
end tell'""")
cmd='osascript -e '+cmd #;print(cmd);1/0
ret = subprocess.check_output(cmd,
shell=True).decode(encoding="utf-8").strip().split(", ")
return ret
start_time = timeit.default_timer()
windows = getAllTitles2()
duration = timeit.default_timer() - start_time
print(f'windows number = {len(windows)}\n'
f'getAllTitles2() duration = {duration:.3f}')
start_time = timeit.default_timer()
windows = getAllTitles()
duration = timeit.default_timer() - start_time
print(f'windows number = {len(windows)}\n'
f'getAllTitles() duration = {duration:.3f}')
macosx-10.10-x86_64
PyWinctl v0.0.18
windows number = 17
getAllTitles2() duration = 0.256
windows number = 18
getAllTitles() duration = 0.085
Windows number discrepancy Feature request: version method
Cheers
Hi!
First off, It seems that the improvement has worked. To be honest, I didn't expect that enormous difference (from 6.17 to 0.085!!!). Happy to see that, anyway.
About windows number discrepancy, totally lost. There was a huge difference compared with v0.0.17, but not with getAllTitle2(), which was my first solution approach to improve performance. The difference between getAllTitles2() and getAllTitles() inside v0.0.18 version is basically... none! It's the same code, reordered in a different way (I realized "repeat" is really slow). This is actually the new 0.0.18 script:
cmd = """osascript -e 'tell application "System Events"
set winNames to {}
try
set winNames to name of (every window of (every process whose background only is false))
end try
end tell
return winNames'"""
Whenever you try it again, please print the list of window names, so we can identify if there is something I am not properly catching. If I am not asking too much, please also include in your measurment tests getAllWindows() and getMenu() methods to check how they behave in "real" environments.
Finally, version method included. I am uploading a new version (0.0.19) which already contains this new method (version/getVersion).
Thank you SO much!
Please also include in your measurment tests getAllWindows() and getMenu() methods to check how they behave in "real" environments.
Please provide the "real" code you need to test in "real" environment ;-)
Finally, version method included. I am uploading a new version (0.0.19) which already contains this new method (
version/getVersion
)
Nice, but I suggest version() == pywinctl.__version__
and getVersion(() as it is...
Whenever you try it again, please print the list of window names, so we can identify if there is something I am not properly catching.
Short private mail follows
Sure!
First, download wheel file and install 0.0.19 version:
pip uninstall pywinctl
pip install PyWinCtl-0.0.19-py3-none-any.whl
And then run this:
import pywinctl
import subprocess
import timeit
start_time = timeit.default_timer()
windows = getAllWindows()
duration = timeit.default_timer() - start_time
print(f'windows number = {len(windows)}\n'
f'getAllWindows() duration = {duration:.3f}')
start_time = timeit.default_timer()
windows = getAllTitles()
duration = timeit.default_timer() - start_time
print(f'windows number = {len(windows)}\n'
f'getAllTitles() duration = {duration:.3f}')
subprocess.Popen(['open', '-a', 'TextEdit'])
start_time = timeit.default_timer()
windows = getWindowsWithTitle('TextEdit')
duration = timeit.default_timer() - start_time
print(f'windows number = {len(windows)}\n'
f'getWindowsWithTitle() duration = {duration:.3f}')
if windows:
win = windows[0]
start_time = timeit.default_timer()
menu = win.getMenu()
duration = timeit.default_timer() - start_time
print(f'windows number = {len(windows)}\n'
f'getMenu() duration = {duration:.3f}')
Regarding version methods, you mean version() should return "0.0.19" and getVersion() should return "PyWinCtl 0.0.19", right?
I have received your mail. Thank you! I see there is an empty ('') window in the list of getAllTitles() which is not present in getAllTitles2()... Extremely weird when comparing both scripts, and it doesn't happen in my virtual machine. I will look into it.
Found!
You won't believe it. It only happens in Yosemite. The solution is as simple as THIS (note the lack of parenthesis):
cmd = """osascript -e 'tell application "System Events"
set winNames to {}
try
set winNames to name of every window of every process whose background only is false
end try
end tell
return winNames'"""
Well seen!! Thank you!
https://github.com/Kalmat/PyWinCtl/issues/2#issuecomment-1057183118
macosx-10.10-x86_64
PyWinCtl 0.0.19
windows number = 6
getAllWindows() duration = 0.156
windows number = 17
getAllTitles() duration = 0.098
windows number = 0
getWindowsWithTitle() duration = 0.531
When an updated wheel exist let me know
Found and fixed. Yesterday wasn't a good day for me at all, but this doesn't mean to waste others' time. Sorry for that. Version 0.0.20 is uploaded.
Besides, my code was wrong. I was trying to avoid language problems (if I open TextEdit, the window name is "Abrir", which I guess would not work on your system). This is the rigth code to test:
import pywinctl
import subprocess
import timeit
start_time = timeit.default_timer()
windows = getAllWindows()
duration = timeit.default_timer() - start_time
print(f'windows number = {len(windows)}\n'
f'getAllWindows() duration = {duration:.3f}')
start_time = timeit.default_timer()
windows = getAllTitles()
duration = timeit.default_timer() - start_time
print(f'windows number = {len(windows)}\n'
f'getAllTitles() duration = {duration:.3f}')
subprocess.Popen(['touch', 'test.txt'])
time.sleep(2)
subprocess.Popen(['open', '-a', 'TextEdit', 'test.txt'])
time.sleep(3)
start_time = timeit.default_timer()
windows = getWindowsWithTitle('test.txt')
duration = timeit.default_timer() - start_time
print(f'windows number = {len(windows)}\n'
f'getWindowsWithTitle() duration = {duration:.3f}')
win = None
if windows:
win = windows[0]
if win:
start_time = timeit.default_timer()
menu = win.menu.getMenu()
duration = timeit.default_timer() - start_time
print(f'keys number = {len(menu.keys())}\n'
f'getMenu() duration = {duration:.3f}')
win.close()
subprocess.Popen(['rm', 'test.txt'])
Thank you for your patience, and my apologies again.
macosx-10.10-x86_64
PyWinCtl-0.0.20
windows number = 6
getAllWindows() duration = 0.184
windows number = 19
getAllTitles() duration = 0.119
windows number = 1
getWindowsWithTitle() duration = 0.174
[MacOSWindow(hWnd=<NSRunningApplication: 0x7f9322e2f720 (com.apple.TextEdit - 16732)>)]
keys number = 8
getMenu() duration = 1.604
getMenu() keys() = dict_keys(['Apple', 'TextEdit', 'Fichier', 'Édition', 'Format', 'Présentation', 'Fenêtre', 'Aide'])
I have to make this change:
getWindowsWithTitle('TextEdit') => getWindowsWithTitle('test.txt')
Thank you!
Extremely weird that behavior: 6 vs. 19!?!?!?!?! When testing on both Yosemite and Catalina, this is what I get on version 0.0.20:
windows number = 10
getAllTitles() duration = 0.220
['Proyectos', 'pywinctl — osascript ◂ Python _pywinctl_macos.py — 80×24', 'Favoritos', 'Notas', 'Contacts', 'Abrir', 'Abrir', 'Recordatorios', 'Calendario', 'Configuración de Acciones de Carpeta']
windows number = 10
getAllWindows() duration = 0.556
['Proyectos', 'pywinctl — osascript ◂ Python _pywinctl_macos.py — 80×24', 'Favoritos', 'Notas', 'Contacts', 'Abrir', 'Abrir', 'Recordatorios', 'Calendario', 'Configuración de Acciones de Carpeta']
windows number = 1
getWindowsWithTitle() duration = 0.542
keys number = 8
getMenu() duration = 3.084
Do you know if 6 or 19 or other is the right number of open windows on your system when running the test? Could you please email me with the print result for both getAllWindows() and getAllTitles()? Before that, please, donwload wheel again and uninstall/reinstall it on your system. Sorry, but I am not sure if we properly synced since getWindowsWithTitle('test.txt') was already present in my previous comment. In addition to all this fixes, I just uploaded a new wheel which contains one-liner versions of all problemmatic scripts (not for getMenu(), which still I don't know how to improve it), which should be even faster!
Of course, no compromise nor urgency.
macosx-10.10-x86_64
PyWinCtl-0.0.20
windows number = 4
getAllWindows() duration = 0.175
[MacOSWindow(hWnd=<NSRunningApplication: 0x7f9ede160de0 (com.apple.iCal - 945)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f9ede1611e0 (com.barebones.bbedit - 9493)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f9ede1626e0 (com.apple.Terminal - 20049)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f9ede1627e0 (com.cocoatech.PathFinder - 20087)>)]
com.apple.iCal com.barebones.bbedit com.apple.Terminal com.cocoatech.PathFinder (with number of windows)
but other apps runningin MacOS Dock as Firefox, Safari, TextEdit are absent...
In the end, one-liner scripts are harder than I thought. They are way faster, but they produce very complex data structures which were misleading me. I think I finally catched it with your extremely appreciated help!!!
I am filtering for running apps only, this is why those other apps are not included in the ouput. Minimized apps/windows will be included, however. If you run this on a terminal: defaults write com.apple.dock static-only -bool true; killall Dock
, those other apps shouldn't show as "running" on the dock.
I'm uploading version 0.0.21 which hopefully solves this.
macosx-10.10-x86_64
0.0.21
windows number = 17
getAllWindows() duration = 0.186
[MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9796e50 (com.cocoatech.PathFinder - 414)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9797050 (com.barebones.bbedit - 455)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9797750 (org.mozilla.thunderbird - 2321)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9797850 (org.mozilla.firefox - 2328)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9797e50 (com.apple.TextEdit - 2489)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9797f50 (com.apple.Terminal - 2527)>),
MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9798050 (com.apple.Safari - 2626)>)]
windows number = 19
getAllTitles() duration = 0.119
windows number = 1
getWindowsWithTitle() duration = 0.182
[MacOSWindow(hWnd=<NSRunningApplication: 0x7f89c9797e50 (com.apple.TextEdit - 2489)>)]
keys number = 8
getMenu() duration = 1.535
getMenu() keys() = dict_keys(['Apple', 'TextEdit', 'Fichier', 'Édition', 'Format',
'Présentation', 'Fenêtre', 'Aide'])
Getting closer... HAHAHAHAHA! Can you please email me the list of titles the next time you test?
I will dig into it anyway.
Thanks a lot!!!
Again, complex structures issues. I even installed PathFinder and BBEdit to test on my own system. I think (hope) it is working OK now. Version 0.0.22 uploaded!!!
So, is 0.0.22 ok in terms of time and windows number?
Test:
getAllTitles() duration=6.178130242