windows-mcp 0.6.0__py3-none-any.whl → 0.6.2__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.
@@ -1,5 +1,6 @@
1
- from windows_mcp.desktop.config import BROWSER_NAMES, PROCESS_PER_MONITOR_DPI_AWARE
2
- from windows_mcp.desktop.views import DesktopState, App, Status, Size
1
+ from windows_mcp.vdm.core import get_all_desktops, get_current_desktop, is_window_on_current_desktop
2
+ from windows_mcp.desktop.views import DesktopState, Window, Browser, Status, Size
3
+ from windows_mcp.desktop.config import PROCESS_PER_MONITOR_DPI_AWARE
3
4
  from windows_mcp.tree.views import BoundingBox, TreeElementNode
4
5
  from concurrent.futures import ThreadPoolExecutor, as_completed
5
6
  from PIL import ImageGrab, ImageFont, ImageDraw, Image
@@ -45,25 +46,36 @@ class Desktop:
45
46
  self.tree=Tree(self)
46
47
  self.desktop_state=None
47
48
 
48
- def get_state(self,use_annotation:bool=True,use_vision:bool=False,use_dom:bool=False,as_bytes:bool=False,scale:float=1.0)->DesktopState:
49
- sleep(0.1)
49
+ def get_state(self,use_annotation:bool|str=True,use_vision:bool|str=False,use_dom:bool|str=False,as_bytes:bool|str=False,scale:float=1.0)->DesktopState:
50
+ use_annotation = use_annotation is True or (isinstance(use_annotation, str) and use_annotation.lower() == 'true')
51
+ use_vision = use_vision is True or (isinstance(use_vision, str) and use_vision.lower() == 'true')
52
+ use_dom = use_dom is True or (isinstance(use_dom, str) and use_dom.lower() == 'true')
53
+ as_bytes = as_bytes is True or (isinstance(as_bytes, str) and as_bytes.lower() == 'true')
54
+
50
55
  start_time = time()
51
56
 
52
57
  controls_handles=self.get_controls_handles() # Taskbar,Program Manager,Apps, Dialogs
53
- apps,apps_handles=self.get_apps(controls_handles=controls_handles) # Apps
54
- active_app=self.get_active_app(apps=apps) #Active App
55
- active_app_handle=active_app.handle if active_app else None
58
+ windows,windows_handles=self.get_windows(controls_handles=controls_handles) # Apps
59
+ active_window=self.get_active_window(windows=windows) #Active Window
60
+ active_window_handle=active_window.handle if active_window else None
61
+
62
+ try:
63
+ active_desktop=get_current_desktop()
64
+ all_desktops=get_all_desktops()
65
+ except RuntimeError:
66
+ active_desktop = {'id': '00000000-0000-0000-0000-000000000000', 'name': 'Default Desktop'}
67
+ all_desktops = [active_desktop]
56
68
 
57
- if active_app is not None and active_app in apps:
58
- apps.remove(active_app)
69
+ if active_window is not None and active_window in windows:
70
+ windows.remove(active_window)
59
71
 
60
- logger.debug(f"Active app: {active_app or 'No Active App Found'}")
61
- logger.debug(f"Apps: {apps}")
72
+ logger.debug(f"Active window: {active_window or 'No Active Window Found'}")
73
+ logger.debug(f"Windows: {windows}")
62
74
 
63
75
  #Preparing handles for Tree
64
- other_apps_handles=list(controls_handles-apps_handles)
76
+ other_windows_handles=list(controls_handles-windows_handles)
65
77
 
66
- tree_state=self.tree.get_state(active_app_handle,other_apps_handles,use_dom=use_dom)
78
+ tree_state=self.tree.get_state(active_window_handle,other_windows_handles,use_dom=use_dom)
67
79
 
68
80
  if use_vision:
69
81
  if use_annotation:
@@ -83,13 +95,20 @@ class Desktop:
83
95
  else:
84
96
  screenshot=None
85
97
 
