Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ disallow_untyped_defs = False
disable_error_code =
# https://github.com/python/mypy/issues/6232 (redefinition with correct type)
attr-defined, assignment,

# https://github.com/ronaldoussoren/pyobjc/issues/198
# https://github.com/ronaldoussoren/pyobjc/issues/417
# https://github.com/ronaldoussoren/pyobjc/issues/419
[mypy-objc.*]
follow_untyped_imports = True
[mypy-Quartz.*]
allow_any_generics = True
20 changes: 14 additions & 6 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ future-annotations = true
# https://docs.astral.sh/ruff/rules/
extend-select = [
"ANN2", # flake8-annotations: missing-return-type
"PYI", # flake8-pyi
"C4", # flake8-comprehensions
"F401", # unused-import
"F404", # late-future-import
"FA", # flake8-future-annotations
"FLY", # Flynt
"ICN", # flake8-import-conventions
"F401", # unused-import
"YTT", # flake8-2020
"PERF", # Perflint
"PGH", # pygrep-hooks (blanket-* rules)
"PYI", # flake8-pyi
"RUF", # Ruff-specific rules
"SIM9", # flake8-simplify: split-static-string + zip-dict-keys-and-values
"SLOT", # flake8-slots
"TC", # flake8-type-checking
"TID", # flake8-tidy-imports
"UP", # pyupgrade
"RUF", # Ruff-specific rules
"F404", # late-future-import
"PGH", # pygrep-hooks (blanket-* rules)
"YTT", # flake8-2020
]
ignore = [
# Only enforce return types on public functions. Where otherwise mypy infers as Any
Expand All @@ -34,6 +39,9 @@ ignore = [
"PYI011", # typed-argument-default-in-stub
"PYI014", # argument-default-in-stub
"PYI053", # string-or-bytes-too-long
# Good to watch for, but often unfixable
# Anyway Python 3.11 introduced "zero cost" exception handling
"PERF203", # try-except-in-loop,

# TODO: Consider later
"UP031", # printf-string-formatting
Expand Down
95 changes: 47 additions & 48 deletions src/pywinctl/_pywinctl_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@

import Xlib.display
import Xlib.error
import Xlib.ext
import Xlib.protocol
import Xlib.X
import Xlib.Xatom
import Xlib.Xutil
from ewmhlib import EwmhRoot, EwmhWindow, Props, defaultEwmhRoot
from ewmhlib._ewmhlib import _xlibGetAllWindows
from pywinbox import Point, Rect, Size, pointInBox
Expand Down Expand Up @@ -129,7 +126,7 @@ def getAllTitles() -> list[str]:
return [window.title for window in getAllWindows()]


def getWindowsWithTitle(title: str | re.Pattern[str], app: tuple[str, ...] | None = (), condition: int = Re.IS, flags: int = 0):
def getWindowsWithTitle(title: str | re.Pattern[str], app: tuple[str, ...] | None = (), condition: int = Re.IS, flags: int = 0) -> list[LinuxWindow]:
"""
Get the list of window objects whose title match the given string with condition and flags.
Use ''condition'' to delimit the search. Allowed values are stored in pywinctl.Re sub-class (e.g. pywinctl.Re.CONTAINS)
Expand All @@ -154,25 +151,26 @@ def getWindowsWithTitle(title: str | re.Pattern[str], app: tuple[str, ...] | Non
:param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags)
:return: list of Window objects
"""
matches: list[LinuxWindow] = []
if title and condition in Re._cond_dic:
lower = False
if condition in (Re.MATCH, Re.NOTMATCH):
title = re.compile(title, flags)
elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO):
if not isinstance(flags, int) or not (0 < flags <= 100):
flags = 90
elif flags == Re.IGNORECASE:
lower = True
if isinstance(title, re.Pattern):
title = title.pattern
else:
title = title.lower()
for win in getAllWindows():
if win.title and Re._cond_dic[condition](title, win.title.lower() if lower else win.title, flags) \
and (not app or (app and win.getAppName() in app)):
matches.append(win)
return matches
if not (title and condition in Re._cond_dic):
return []

lower = False
if condition in (Re.MATCH, Re.NOTMATCH):
title = re.compile(title, flags)
elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO):
if not isinstance(flags, int) or not (0 < flags <= 100):
flags = 90
elif flags == Re.IGNORECASE:
lower = True
if isinstance(title, re.Pattern):
title = title.pattern
else:
title = title.lower()
return [
win for win in getAllWindows()
if win.title and Re._cond_dic[condition](title, win.title.lower() if lower else win.title, flags)
and (not app or win.getAppName() in app)
]


def getAllAppsNames() -> list[str]:
Expand All @@ -184,7 +182,7 @@ def getAllAppsNames() -> list[str]:
return list(getAllAppsWindowsTitles())


def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0):
def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0) -> list[str]:
"""
Get the list of app names which match the given string using the given condition and flags.
Use ''condition'' to delimit the search. Allowed values are stored in pywinctl.Re sub-class (e.g. pywinctl.Re.CONTAINS)
Expand All @@ -208,24 +206,25 @@ def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags:
:param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags)
:return: list of app names
"""
matches: list[str] = []
if name and condition in Re._cond_dic:
lower = False
if condition in (Re.MATCH, Re.NOTMATCH):
name = re.compile(name, flags)
elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO):
if not isinstance(flags, int) or not (0 < flags <= 100):
flags = 90
elif flags == Re.IGNORECASE:
lower = True
if isinstance(name, re.Pattern):
name = name.pattern
else:
name = name.lower()
for title in getAllAppsNames():
if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags):
matches.append(title)
return matches
if not (name and condition in Re._cond_dic):
return []

