python-win-windows-manager 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-win-windows-manager
3
+ Version: 0.1.0
4
+ Summary: Window manager and monitor for Windows
5
+ Project-URL: Homepage, https://github.com/username/repo
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: psutil>=7.2.2
8
+ Requires-Dist: pywin32>=311
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Windows Window Manager
12
+
13
+ A Python library for managing Windows windows using `win32gui` and `ctypes`. This project provides a robust interface for identifying, controlling, and monitoring windows on Windows systems.
14
+
15
+ ## Features
16
+
17
+ - **Window Identification**: Get detailed information about all open windows (Handle, Title, Class Name, PID, Process Name).
18
+ - **Window Search**: Find windows by title or class name with exact or partial matching.
19
+ - **Window Control**:
20
+ - Minimize, Maximize, Restore
21
+ - Close
22
+ - Move and Resize
23
+ - Set to Foreground
24
+ - **Real-time Monitoring**: Monitor window creation, destruction, and state changes.
25
+ - **Robust Error Handling**: Handles permissions and invalid handles gracefully.
26
+
27
+ ## Installation
28
+
29
+ This project uses `uv` for dependency management.
30
+
31
+ 1. Clone the repository.
32
+ 2. Install dependencies:
33
+ ```bash
34
+ uv sync
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Basic Example
40
+
41
+ ```python
42
+ from window_manager import WindowManager
43
+
44
+ manager = WindowManager()
45
+
46
+ # List all visible windows
47
+ windows = manager.get_all_windows(visible_only=True)
48
+ for window in windows:
49
+ print(window)
50
+
51
+ # Find a window
52
+ notepad = manager.find_windows(title="Notepad")[0]
53
+
54
+ # Minimize
55
+ manager.minimize_window(notepad.handle)
56
+
57
+ # Restore
58
+ manager.restore_window(notepad.handle)
59
+ ```
60
+
61
+ ### Monitoring Example
62
+
63
+ ```python
64
+ import time
65
+ from window_manager import WindowMonitor
66
+
67
+ def callback(event, hwnd, title):
68
+ print(f"Event: {event}, Window: {title}")
69
+
70
+ monitor = WindowMonitor(callback=callback)
71
+ monitor.start()
72
+
73
+ try:
74
+ while True:
75
+ time.sleep(1)
76
+ except KeyboardInterrupt:
77
+ monitor.stop()
78
+ ```
79
+
80
+ ## Running Tests
81
+
82
+ ```bash
83
+ uv run pytest tests
84
+ ```
85
+
86
+ ## Documentation
87
+
88
+ See [docs/API.md](docs/API.md) for detailed API documentation.
@@ -0,0 +1,78 @@
1
+ # Windows Window Manager
2
+
3
+ A Python library for managing Windows windows using `win32gui` and `ctypes`. This project provides a robust interface for identifying, controlling, and monitoring windows on Windows systems.
4
+
5
+ ## Features
6
+
7
+ - **Window Identification**: Get detailed information about all open windows (Handle, Title, Class Name, PID, Process Name).
8
+ - **Window Search**: Find windows by title or class name with exact or partial matching.
9
+ - **Window Control**:
10
+ - Minimize, Maximize, Restore
11
+ - Close
12
+ - Move and Resize
13
+ - Set to Foreground
14
+ - **Real-time Monitoring**: Monitor window creation, destruction, and state changes.
15
+ - **Robust Error Handling**: Handles permissions and invalid handles gracefully.
16
+
17
+ ## Installation
18
+
19
+ This project uses `uv` for dependency management.
20
+
21
+ 1. Clone the repository.
22
+ 2. Install dependencies:
23
+ ```bash
24
+ uv sync
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Basic Example
30
+
31
+ ```python
32
+ from window_manager import WindowManager
33
+
34
+ manager = WindowManager()
35
+
36
+ # List all visible windows
37
+ windows = manager.get_all_windows(visible_only=True)
38
+ for window in windows:
39
+ print(window)
40
+
41
+ # Find a window
42
+ notepad = manager.find_windows(title="Notepad")[0]
43
+
44
+ # Minimize
45
+ manager.minimize_window(notepad.handle)
46
+
47
+ # Restore
48
+ manager.restore_window(notepad.handle)
49
+ ```
50
+
51
+ ### Monitoring Example
52
+
53
+ ```python
54
+ import time
55
+ from window_manager import WindowMonitor
56
+
57
+ def callback(event, hwnd, title):
58
+ print(f"Event: {event}, Window: {title}")
59
+
60
+ monitor = WindowMonitor(callback=callback)
61
+ monitor.start()
62
+
63
+ try:
64
+ while True:
65
+ time.sleep(1)
66
+ except KeyboardInterrupt:
67
+ monitor.stop()
68
+ ```
69
+
70
+ ## Running Tests
71
+
72
+ ```bash
73
+ uv run pytest tests
74
+ ```
75
+
76
+ ## Documentation
77
+
78
+ See [docs/API.md](docs/API.md) for detailed API documentation.
@@ -0,0 +1,85 @@
1
+ # API Reference
2
+
3
+ ## WindowManager
4
+
5
+ The core class for managing Windows windows.
6
+
7
+ ### `get_all_windows(visible_only: bool = True) -> List[WindowInfo]`
8
+
9
+ Retrieves all open windows.
10
+
11
+ - **Parameters**:
12
+ - `visible_only` (bool): If `True`, only returns visible windows. Defaults to `True`.
13
+ - **Returns**:
14
+ - A list of `WindowInfo` objects.
15
+
16
+ ### `find_windows(title: Optional[str] = None, class_name: Optional[str] = None, exact_match: bool = False) -> List[WindowInfo]`
17
+
18
+ Finds windows matching the given criteria.
19
+
20
+ - **Parameters**:
21
+ - `title` (str, optional): The window title to search for.
22
+ - `class_name` (str, optional): The window class name to search for.
23
+ - `exact_match` (bool): If `True`, requires exact string match. Otherwise, checks for containment. Defaults to `False`.
24
+ - **Returns**:
25
+ - A list of matching `WindowInfo` objects.
26
+
27
+ ### `get_window_by_handle(handle: int) -> Optional[WindowInfo]`
28
+
29
+ Retrieves window information by handle.
30
+
31
+ - **Parameters**:
32
+ - `handle` (int): The window handle.
33
+ - **Returns**:
34
+ - A `WindowInfo` object if found, otherwise `None`.
35
+
36
+ ### Window Controls
37
+
38
+ - `close_window(handle: int) -> bool`: Closes the window.
39
+ - `minimize_window(handle: int) -> bool`: Minimizes the window.
40
+ - `maximize_window(handle: int) -> bool`: Maximizes the window.
41
+ - `restore_window(handle: int) -> bool`: Restores the window.
42
+ - `move_window(handle: int, x: int, y: int, width: int, height: int) -> bool`: Moves and resizes the window.
43
+ - `set_foreground(handle: int) -> bool`: Brings the window to the foreground.
44
+
45
+ ## WindowInfo
46
+
47
+ A dataclass representing window information.
48
+
49
+ - **Attributes**:
50
+ - `handle` (int): The window handle (HWND).
51
+ - `title` (str): The window title.
52
+ - `class_name` (str): The window class name.
53
+ - `pid` (int): The process ID associated with the window.
54
+ - `process_name` (str, optional): The name of the process.
55
+ - `rect` (tuple, optional): The window rectangle (left, top, right, bottom).
56
+ - `is_visible` (bool): Whether the window is visible.
57
+
58
+ ## WindowMonitor
59
+
60
+ Monitors window events such as creation, destruction, and state changes.
61
+
62
+ ### `__init__(callback: Optional[Callable[[str, int, str], None]] = None)`
63
+
64
+ Initializes the monitor.
65
+
66
+ - **Parameters**:
67
+ - `callback`: A function to call when an event occurs. Signature: `callback(event_name, hwnd, title)`.
68
+
69
+ ### `start()`
70
+
71
+ Starts the monitoring thread.
72
+
73
+ ### `stop()`
74
+
75
+ Stops the monitoring thread.
76
+
77
+ ## Events
78
+
79
+ The following events are monitored:
80
+ - `Create`: Window created.
81
+ - `Destroy`: Window destroyed.
82
+ - `Show`: Window shown.
83
+ - `Hide`: Window hidden.
84
+ - `Foreground`: Window became foreground.
85
+ - `NameChange`: Window title changed.
@@ -0,0 +1,94 @@
1
+ import sys
2
+ import time
3
+ import subprocess
4
+ import os
5
+
6
+ # Add src to python path
7
+ sys.path.append(os.path.join(os.path.dirname(__file__), "../src"))
8
+
9
+ from window_manager import WindowManager, WindowMonitor, setup_logger
10
+
11
+ # Setup logging
12
+ logger = setup_logger("Demo", level=10) # DEBUG
13
+
14
+ def monitor_callback(event, hwnd, title):
15
+ print(f"[MONITOR] Event: {event}, HWND: {hwnd}, Title: '{title}'")
16
+
17
+ def main():
18
+ manager = WindowManager()
19
+ monitor = WindowMonitor(callback=monitor_callback)
20
+
21
+ print("Starting Window Monitor...")
22
+ monitor.start()
23
+
24
+ # List all windows
25
+ print("\nListing all visible windows:")
26
+ windows = manager.get_all_windows(visible_only=True)
27
+ for w in windows[:5]: # Show first 5
28
+ print(f" {w}")
29
+ print(f"Total visible windows: {len(windows)}")
30
+
31
+ # Create a test window (Notepad)
32
+ print("\nLaunching Notepad for testing...")
33
+ notepad_process = subprocess.Popen("notepad.exe")
34
+ time.sleep(2) # Wait for window to appear
35
+
36
+ # Find Notepad window
37
+ print("\nFinding Notepad window...")
38
+ notepad_windows = manager.find_windows(title="Notepad", exact_match=False)
39
+
40
+ if not notepad_windows:
41
+ print("Could not find Notepad window!")
42
+ monitor.stop()
43
+ notepad_process.terminate()
44
+ return
45
+
46
+ target_window = notepad_windows[0]
47
+ print(f"Found: {target_window}")
48
+
49
+ hwnd = target_window.handle
50
+
51
+ # Test manipulations
52
+ print("\nTesting manipulations:")
53
+
54
+ print(" Minimizing...")
55
+ manager.minimize_window(hwnd)
56
+ time.sleep(1)
57
+
58
+ print(" Restoring...")
59
+ manager.restore_window(hwnd)
60
+ time.sleep(1)
61
+
62
+ print(" Maximizing...")
63
+ manager.maximize_window(hwnd)
64
+ time.sleep(1)
65
+
66
+ print(" Restoring...")
67
+ manager.restore_window(hwnd)
68
+ time.sleep(1)
69
+
70
+ print(" Moving to (100, 100) with size 800x600...")
71
+ manager.move_window(hwnd, 100, 100, 800, 600)
72
+ time.sleep(1)
73
+
74
+ # Verify monitoring caught these events
75
+ print("\nWaiting for events to be logged...")
76
+ time.sleep(2)
77
+
78
+ # Close Notepad
79
+ print("\nClosing Notepad...")
80
+ manager.close_window(hwnd)
81
+ time.sleep(1)
82
+
83
+ # Stop monitor
84
+ print("\nStopping Monitor...")
85
+ monitor.stop()
86
+
87
+ # Ensure process is gone
88
+ if notepad_process.poll() is None:
89
+ notepad_process.terminate()
90
+
91
+ print("\nDemo completed.")
92
+
93
+ if __name__ == "__main__":
94
+ main()
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "python-win-windows-manager"
3
+ version = "0.1.0"
4
+ description = "Window manager and monitor for Windows"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "psutil>=7.2.2",
9
+ "pywin32>=311",
10
+ ]
11
+
12
+ [dependency-groups]
13
+ dev = [
14
+ "pytest>=9.0.2",
15
+ ]
16
+ [project.urls]
17
+ "Homepage" = "https://github.com/username/repo"
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["src/window_manager"]
@@ -0,0 +1,6 @@
1
+ from .core import WindowManager
2
+ from .models import WindowInfo
3
+ from .monitor import WindowMonitor
4
+ from .utils import setup_logger
5
+
6
+ __all__ = ["WindowManager", "WindowInfo", "WindowMonitor", "setup_logger"]
@@ -0,0 +1,228 @@
1
+ import logging
2
+ import win32gui
3
+ import win32process
4
+ import win32con
5
+ import psutil
6
+ from typing import List, Optional, Tuple, Dict, Any
7
+ from .models import WindowInfo
8
+ from .utils import setup_logger
9
+
10
+ logger = setup_logger()
11
+
12
+ class WindowManagerError(Exception):
13
+ """Base exception for WindowManager errors."""
14
+ pass
15
+
16
+ class WindowManager:
17
+ """
18
+ Core class for managing Windows windows.
19
+ """
20
+
21
+ def __init__(self):
22
+ """Initializes the WindowManager."""
23
+ self._logger = logger
24
+
25
+ def get_all_windows(self, visible_only: bool = True) -> List[WindowInfo]:
26
+ """
27
+ Retrieves all open windows.
28
+
29
+ Args:
30
+ visible_only: If True, only returns visible windows.
31
+
32
+ Returns:
33
+ A list of WindowInfo objects.
34
+ """
35
+ windows: List[WindowInfo] = []
36
+
37
+ def enum_handler(hwnd, ctx):
38
+ if visible_only and not win32gui.IsWindowVisible(hwnd):
39
+ return
40
+
41
+ try:
42
+ title = win32gui.GetWindowText(hwnd)
43
+ class_name = win32gui.GetClassName(hwnd)
44
+
45
+ # Get process ID
46
+ _, pid = win32process.GetWindowThreadProcessId(hwnd)
47
+
48
+ # Get process name
49
+ try:
50
+ process = psutil.Process(pid)
51
+ process_name = process.name()
52
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
53
+ process_name = "Unknown"
54
+
55
+ # Get window rect
56
+ try:
57
+ rect = win32gui.GetWindowRect(hwnd)
58
+ except Exception:
59
+ rect = None
60
+
61
+ window_info = WindowInfo(
62
+ handle=hwnd,
63
+ title=title,
64
+ class_name=class_name,
65
+ pid=pid,
66
+ process_name=process_name,
67
+ rect=rect,
68
+ is_visible=win32gui.IsWindowVisible(hwnd)
69
+ )
70
+ windows.append(window_info)
71
+ except Exception as e:
72
+ self._logger.warning(f"Error processing window {hwnd}: {e}")
73
+
74
+ try:
75
+ win32gui.EnumWindows(enum_handler, None)
76
+ except Exception as e:
77
+ self._logger.error(f"Failed to enumerate windows: {e}")
78
+ raise WindowManagerError(f"Failed to enumerate windows: {e}")
79
+
80
+ return windows
81
+
82
+ def find_windows(self, title: Optional[str] = None, class_name: Optional[str] = None, process_name: Optional[str] = None, exact_match: bool = False) -> List[WindowInfo]:
83
+ """
84
+ Finds windows matching the given criteria.
85
+
86
+ Args:
87
+ title: The window title to search for.
88
+ class_name: The window class name to search for.
89
+ exact_match: If True, requires exact string match. Otherwise, checks for containment.
90
+
91
+ Returns:
92
+ A list of matching WindowInfo objects.
93
+ """
94
+ all_windows = self.get_all_windows(visible_only=False)
95
+ matched_windows = []
96
+
97
+ for window in all_windows:
98
+ match = True
99
+
100
+ if title:
101
+ if exact_match:
102
+ if window.title != title:
103
+ match = False
104
+ else:
105
+ if title.lower() not in window.title.lower():
106
+ match = False
107
+
108
+ if match and class_name:
109
+ if exact_match:
110
+ if window.class_name != class_name:
111
+ match = False
112
+ else:
113
+ if class_name.lower() not in window.class_name.lower():
114
+ match = False
115
+
116
+ if match and process_name:
117
+ if window.process_name is None or window.process_name.lower() != process_name.lower():
118
+ match = False
119
+
120
+ if match and (title or class_name or process_name):
121
+ matched_windows.append(window)
122
+
123
+ return matched_windows
124
+
125
+ def get_window_by_handle(self, handle: int) -> Optional[WindowInfo]:
126
+ """
127
+ Retrieves window information by handle.
128
+
129
+ Args:
130
+ handle: The window handle.
131
+
132
+ Returns:
133
+ A WindowInfo object if found, otherwise None.
134
+ """
135
+ if not win32gui.IsWindow(handle):
136
+ return None
137
+
138
+ try:
139
+ title = win32gui.GetWindowText(handle)
140
+ class_name = win32gui.GetClassName(handle)
141
+ _, pid = win32process.GetWindowThreadProcessId(handle)
142
+
143
+ try:
144
+ process = psutil.Process(pid)
145
+ process_name = process.name()
146
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
147
+ process_name = "Unknown"
148
+
149
+ try:
150
+ rect = win32gui.GetWindowRect(handle)
151
+ except Exception:
152
+ rect = None
153
+
154
+ return WindowInfo(
155
+ handle=handle,
156
+ title=title,
157
+ class_name=class_name,
158
+ pid=pid,
159
+ process_name=process_name,
160
+ rect=rect,
161
+ is_visible=win32gui.IsWindowVisible(handle)
162
+ )
163
+ except Exception as e:
164
+ self._logger.error(f"Error getting window info for handle {handle}: {e}")
165
+ return None
166
+
167
+ def close_window(self, handle: int) -> bool:
168
+ """Closes the window with the given handle."""
169
+ try:
170
+ win32gui.PostMessage(handle, win32con.WM_CLOSE, 0, 0)
171
+ self._logger.info(f"Closed window {handle}")
172
+ return True
173
+ except Exception as e:
174
+ self._logger.error(f"Failed to close window {handle}: {e}")
175
+ return False
176
+
177
+ def minimize_window(self, handle: int) -> bool:
178
+ """Minimizes the window."""
179
+ try:
180
+ win32gui.ShowWindow(handle, win32con.SW_MINIMIZE)
181
+ self._logger.info(f"Minimized window {handle}")
182
+ return True
183
+ except Exception as e:
184
+ self._logger.error(f"Failed to minimize window {handle}: {e}")
185
+ return False
186
+
187
+ def maximize_window(self, handle: int) -> bool:
188
+ """Maximizes the window."""
189
+ try:
190
+ win32gui.ShowWindow(handle, win32con.SW_MAXIMIZE)
191
+ self._logger.info(f"Maximized window {handle}")
192
+ return True
193
+ except Exception as e:
194
+ self._logger.error(f"Failed to maximize window {handle}: {e}")
195
+ return False
196
+
197
+ def restore_window(self, handle: int) -> bool:
198
+ """Restores the window."""
199
+ try:
200
+ win32gui.ShowWindow(handle, win32con.SW_RESTORE)
201
+ self._logger.info(f"Restored window {handle}")
202
+ return True
203
+ except Exception as e:
204
+ self._logger.error(f"Failed to restore window {handle}: {e}")
205
+ return False
206
+
207
+ def move_window(self, handle: int, x: int, y: int, width: int, height: int) -> bool:
208
+ """Moves and resizes the window."""
209
+ try:
210
+ win32gui.MoveWindow(handle, x, y, width, height, True)
211
+ self._logger.info(f"Moved window {handle} to ({x}, {y}, {width}, {height})")
212
+ return True
213
+ except Exception as e:
214
+ self._logger.error(f"Failed to move window {handle}: {e}")
215
+ return False
216
+
217
+ def set_foreground(self, handle: int) -> bool:
218
+ """Brings the window to the foreground."""
219
+ try:
220
+ # Sometimes setting foreground requires specific conditions or tricks
221
+ # like attaching thread input if the foreground lock timeout is set.
222
+ # For simplicity, we just try SetForegroundWindow.
223
+ win32gui.SetForegroundWindow(handle)
224
+ self._logger.info(f"Set window {handle} to foreground")
225
+ return True
226
+ except Exception as e:
227
+ self._logger.error(f"Failed to set foreground window {handle}: {e}")
228
+ return False
@@ -0,0 +1,20 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ @dataclass
5
+ class WindowInfo:
6
+ """
7
+ Represents information about a window.
8
+ """
9
+ handle: int
10
+ title: str
11
+ class_name: str
12
+ pid: int
13
+ process_name: Optional[str] = None
14
+ rect: Optional[tuple[int, int, int, int]] = None # (left, top, right, bottom)
15
+ is_visible: bool = False
16
+
17
+ def __str__(self):
18
+ return (f"Window(handle={self.handle}, title='{self.title}', "
19
+ f"class='{self.class_name}', pid={self.pid}, "
20
+ f"process='{self.process_name}')")
@@ -0,0 +1,164 @@
1
+ import ctypes
2
+ from ctypes import wintypes
3
+ import threading
4
+ import time
5
+ import win32con
6
+ import win32api
7
+ import win32gui
8
+ from typing import Callable, Optional
9
+ from .utils import setup_logger
10
+
11
+ logger = setup_logger("WindowMonitor")
12
+
13
+ # Constants for SetWinEventHook
14
+ EVENT_MIN = 0x00000001
15
+ EVENT_MAX = 0x7FFFFFFF
16
+ EVENT_SYSTEM_FOREGROUND = 0x0003
17
+ EVENT_OBJECT_CREATE = 0x8000
18
+ EVENT_OBJECT_DESTROY = 0x8001
19
+ EVENT_OBJECT_SHOW = 0x8002
20
+ EVENT_OBJECT_HIDE = 0x8003
21
+ EVENT_OBJECT_NAMECHANGE = 0x800C
22
+ WINEVENT_OUTOFCONTEXT = 0x0000
23
+
24
+ # Callback function type
25
+ WinEventProcType = ctypes.WINFUNCTYPE(
26
+ None,
27
+ wintypes.HANDLE,
28
+ wintypes.DWORD,
29
+ wintypes.HWND,
30
+ wintypes.LONG,
31
+ wintypes.LONG,
32
+ wintypes.DWORD,
33
+ wintypes.DWORD
34
+ )
35
+
36
+ user32 = ctypes.windll.user32
37
+
38
+ class WindowMonitor:
39
+ """
40
+ Monitors window events such as creation, destruction, and state changes.
41
+ """
42
+
43
+ def __init__(self, callback: Optional[Callable[[str, int, int], None]] = None):
44
+ """
45
+ Initializes the WindowMonitor.
46
+
47
+ Args:
48
+ callback: A function to call when an event occurs.
49
+ Signature: callback(event_name, hwnd, id_object)
50
+ """
51
+ self._callback = callback
52
+ self._hook = None
53
+ self._thread = None
54
+ self._stop_event = threading.Event()
55
+ self._logger = logger
56
+
57
+ def _event_handler(self, hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime):
58
+ """
59
+ Internal callback for SetWinEventHook.
60
+ """
61
+ if idObject != 0: # OBJID_WINDOW = 0
62
+ return
63
+
64
+ event_name = "Unknown"
65
+ if event == EVENT_OBJECT_CREATE:
66
+ event_name = "Create"
67
+ elif event == EVENT_OBJECT_DESTROY:
68
+ event_name = "Destroy"
69
+ elif event == EVENT_OBJECT_SHOW:
70
+ event_name = "Show"
71
+ elif event == EVENT_OBJECT_HIDE:
72
+ event_name = "Hide"
73
+ elif event == EVENT_SYSTEM_FOREGROUND:
74
+ event_name = "Foreground"
75
+ elif event == EVENT_OBJECT_NAMECHANGE:
76
+ event_name = "NameChange"
77
+
78
+ # Only log/callback for interesting events on actual windows
79
+ # Filter out some noise if necessary, but for now report all OBJID_WINDOW
80
+
81
+ # We can try to get window title here, but be careful as the window might be destroying
82
+ title = ""
83
+ try:
84
+ if event != EVENT_OBJECT_DESTROY:
85
+ # win32gui.GetWindowText might fail if window is gone
86
+ length = user32.GetWindowTextLengthW(hwnd)
87
+ buff = ctypes.create_unicode_buffer(length + 1)
88
+ user32.GetWindowTextW(hwnd, buff, length + 1)
89
+ title = buff.value
90
+ except Exception:
91
+ pass
92
+
93
+ log_msg = f"Event: {event_name}, HWND: {hwnd}, Title: {title}"
94
+ self._logger.info(log_msg)
95
+
96
+ if self._callback:
97
+ try:
98
+ self._callback(event_name, hwnd, title)
99
+ except Exception as e:
100
+ self._logger.error(f"Error in user callback: {e}")
101
+
102
+ def _run(self):
103
+ """
104
+ The thread function that runs the message loop.
105
+ """
106
+ self._logger.info("Starting monitor thread...")
107
+
108
+ # Keep reference to the callback to prevent garbage collection
109
+ self._c_callback = WinEventProcType(self._event_handler)
110
+
111
+ # Set hook
112
+ self._hook = user32.SetWinEventHook(
113
+ EVENT_MIN,
114
+ EVENT_MAX,
115
+ 0,
116
+ self._c_callback,
117
+ 0,
118
+ 0,
119
+ WINEVENT_OUTOFCONTEXT
120
+ )
121
+
122
+ if not self._hook:
123
+ self._logger.error("Failed to set win event hook")
124
+ return
125
+
126
+ # Message loop
127
+ msg = wintypes.MSG()
128
+ while not self._stop_event.is_set():
129
+ # PeekMessage is non-blocking, so we can check stop_event
130
+ # But SetWinEventHook needs a message pump.
131
+ # GetMessage blocks.
132
+ # We can use MsgWaitForMultipleObjects or just PeekMessage with a sleep.
133
+
134
+ if user32.PeekMessageW(ctypes.byref(msg), 0, 0, 0, 1): # PM_REMOVE = 1
135
+ if msg.message == win32con.WM_QUIT:
136
+ break
137
+ user32.TranslateMessage(ctypes.byref(msg))
138
+ user32.DispatchMessageW(ctypes.byref(msg))
139
+ else:
140
+ time.sleep(0.01) # Small sleep to avoid CPU spin
141
+
142
+ # Unhook
143
+ user32.UnhookWinEvent(self._hook)
144
+ self._logger.info("Monitor thread stopped.")
145
+
146
+ def start(self):
147
+ """Starts the monitoring thread."""
148
+ if self._thread and self._thread.is_alive():
149
+ self._logger.warning("Monitor already running")
150
+ return
151
+
152
+ self._stop_event.clear()
153
+ self._thread = threading.Thread(target=self._run, daemon=True)
154
+ self._thread.start()
155
+
156
+ def stop(self):
157
+ """Stops the monitoring thread."""
158
+ if not self._thread or not self._thread.is_alive():
159
+ return
160
+
161
+ self._stop_event.set()
162
+ self._thread.join(timeout=2)
163
+ if self._thread.is_alive():
164
+ self._logger.warning("Monitor thread did not stop gracefully")
@@ -0,0 +1,38 @@
1
+ import logging
2
+ import sys
3
+ from typing import Optional
4
+
5
+ def setup_logger(name: str = "WindowManager", level: int = logging.INFO, log_file: Optional[str] = None) -> logging.Logger:
6
+ """
7
+ Sets up a logger with the specified name, level, and optional file output.
8
+
9
+ Args:
10
+ name: The name of the logger.
11
+ level: The logging level.
12
+ log_file: Optional path to a log file.
13
+
14
+ Returns:
15
+ A configured logging.Logger instance.
16
+ """
17
+ logger = logging.getLogger(name)
18
+ logger.setLevel(level)
19
+
20
+ if logger.hasHandlers():
21
+ return logger
22
+
23
+ formatter = logging.Formatter(
24
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25
+ )
26
+
27
+ # Console handler
28
+ console_handler = logging.StreamHandler(sys.stdout)
29
+ console_handler.setFormatter(formatter)
30
+ logger.addHandler(console_handler)
31
+
32
+ # File handler
33
+ if log_file:
34
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
35
+ file_handler.setFormatter(formatter)
36
+ logger.addHandler(file_handler)
37
+
38
+ return logger
@@ -0,0 +1,4 @@
1
+ import sys
2
+ import os
3
+
4
+ sys.path.append(os.path.join(os.path.dirname(__file__), "../src"))
@@ -0,0 +1,114 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock, patch
3
+ from window_manager import WindowManager, WindowInfo
4
+
5
+ @pytest.fixture
6
+ def mock_win32gui():
7
+ with patch("window_manager.core.win32gui") as mock:
8
+ yield mock
9
+
10
+ @pytest.fixture
11
+ def mock_win32process():
12
+ with patch("window_manager.core.win32process") as mock:
13
+ yield mock
14
+
15
+ @pytest.fixture
16
+ def mock_psutil():
17
+ with patch("window_manager.core.psutil") as mock:
18
+ yield mock
19
+
20
+ @pytest.fixture
21
+ def manager():
22
+ return WindowManager()
23
+
24
+ def test_get_all_windows(manager, mock_win32gui, mock_win32process, mock_psutil):
25
+ # Setup mock data
26
+ mock_win32gui.IsWindowVisible.return_value = True
27
+ mock_win32gui.GetWindowText.return_value = "Test Window"
28
+ mock_win32gui.GetClassName.return_value = "TestClass"
29
+ mock_win32gui.GetWindowRect.return_value = (0, 0, 100, 100)
30
+ mock_win32process.GetWindowThreadProcessId.return_value = (0, 1234)
31
+
32
+ mock_process = MagicMock()
33
+ mock_process.name.return_value = "test.exe"
34
+ mock_psutil.Process.return_value = mock_process
35
+
36
+ # Simulate EnumWindows calling the callback once
37
+ def side_effect(callback, ctx):
38
+ callback(1, ctx)
39
+ mock_win32gui.EnumWindows.side_effect = side_effect
40
+
41
+ windows = manager.get_all_windows()
42
+
43
+ assert len(windows) == 1
44
+ assert windows[0].handle == 1
45
+ assert windows[0].title == "Test Window"
46
+ assert windows[0].class_name == "TestClass"
47
+ assert windows[0].pid == 1234
48
+ assert windows[0].process_name == "test.exe"
49
+
50
+ def test_find_windows(manager, mock_win32gui, mock_win32process, mock_psutil):
51
+ # Setup mock data
52
+ mock_win32gui.IsWindowVisible.return_value = True
53
+
54
+ def get_text(hwnd):
55
+ if hwnd == 1: return "Target Window"
56
+ if hwnd == 2: return "Other Window"
57
+ return ""
58
+ mock_win32gui.GetWindowText.side_effect = get_text
59
+
60
+ mock_win32gui.GetClassName.return_value = "TestClass"
61
+ mock_win32process.GetWindowThreadProcessId.return_value = (0, 1234)
62
+
63
+ mock_process = MagicMock()
64
+ mock_process.name.return_value = "test.exe"
65
+ mock_psutil.Process.return_value = mock_process
66
+
67
+ def side_effect(callback, ctx):
68
+ callback(1, ctx)
69
+ callback(2, ctx)
70
+ mock_win32gui.EnumWindows.side_effect = side_effect
71
+
72
+ # Test find by title
73
+ found = manager.find_windows(title="Target")
74
+ assert len(found) == 1
75
+ assert found[0].title == "Target Window"
76
+
77
+ # Test find by exact title
78
+ found = manager.find_windows(title="Target", exact_match=True)
79
+ assert len(found) == 0
80
+
81
+ found = manager.find_windows(title="Target Window", exact_match=True)
82
+ assert len(found) == 1
83
+
84
+ # Test find by process_name
85
+ found = manager.find_windows(process_name="test.exe")
86
+ assert len(found) == 2 # Both windows have same PID in this mock
87
+
88
+ found = manager.find_windows(process_name="other.exe")
89
+ assert len(found) == 0
90
+
91
+
92
+ def test_window_controls(manager, mock_win32gui):
93
+ handle = 12345
94
+
95
+ # Close
96
+ manager.close_window(handle)
97
+ mock_win32gui.PostMessage.assert_called_with(handle, 16, 0, 0) # WM_CLOSE = 16 (0x10)
98
+
99
+ # Minimize
100
+ manager.minimize_window(handle)
101
+ mock_win32gui.ShowWindow.assert_called_with(handle, 6) # SW_MINIMIZE = 6
102
+
103
+ # Maximize
104
+ manager.maximize_window(handle)
105
+ mock_win32gui.ShowWindow.assert_called_with(handle, 3) # SW_MAXIMIZE = 3
106
+
107
+ # Restore
108
+ manager.restore_window(handle)
109
+ mock_win32gui.ShowWindow.assert_called_with(handle, 9) # SW_RESTORE = 9
110
+
111
+ # Move
112
+ manager.move_window(handle, 10, 20, 300, 200)
113
+ mock_win32gui.MoveWindow.assert_called_with(handle, 10, 20, 300, 200, True)
114
+
@@ -0,0 +1,39 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock, patch
3
+ import time
4
+ from window_manager import WindowMonitor
5
+
6
+ @pytest.fixture
7
+ def mock_user32():
8
+ with patch("window_manager.monitor.user32") as mock:
9
+ yield mock
10
+
11
+ def test_monitor_lifecycle(mock_user32):
12
+ # Mock SetWinEventHook to return a fake hook handle
13
+ mock_user32.SetWinEventHook.return_value = 12345
14
+ mock_user32.UnhookWinEvent.return_value = 1
15
+
16
+ # Mock PeekMessage/GetMessage behavior to avoid infinite loop or blocking
17
+ # We want PeekMessage to return False initially to simulate no messages
18
+ # Then we want the loop to exit when stop is called.
19
+ # The loop condition is `while not self._stop_event.is_set():`
20
+ # So we just need to ensure PeekMessage doesn't block or return WM_QUIT immediately unless we want to test that.
21
+
22
+ mock_user32.PeekMessageW.return_value = 0
23
+
24
+ monitor = WindowMonitor()
25
+ monitor.start()
26
+
27
+ # Allow thread to start
28
+ time.sleep(0.1)
29
+
30
+ assert monitor._thread.is_alive()
31
+ assert monitor._hook == 12345
32
+
33
+ monitor.stop()
34
+
35
+ # Allow thread to stop
36
+ time.sleep(0.1)
37
+
38
+ assert not monitor._thread.is_alive()
39
+ mock_user32.UnhookWinEvent.assert_called_with(12345)
@@ -0,0 +1,128 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "iniconfig"
16
+ version = "2.3.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "packaging"
25
+ version = "26.0"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "pluggy"
34
+ version = "1.6.0"
35
+ source = { registry = "https://pypi.org/simple" }
36
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
37
+ wheels = [
38
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
39
+ ]
40
+
41
+ [[package]]
42
+ name = "psutil"
43
+ version = "7.2.2"
44
+ source = { registry = "https://pypi.org/simple" }
45
+ sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
46
+ wheels = [
47
+ { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
48
+ { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
49
+ { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
50
+ { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
51
+ { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
52
+ { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
53
+ { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
54
+ { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
55
+ { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
56
+ { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
57
+ { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
58
+ { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
59
+ { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
60
+ { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
61
+ { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
62
+ { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
63
+ { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
64
+ { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
65
+ { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
66
+ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
67
+ ]
68
+
69
+ [[package]]
70
+ name = "pygments"
71
+ version = "2.19.2"
72
+ source = { registry = "https://pypi.org/simple" }
73
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
74
+ wheels = [
75
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
76
+ ]
77
+
78
+ [[package]]
79
+ name = "pytest"
80
+ version = "9.0.2"
81
+ source = { registry = "https://pypi.org/simple" }
82
+ dependencies = [
83
+ { name = "colorama", marker = "sys_platform == 'win32'" },
84
+ { name = "iniconfig" },
85
+ { name = "packaging" },
86
+ { name = "pluggy" },
87
+ { name = "pygments" },
88
+ ]
89
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
90
+ wheels = [
91
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
92
+ ]
93
+
94
+ [[package]]
95
+ name = "pywin32"
96
+ version = "311"
97
+ source = { registry = "https://pypi.org/simple" }
98
+ wheels = [
99
+ { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
100
+ { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
101
+ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
102
+ { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
103
+ { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
104
+ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
105
+ ]
106
+
107
+ [[package]]
108
+ name = "window-manager"
109
+ version = "0.1.0"
110
+ source = { editable = "." }
111
+ dependencies = [
112
+ { name = "psutil" },
113
+ { name = "pywin32" },
114
+ ]
115
+
116
+ [package.dev-dependencies]
117
+ dev = [
118
+ { name = "pytest" },
119
+ ]
120
+
121
+ [package.metadata]
122
+ requires-dist = [
123
+ { name = "psutil", specifier = ">=7.2.2" },
124
+ { name = "pywin32", specifier = ">=311" },
125
+ ]
126
+
127
+ [package.metadata.requires-dev]
128
+ dev = [{ name = "pytest", specifier = ">=9.0.2" }]