86
- self.desktop_state=DesktopState(apps= apps,active_app=active_app,screenshot=screenshot,tree_state=tree_state)
98
+ self.desktop_state=DesktopState(
99
+ active_window=active_window,
100
+ windows=windows,
101
+ active_desktop=active_desktop,
102
+ all_desktops=all_desktops,
103
+ screenshot=screenshot,
104
+ tree_state=tree_state
105
+ )
87
106
  # Log the time taken to capture the state
88
107
  end_time = time()
89
108
  logger.info(f"Desktop State capture took {end_time - start_time:.2f} seconds")
90
109
  return self.desktop_state
91
110
 
92
- def get_app_status(self,control:uia.Control)->Status:
111
+ def get_window_status(self,control:uia.Control)->Status:
93
112
  if uia.IsIconic(control.NativeWindowHandle):
94
113
  return Status.MINIMIZED
95
114
  elif uia.IsZoomed(control.NativeWindowHandle):
@@ -117,8 +136,8 @@ class Desktop:
117
136
  try:
118
137
  reader = csv.DictReader(io.StringIO(apps_info.strip()))
119
138
  return {
120
- row.get('Name').lower(): row.get('AppID')
121
- for row in reader
139
+ row.get('Name', '').lower(): row.get('AppID', '')
140
+ for row in reader
122
141
  if row.get('Name') and row.get('AppID')
123
142
  }
124
143
  except Exception as e:
@@ -129,10 +148,11 @@ class Desktop:
129
148
  try:
130
149
  encoded = base64.b64encode(command.encode("utf-16le")).decode("ascii")
131
150
  result = subprocess.run(
132
- ['powershell', '-NoProfile', '-EncodedCommand', encoded],
151
+ ['powershell', '-NoProfile', '-OutputFormat', 'Text', '-EncodedCommand', encoded],
133
152
  capture_output=True, # No errors='ignore' - let subprocess return bytes
134
153
  timeout=timeout,
135
- cwd=os.path.expanduser(path='~')
154
+ cwd=os.path.expanduser(path='~'),
155
+ env=os.environ.copy() # Inherit environment variables including PATH
136
156
  )
137
157
  # Handle both bytes and str output (subprocess behavior varies by environment)
138
158
  stdout = result.stdout
@@ -147,11 +167,11 @@ class Desktop:
147
167
  except Exception as e:
148
168
  return (f'Command execution failed: {type(e).__name__}: {e}', 1)
149
169
 
150
- def is_app_browser(self,node:uia.Control):
170
+ def is_window_browser(self,node:uia.Control):
151
171
  '''Give any node of the app and it will return True if the app is a browser, False otherwise.'''
152
172
  try:
153
173
  process=Process(node.ProcessId)
154
- return process.name() in BROWSER_NAMES
174
+ return Browser.has_process(process.name())
155
175
  except:
156
176
  return False
157
177
 
@@ -162,32 +182,32 @@ class Desktop:
162
182
  return "".join([row.get('DisplayName') for row in reader])
163
183
 
164
184
  def resize_app(self,size:tuple[int,int]=None,loc:tuple[int,int]=None)->tuple[str,int]:
165
- active_app=self.desktop_state.active_app
166
- if active_app is None:
167
- return "No active app found",1
168
- if active_app.status==Status.MINIMIZED:
169
- return f"{active_app.name} is minimized",1
170
- elif active_app.status==Status.MAXIMIZED:
171
- return f"{active_app.name} is maximized",1
185
+ active_window=self.desktop_state.active_window
186
+ if active_window is None:
187
+ return "No active window found",1
188
+ if active_window.status==Status.MINIMIZED:
189
+ return f"{active_window.name} is minimized",1
190
+ elif active_window.status==Status.MAXIMIZED:
191
+ return f"{active_window.name} is maximized",1
172
192
  else:
173
- app_control=uia.ControlFromHandle(active_app.handle)
193
+ window_control=uia.ControlFromHandle(active_window.handle)
174
194
  if loc is None:
175
- x=app_control.BoundingRectangle.left
176
- y=app_control.BoundingRectangle.top
195
+ x=window_control.BoundingRectangle.left
196
+ y=window_control.BoundingRectangle.top
177
197
  loc=(x,y)
178
198
  if size is None:
179
- width=app_control.BoundingRectangle.width()
180
- height=app_control.BoundingRectangle.height()
199
+ width=window_control.BoundingRectangle.width()
200
+ height=window_control.BoundingRectangle.height()
181
201
  size=(width,height)
182
202
  x,y=loc
183
203
  width,height=size
184
- app_control.MoveWindow(x,y,width,height)
185
- return (f'{active_app.name} resized to {width}x{height} at {x},{y}.',0)
204
+ window_control.MoveWindow(x,y,width,height)
205
+ return (f'{active_window.name} resized to {width}x{height} at {x},{y}.',0)
186
206
 
187
207
  def is_app_running(self,name:str)->bool:
188
- apps, _ = self.get_apps()
189
- apps_dict = {app.name: app for app in apps}
190
- return process.extractOne(name,list(apps_dict.keys()),score_cutoff=60) is not None
208
+ windows, _ = self.get_windows()
209
+ windows_dict = {window.name: window for window in windows}
210
+ return process.extractOne(name,list(windows_dict.keys()),score_cutoff=60) is not None
191
211
 
192
212
  def app(self,mode:Literal['launch','switch','resize'],name:Optional[str]=None,loc:Optional[tuple[int,int]]=None,size:Optional[tuple[int,int]]=None):
193
213
  match mode:
@@ -207,7 +227,7 @@ class Desktop:
207
227
  safe_name = re.escape(name)
208
228
  if uia.WindowControl(RegexName=f'(?i).*{safe_name}.*').Exists(maxSearchSeconds=10):
209
229
  launched = True
210
-
230
+
211
231
  if launched:
212
232
  return f'{name.title()} launched.'
213
233
  return f'Launching {name.title()} sent, but window not detected yet.'
@@ -232,37 +252,41 @@ class Desktop:
232
252
  app_name,_=matched_app
233
253
  appid=apps_map.get(app_name)
234
254
  if appid is None:
235
- return (name,f'{name.title()} not found in start menu.',1,0)
255
+ return (f'{name.title()} not found in start menu.',1,0)
236
256
 
237
257
  pid = 0
238
258
  if os.path.exists(appid) or "\\" in appid:
239
259
  # It's a file path, we can try to get the PID using PassThru
240
- command = f'Start-Process "{appid}" -PassThru | Select-Object -ExpandProperty Id'
260
+ # Escape any single quotes and wrap in single quotes for PowerShell safety
261
+ safe_appid = appid.replace("'", "''")
262
+ command = f"Start-Process '{safe_appid}' -PassThru | Select-Object -ExpandProperty Id"
241
263
  response, status = self.execute_command(command)
242
264
  if status == 0 and response.strip().isdigit():
243
265
  pid = int(response.strip())
244
266
  else:
245
- # It's an AUMID (Store App)
267
+ # It's an AUMID (Store App) - validate it only contains expected characters
268
+ if not appid.replace('\\', '').replace('_', '').replace('.', '').replace('-', '').isalnum():
269
+ return (f'Invalid app identifier: {appid}', 1, 0)
246
270
  command = f'Start-Process "shell:AppsFolder\\{appid}"'
247
271
  response, status = self.execute_command(command)
248
272
 
249
273
  return response, status, pid
250
274
 
251
275
  def switch_app(self,name:str):
252
- apps={app.name:app for app in [self.desktop_state.active_app]+self.desktop_state.apps if app is not None}
253
- matched_app:Optional[tuple[str,float]]=process.extractOne(name,list(apps.keys()),score_cutoff=70)
254
- if matched_app is None:
276
+ windows={window.name:window for window in [self.desktop_state.active_window]+self.desktop_state.windows if window is not None}
277
+ matched_window:Optional[tuple[str,float]]=process.extractOne(name,list(windows.keys()),score_cutoff=70)
278
+ if matched_window is None:
255
279
  return (f'Application {name.title()} not found.',1)
256
- app_name,_=matched_app
257
- app=apps.get(app_name)
258
- target_handle=app.handle
280
+ window_name,_=matched_window
281
+ window=windows.get(window_name)
282
+ target_handle=window.handle
259
283
 