lower = False
if condition in (Re.MATCH, Re.NOTMATCH):
name = re.compile(name, flags)
elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO):
if not isinstance(flags, int) or not (0 < flags <= 100):
flags = 90
elif flags == Re.IGNORECASE:
lower = True
if isinstance(name, re.Pattern):
name = name.pattern
else:
name = name.lower()
return [
title for title in getAllAppsNames()
if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags)
]


def _getAllApps():
Expand Down Expand Up @@ -309,19 +308,19 @@ def getAllWindowsDict(tryToFilter: bool = False) -> dict[str, _WINDICT]:
return result


def getWindowsAt(x: int, y: int):
def getWindowsAt(x: int, y: int) -> list[LinuxWindow]:
"""
Get the list of Window objects whose windows contain the point ``(x, y)`` on screen

:param x: X screen coordinate of the window(s)
:param y: Y screen coordinate of the window(s)
:return: list of Window objects
"""
windowBoxGenerator = ((window, window.box) for window in getAllWindows())
return [
window for (window, box)
in windowBoxGenerator
if pointInBox(x, y, box)]
window for window
in getAllWindows()
if pointInBox(x, y, window.box)
]


def getTopWindowAt(x: int, y: int):
Expand Down Expand Up @@ -742,7 +741,7 @@ def acceptInput(self, setTo: bool):

ret = self._win.getProperty("_MOTIF_WM_HINTS")
# Cinnamon uses this as default: [2, 1, 1, 0, 0]
self._motifHints = [a for a in ret.value] if ret and hasattr(ret, "value") else [2, 0, 0, 0, 0]
self._motifHints = list(ret.value) if ret and hasattr(ret, "value") else [2, 0, 0, 0, 0]
self._win.changeProperty("_MOTIF_WM_HINTS", [0, 0, 0, 0, 0])

self._win.setWmWindowType(Props.WindowType.DESKTOP)
Expand Down
62 changes: 28 additions & 34 deletions src/pywinctl/_pywinctl_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,7 @@ def getAllTitles() -> list[str]:
.replace('missing value', '"missing value"') \
.replace("{", "[").replace("}", "]")
res = ast.literal_eval(ret)
matches: list[str] = []
if len(res) > 0:
for item in res[0]:
for title in item:
matches.append(title)
return matches
return [title for item in res[0] for title in item] if res else []


def getWindowsWithTitle(title: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0) -> list[MacOSWindow]:
Expand Down Expand Up @@ -241,7 +236,7 @@ def getAllAppsNames() -> list[str]:
return res or []


def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0):
def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0) -> list[str]:
"""
Get the list of app names which match the given string using the given condition and flags.
Use ''condition'' to delimit the search. Allowed values are stored in pywinctl.Re sub-class (e.g. pywinctl.Re.CONTAINS)
Expand All @@ -265,23 +260,24 @@ def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags:
:param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags)
:return: list of app names
"""
matches: list[str] = []
if name and condition in Re._cond_dic:
lower = False
if condition in (Re.MATCH, Re.NOTMATCH):
name = re.compile(name, flags)
elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO):
if not isinstance(flags, int) or not (0 < flags <= 100):
flags = 90
elif flags == Re.IGNORECASE:
lower = True
if isinstance(name, re.Pattern):
name = name.pattern
name = name.lower()
for title in getAllAppsNames():
if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags):
matches.append(title)
return matches
if not (name and condition in Re._cond_dic):
return []

lower = False
if condition in (Re.MATCH, Re.NOTMATCH):
name = re.compile(name, flags)
elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO):
if not isinstance(flags, int) or not (0 < flags <= 100):
flags = 90
elif flags == Re.IGNORECASE:
lower = True
if isinstance(name, re.Pattern):
name = name.pattern
name = name.lower()
return [
title for title in getAllAppsNames()
if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags)
]


def getAllAppsWindowsTitles():
Expand Down Expand Up @@ -387,12 +383,11 @@ def getWindowsAt(x: int, y: int, allWindows: list[MacOSWindow] | None = None) ->
:param allWindows: (optional) list of window objects (required to improve performance in Apple Script version)
:return: list of Window objects
"""
windows = allWindows if allWindows else getAllWindows()
windowBoxGenerator = ((window, window.box) for window in windows)
return [
window for (window, box)
in windowBoxGenerator
if pointInBox(x, y, box)]
window for window
in (allWindows or getAllWindows())
if pointInBox(x, y, window.box)
]


def getTopWindowAt(x: int, y: int, allWindows: list[MacOSWindow] | None = None) -> MacOSWindow | None:
Expand All @@ -413,11 +408,10 @@ def getTopWindowAt(x: int, y: int, allWindows: list[MacOSWindow] | None = None)


def _getAllApps(userOnly: bool = True):
matches: list[AppKit.NSRunningApplication] = []
for app in AppKit.NSWorkspace.sharedWorkspace().runningApplications():
if not userOnly or (userOnly and app.activationPolicy() == Quartz.NSApplicationActivationPolicyRegular):
matches.append(app)
return matches
return [
app for app in AppKit.NSWorkspace.sharedWorkspace().runningApplications()
if not userOnly or app.activationPolicy() == Quartz.NSApplicationActivationPolicyRegular
]


def _getAppWindowsTitles(app: AppKit.NSRunningApplication):
Expand Down
Loading