windows-mcp 0.5.8__py3-none-any.whl → 0.5.9__py3-none-any.whl
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.
- windows_mcp/__main__.py +299 -314
- windows_mcp/analytics.py +0 -5
- windows_mcp/desktop/service.py +638 -458
- windows_mcp/desktop/views.py +7 -5
- windows_mcp/tree/cache_utils.py +126 -0
- windows_mcp/tree/config.py +25 -0
- windows_mcp/tree/service.py +543 -601
- windows_mcp/tree/views.py +142 -116
- windows_mcp/uia/controls.py +11 -2
- windows_mcp/uia/core.py +9 -0
- windows_mcp/vdm/__init__.py +1 -0
- windows_mcp/vdm/core.py +490 -0
- windows_mcp/watchdog/event_handlers.py +13 -9
- windows_mcp/watchdog/service.py +15 -4
- {windows_mcp-0.5.8.dist-info → windows_mcp-0.5.9.dist-info}/METADATA +27 -21
- windows_mcp-0.5.9.dist-info/RECORD +29 -0
- windows_mcp-0.5.8.dist-info/RECORD +0 -26
- {windows_mcp-0.5.8.dist-info → windows_mcp-0.5.9.dist-info}/WHEEL +0 -0
- {windows_mcp-0.5.8.dist-info → windows_mcp-0.5.9.dist-info}/entry_points.txt +0 -0
- {windows_mcp-0.5.8.dist-info → windows_mcp-0.5.9.dist-info}/licenses/LICENSE.md +0 -0
windows_mcp/vdm/core.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import comtypes.client
|
|
2
|
+
from comtypes import GUID, IUnknown, COMMETHOD, HRESULT
|
|
3
|
+
from ctypes import POINTER
|
|
4
|
+
from ctypes.wintypes import HWND, BOOL
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import sys
|
|
8
|
+
import ctypes
|
|
9
|
+
from ctypes import HRESULT, POINTER, c_void_p, c_uint32, byref
|
|
10
|
+
from ctypes.wintypes import HWND, BOOL, UINT, INT, WCHAR, LPVOID
|
|
11
|
+
from comtypes import GUID, IUnknown, COMMETHOD, STDMETHOD
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
import secrets
|
|
16
|
+
import string
|
|
17
|
+
|
|
18
|
+
_thread_local = threading.local()
|
|
19
|
+
|
|
20
|
+
# Global mapping for Simple ID (Short) <-> GUID (Long)
|
|
21
|
+
_SIMPLE_TO_GUID = {}
|
|
22
|
+
_GUID_TO_SIMPLE = {}
|
|
23
|
+
_MAP_LOCK = threading.Lock()
|
|
24
|
+
|
|
25
|
+
def _get_simple_id(guid_str: str) -> str:
|
|
26
|
+
"""Gets or creates a simple 8-char ID for a given GUID string."""
|
|
27
|
+
guid_str = guid_str.upper() # Normalize GUIDs
|
|
28
|
+
with _MAP_LOCK:
|
|
29
|
+
if guid_str in _GUID_TO_SIMPLE:
|
|
30
|
+
return _GUID_TO_SIMPLE[guid_str]
|
|
31
|
+
|
|
32
|
+
# Generate unique short ID
|
|
33
|
+
while True:
|
|
34
|
+
# 8 char random hex-like string (easy to read)
|
|
35
|
+
alphabet = string.ascii_lowercase + string.digits
|
|
36
|
+
short_id = ''.join(secrets.choice(alphabet) for _ in range(8))
|
|
37
|
+
if short_id not in _SIMPLE_TO_GUID:
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
_SIMPLE_TO_GUID[short_id] = guid_str
|
|
41
|
+
_GUID_TO_SIMPLE[guid_str] = short_id
|
|
42
|
+
return short_id
|
|
43
|
+
|
|
44
|
+
def _resolve_guid(simple_id: str) -> str:
|
|
45
|
+
"""Resolves a simple ID to its GUID string."""
|
|
46
|
+
with _MAP_LOCK:
|
|
47
|
+
if simple_id in _SIMPLE_TO_GUID:
|
|
48
|
+
return _SIMPLE_TO_GUID[simple_id]
|
|
49
|
+
# Fallback: maybe the user passed a real GUID?
|
|
50
|
+
return simple_id
|
|
51
|
+
|
|
52
|
+
def _get_manager():
|
|
53
|
+
if not hasattr(_thread_local, "manager"):
|
|
54
|
+
_thread_local.manager = VirtualDesktopManager()
|
|
55
|
+
return _thread_local.manager
|
|
56
|
+
|
|
57
|
+
def is_window_on_current_desktop(hwnd: int) -> bool:
|
|
58
|
+
return _get_manager().is_window_on_current_desktop(hwnd)
|
|
59
|
+
|
|
60
|
+
def get_window_desktop_id(hwnd: int) -> str:
|
|
61
|
+
return _get_manager().get_window_desktop_id(hwnd)
|
|
62
|
+
|
|
63
|
+
def move_window_to_desktop(hwnd: int, desktop_id: str):
|
|
64
|
+
_get_manager().move_window_to_desktop(hwnd, desktop_id)
|
|
65
|
+
|
|
66
|
+
# standard COM CLSIDs for VirtualDesktopManager
|
|
67
|
+
CLSID_VirtualDesktopManager = GUID("{aa509086-5ca9-4c25-8f95-589d3c07b48a}")
|
|
68
|
+
|
|
69
|
+
class IVirtualDesktopManager(IUnknown):
|
|
70
|
+
_iid_ = GUID("{a5cd92ff-29be-454c-8d04-d82879fb3f1b}")
|
|
71
|
+
_methods_ = [
|
|
72
|
+
COMMETHOD([], HRESULT, "IsWindowOnCurrentVirtualDesktop",
|
|
73
|
+
(['in'], HWND, "topLevelWindow"),
|
|
74
|
+
(['out', 'retval'], POINTER(BOOL), "onCurrentDesktop")),
|
|
75
|
+
COMMETHOD([], HRESULT, "GetWindowDesktopId",
|
|
76
|
+
(['in'], HWND, "topLevelWindow"),
|
|
77
|
+
(['out', 'retval'], POINTER(GUID), "desktopId")),
|
|
78
|
+
COMMETHOD([], HRESULT, "MoveWindowToDesktop",
|
|
79
|
+
(['in'], HWND, "topLevelWindow"),
|
|
80
|
+
(['in'], GUID, "desktopId")),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# Internal COM Interfaces for Windows 11
|
|
84
|
+
CLSID_ImmersiveShell = GUID("{C2F03A33-21F5-47FA-B4BB-156362A2F239}")
|
|
85
|
+
CLSID_VirtualDesktopManagerInternal = GUID("{C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B}")
|
|
86
|
+
IID_IServiceProvider = GUID("{6D5140C1-7436-11CE-8034-00AA006009FA}")
|
|
87
|
+
|
|
88
|
+
class IServiceProvider(IUnknown):
|
|
89
|
+
_iid_ = IID_IServiceProvider
|
|
90
|
+
_methods_ = [
|
|
91
|
+
COMMETHOD([], HRESULT, "QueryService",
|
|
92
|
+
(['in'], GUID, "guidService"),
|
|
93
|
+
(['in'], GUID, "riid"),
|
|
94
|
+
(['out'], POINTER(POINTER(IUnknown)), "ppvObject")),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# IObjectArray for iterating desktops
|
|
98
|
+
class IObjectArray(IUnknown):
|
|
99
|
+
_iid_ = GUID("{92CA9DCD-5622-4BBA-A805-5E9F541BD8CC}")
|
|
100
|
+
_methods_ = [
|
|
101
|
+
COMMETHOD([], HRESULT, "GetCount",
|
|
102
|
+
(['out'], POINTER(UINT), "pcObjects")),
|
|
103
|
+
COMMETHOD([], HRESULT, "GetAt",
|
|
104
|
+
(['in'], UINT, "uiIndex"),
|
|
105
|
+
(['in'], GUID, "riid"),
|
|
106
|
+
(['out'], POINTER(POINTER(IUnknown)), "ppv")),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
# Wrapper for HSTRING
|
|
110
|
+
class HSTRING(c_void_p):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
_combase = ctypes.windll.combase
|
|
115
|
+
_WindowsCreateString = _combase.WindowsCreateString
|
|
116
|
+
_WindowsCreateString.argtypes = [ctypes.c_wchar_p, UINT, POINTER(HSTRING)]
|
|
117
|
+
_WindowsCreateString.restype = HRESULT
|
|
118
|
+
_WindowsDeleteString = _combase.WindowsDeleteString
|
|
119
|
+
_WindowsDeleteString.argtypes = [HSTRING]
|
|
120
|
+
_WindowsDeleteString.restype = HRESULT
|
|
121
|
+
except Exception:
|
|
122
|
+
_WindowsCreateString = None
|
|
123
|
+
_WindowsDeleteString = None
|
|
124
|
+
|
|
125
|
+
def create_hstring(text):
|
|
126
|
+
if not _WindowsCreateString:
|
|
127
|
+
return HSTRING(0)
|
|
128
|
+
hs = HSTRING()
|
|
129
|
+
hr = _WindowsCreateString(text, len(text), byref(hs))
|
|
130
|
+
if hr != 0:
|
|
131
|
+
raise OSError(f"WindowsCreateString failed: {hr}")
|
|
132
|
+
return hs
|
|
133
|
+
|
|
134
|
+
def delete_hstring(hs):
|
|
135
|
+
if _WindowsDeleteString and hs:
|
|
136
|
+
_WindowsDeleteString(hs)
|
|
137
|
+
|
|
138
|
+
# Interface definitions depend on build number
|
|
139
|
+
BUILD = sys.getwindowsversion().build
|
|
140
|
+
|
|
141
|
+
if BUILD >= 26100:
|
|
142
|
+
IID_IVirtualDesktopManagerInternal = GUID("{53F5CA0B-158F-4124-900C-057158060B27}")
|
|
143
|
+
IID_IVirtualDesktop = GUID("{3F07F4BE-B107-441A-AF0F-39D82529072C}")
|
|
144
|
+
elif BUILD >= 22621:
|
|
145
|
+
IID_IVirtualDesktopManagerInternal = GUID("{A3175F2D-239C-4BD2-8AA0-EEBA8B0B138E}")
|
|
146
|
+
IID_IVirtualDesktop = GUID("{3F07F4BE-B107-441A-AF0F-39D82529072C}")
|
|
147
|
+
else:
|
|
148
|
+
# Windows 10 (Build 19041+)
|
|
149
|
+
IID_IVirtualDesktopManagerInternal = GUID("{F31574D6-B682-4CDC-BD56-1827860ABEC6}")
|
|
150
|
+
IID_IVirtualDesktop = GUID("{FF72FFDD-BE7E-43FC-9C03-AD81681E88E4}")
|
|
151
|
+
|
|
152
|
+
class IVirtualDesktop(IUnknown):
|
|
153
|
+
_iid_ = IID_IVirtualDesktop
|
|
154
|
+
# Methods for 22621+
|
|
155
|
+
_methods_ = [
|
|
156
|
+
STDMETHOD(HRESULT, "IsViewVisible", (POINTER(IUnknown), POINTER(UINT))), # IApplicationView
|
|
157
|
+
COMMETHOD([], HRESULT, "GetID", (['out'], POINTER(GUID), "pGuid")),
|
|
158
|
+
COMMETHOD([], HRESULT, "GetName", (['out'], POINTER(HSTRING), "pName")),
|
|
159
|
+
COMMETHOD([], HRESULT, "GetWallpaperPath", (['out'], POINTER(HSTRING), "pPath")),
|
|
160
|
+
COMMETHOD([], HRESULT, "IsRemote", (['out'], POINTER(HWND), "pW")),
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
# Needed placeholders
|
|
164
|
+
class IApplicationView(IUnknown):
|
|
165
|
+
_iid_ = GUID("{372E1D3B-38D3-42E4-A15B-8AB2B178F513}") # Generic match
|
|
166
|
+
|
|
167
|
+
class IVirtualDesktopManagerInternal(IUnknown):
|
|
168
|
+
_iid_ = IID_IVirtualDesktopManagerInternal
|
|
169
|
+
if BUILD >= 26100:
|
|
170
|
+
_methods_ = [
|
|
171
|
+
COMMETHOD([], HRESULT, "GetCount", (['out'], POINTER(UINT), "pCount")),
|
|
172
|
+
STDMETHOD(HRESULT, "MoveViewToDesktop", (POINTER(IApplicationView), POINTER(IVirtualDesktop))),
|
|
173
|
+
STDMETHOD(HRESULT, "CanViewMoveDesktops", (POINTER(IApplicationView), POINTER(UINT))),
|
|
174
|
+
COMMETHOD([], HRESULT, "GetCurrentDesktop", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
|
|
175
|
+
COMMETHOD([], HRESULT, "GetDesktops", (['out'], POINTER(POINTER(IObjectArray)), "array")),
|
|
176
|
+
STDMETHOD(HRESULT, "GetAdjacentDesktop", (POINTER(IVirtualDesktop), UINT, POINTER(POINTER(IVirtualDesktop)))),
|
|
177
|
+
STDMETHOD(HRESULT, "SwitchDesktop", (POINTER(IVirtualDesktop),)),
|
|
178
|
+
STDMETHOD(HRESULT, "SwitchDesktopAndMoveForegroundView", (POINTER(IVirtualDesktop),)),
|
|
179
|
+
COMMETHOD([], HRESULT, "CreateDesktopW", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
|
|
180
|
+
STDMETHOD(HRESULT, "MoveDesktop", (POINTER(IVirtualDesktop), UINT)),
|
|
181
|
+
COMMETHOD([], HRESULT, "RemoveDesktop", (['in'], POINTER(IVirtualDesktop), "destroyDesktop"), (['in'], POINTER(IVirtualDesktop), "fallbackDesktop")),
|
|
182
|
+
COMMETHOD([], HRESULT, "FindDesktop", (['in'], POINTER(GUID), "pGuid"), (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
|
|
183
|
+
STDMETHOD(HRESULT, "GetDesktopSwitchIncludeExcludeViews", (POINTER(IVirtualDesktop), POINTER(POINTER(IObjectArray)), POINTER(POINTER(IObjectArray)))),
|
|
184
|
+
COMMETHOD([], HRESULT, "SetName", (['in'], POINTER(IVirtualDesktop), "pDesktop"), (['in'], HSTRING, "name")),
|
|
185
|
+
# Others omitted for now
|
|
186
|
+
]
|
|
187
|
+
elif BUILD >= 22621:
|
|
188
|
+
_methods_ = [
|
|
189
|
+
COMMETHOD([], HRESULT, "GetCount", (['out'], POINTER(UINT), "pCount")),
|
|
190
|
+
STDMETHOD(HRESULT, "MoveViewToDesktop", (POINTER(IApplicationView), POINTER(IVirtualDesktop))),
|
|
191
|
+
STDMETHOD(HRESULT, "CanViewMoveDesktops", (POINTER(IApplicationView), POINTER(UINT))),
|
|
192
|
+
COMMETHOD([], HRESULT, "GetCurrentDesktop", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
|
|
193
|
+
COMMETHOD([], HRESULT, "GetDesktops", (['out'], POINTER(POINTER(IObjectArray)), "array")),
|
|
194
|
+
STDMETHOD(HRESULT, "GetAdjacentDesktop", (POINTER(IVirtualDesktop), UINT, POINTER(POINTER(IVirtualDesktop)))),
|
|
195
|
+
STDMETHOD(HRESULT, "SwitchDesktop", (POINTER(IVirtualDesktop),)),
|
|
196
|
+
COMMETHOD([], HRESULT, "CreateDesktopW", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
|
|
197
|
+
STDMETHOD(HRESULT, "MoveDesktop", (POINTER(IVirtualDesktop), UINT)),
|
|
198
|
+
COMMETHOD([], HRESULT, "RemoveDesktop", (['in'], POINTER(IVirtualDesktop), "destroyDesktop"), (['in'], POINTER(IVirtualDesktop), "fallbackDesktop")),
|
|
199
|
+
COMMETHOD([], HRESULT, "FindDesktop", (['in'], POINTER(GUID), "pGuid"), (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
|
|
200
|
+
STDMETHOD(HRESULT, "GetDesktopSwitchIncludeExcludeViews", (POINTER(IVirtualDesktop), POINTER(POINTER(IObjectArray)), POINTER(POINTER(IObjectArray)))),
|
|
201
|
+
COMMETHOD([], HRESULT, "SetName", (['in'], POINTER(IVirtualDesktop), "pDesktop"), (['in'], HSTRING, "name")),
|
|
202
|
+
]
|
|
203
|
+
else:
|
|
204
|
+
# Windows 10
|
|
205
|
+
_methods_ = [
|
|
206
|
+
COMMETHOD([], HRESULT, "GetCount", (['out'], POINTER(UINT), "pCount")),
|
|
207
|
+
STDMETHOD(HRESULT, "MoveViewToDesktop", (POINTER(IApplicationView), POINTER(IVirtualDesktop))),
|
|
208
|
+
STDMETHOD(HRESULT, "CanViewMoveDesktops", (POINTER(IApplicationView), POINTER(UINT))),
|
|
209
|
+
COMMETHOD([], HRESULT, "GetCurrentDesktop", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
|
|
210
|
+
COMMETHOD([], HRESULT, "GetDesktops", (['out'], POINTER(POINTER(IObjectArray)), "array")),
|
|
211
|
+
STDMETHOD(HRESULT, "GetAdjacentDesktop", (POINTER(IVirtualDesktop), UINT, POINTER(POINTER(IVirtualDesktop)))),
|
|
212
|
+
STDMETHOD(HRESULT, "SwitchDesktop", (POINTER(IVirtualDesktop),)),
|
|
213
|
+
COMMETHOD([], HRESULT, "CreateDesktopW", (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
|
|
214
|
+
COMMETHOD([], HRESULT, "RemoveDesktop", (['in'], POINTER(IVirtualDesktop), "destroyDesktop"), (['in'], POINTER(IVirtualDesktop), "fallbackDesktop")),
|
|
215
|
+
COMMETHOD([], HRESULT, "FindDesktop", (['in'], POINTER(GUID), "pGuid"), (['out'], POINTER(POINTER(IVirtualDesktop)), "pDesktop")),
|
|
216
|
+
# No SetName on this interface for Win10 typically
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
class VirtualDesktopManager:
|
|
220
|
+
"""
|
|
221
|
+
Wrapper around the Windows IVirtualDesktopManager interface.
|
|
222
|
+
Allows checking if a window is on the current virtual desktop, getting its Desktop ID,
|
|
223
|
+
and moving windows between desktops.
|
|
224
|
+
"""
|
|
225
|
+
def __init__(self):
|
|
226
|
+
self._manager = None
|
|
227
|
+
try:
|
|
228
|
+
# Ensure COM is initialized on this thread (RPC_E_CHANGED_MODE or S_OK/S_FALSE)
|
|
229
|
+
# COINIT_APARTMENTTHREADED = 0x2, COINIT_MULTITHREADED = 0x0
|
|
230
|
+
# We try standard init.
|
|
231
|
+
import ctypes
|
|
232
|
+
try:
|
|
233
|
+
ctypes.windll.ole32.CoInitialize(None)
|
|
234
|
+
except Exception:
|
|
235
|
+
pass # Already initialized or failed, try proceeding
|
|
236
|
+
|
|
237
|
+
self._manager = comtypes.client.CreateObject(CLSID_VirtualDesktopManager, interface=IVirtualDesktopManager)
|
|
238
|
+
|
|
239
|
+
# Initialize Internal Manager
|
|
240
|
+
try:
|
|
241
|
+
service_provider = comtypes.client.CreateObject(CLSID_ImmersiveShell, interface=IServiceProvider)
|
|
242
|
+
unk = service_provider.QueryService(CLSID_VirtualDesktopManagerInternal, IVirtualDesktopManagerInternal._iid_)
|
|
243
|
+
self._internal_manager = unk.QueryInterface(IVirtualDesktopManagerInternal)
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.warning(f"Failed to initialize VirtualDesktopManagerInternal: {e}")
|
|
246
|
+
self._internal_manager = None
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error(f"Failed to initialize VirtualDesktopManager: {e}")
|
|
250
|
+
|
|
251
|
+
def is_window_on_current_desktop(self, hwnd: int) -> bool:
|
|
252
|
+
"""
|
|
253
|
+
Checks if the specified window is on the currently active virtual desktop.
|
|
254
|
+
"""
|
|
255
|
+
if not self._manager:
|
|
256
|
+
return True # Fallback: assume visible if manager failed
|
|
257
|
+
try:
|
|
258
|
+
return self._manager.IsWindowOnCurrentVirtualDesktop(hwnd)
|
|
259
|
+
except Exception:
|
|
260
|
+
return True # Fail open
|
|
261
|
+
|
|
262
|
+
def get_window_desktop_id(self, hwnd: int) -> str:
|
|
263
|
+
"""
|
|
264
|
+
Returns the GUID (as a string) of the virtual desktop the window is on.
|
|
265
|
+
"""
|
|
266
|
+
if not self._manager:
|
|
267
|
+
return ""
|
|
268
|
+
try:
|
|
269
|
+
guid = self._manager.GetWindowDesktopId(hwnd)
|
|
270
|
+
return _get_simple_id(str(guid))
|
|
271
|
+
except Exception:
|
|
272
|
+
return ""
|
|
273
|
+
|
|
274
|
+
def move_window_to_desktop(self, hwnd: int, desktop_id: str):
|
|
275
|
+
"""
|
|
276
|
+
Moves a window to the specified virtual desktop (by GUID string).
|
|
277
|
+
"""
|
|
278
|
+
if not self._manager:
|
|
279
|
+
return
|
|
280
|
+
try:
|
|
281
|
+
target_guid_str = _resolve_guid(desktop_id)
|
|
282
|
+
guid = GUID(target_guid_str)
|
|
283
|
+
self._manager.MoveWindowToDesktop(hwnd, guid)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
logger.error(f"Failed to move window to desktop: {e}")
|
|
286
|
+
|
|
287
|
+
def create_desktop(self, name: str = None) -> str:
|
|
288
|
+
"""
|
|
289
|
+
Creates a new virtual desktop and returns its ID.
|
|
290
|
+
Optionally sets the name of the new desktop.
|
|
291
|
+
"""
|
|
292
|
+
if not self._internal_manager:
|
|
293
|
+
raise RuntimeError("Internal VDM not initialized")
|
|
294
|
+
|
|
295
|
+
desktop = self._internal_manager.CreateDesktopW()
|
|
296
|
+
guid = desktop.GetID()
|
|
297
|
+
guid_str = str(guid)
|
|
298
|
+
|
|
299
|
+
# Get simple ID
|
|
300
|
+
simple_id = _get_simple_id(guid_str)
|
|
301
|
+
|
|
302
|
+
if name:
|
|
303
|
+
self.rename_desktop(simple_id, name) # Use simple ID internally too now
|
|
304
|
+
|
|
305
|
+
return simple_id
|
|
306
|
+
|
|
307
|
+
def remove_desktop(self, desktop_id: str):
|
|
308
|
+
"""
|
|
309
|
+
Removes a virtual desktop by ID.
|
|
310
|
+
Will try to fallback to the first available desktop that is not the one being deleted.
|
|
311
|
+
"""
|
|
312
|
+
if not self._internal_manager:
|
|
313
|
+
raise RuntimeError("Internal VDM not initialized")
|
|
314
|
+
|
|
315
|
+
target_guid_str = _resolve_guid(desktop_id)
|
|
316
|
+
target_guid = GUID(target_guid_str)
|
|
317
|
+
|
|
318
|
+
# We need the IVirtualDesktop object for the target
|
|
319
|
+
try:
|
|
320
|
+
target_desktop = self._internal_manager.FindDesktop(target_guid)
|
|
321
|
+
except Exception:
|
|
322
|
+
logger.error(f"Could not find desktop with ID {desktop_id}")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
# Find a fallback desktop
|
|
326
|
+
desktops_array = self._internal_manager.GetDesktops()
|
|
327
|
+
count = desktops_array.GetCount()
|
|
328
|
+
fallback_desktop = None
|
|
329
|
+
|
|
330
|
+
for i in range(count):
|
|
331
|
+
unk = desktops_array.GetAt(i, IVirtualDesktop._iid_)
|
|
332
|
+
candidate = unk.QueryInterface(IVirtualDesktop)
|
|
333
|
+
candidate_id = candidate.GetID()
|
|
334
|
+
if str(candidate_id) != str(target_guid):
|
|
335
|
+
fallback_desktop = candidate
|
|
336
|
+
break
|
|
337
|
+
|
|
338
|
+
if not fallback_desktop:
|
|
339
|
+
# If no other desktop, we can't delete the only one? Or create one?
|
|
340
|
+
# Windows usually prevents deleting the last one.
|
|
341
|
+
logger.error("No fallback desktop found (cannot delete the only desktop)")
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
self._internal_manager.RemoveDesktop(target_desktop, fallback_desktop)
|
|
345
|
+
|
|
346
|
+
def rename_desktop(self, desktop_id: str, new_name: str):
|
|
347
|
+
"""
|
|
348
|
+
Renames a virtual desktop.
|
|
349
|
+
"""
|
|
350
|
+
if not self._internal_manager:
|
|
351
|
+
raise RuntimeError("Internal VDM not initialized")
|
|
352
|
+
|
|
353
|
+
target_guid_str = _resolve_guid(desktop_id)
|
|
354
|
+
target_guid = GUID(target_guid_str)
|
|
355
|
+
try:
|
|
356
|
+
target_desktop = self._internal_manager.FindDesktop(target_guid)
|
|
357
|
+
except Exception:
|
|
358
|
+
logger.error(f"Could not find desktop with ID {desktop_id}")
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
hs_name = create_hstring(new_name)
|
|
362
|
+
try:
|
|
363
|
+
# Check if SetName method exists (it might not on Windows 10)
|
|
364
|
+
if hasattr(self._internal_manager, "SetName"):
|
|
365
|
+
self._internal_manager.SetName(target_desktop, hs_name)
|
|
366
|
+
else:
|
|
367
|
+
logger.warning("Rename desktop is not supported on this Windows build.")
|
|
368
|
+
except Exception as e:
|
|
369
|
+
logger.error(f"Failed to rename desktop: {e}")
|
|
370
|
+
finally:
|
|
371
|
+
delete_hstring(hs_name)
|
|
372
|
+
|
|
373
|
+
def switch_desktop(self, desktop_id: str):
|
|
374
|
+
"""
|
|
375
|
+
Switches to the specified virtual desktop.
|
|
376
|
+
"""
|
|
377
|
+
if not self._internal_manager:
|
|
378
|
+
raise RuntimeError("Internal VDM not initialized")
|
|
379
|
+
|
|
380
|
+
target_guid_str = _resolve_guid(desktop_id)
|
|
381
|
+
target_guid = GUID(target_guid_str)
|
|
382
|
+
try:
|
|
383
|
+
target_desktop = self._internal_manager.FindDesktop(target_guid)
|
|
384
|
+
except Exception:
|
|
385
|
+
logger.error(f"Could not find desktop with ID {desktop_id}")
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
self._internal_manager.SwitchDesktop(target_desktop)
|
|
389
|
+
|
|
390
|
+
def get_all_desktops(self) -> list[dict]:
|
|
391
|
+
"""
|
|
392
|
+
Returns a list of all virtual desktops.
|
|
393
|
+
Each entry is a dict: {'id': str, 'name': str}
|
|
394
|
+
"""
|
|
395
|
+
if not self._internal_manager:
|
|
396
|
+
raise RuntimeError("Internal VDM not initialized")
|
|
397
|
+
|
|
398
|
+
desktops_array = self._internal_manager.GetDesktops()
|
|
399
|
+
count = desktops_array.GetCount()
|
|
400
|
+
|
|
401
|
+
result = []
|
|
402
|
+
for i in range(count):
|
|
403
|
+
try:
|
|
404
|
+
unk = desktops_array.GetAt(i, IVirtualDesktop._iid_)
|
|
405
|
+
desktop = unk.QueryInterface(IVirtualDesktop)
|
|
406
|
+
guid = desktop.GetID()
|
|
407
|
+
simple_id = _get_simple_id(str(guid))
|
|
408
|
+
|
|
409
|
+
name = ""
|
|
410
|
+
try:
|
|
411
|
+
# Windows 10 interface might not support GetName commonly or it might fail
|
|
412
|
+
if hasattr(desktop, "GetName"):
|
|
413
|
+
hname = desktop.GetName()
|
|
414
|
+
name = ctypes.wstring_at(hname)
|
|
415
|
+
delete_hstring(hname)
|
|
416
|
+
except Exception:
|
|
417
|
+
pass # Name retrieval failed, ignore
|
|
418
|
+
|
|
419
|
+
if not name:
|
|
420
|
+
name = f"Desktop {i+1}"
|
|
421
|
+
|
|
422
|
+
result.append({'id': simple_id, 'name': name})
|
|
423
|
+
except Exception as e:
|
|
424
|
+
logger.error(f"Error retrieving desktop at index {i}: {e}")
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
return result
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def get_current_desktop(self) -> dict:
|
|
431
|
+
"""
|
|
432
|
+
Returns info about the current virtual desktop.
|
|
433
|
+
Returns: {'id': str, 'name': str}
|
|
434
|
+
"""
|
|
435
|
+
if not self._internal_manager:
|
|
436
|
+
raise RuntimeError("Internal VDM not initialized")
|
|
437
|
+
|
|
438
|
+
current_desktop = self._internal_manager.GetCurrentDesktop()
|
|
439
|
+
guid = current_desktop.GetID()
|
|
440
|
+
simple_id = _get_simple_id(str(guid))
|
|
441
|
+
|
|
442
|
+
name = ""
|
|
443
|
+
try:
|
|
444
|
+
# Windows 10 interface might not support GetName commonly or it might fail
|
|
445
|
+
if hasattr(current_desktop, "GetName"):
|
|
446
|
+
hname = current_desktop.GetName()
|
|
447
|
+
name = ctypes.wstring_at(hname)
|
|
448
|
+
delete_hstring(hname)
|
|
449
|
+
except Exception:
|
|
450
|
+
pass
|
|
451
|
+
|
|
452
|
+
if not name:
|
|
453
|
+
# Fallback logic
|
|
454
|
+
desktops_array = self._internal_manager.GetDesktops()
|
|
455
|
+
count = desktops_array.GetCount()
|
|
456
|
+
current_guid_str = str(guid)
|
|
457
|
+
|
|
458
|
+
found_name = "Current Desktop"
|
|
459
|
+
for i in range(count):
|
|
460
|
+
try:
|
|
461
|
+
unk = desktops_array.GetAt(i, IVirtualDesktop._iid_)
|
|
462
|
+
candidate = unk.QueryInterface(IVirtualDesktop)
|
|
463
|
+
candidate_guid_str = str(candidate.GetID())
|
|
464
|
+
if candidate_guid_str == current_guid_str:
|
|
465
|
+
found_name = f"Desktop {i+1}"
|
|
466
|
+
break
|
|
467
|
+
except Exception:
|
|
468
|
+
continue
|
|
469
|
+
name = found_name
|
|
470
|
+
|
|
471
|
+
return {'id': simple_id, 'name': name}
|
|
472
|
+
|
|
473
|
+
def create_desktop(name: str = None) -> str:
|
|
474
|
+
return _get_manager().create_desktop(name)
|
|
475
|
+
|
|
476
|
+
def remove_desktop(desktop_id: str):
|
|
477
|
+
_get_manager().remove_desktop(desktop_id)
|
|
478
|
+
|
|
479
|
+
def rename_desktop(desktop_id: str, new_name: str):
|
|
480
|
+
_get_manager().rename_desktop(desktop_id, new_name)
|
|
481
|
+
|
|
482
|
+
def switch_desktop(desktop_id: str):
|
|
483
|
+
_get_manager().switch_desktop(desktop_id)
|
|
484
|
+
|
|
485
|
+
def get_all_desktops() -> list[dict]:
|
|
486
|
+
return _get_manager().get_all_desktops()
|
|
487
|
+
|
|
488
|
+
def get_current_desktop() -> dict:
|
|
489
|
+
return _get_manager().get_current_desktop()
|
|
490
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from windows_mcp.uia.core import _AutomationClient
|
|
2
2
|
import comtypes
|
|
3
|
+
import weakref
|
|
3
4
|
|
|
4
5
|
# Get UIA Interface for COM definitions
|
|
5
6
|
uia_client = _AutomationClient.instance()
|
|
@@ -9,13 +10,14 @@ class FocusChangedEventHandler(comtypes.COMObject):
|
|
|
9
10
|
_com_interfaces_ = [UIA.IUIAutomationFocusChangedEventHandler]
|
|
10
11
|
|
|
11
12
|
def __init__(self, parent):
|
|
12
|
-
self.
|
|
13
|
+
self._parent = weakref.ref(parent)
|
|
13
14
|
super(FocusChangedEventHandler, self).__init__()
|
|
14
15
|
|
|
15
16
|
def HandleFocusChangedEvent(self, sender):
|
|
16
17
|
try:
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
parent = self._parent()
|
|
19
|
+
if parent and parent._focus_callback:
|
|
20
|
+
parent._focus_callback(sender)
|
|
19
21
|
except Exception as e:
|
|
20
22
|
print(f"Error in focus callback: {e}")
|
|
21
23
|
return 0 # S_OK
|
|
@@ -24,13 +26,14 @@ class StructureChangedEventHandler(comtypes.COMObject):
|
|
|
24
26
|
_com_interfaces_ = [UIA.IUIAutomationStructureChangedEventHandler]
|
|
25
27
|
|
|
26
28
|
def __init__(self, parent):
|
|
27
|
-
self.
|
|
29
|
+
self._parent = weakref.ref(parent)
|
|
28
30
|
super(StructureChangedEventHandler, self).__init__()
|
|
29
31
|
|
|
30
32
|
def HandleStructureChangedEvent(self, sender, changeType, runtimeId):
|
|
31
33
|
try:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
parent = self._parent()
|
|
35
|
+
if parent and parent._structure_callback:
|
|
36
|
+
parent._structure_callback(sender, changeType, runtimeId)
|
|
34
37
|
except Exception as e:
|
|
35
38
|
print(f"Error in structure callback: {e}")
|
|
36
39
|
return 0 # S_OK
|
|
@@ -39,13 +42,14 @@ class PropertyChangedEventHandler(comtypes.COMObject):
|
|
|
39
42
|
_com_interfaces_ = [UIA.IUIAutomationPropertyChangedEventHandler]
|
|
40
43
|
|
|
41
44
|
def __init__(self, parent):
|
|
42
|
-
self.
|
|
45
|
+
self._parent = weakref.ref(parent)
|
|
43
46
|
super(PropertyChangedEventHandler, self).__init__()
|
|
44
47
|
|
|
45
48
|
def HandlePropertyChangedEvent(self, sender, propertyId, newValue):
|
|
46
49
|
try:
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
parent = self._parent()
|
|
51
|
+
if parent and parent._property_callback:
|
|
52
|
+
parent._property_callback(sender, propertyId, newValue)
|
|
49
53
|
except Exception as e:
|
|
50
54
|
print(f"Error in property callback: {e}")
|
|
51
55
|
return 0 # S_OK
|
windows_mcp/watchdog/service.py
CHANGED
|
@@ -7,6 +7,8 @@ from windows_mcp.uia.enums import TreeScope
|
|
|
7
7
|
from threading import Thread, Event
|
|
8
8
|
import comtypes.client
|
|
9
9
|
import comtypes
|
|
10
|
+
import logging
|
|
11
|
+
import weakref
|
|
10
12
|
|
|
11
13
|
from .event_handlers import (
|
|
12
14
|
FocusChangedEventHandler,
|
|
@@ -14,6 +16,9 @@ from .event_handlers import (
|
|
|
14
16
|
PropertyChangedEventHandler
|
|
15
17
|
)
|
|
16
18
|
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
logger.setLevel(logging.INFO)
|
|
21
|
+
|
|
17
22
|
class WatchDog:
|
|
18
23
|
def __init__(self):
|
|
19
24
|
self.uia_client = _AutomationClient.instance()
|
|
@@ -90,12 +95,12 @@ class WatchDog:
|
|
|
90
95
|
self._focus_handler = FocusChangedEventHandler(self)
|
|
91
96
|
self.uia.AddFocusChangedEventHandler(None, self._focus_handler)
|
|
92
97
|
except Exception as e:
|
|
93
|
-
|
|
98
|
+
logger.debug(f"Failed to add focus handler: {e}")
|
|
94
99
|
elif not self._focus_callback and self._focus_handler:
|
|
95
100
|
try:
|
|
96
101
|
self.uia.RemoveFocusChangedEventHandler(self._focus_handler)
|
|
97
102
|
except Exception as e:
|
|
98
|
-
|
|
103
|
+
logger.debug(f"Failed to remove focus handler: {e}")
|
|
99
104
|
self._focus_handler = None
|
|
100
105
|
|
|
101
106
|
# --- Structure Monitoring ---
|
|
@@ -110,7 +115,7 @@ class WatchDog:
|
|
|
110
115
|
target = self._active_structure_element if self._active_structure_element else self.uia.GetRootElement()
|
|
111
116
|
self.uia.RemoveStructureChangedEventHandler(target, self._structure_handler)
|
|
112
117
|
except Exception as e:
|
|
113
|
-
|
|
118
|
+
logger.debug(f"Failed to remove structure handler: {e}")
|
|
114
119
|
self._structure_handler = None
|
|
115
120
|
self._active_structure_element = None
|
|
116
121
|
is_active = False
|
|
@@ -124,7 +129,7 @@ class WatchDog:
|
|
|
124
129
|
self.uia.AddStructureChangedEventHandler(target, scope, None, self._structure_handler)
|
|
125
130
|
self._active_structure_element = target
|
|
126
131
|
except Exception as e:
|
|
127
|
-
|
|
132
|
+
logger.debug(f"Failed to add structure handler: {e}")
|
|
128
133
|
|
|
129
134
|
# --- Property Monitoring ---
|
|
130
135
|
config_changed = (self._property_element != self._active_property_element) or \
|
|
@@ -172,17 +177,23 @@ class WatchDog:
|
|
|
172
177
|
try:
|
|
173
178
|
self.uia.RemoveFocusChangedEventHandler(self._focus_handler)
|
|
174
179
|
except: pass
|
|
180
|
+
self._focus_handler = None
|
|
175
181
|
|
|
176
182
|
if self._structure_handler:
|
|
177
183
|
try:
|
|
178
184
|
target = self._active_structure_element if self._active_structure_element else self.uia.GetRootElement()
|
|
179
185
|
self.uia.RemoveStructureChangedEventHandler(target, self._structure_handler)
|
|
180
186
|
except: pass
|
|
187
|
+
self._structure_handler = None
|
|
188
|
+
self._active_structure_element = None
|
|
181
189
|
|
|
182
190
|
if self._property_handler:
|
|
183
191
|
try:
|
|
184
192
|
target = self._active_property_element if self._active_property_element else self.uia.GetRootElement()
|
|
185
193
|
self.uia.RemovePropertyChangedEventHandler(target, self._property_handler)
|
|
186
194
|
except: pass
|
|
195
|
+
self._property_handler = None
|
|
196
|
+
self._active_property_element = None
|
|
197
|
+
self._active_property_ids = None
|
|
187
198
|
|
|
188
199
|
comtypes.CoUninitialize()
|