260
284
  if uia.IsIconic(target_handle):
261
285
  uia.ShowWindow(target_handle, win32con.SW_RESTORE)
262
- content=f'{app_name.title()} restored from Minimized state.'
286
+ content=f'{window_name.title()} restored from Minimized state.'
263
287
  else:
264
288
  self.bring_window_to_top(target_handle)
265
- content=f'Switched to {app_name.title()} window.'
289
+ content=f'Switched to {window_name.title()} window.'
266
290
  return content,0
267
291
 
268
292
  def bring_window_to_top(self, target_handle: int):
@@ -274,6 +298,14 @@ class Desktop:
274
298
  win32gui.ShowWindow(target_handle, win32con.SW_RESTORE)
275
299
 
276
300
  foreground_handle = win32gui.GetForegroundWindow()
301
+
302
+ # Validate both handles before proceeding
303
+ if not win32gui.IsWindow(foreground_handle):
304
+ # No valid foreground window, just try to set target as foreground
305
+ win32gui.SetForegroundWindow(target_handle)
306
+ win32gui.BringWindowToTop(target_handle)
307
+ return
308
+
277
309
  foreground_thread, _ = win32process.GetWindowThreadProcessId(foreground_handle)
278
310
  target_thread, _ = win32process.GetWindowThreadProcessId(target_handle)
279
311
 
@@ -322,7 +354,7 @@ class Desktop:
322
354
  x,y=loc
323
355
  pg.click(x,y,button=button,clicks=clicks,duration=0.1)
324
356
 
325
- def type(self,loc:tuple[int,int],text:str,caret_position:Literal['start','end','none']='none',clear:Literal['true','false']='false',press_enter:Literal['true','false']='false'):
357
+ def type(self,loc:tuple[int,int],text:str,caret_position:Literal['start', 'idle', 'end']='idle',clear:bool|str=False,press_enter:bool|str=False):
326
358
  x,y=loc
327
359
  pg.leftClick(x,y)
328
360
  if caret_position == 'start':
@@ -331,12 +363,16 @@ class Desktop:
331
363
  pg.press('end')
332
364
  else:
333
365
  pass
334
- if clear=='true':
366
+
367
+ # Handle both boolean and string 'true'/'false'
368
+ if clear is True or (isinstance(clear, str) and clear.lower() == 'true'):
335
369
  pg.sleep(0.5)
336
370
  pg.hotkey('ctrl','a')
337
371
  pg.press('backspace')
372
+
338
373
  pg.typewrite(text,interval=0.02)
339
- if press_enter=='true':
374
+
375
+ if press_enter is True or (isinstance(press_enter, str) and press_enter.lower() == 'true'):
340
376
  pg.press('enter')
341
377
 
342
378
  def scroll(self,loc:tuple[int,int]=None,type:Literal['horizontal','vertical']='vertical',direction:Literal['up','down','left','right']='down',wheel_times:int=1)->str|None:
@@ -387,19 +423,20 @@ class Desktop:
387
423
  else:
388
424
  pg.press(''.join(shortcut))
389
425
 
390
- def multi_select(self,press_ctrl:Literal['true','false']='false',elements:list[tuple[int,int]|int]=[]):
391
- if press_ctrl=='true':
426
+ def multi_select(self,press_ctrl:bool|str=False,locs:list[tuple[int,int]]=[]):
427
+ press_ctrl = press_ctrl is True or (isinstance(press_ctrl, str) and press_ctrl.lower() == 'true')
428
+ if press_ctrl:
392
429
  pg.keyDown('ctrl')
393
- for element in elements:
394
- x,y=element
430
+ for loc in locs:
431
+ x,y=loc
395
432
  pg.click(x,y,duration=0.2)
396
433
  pg.sleep(0.5)
397
434
  pg.keyUp('ctrl')
398
435
 
399
- def multi_edit(self,elements:list[tuple[int,int,str]|tuple[int,str]]):
400
- for element in elements:
401
- x,y,text=element
402
- self.type((x,y),text=text,clear='true')
436
+ def multi_edit(self,locs:list[tuple[int,int,str]]):
437
+ for loc in locs:
438
+ x,y,text=loc
439
+ self.type((x,y),text=text,clear=True)
403
440
 
404
441
  def scrape(self,url:str)->str:
405
442
  response=requests.get(url,timeout=10)
@@ -407,56 +444,105 @@ class Desktop:
407
444
  content=markdownify(html=html)
408
445
  return content
409
446
 
410
- def get_app_from_element(self,element:uia.Control)->App|None:
447
+ def get_window_from_element(self,element:uia.Control)->Window|None:
411
448
  if element is None:
412
449
  return None
413
450
  top_window=element.GetTopLevelControl()
414
451
  if top_window is None:
415
452
  return None
416
453
  handle=top_window.NativeWindowHandle
417
- apps,_=self.get_apps()
418
- for app in apps:
419
- if app.handle==handle:
420
- return app
454
+ windows,_=self.get_windows()
455
+ for window in windows:
456
+ if window.handle==handle:
457
+ return window
421
458
  return None
422
459
 
423
- def is_app_visible(self,app:uia.Control)->bool:
424
- is_minimized=self.get_app_status(app)!=Status.MINIMIZED
425
- size=app.BoundingRectangle
460
+ def is_window_visible(self,window:uia.Control)->bool:
461
+ is_minimized=self.get_window_status(window)!=Status.MINIMIZED
462
+ size=window.BoundingRectangle
426
463
  area=size.width()*size.height()
427
- is_overlay=self.is_overlay_app(app)
464
+ is_overlay=self.is_overlay_window(window)
428
465
  return not is_overlay and is_minimized and area>10
429
466
 
430
- def is_overlay_app(self,element:uia.Control) -> bool:
467
+ def is_overlay_window(self,element:uia.Control) -> bool:
431
468
  no_children = len(element.GetChildren()) == 0
432
469
  is_name = "Overlay" in element.Name.strip()
433
470
  return no_children or is_name
434
471
 
435
- def get_controls_handles(self):
472
+ def get_controls_handles(self,optimized:bool=False):
436
473
  handles = set()
437
- root=uia.GetRootControl()
438
- children=root.GetChildren()
439
- for child in children:
440
- handles.add(child.NativeWindowHandle)
474
+ # For even more faster results (still under development)
475
+ def callback(hwnd, _):
476
+ try:
477
+ # Validate handle before checking properties
478
+ if win32gui.IsWindow(hwnd) and win32gui.IsWindowVisible(hwnd) and is_window_on_current_desktop(hwnd):
479
+ handles.add(hwnd)
480
+ except Exception:
481
+ # Skip invalid handles without logging (common during window enumeration)
482
+ pass
483
+
484
+ win32gui.EnumWindows(callback, None)
485
+
486
+ if desktop_hwnd:= win32gui.FindWindow('Progman',None):
487
+ handles.add(desktop_hwnd)
488
+ if taskbar_hwnd:= win32gui.FindWindow('Shell_TrayWnd',None):
489
+ handles.add(taskbar_hwnd)
490
+ if secondary_taskbar_hwnd:= win32gui.FindWindow('Shell_SecondaryTrayWnd',None):
491
+ handles.add(secondary_taskbar_hwnd)
441
492
  return handles
442
493
 
443
- def get_active_app(self,apps:list[App]|None=None)->App|None:
494
+ def get_active_window(self,windows:list[Window]|None=None)->Window|None:
444
495
  try:
445
- if apps is None:
446
- apps,_=self.get_apps()
447
- handle=uia.GetForegroundWindow()
448
- for app in apps:
449
- if app.handle!=handle:
496
+ if windows is None:
497
+ windows,_=self.get_windows()
498
+ active_window=self.get_foreground_window()
499
+ if active_window.ClassName=="Progman":
500
+ return None
501
+ active_window_handle=active_window.NativeWindowHandle
502
+ for window in windows:
503
+ if window.handle!=active_window_handle:
450
504
  continue
451
- return app
505
+ return window
506
+ # In case active window is not present in the windows list
507
+ return Window(**{
508
+ "name":active_window.Name,
509
+ "is_browser":self.is_window_browser(active_window),
510
+ "depth":0,
511
+ "bounding_box":BoundingBox(
512
+ left=active_window.BoundingRectangle.left,
513
+ top=active_window.BoundingRectangle.top,
514
+ right=active_window.BoundingRectangle.right,
515
+ bottom=active_window.BoundingRectangle.bottom,
516
+ width=active_window.BoundingRectangle.width(),
517
+ height=active_window.BoundingRectangle.height()
518
+ ),
519
+ "status":self.get_window_status(active_window),
520
+ "handle":active_window_handle,
521
+ "process_id":active_window.ProcessId,
522
+ })
452
523
  except Exception as ex:
453
- logger.error(f"Error in get_active_app: {ex}")
524
+ logger.error(f"Error in get_active_window: {ex}")
454
525
  return None
526
+
527
+ def get_foreground_window(self)->uia.Control:
528
+ handle=uia.GetForegroundWindow()
529
+ active_window=self.get_window_from_element_handle(handle)
530
+ return active_window
531
+
532
+ def get_window_from_element_handle(self, element_handle: int) -> uia.Control:
533
+ current = uia.ControlFromHandle(element_handle)
534
+ root_handle = uia.GetRootControl().NativeWindowHandle
455
535
 
456
- def get_apps(self,controls_handles:set[int]|None=None) -> tuple[list[App],set[int]]:
536
+ while True:
537
+ parent = current.GetParentControl()
538
+ if parent is None or parent.NativeWindowHandle == root_handle:
539
+ return current
540
+ current = parent
541
+
542
+ def get_windows(self,controls_handles:set[int]|None=None) -> tuple[list[Window],set[int]]:
457
543
  try:
458
- apps = []
459
- handles = set()
544
+ windows = []
545
+ window_handles = set()
460
546
  controls_handles=controls_handles or self.get_controls_handles()
461
547
  for depth, hwnd in enumerate(controls_handles):
462
548
  try:
@@ -465,7 +551,7 @@ class Desktop:
465
551
  continue
466
552
 
467
553
  # Filter out Overlays (e.g. NVIDIA, Steam)
468
- if self.is_overlay_app(child):
554
+ if self.is_overlay_window(child):
469
555
  continue
470
556
 
471
557
  if isinstance(child,(uia.WindowControl,uia.PaneControl)):
@@ -474,15 +560,14 @@ class Desktop:
474
560
  continue
475
561
 
476
562
  if window_pattern.CanMinimize and window_pattern.CanMaximize:
477
- status = self.get_app_status(child)
563
+ status = self.get_window_status(child)
478
564
 
479
565
  bounding_rect=child.BoundingRectangle
480
566
  if bounding_rect.isempty() and status!=Status.MINIMIZED:
481
567
  continue
482
568
 
483
- apps.append(App(**{
569
+ windows.append(Window(**{
484
570
  "name":child.Name,
485
- "runtime_id":tuple(child.GetRuntimeId()),
486
571
  "depth":depth,
487
572
  "status":status,
488
573
  "bounding_box":BoundingBox(
@@ -495,13 +580,13 @@ class Desktop:
495
580
  ),
496
581
  "handle":child.NativeWindowHandle,
497
582
  "process_id":child.ProcessId,
498
- "is_browser":self.is_app_browser(child)
583
+ "is_browser":self.is_window_browser(child)
499
584
  }))
500
- handles.add(child.NativeWindowHandle)
585
+ window_handles.add(child.NativeWindowHandle)
501
586
  except Exception as ex:
502
- logger.error(f"Error in get_apps: {ex}")
503
- apps = []
504
- return apps,handles
587
+ logger.error(f"Error in get_windows: {ex}")
588
+ windows = []
589
+ return windows,window_handles
505
590
 
506
591
  def get_xpath_from_element(self,element:uia.Control):
507
592
  current=element
@@ -556,9 +641,13 @@ class Desktop:
556
641
  return "Local Account" if response.strip()=='Local' else "Microsoft Account" if status==0 else "Local Account"
557
642
 
558
643
  def get_dpi_scaling(self):
559
- user32 = ctypes.windll.user32
560
- dpi = user32.GetDpiForSystem()
561
- return dpi / 96.0
644
+ try:
645
+ user32 = ctypes.windll.user32
646
+ dpi = user32.GetDpiForSystem()
647
+ return dpi / 96.0 if dpi > 0 else 1.0
648
+ except Exception:
649
+ # Fallback to standard DPI if system call fails
650
+ return 1.0
562
651
 
563
652
  def get_screen_size(self)->Size:
564
653
  width, height = uia.GetVirtualScreenSize()
@@ -568,12 +657,11 @@ class Desktop:
568
657
  try:
569
658
  return ImageGrab.grab(all_screens=True)
570
659
  except Exception as e:
571
- logger.warning(f"Failed to capture all screens: {e}. Fallback to primary.")
660
+ logger.warning(f"Failed to capture virtual screen, using primary screen")
572
661
  return pg.screenshot()
573
662
 
574
663
  def get_annotated_screenshot(self, nodes: list[TreeElementNode]) -> Image.Image:
575
664
  screenshot = self.get_screenshot()
576
- sleep(0.10)
577
665
  # Add padding
578
666
  padding = 5
579
667
  width = int(screenshot.width + (1.5 * padding))
@@ -6,9 +6,15 @@ from PIL.Image import Image
6
6
  from enum import Enum
7
7
 
8
8
  class Browser(Enum):
9
- CHROME='Chrome'
10
- EDGE='Edge'
11
- FIREFOX='Firefox'
9
+ CHROME='chrome'
10
+ EDGE='msedge'
11
+ FIREFOX='firefox'
12
+
13
+ @classmethod
14
+ def has_process(cls, process_name: str) -> bool:
15
+ if not hasattr(cls, '_process_names'):
16
+ cls._process_names = {f'{b.value}.exe' for b in cls}
17
+ return process_name.lower() in cls._process_names
12
18
 
13
19
  class Status(Enum):
14
20
  MAXIMIZED='Maximized'
@@ -18,9 +24,8 @@ class Status(Enum):
18
24
 
19
25
 
20
26
  @dataclass
21
- class App:
27
+ class Window:
22
28
  name:str
23
- runtime_id:tuple[int]
24
29
  is_browser:bool
25
30
  depth:int
26
31
  status:Status
@@ -41,20 +46,32 @@ class Size:
41
46
 
42
47
  @dataclass
43
48
  class DesktopState:
44
- apps:list[App]
45
- active_app:Optional[App]
49
+ active_desktop:dict
50
+ all_desktops:list[dict]
51
+ active_window:Optional[Window]
52
+ windows:list[Window]
46
53
  screenshot:Optional[Image]=None
47
54
  tree_state:Optional[TreeState]=None
48
55
 
49
- def active_app_to_string(self):
50
- if self.active_app is None:
51
- return 'No active app found'
56
+ def active_desktop_to_string(self):
57
+ desktop_name=self.active_desktop.get('name')
58
+ headers=["Name"]
59
+ return tabulate([[desktop_name]], headers=headers, tablefmt="simple")
60
+
61
+ def desktops_to_string(self):
62
+ headers=["Name"]
63
+ rows=[[desktop.get("name")] for desktop in self.all_desktops]
64
+ return tabulate(rows, headers=headers, tablefmt="simple")
65
+
66
+ def active_window_to_string(self):
67
+ if not self.active_window:
68
+ return 'No active window found'
52
69
  headers = ["Name", "Depth", "Status", "Width", "Height", "Handle"]
53
- return tabulate([self.active_app.to_row()], headers=headers, tablefmt="simple")
70
+ return tabulate([self.active_window.to_row()], headers=headers, tablefmt="simple")
54
71
 
55
- def apps_to_string(self):
56
- if not self.apps:
57
- return 'No apps running in background'
72
+ def windows_to_string(self):
73
+ if not self.windows:
74
+ return 'No windows found'
58
75
  headers = ["Name", "Depth", "Status", "Width", "Height", "Handle"]
59
- rows = [app.to_row() for app in self.apps]
76
+ rows = [window.to_row() for window in self.windows]
60
77
  return tabulate(rows, headers=headers, tablefmt="simple")