open-alo-core 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,1735 @@
1
+ # open_alo_core API Reference
2
+
3
+ **Version:** 0.1.0
4
+ **Platform:** Linux (Wayland/X11)
5
+ **License:** MIT
6
+
7
+ ## Table of Contents
8
+
9
+ - [Overview](#overview)
10
+ - [Installation](#installation)
11
+ - [Quick Start](#quick-start)
12
+ - [Core Classes](#core-classes)
13
+ - [UnifiedRemoteDesktop](#unifiedremotedesktop) ⭐ **RECOMMENDED for AI Agents**
14
+ - [WaylandInput](#waylandinput) (Legacy)
15
+ - [WaylandCapture](#waylandcapture) (Legacy)
16
+ - [WindowManager](#windowmanager)
17
+ - [Types](#types)
18
+ - [Point](#point)
19
+ - [Size](#size)
20
+ - [Rect](#rect)
21
+ - [WindowInfo](#windowinfo)
22
+ - [WindowType](#windowtype)
23
+ - [FrameType](#frametype)
24
+ - [Exceptions](#exceptions)
25
+ - [Utilities](#utilities)
26
+ - [Constants](#constants)
27
+ - [Complete API Index](#complete-api-index)
28
+
29
+ ---
30
+
31
+ ## Overview
32
+
33
+ `open_alo_core` is a standalone desktop automation SDK for Linux that provides:
34
+
35
+ - **Input Control**: Mouse and keyboard control via XDG RemoteDesktop Portal
36
+ - **Screen Capture**: Native Wayland screenshot via PipeWire and XDG ScreenCast Portal
37
+ - **Window Management**: Comprehensive window control via GNOME Shell D-Bus (requires Window Calls extension)
38
+ - **Zero Dependencies on AI/ML**: Pure hardware abstraction layer
39
+ - **Wayland Native**: No X11 fallback, works on modern Wayland compositors
40
+
41
+ ### Architecture
42
+
43
+ ```
44
+ open_alo_core/
45
+ ├── wayland/
46
+ │ ├── input.py # WaylandInput - Mouse & keyboard control
47
+ │ └── capture.py # WaylandCapture - Screen capture
48
+ ├── window_manager.py # WindowManager - Window management
49
+ ├── types.py # Data types (Point, Size, Rect)
50
+ ├── exceptions.py # Exception hierarchy
51
+ └── utils/ # Utility functions
52
+ ```
53
+
54
+ ### System Requirements
55
+
56
+ - **OS**: Linux with Wayland compositor (GNOME Shell, KDE Plasma, Sway, etc.)
57
+ - **Python**: 3.8+
58
+ - **Dependencies**:
59
+ - PyGObject (python3-gi)
60
+ - GStreamer 1.0 (for capture)
61
+ - PipeWire (for capture)
62
+ - **Window Management** (GNOME only):
63
+ - [Window Calls Extension](https://extensions.gnome.org/extension/4724/window-calls/)
64
+ - Install and enable: `gnome-extensions enable window-calls@domandoman.github.com`
65
+
66
+ **Tested Environment:**
67
+ - Ubuntu 25.10 (Questing), Wayland + GNOME/Unity
68
+ - Window Calls extension v13+
69
+
70
+ ---
71
+
72
+ ## Installation
73
+
74
+ ```bash
75
+ # Install from source
76
+ cd open_alo_core
77
+ pip install -e .
78
+
79
+ # Or install dependencies manually
80
+ sudo apt install python3-gi gstreamer1.0-tools pipewire
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Quick Start
86
+
87
+ ### Unified Approach (Recommended for AI Agents) ⭐
88
+
89
+ ```python
90
+ from open_alo_core import UnifiedRemoteDesktop, Point
91
+
92
+ # ONE permission dialog for both input and capture
93
+ with UnifiedRemoteDesktop() as remote:
94
+ remote.initialize(persist_mode=2, enable_capture=True)
95
+
96
+ # Screen capture
97
+ screenshot = remote.capture_screenshot() # PNG bytes
98
+ frame = remote.get_frame() # Real-time stream
99
+ width, height = remote.get_screen_size()
100
+
101
+ # Input control
102
+ remote.click(Point(500, 500))
103
+ remote.type_text("Hello World!")
104
+ remote.key_combo(["ctrl", "c"])
105
+ ```
106
+
107
+ ### Legacy Approach (Separate Input/Capture)
108
+
109
+ ```python
110
+ from open_alo_core import WaylandInput, Point
111
+
112
+ # Context manager automatically cleans up
113
+ with WaylandInput() as ctrl:
114
+ # Initialize with persistent permissions (approve once)
115
+ ctrl.initialize(persist_mode=2)
116
+
117
+ # Click at coordinates
118
+ ctrl.click(Point(500, 500))
119
+
120
+ # Type text
121
+ ctrl.type_text("Hello World!")
122
+
123
+ # Press keys
124
+ ctrl.press_key("Return")
125
+
126
+ # Keyboard shortcuts
127
+ ctrl.key_combo(["Control", "a"]) # Select all
128
+ ctrl.key_combo(["Control", "c"]) # Copy
129
+ ```
130
+
131
+ ### Screen Capture
132
+
133
+ ```python
134
+ from open_alo_core import WaylandCapture
135
+ from pathlib import Path
136
+
137
+ with WaylandCapture() as capture:
138
+ # User will be prompted to select screen/window
139
+ result = capture.capture_screen()
140
+
141
+ # Save as PNG
142
+ Path("/tmp/screenshot.png").write_bytes(result.data)
143
+
144
+ print(f"Captured {result.source_type}: {result.size}")
145
+ ```
146
+
147
+ ### Window Management
148
+
149
+ ```python
150
+ from open_alo_core import WindowManager, activate_window
151
+
152
+ # Simple activation
153
+ activate_window("Text Editor")
154
+
155
+ # Full API
156
+ wm = WindowManager()
157
+ windows = wm.list_windows()
158
+ editor = wm.find_window("Text Editor")
159
+
160
+ if editor:
161
+ wm.activate(editor.id)
162
+ wm.maximize(editor.id)
163
+ wm.move_resize(editor.id, 0, 0, 1920, 1080)
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Core Classes
169
+
170
+ ### UnifiedRemoteDesktop ⭐
171
+
172
+ **Full path**: `open_alo_core.wayland.unified.UnifiedRemoteDesktop`
173
+
174
+ **RECOMMENDED for AI Agents** - Provides both input control and screen capture with a single permission dialog.
175
+
176
+ #### Why Use UnifiedRemoteDesktop?
177
+
178
+ - ✅ **ONE permission dialog** (vs two separate dialogs)
179
+ - ✅ **Simpler code** (single class vs WaylandInput + WaylandCapture)
180
+ - ✅ **Better UX** (same approach as RustDesk, Chrome Remote Desktop)
181
+ - ✅ **AI-ready** (real-time screen streaming + input control)
182
+
183
+ #### Constructor
184
+
185
+ ```python
186
+ UnifiedRemoteDesktop(token_path: Optional[Path] = None)
187
+ ```
188
+
189
+ **Parameters:**
190
+ - `token_path` (Optional[Path]): Custom path for storing permission tokens. Defaults to `~/.config/open_alo_core/unified_token.json`
191
+
192
+ #### Methods
193
+
194
+ ##### `initialize(persist_mode: int = 2, enable_capture: bool = True) -> bool`
195
+
196
+ Initialize unified remote desktop session. Shows ONE permission dialog for both input and capture.
197
+
198
+ **Parameters:**
199
+ - `persist_mode` (int): Permission persistence
200
+ - `0` = Ask every time (testing)
201
+ - `1` = Persist during app session
202
+ - `2` = Persist until revoked (recommended)
203
+ - `enable_capture` (bool): Enable screen capture capabilities. Set `True` for AI agents.
204
+
205
+ **Returns:**
206
+ - `bool`: True if initialization succeeded
207
+
208
+ **Raises:**
209
+ - `PermissionDenied`: User denied permission
210
+ - `SessionError`: Portal communication failed
211
+
212
+ **Example:**
213
+ ```python
214
+ with UnifiedRemoteDesktop() as remote:
215
+ remote.initialize(persist_mode=2, enable_capture=True)
216
+ # Ready to use!
217
+ ```
218
+
219
+ ##### `capture_screenshot() -> bytes`
220
+
221
+ Capture current screen as PNG image.
222
+
223
+ **Returns:**
224
+ - `bytes`: PNG image data
225
+
226
+ **Example:**
227
+ ```python
228
+ screenshot = remote.capture_screenshot()
229
+ Path("/tmp/screenshot.png").write_bytes(screenshot)
230
+ ```
231
+
232
+ ##### `get_frame() -> bytes`
233
+
234
+ Get real-time frame from video stream (for continuous monitoring).
235
+
236
+ **Returns:**
237
+ - `bytes`: PNG image data
238
+
239
+ **Example:**
240
+ ```python
241
+ while agent_running:
242
+ frame = remote.get_frame()
243
+ action = ai_model.decide(frame)
244
+ # Execute action...
245
+ ```
246
+
247
+ ##### `get_screen_size() -> Tuple[int, int]`
248
+
249
+ Get screen resolution.
250
+
251
+ **Returns:**
252
+ - `Tuple[int, int]`: (width, height) in pixels
253
+
254
+ **Example:**
255
+ ```python
256
+ width, height = remote.get_screen_size()
257
+ center = Point(width // 2, height // 2)
258
+ ```
259
+
260
+ ##### `click(point: Point, button: int = 1) -> None`
261
+
262
+ Click mouse at specific coordinates.
263
+
264
+ **Parameters:**
265
+ - `point` (Point): Click coordinates
266
+ - `button` (int): Mouse button (1=left, 2=middle, 3=right)
267
+
268
+ **Example:**
269
+ ```python
270
+ remote.click(Point(100, 200)) # Left click
271
+ remote.click(Point(100, 200), button=3) # Right click
272
+ ```
273
+
274
+ ##### `move_mouse(point: Point) -> None`
275
+
276
+ Move mouse cursor to coordinates.
277
+
278
+ **Parameters:**
279
+ - `point` (Point): Target coordinates
280
+
281
+ **Example:**
282
+ ```python
283
+ remote.move_mouse(Point(500, 500))
284
+ ```
285
+
286
+ ##### `type_text(text: str, interval: float = 0.05) -> None`
287
+
288
+ Type text with optional delay between characters.
289
+
290
+ **Parameters:**
291
+ - `text` (str): Text to type
292
+ - `interval` (float): Delay between characters in seconds
293
+
294
+ **Example:**
295
+ ```python
296
+ remote.type_text("Hello World!\n")
297
+ remote.type_text("fast typing", interval=0.01)
298
+ ```
299
+
300
+ ##### `press_key(key: Union[str, int]) -> None`
301
+
302
+ Press and release a single key.
303
+
304
+ **Parameters:**
305
+ - `key` (Union[str, int]): Key name or code
306
+
307
+ **Example:**
308
+ ```python
309
+ remote.press_key("enter")
310
+ remote.press_key("escape")
311
+ remote.press_key("f5")
312
+ ```
313
+
314
+ ##### `key_combo(keys: List[Union[str, int]]) -> None`
315
+
316
+ Press keyboard shortcut (hold all keys, then release).
317
+
318
+ **Parameters:**
319
+ - `keys` (List[Union[str, int]]): List of keys to press together
320
+
321
+ **Example:**
322
+ ```python
323
+ remote.key_combo(["ctrl", "c"]) # Copy
324
+ remote.key_combo(["ctrl", "shift", "t"]) # New terminal tab
325
+ remote.key_combo(["alt", "f4"]) # Close window
326
+ ```
327
+
328
+ ##### `close() -> None`
329
+
330
+ Release resources and close session. Called automatically when using context manager.
331
+
332
+ **Example:**
333
+ ```python
334
+ # Manual cleanup
335
+ remote = UnifiedRemoteDesktop()
336
+ remote.initialize()
337
+ # ... use remote ...
338
+ remote.close()
339
+
340
+ # Auto cleanup (recommended)
341
+ with UnifiedRemoteDesktop() as remote:
342
+ remote.initialize()
343
+ # ... use remote ...
344
+ # Automatically closed
345
+ ```
346
+
347
+ #### Complete Example
348
+
349
+ ```python
350
+ from open_alo_core import UnifiedRemoteDesktop, WindowManager, Point
351
+ from pathlib import Path
352
+
353
+ # Setup window
354
+ wm = WindowManager()
355
+ app = wm.find_window("TextEditor")
356
+ wm.activate(app.id)
357
+
358
+ # AI agent loop
359
+ with UnifiedRemoteDesktop() as remote:
360
+ remote.initialize(persist_mode=2, enable_capture=True)
361
+
362
+ while True:
363
+ # 1. Capture screen
364
+ frame = remote.get_frame()
365
+
366
+ # 2. AI decides action
367
+ action = ai_model.process(frame)
368
+
369
+ # 3. Execute
370
+ if action['type'] == 'click':
371
+ remote.click(Point(action['x'], action['y']))
372
+ elif action['type'] == 'type':
373
+ remote.type_text(action['text'])
374
+ elif action['type'] == 'screenshot':
375
+ screenshot = remote.capture_screenshot()
376
+ Path(f"capture_{time.time()}.png").write_bytes(screenshot)
377
+ ```
378
+
379
+ ---
380
+
381
+ ### WaylandInput (Legacy)
382
+
383
+ **Full path**: `open_alo_core.wayland.input.WaylandInput`
384
+
385
+ > **Note:** For new projects, prefer `UnifiedRemoteDesktop` which combines input and capture in one permission dialog.
386
+
387
+ Provides mouse and keyboard control on Wayland using XDG RemoteDesktop Portal.
388
+
389
+ #### Constructor
390
+
391
+ ```python
392
+ WaylandInput(token_path: Optional[Path] = None)
393
+ ```
394
+
395
+ **Parameters:**
396
+ - `token_path` (Optional[Path]): Custom path for storing permission tokens. If `None`, uses `~/.config/open_alo_core/tokens.json`
397
+
398
+ **Example:**
399
+ ```python
400
+ # Default token location
401
+ ctrl = WaylandInput()
402
+
403
+ # Custom token location
404
+ ctrl = WaylandInput(token_path=Path("/tmp/tokens.json"))
405
+
406
+ # Ephemeral session (no persistence)
407
+ ctrl = WaylandInput()
408
+ ctrl.initialize(persist_mode=0)
409
+ ```
410
+
411
+ #### Methods
412
+
413
+ ##### `initialize(persist_mode: int = 0) -> None`
414
+
415
+ Initialize portal session and request permissions.
416
+
417
+ **Parameters:**
418
+ - `persist_mode` (int):
419
+ - `0`: Never persist (dialog every time)
420
+ - `1`: Persist while app running
421
+ - `2`: Persist until revoked (recommended)
422
+
423
+ **Raises:**
424
+ - `PermissionDenied`: User denied permission
425
+ - `SessionError`: Session creation failed
426
+ - `RuntimeError`: Not running on Wayland
427
+
428
+ **Example:**
429
+ ```python
430
+ ctrl = WaylandInput()
431
+ ctrl.initialize(persist_mode=2) # Approve once, persist forever
432
+ ```
433
+
434
+ ##### `click(point: Point, button: int = 1) -> None`
435
+
436
+ Click at screen coordinates.
437
+
438
+ **Parameters:**
439
+ - `point` (Point): Screen coordinates (x, y)
440
+ - `button` (int): Mouse button
441
+ - `1`: Left button (default)
442
+ - `2`: Middle button
443
+ - `3`: Right button
444
+
445
+ **Raises:**
446
+ - `RuntimeError`: Not initialized
447
+ - `InputError`: Click failed
448
+
449
+ **Example:**
450
+ ```python
451
+ # Left click
452
+ ctrl.click(Point(500, 500))
453
+
454
+ # Right click
455
+ ctrl.click(Point(100, 100), button=3)
456
+
457
+ # Middle click
458
+ ctrl.click(Point(200, 200), button=2)
459
+ ```
460
+
461
+ ##### `move_mouse(point: Point) -> None`
462
+
463
+ Move mouse cursor to coordinates.
464
+
465
+ **Parameters:**
466
+ - `point` (Point): Target coordinates
467
+
468
+ **Raises:**
469
+ - `RuntimeError`: Not initialized
470
+ - `InputError`: Move failed
471
+
472
+ **Example:**
473
+ ```python
474
+ ctrl.move_mouse(Point(800, 600))
475
+ ```
476
+
477
+ ##### `type_text(text: str, interval: float = 0.01) -> None`
478
+
479
+ Type a text string character by character.
480
+
481
+ **Parameters:**
482
+ - `text` (str): Unicode text to type
483
+ - `interval` (float): Delay between characters in seconds (default: 0.01)
484
+
485
+ **Raises:**
486
+ - `RuntimeError`: Not initialized
487
+ - `InputError`: Typing failed
488
+
489
+ **Example:**
490
+ ```python
491
+ # Normal typing
492
+ ctrl.type_text("Hello World!")
493
+
494
+ # Fast typing
495
+ ctrl.type_text("Speed typing", interval=0.001)
496
+
497
+ # Unicode support
498
+ ctrl.type_text("你好世界 🌍")
499
+ ```
500
+
501
+ ##### `press_key(key: str) -> None`
502
+
503
+ Press and release a single key.
504
+
505
+ **Parameters:**
506
+ - `key` (str): Key name (e.g., "Return", "Escape", "a"). See [Key Names](#key-names) for full list.
507
+
508
+ **Raises:**
509
+ - `RuntimeError`: Not initialized
510
+ - `InputError`: Key press failed
511
+
512
+ **Example:**
513
+ ```python
514
+ ctrl.press_key("Return") # Enter
515
+ ctrl.press_key("Escape") # Esc
516
+ ctrl.press_key("Tab") # Tab
517
+ ctrl.press_key("space") # Spacebar
518
+ ctrl.press_key("a") # Letter 'a'
519
+ ```
520
+
521
+ ##### `key_combo(keys: List[str]) -> None`
522
+
523
+ Press multiple keys together (keyboard shortcut).
524
+
525
+ **Parameters:**
526
+ - `keys` (List[str]): List of keys to press together
527
+
528
+ **Raises:**
529
+ - `RuntimeError`: Not initialized
530
+ - `InputError`: Key combo failed
531
+
532
+ **Example:**
533
+ ```python
534
+ # Common shortcuts
535
+ ctrl.key_combo(["Control", "a"]) # Select all
536
+ ctrl.key_combo(["Control", "c"]) # Copy
537
+ ctrl.key_combo(["Control", "v"]) # Paste
538
+ ctrl.key_combo(["Control", "Shift", "t"]) # Reopen tab
539
+ ctrl.key_combo(["Alt", "Tab"]) # Switch window
540
+ ctrl.key_combo(["Super", "d"]) # Show desktop
541
+ ```
542
+
543
+ ##### `close() -> None`
544
+
545
+ Release resources and close portal session.
546
+
547
+ **Example:**
548
+ ```python
549
+ ctrl = WaylandInput()
550
+ ctrl.initialize(persist_mode=2)
551
+ # ... use controller ...
552
+ ctrl.close() # Clean up
553
+
554
+ # Or use context manager (auto cleanup)
555
+ with WaylandInput() as ctrl:
556
+ ctrl.initialize(persist_mode=2)
557
+ # ... automatically closed
558
+ ```
559
+
560
+ #### Key Names
561
+
562
+ The following key names are supported (case-insensitive aliases normalized automatically):
563
+
564
+ **Special Keys:**
565
+ - `Return`, `Enter` → Enter key
566
+ - `Escape`, `Esc` → Escape
567
+ - `Tab` → Tab
568
+ - `space` → Spacebar
569
+ - `BackSpace` → Backspace
570
+ - `Delete`, `Del` → Delete
571
+
572
+ **Navigation:**
573
+ - `Home`, `End`
574
+ - `Page_Up`, `PageUp`
575
+ - `Page_Down`, `PageDown`
576
+ - `Left`, `Right`, `Up`, `Down` → Arrow keys
577
+
578
+ **Modifiers:**
579
+ - `Control`, `Ctrl` → Control key
580
+ - `Alt` → Alt key
581
+ - `Shift` → Shift key
582
+ - `Super`, `Win`, `Cmd`, `Command` → Super/Windows/Command key
583
+
584
+ **Function Keys:**
585
+ - `F1` through `F12`
586
+
587
+ **Characters:**
588
+ - Any letter: `a`, `b`, `c`, ..., `z`
589
+ - Any number: `0`, `1`, `2`, ..., `9`
590
+ - Symbols: `-`, `=`, `[`, `]`, `;`, `'`, etc.
591
+
592
+ ---
593
+
594
+ ### WaylandCapture (Legacy)
595
+
596
+ **Full path**: `open_alo_core.wayland.capture.WaylandCapture`
597
+
598
+ > **Note:** For new projects, prefer `UnifiedRemoteDesktop` which combines input and capture in one permission dialog.
599
+
600
+ Provides screen capture on Wayland using PipeWire and XDG ScreenCast Portal.
601
+
602
+ #### Constructor
603
+
604
+ ```python
605
+ WaylandCapture()
606
+ ```
607
+
608
+ No parameters needed.
609
+
610
+ **Example:**
611
+ ```python
612
+ capture = WaylandCapture()
613
+
614
+ # Or use context manager
615
+ with WaylandCapture() as capture:
616
+ result = capture.capture_screen()
617
+ ```
618
+
619
+ #### Methods
620
+
621
+ ##### `capture_screen() -> CaptureResult`
622
+
623
+ Capture the screen with user selection.
624
+
625
+ Shows a permission dialog asking the user which screen/window to capture. Returns PNG image data.
626
+
627
+ **Returns:**
628
+ - `CaptureResult`: Object containing:
629
+ - `data` (bytes): PNG image data
630
+ - `source_type` (str): Source type ("monitor", "window", "camera")
631
+ - `size` (Tuple[int, int]): Image dimensions (width, height)
632
+
633
+ **Raises:**
634
+ - `CaptureError`: Capture failed
635
+ - `PermissionDenied`: User denied permission
636
+
637
+ **Example:**
638
+ ```python
639
+ with WaylandCapture() as capture:
640
+ # User selects screen/window
641
+ result = capture.capture_screen()
642
+
643
+ # Save to file
644
+ from pathlib import Path
645
+ Path("/tmp/screenshot.png").write_bytes(result.data)
646
+
647
+ # Get info
648
+ print(f"Captured {result.source_type}")
649
+ print(f"Size: {result.size[0]}x{result.size[1]}")
650
+ print(f"Data: {len(result.data)} bytes")
651
+ ```
652
+
653
+ ##### `close() -> None`
654
+
655
+ Release resources and close session.
656
+
657
+ **Example:**
658
+ ```python
659
+ capture = WaylandCapture()
660
+ result = capture.capture_screen()
661
+ capture.close()
662
+
663
+ # Or use context manager
664
+ with WaylandCapture() as capture:
665
+ result = capture.capture_screen()
666
+ # Automatically closed
667
+ ```
668
+
669
+ #### CaptureResult
670
+
671
+ Result object returned by `capture_screen()`.
672
+
673
+ **Attributes:**
674
+ - `data` (bytes): PNG image data (ready to save or process)
675
+ - `source_type` (str): Type of source captured
676
+ - `"monitor"`: Full screen capture
677
+ - `"window"`: Single window capture
678
+ - `"camera"`: Camera capture (rare)
679
+ - `size` (Tuple[int, int]): Image dimensions as (width, height)
680
+
681
+ **Example:**
682
+ ```python
683
+ result = capture.capture_screen()
684
+
685
+ # Save to file
686
+ with open("/tmp/shot.png", "wb") as f:
687
+ f.write(result.data)
688
+
689
+ # Load with PIL
690
+ from PIL import Image
691
+ from io import BytesIO
692
+ img = Image.open(BytesIO(result.data))
693
+
694
+ # Get dimensions
695
+ width, height = result.size
696
+ ```
697
+
698
+ ---
699
+
700
+ ### WindowManager
701
+
702
+ **Full path**: `open_alo_core.window_manager.WindowManager`
703
+
704
+ Comprehensive window management for GNOME Shell via D-Bus.
705
+
706
+ **Requirements:**
707
+ - GNOME Shell with Wayland or X11
708
+ - [Window Calls Extension](https://extensions.gnome.org/extension/4724/window-calls/) installed and enabled
709
+
710
+ #### Constructor
711
+
712
+ ```python
713
+ WindowManager(timeout: int = 5)
714
+ ```
715
+
716
+ **Parameters:**
717
+ - `timeout` (int): Default timeout for D-Bus calls in seconds (default: 5)
718
+
719
+ **Raises:**
720
+ - `RuntimeError`: Window Calls extension not available
721
+
722
+ **Example:**
723
+ ```python
724
+ wm = WindowManager() # Default 5s timeout
725
+ wm = WindowManager(timeout=10) # Custom timeout
726
+ ```
727
+
728
+ #### Window Listing & Search Methods
729
+
730
+ ##### `list_windows(current_workspace_only: bool = False) -> List[WindowInfo]`
731
+
732
+ List all open windows.
733
+
734
+ **Parameters:**
735
+ - `current_workspace_only` (bool): Only return windows in current workspace (default: False)
736
+
737
+ **Returns:**
738
+ - `List[WindowInfo]`: List of window information objects
739
+
740
+ **Example:**
741
+ ```python
742
+ # All windows
743
+ all_windows = wm.list_windows()
744
+ for win in all_windows:
745
+ print(f"{win.wm_class}: {win.title}")
746
+
747
+ # Current workspace only
748
+ current = wm.list_windows(current_workspace_only=True)
749
+ print(f"Windows on current workspace: {len(current)}")
750
+ ```
751
+
752
+ ##### `find_window(query: str, match_title: bool = True) -> Optional[WindowInfo]`
753
+
754
+ Find first window matching query.
755
+
756
+ **Parameters:**
757
+ - `query` (str): Search string (case-insensitive)
758
+ - `match_title` (bool): Also search in window titles (default: True)
759
+
760
+ **Returns:**
761
+ - `WindowInfo`: First matching window, or `None` if not found
762
+
763
+ **Example:**
764
+ ```python
765
+ # Find by wm_class (fast)
766
+ editor = wm.find_window("gedit")
767
+
768
+ # Find by title
769
+ browser = wm.find_window("Google Chrome", match_title=True)
770
+
771
+ # wm_class only (faster, skips title search)
772
+ terminal = wm.find_window("gnome-terminal", match_title=False)
773
+
774
+ # Case-insensitive
775
+ vscode = wm.find_window("CODE") # Finds "code"
776
+ ```
777
+
778
+ ##### `find_all_windows(query: str, match_title: bool = True) -> List[WindowInfo]`
779
+
780
+ Find all windows matching query.
781
+
782
+ **Parameters:**
783
+ - `query` (str): Search string (case-insensitive)
784
+ - `match_title` (bool): Also search in window titles (default: True)
785
+
786
+ **Returns:**
787
+ - `List[WindowInfo]`: All matching windows
788
+
789
+ **Example:**
790
+ ```python
791
+ # Find all terminal windows
792
+ terminals = wm.find_all_windows("terminal")
793
+ print(f"Found {len(terminals)} terminal windows")
794
+
795
+ # Find all browser windows
796
+ browsers = wm.find_all_windows("browser")
797
+ ```
798
+
799
+ ##### `get_focused_window() -> Optional[WindowInfo]`
800
+
801
+ Get currently focused window.
802
+
803
+ **Returns:**
804
+ - `WindowInfo`: Focused window, or `None` if none
805
+
806
+ **Example:**
807
+ ```python
808
+ focused = wm.get_focused_window()
809
+ if focused:
810
+ print(f"Currently focused: {focused.wm_class}")
811
+ print(f"Title: {focused.title}")
812
+ else:
813
+ print("No window focused")
814
+ ```
815
+
816
+ ##### `get_details(window_id: int) -> Optional[Dict]`
817
+
818
+ Get detailed information about a window.
819
+
820
+ **Parameters:**
821
+ - `window_id` (int): Window ID
822
+
823
+ **Returns:**
824
+ - `Dict`: Dictionary with detailed properties, or `None` if failed
825
+
826
+ **Properties returned:**
827
+ - `wm_class`, `wm_class_instance`, `pid`, `id`
828
+ - `x`, `y`, `width`, `height`
829
+ - `maximized`, `focus`, `in_current_workspace`
830
+ - `moveable`, `resizeable`, `canclose`, `canmaximize`, `canminimize`, `canshade`
831
+ - `frame_type`, `window_type`, `layer`, `monitor`
832
+ - `role`, `display`, `area`, `area_all`, `area_cust`
833
+
834
+ **Example:**
835
+ ```python
836
+ details = wm.get_details(window_id)
837
+ if details:
838
+ print(f"Maximized: {details['maximized']}")
839
+ print(f"Can maximize: {details['canmaximize']}")
840
+ print(f"Moveable: {details['moveable']}")
841
+ ```
842
+
843
+ ##### `get_title(window_id: int) -> Optional[str]`
844
+
845
+ Get window title by ID.
846
+
847
+ **Parameters:**
848
+ - `window_id` (int): Window ID
849
+
850
+ **Returns:**
851
+ - `str`: Window title, or `None` if failed
852
+
853
+ **Example:**
854
+ ```python
855
+ title = wm.get_title(window_id)
856
+ print(f"Window title: {title}")
857
+ ```
858
+
859
+ #### Window State Management Methods
860
+
861
+ ##### `activate(window_id: int) -> bool`
862
+
863
+ Activate (focus) a window.
864
+
865
+ **Parameters:**
866
+ - `window_id` (int): Window ID
867
+
868
+ **Returns:**
869
+ - `bool`: True if successful
870
+
871
+ **Example:**
872
+ ```python
873
+ editor = wm.find_window("Text Editor")
874
+ if editor:
875
+ wm.activate(editor.id)
876
+ ```
877
+
878
+ ##### `maximize(window_id: int) -> bool`
879
+
880
+ Maximize a window.
881
+
882
+ **Parameters:**
883
+ - `window_id` (int): Window ID
884
+
885
+ **Returns:**
886
+ - `bool`: True if successful
887
+
888
+ **Example:**
889
+ ```python
890
+ wm.maximize(window_id)
891
+ ```
892
+
893
+ ##### `unmaximize(window_id: int) -> bool`
894
+
895
+ Unmaximize (restore) a window.
896
+
897
+ **Parameters:**
898
+ - `window_id` (int): Window ID
899
+
900
+ **Returns:**
901
+ - `bool`: True if successful
902
+
903
+ **Example:**
904
+ ```python
905
+ wm.unmaximize(window_id)
906
+ ```
907
+
908
+ ##### `minimize(window_id: int) -> bool`
909
+
910
+ Minimize a window.
911
+
912
+ **Parameters:**
913
+ - `window_id` (int): Window ID
914
+
915
+ **Returns:**
916
+ - `bool`: True if successful
917
+
918
+ **Example:**
919
+ ```python
920
+ wm.minimize(window_id)
921
+ ```
922
+
923
+ ##### `unminimize(window_id: int) -> bool`
924
+
925
+ Unminimize (restore) a window.
926
+
927
+ **Parameters:**
928
+ - `window_id` (int): Window ID
929
+
930
+ **Returns:**
931
+ - `bool`: True if successful
932
+
933
+ **Example:**
934
+ ```python
935
+ wm.unminimize(window_id)
936
+ ```
937
+
938
+ ##### `close(window_id: int) -> bool`
939
+
940
+ Close a window.
941
+
942
+ **Parameters:**
943
+ - `window_id` (int): Window ID
944
+
945
+ **Returns:**
946
+ - `bool`: True if successful
947
+
948
+ **Example:**
949
+ ```python
950
+ # Close a window
951
+ if wm.close(window_id):
952
+ print("Window closed")
953
+ ```
954
+
955
+ #### Window Positioning Methods
956
+
957
+ ##### `move(window_id: int, x: int, y: int) -> bool`
958
+
959
+ Move window to position.
960
+
961
+ **Parameters:**
962
+ - `window_id` (int): Window ID
963
+ - `x` (int): X coordinate (can be negative)
964
+ - `y` (int): Y coordinate (can be negative)
965
+
966
+ **Returns:**
967
+ - `bool`: True if successful
968
+
969
+ **Example:**
970
+ ```python
971
+ # Move to top-left
972
+ wm.move(window_id, 0, 0)
973
+
974
+ # Negative coordinates supported (off-screen)
975
+ wm.move(window_id, -100, -50)
976
+ ```
977
+
978
+ ##### `resize(window_id: int, width: int, height: int) -> bool`
979
+
980
+ Resize window.
981
+
982
+ **Parameters:**
983
+ - `window_id` (int): Window ID
984
+ - `width` (int): New width in pixels
985
+ - `height` (int): New height in pixels
986
+
987
+ **Returns:**
988
+ - `bool`: True if successful
989
+
990
+ **Example:**
991
+ ```python
992
+ # Standard HD resolution
993
+ wm.resize(window_id, 1920, 1080)
994
+
995
+ # Custom size
996
+ wm.resize(window_id, 800, 600)
997
+ ```
998
+
999
+ ##### `move_resize(window_id: int, x: int, y: int, width: int, height: int) -> bool`
1000
+
1001
+ Move and resize window in one operation (more efficient).
1002
+
1003
+ **Parameters:**
1004
+ - `window_id` (int): Window ID
1005
+ - `x` (int): X coordinate
1006
+ - `y` (int): Y coordinate
1007
+ - `width` (int): Width in pixels
1008
+ - `height` (int): Height in pixels
1009
+
1010
+ **Returns:**
1011
+ - `bool`: True if successful
1012
+
1013
+ **Example:**
1014
+ ```python
1015
+ # Position at (100, 100) with size 1920x1080
1016
+ wm.move_resize(window_id, 100, 100, 1920, 1080)
1017
+
1018
+ # Tile left half of 1920x1080 screen
1019
+ wm.move_resize(window_id, 0, 0, 960, 1080)
1020
+ ```
1021
+
1022
+ ##### `get_frame_rect(window_id: int) -> Optional[Dict]`
1023
+
1024
+ Get window frame rectangle.
1025
+
1026
+ **Parameters:**
1027
+ - `window_id` (int): Window ID
1028
+
1029
+ **Returns:**
1030
+ - `Dict`: Dictionary with `x`, `y`, `width`, `height`, or `None` if failed
1031
+
1032
+ **Example:**
1033
+ ```python
1034
+ frame = wm.get_frame_rect(window_id)
1035
+ if frame:
1036
+ print(f"Position: ({frame['x']}, {frame['y']})")
1037
+ print(f"Size: {frame['width']}x{frame['height']}")
1038
+ ```
1039
+
1040
+ ##### `get_frame_bounds(window_id: int) -> Optional[Dict]`
1041
+
1042
+ Get window frame bounds (may not work in GNOME 43+).
1043
+
1044
+ **Parameters:**
1045
+ - `window_id` (int): Window ID
1046
+
1047
+ **Returns:**
1048
+ - `Dict`: Frame bounds dictionary, or `None` if failed
1049
+
1050
+ **Example:**
1051
+ ```python
1052
+ bounds = wm.get_frame_bounds(window_id)
1053
+ ```
1054
+
1055
+ #### Workspace Management Methods
1056
+
1057
+ ##### `move_to_workspace(window_id: int, workspace_num: int) -> bool`
1058
+
1059
+ Move window to different workspace.
1060
+
1061
+ **Parameters:**
1062
+ - `window_id` (int): Window ID
1063
+ - `workspace_num` (int): Target workspace number (0-indexed)
1064
+
1065
+ **Returns:**
1066
+ - `bool`: True if successful
1067
+
1068
+ **Example:**
1069
+ ```python
1070
+ # Move to workspace 0 (first workspace)
1071
+ wm.move_to_workspace(window_id, 0)
1072
+
1073
+ # Move to workspace 2 (third workspace)
1074
+ wm.move_to_workspace(window_id, 2)
1075
+ ```
1076
+
1077
+ ---
1078
+
1079
+ ## Types
1080
+
1081
+ ### Point
1082
+
1083
+ **Full path**: `open_alo_core.types.Point`
1084
+
1085
+ 2D screen coordinates.
1086
+
1087
+ **Definition:**
1088
+ ```python
1089
+ class Point(NamedTuple):
1090
+ x: int
1091
+ y: int
1092
+ ```
1093
+
1094
+ **Example:**
1095
+ ```python
1096
+ from open_alo_core import Point
1097
+
1098
+ # Create point
1099
+ p = Point(500, 500)
1100
+ print(p.x, p.y) # 500 500
1101
+
1102
+ # Use in click
1103
+ ctrl.click(Point(100, 200))
1104
+
1105
+ # Immutable
1106
+ p = Point(10, 20)
1107
+ # p.x = 30 # Error: cannot modify NamedTuple
1108
+ ```
1109
+
1110
+ ---
1111
+
1112
+ ### Size
1113
+
1114
+ **Full path**: `open_alo_core.types.Size`
1115
+
1116
+ Width and height dimensions.
1117
+
1118
+ **Definition:**
1119
+ ```python
1120
+ class Size(NamedTuple):
1121
+ width: int
1122
+ height: int
1123
+ ```
1124
+
1125
+ **Example:**
1126
+ ```python
1127
+ from open_alo_core import Size
1128
+
1129
+ size = Size(1920, 1080)
1130
+ print(f"{size.width}x{size.height}") # 1920x1080
1131
+ ```
1132
+
1133
+ ---
1134
+
1135
+ ### Rect
1136
+
1137
+ **Full path**: `open_alo_core.types.Rect`
1138
+
1139
+ Rectangle with position and size.
1140
+
1141
+ **Definition:**
1142
+ ```python
1143
+ class Rect(NamedTuple):
1144
+ x: int
1145
+ y: int
1146
+ width: int
1147
+ height: int
1148
+ ```
1149
+
1150
+ **Properties:**
1151
+ - `center` → Point: Center point of rectangle
1152
+ - `top_left` → Point: Top-left corner
1153
+ - `bottom_right` → Point: Bottom-right corner
1154
+
1155
+ **Methods:**
1156
+ - `contains(point: Point) -> bool`: Check if point is inside rectangle
1157
+
1158
+ **Example:**
1159
+ ```python
1160
+ from open_alo_core import Rect, Point
1161
+
1162
+ rect = Rect(100, 100, 800, 600)
1163
+
1164
+ # Get center
1165
+ center = rect.center # Point(500, 400)
1166
+
1167
+ # Get corners
1168
+ top_left = rect.top_left # Point(100, 100)
1169
+ bottom_right = rect.bottom_right # Point(900, 700)
1170
+
1171
+ # Check if point inside
1172
+ point = Point(500, 400)
1173
+ if rect.contains(point):
1174
+ print("Point is inside rectangle")
1175
+ ```
1176
+
1177
+ ---
1178
+
1179
+ ### WindowInfo
1180
+
1181
+ **Full path**: `open_alo_core.window_manager.WindowInfo`
1182
+
1183
+ Window information container (dataclass).
1184
+
1185
+ **Attributes:**
1186
+ - `id` (int): Window ID (unique during window lifetime)
1187
+ - `wm_class` (str): Application class (e.g., "org.gnome.Nautilus")
1188
+ - `wm_class_instance` (str): Class instance
1189
+ - `title` (str): Window title
1190
+ - `pid` (int): Process ID
1191
+ - `x` (int): X position
1192
+ - `y` (int): Y position
1193
+ - `width` (int): Width in pixels
1194
+ - `height` (int): Height in pixels
1195
+ - `workspace` (int): Workspace number (0-indexed)
1196
+ - `monitor` (int): Monitor number
1197
+ - `frame_type` (int): Frame type (0=normal, 1=frameless)
1198
+ - `window_type` (int): Window type (0=normal, 1=desktop, 2=dock, etc.)
1199
+ - `focus` (bool): Currently focused
1200
+ - `in_current_workspace` (bool): In current workspace
1201
+ - `maximized` (int): Maximized state
1202
+
1203
+ **Example:**
1204
+ ```python
1205
+ windows = wm.list_windows()
1206
+ for win in windows:
1207
+ print(f"ID: {win.id}")
1208
+ print(f"Class: {win.wm_class}")
1209
+ print(f"Title: {win.title}")
1210
+ print(f"Position: ({win.x}, {win.y})")
1211
+ print(f"Size: {win.width}x{win.height}")
1212
+ print(f"Focused: {win.focus}")
1213
+ print(f"Workspace: {win.workspace}")
1214
+ ```
1215
+
1216
+ ---
1217
+
1218
+ ### WindowType
1219
+
1220
+ **Full path**: `open_alo_core.window_manager.WindowType`
1221
+
1222
+ Window type enumeration.
1223
+
1224
+ **Values:**
1225
+ - `NORMAL = 0`: Normal application window
1226
+ - `DESKTOP = 1`: Desktop window
1227
+ - `DOCK = 2`: Dock/panel window
1228
+ - `DIALOG = 3`: Dialog window
1229
+ - `MODAL_DIALOG = 4`: Modal dialog
1230
+ - `TOOLBAR = 5`: Toolbar window
1231
+ - `MENU = 6`: Menu window
1232
+ - `UTILITY = 7`: Utility window
1233
+ - `SPLASH = 8`: Splash screen
1234
+
1235
+ **Example:**
1236
+ ```python
1237
+ from open_alo_core import WindowType
1238
+
1239
+ windows = wm.list_windows()
1240
+ normal_windows = [w for w in windows if w.window_type == WindowType.NORMAL]
1241
+ ```
1242
+
1243
+ ---
1244
+
1245
+ ### FrameType
1246
+
1247
+ **Full path**: `open_alo_core.window_manager.FrameType`
1248
+
1249
+ Window frame type enumeration.
1250
+
1251
+ **Values:**
1252
+ - `NORMAL = 0`: Normal window with decorations
1253
+ - `FRAMELESS = 1`: Frameless window (no decorations)
1254
+
1255
+ **Example:**
1256
+ ```python
1257
+ from open_alo_core import FrameType
1258
+
1259
+ windows = wm.list_windows()
1260
+ framed = [w for w in windows if w.frame_type == FrameType.NORMAL]
1261
+ ```
1262
+
1263
+ ---
1264
+
1265
+ ## Exceptions
1266
+
1267
+ All exceptions inherit from `CoreError` for easy catching.
1268
+
1269
+ ### CoreError
1270
+
1271
+ **Full path**: `open_alo_core.exceptions.CoreError`
1272
+
1273
+ Base exception for all core errors.
1274
+
1275
+ **Example:**
1276
+ ```python
1277
+ from open_alo_core import CoreError
1278
+
1279
+ try:
1280
+ # ... automation code ...
1281
+ pass
1282
+ except CoreError as e:
1283
+ print(f"Core error: {e}")
1284
+ ```
1285
+
1286
+ ### PermissionDenied
1287
+
1288
+ **Full path**: `open_alo_core.exceptions.PermissionDenied`
1289
+
1290
+ User denied portal permission or insufficient privileges.
1291
+
1292
+ **Raised by:**
1293
+ - `WaylandInput.initialize()`
1294
+ - `WaylandCapture.capture_screen()`
1295
+
1296
+ **Example:**
1297
+ ```python
1298
+ from open_alo_core import PermissionDenied
1299
+
1300
+ try:
1301
+ ctrl.initialize(persist_mode=2)
1302
+ except PermissionDenied:
1303
+ print("User denied permission")
1304
+ ```
1305
+
1306
+ ### CaptureError
1307
+
1308
+ **Full path**: `open_alo_core.exceptions.CaptureError`
1309
+
1310
+ Screen capture failed.
1311
+
1312
+ **Raised by:**
1313
+ - `WaylandCapture.capture_screen()`
1314
+
1315
+ **Example:**
1316
+ ```python
1317
+ from open_alo_core import CaptureError
1318
+
1319
+ try:
1320
+ result = capture.capture_screen()
1321
+ except CaptureError as e:
1322
+ print(f"Capture failed: {e}")
1323
+ ```
1324
+
1325
+ ### InputError
1326
+
1327
+ **Full path**: `open_alo_core.exceptions.InputError`
1328
+
1329
+ Input injection failed.
1330
+
1331
+ **Raised by:**
1332
+ - `WaylandInput.click()`
1333
+ - `WaylandInput.move_mouse()`
1334
+ - `WaylandInput.type_text()`
1335
+ - `WaylandInput.press_key()`
1336
+ - `WaylandInput.key_combo()`
1337
+
1338
+ **Example:**
1339
+ ```python
1340
+ from open_alo_core import InputError
1341
+
1342
+ try:
1343
+ ctrl.click(Point(500, 500))
1344
+ except InputError as e:
1345
+ print(f"Click failed: {e}")
1346
+ ```
1347
+
1348
+ ### SessionError
1349
+
1350
+ **Full path**: `open_alo_core.exceptions.SessionError`
1351
+
1352
+ Portal session creation/management failed.
1353
+
1354
+ **Raised by:**
1355
+ - `WaylandInput.initialize()`
1356
+
1357
+ **Example:**
1358
+ ```python
1359
+ from open_alo_core import SessionError
1360
+
1361
+ try:
1362
+ ctrl.initialize(persist_mode=2)
1363
+ except SessionError as e:
1364
+ print(f"Session error: {e}")
1365
+ ```
1366
+
1367
+ ### BackendNotAvailable
1368
+
1369
+ **Full path**: `open_alo_core.exceptions.BackendNotAvailable`
1370
+
1371
+ Requested backend not available on this system.
1372
+
1373
+ **Example:**
1374
+ ```python
1375
+ from open_alo_core import BackendNotAvailable
1376
+
1377
+ try:
1378
+ # ... initialization ...
1379
+ pass
1380
+ except BackendNotAvailable:
1381
+ print("Backend not available on this system")
1382
+ ```
1383
+
1384
+ ---
1385
+
1386
+ ## Utilities
1387
+
1388
+ ### detect_session_type()
1389
+
1390
+ **Full path**: `open_alo_core.utils.detect_session_type`
1391
+
1392
+ Detect if running in Wayland or X11 session.
1393
+
1394
+ **Returns:**
1395
+ - `"wayland"`: Running on Wayland
1396
+ - `"x11"`: Running on X11
1397
+ - `"unknown"`: Cannot determine
1398
+
1399
+ **Example:**
1400
+ ```python
1401
+ from open_alo_core import detect_session_type
1402
+
1403
+ session = detect_session_type()
1404
+ if session == "wayland":
1405
+ print("Using Wayland backend")
1406
+ elif session == "x11":
1407
+ print("Using X11 backend")
1408
+ else:
1409
+ print("Unknown session type")
1410
+ ```
1411
+
1412
+ ---
1413
+
1414
+ ### is_wayland()
1415
+
1416
+ **Full path**: `open_alo_core.utils.is_wayland`
1417
+
1418
+ Check if running on Wayland.
1419
+
1420
+ **Returns:**
1421
+ - `bool`: True if WAYLAND_DISPLAY environment variable is set
1422
+
1423
+ **Example:**
1424
+ ```python
1425
+ from open_alo_core import is_wayland
1426
+
1427
+ if is_wayland():
1428
+ ctrl = WaylandInput()
1429
+ else:
1430
+ raise RuntimeError("Wayland required")
1431
+ ```
1432
+
1433
+ ---
1434
+
1435
+ ### is_portal_available()
1436
+
1437
+ **Full path**: `open_alo_core.utils.is_portal_available`
1438
+
1439
+ Check if XDG Desktop Portal is available.
1440
+
1441
+ **Returns:**
1442
+ - `bool`: True if portal service is running
1443
+
1444
+ **Example:**
1445
+ ```python
1446
+ from open_alo_core import is_portal_available
1447
+
1448
+ if not is_portal_available():
1449
+ print("Warning: Portal not available")
1450
+ print("Cannot initialize input/capture")
1451
+ ```
1452
+
1453
+ ---
1454
+
1455
+ ### is_pipewire_available()
1456
+
1457
+ **Full path**: `open_alo_core.utils.is_pipewire_available`
1458
+
1459
+ Check if PipeWire is available for screen capture.
1460
+
1461
+ **Returns:**
1462
+ - `bool`: True if PipeWire is running
1463
+
1464
+ **Example:**
1465
+ ```python
1466
+ from open_alo_core import is_pipewire_available
1467
+
1468
+ if is_pipewire_available():
1469
+ print("PipeWire available for capture")
1470
+ else:
1471
+ print("Warning: PipeWire not available")
1472
+ ```
1473
+
1474
+ ---
1475
+
1476
+ ### normalize_key()
1477
+
1478
+ **Full path**: `open_alo_core.types.normalize_key`
1479
+
1480
+ Normalize key name to standard form.
1481
+
1482
+ Converts common key aliases to GTK/GDK standard names.
1483
+
1484
+ **Parameters:**
1485
+ - `key` (str): Key name to normalize
1486
+
1487
+ **Returns:**
1488
+ - `str`: Normalized key name
1489
+
1490
+ **Example:**
1491
+ ```python
1492
+ from open_alo_core import normalize_key
1493
+
1494
+ # Aliases normalized
1495
+ normalize_key("enter") # → "Return"
1496
+ normalize_key("ctrl") # → "Control"
1497
+ normalize_key("esc") # → "Escape"
1498
+ normalize_key("win") # → "Super"
1499
+
1500
+ # Already normalized
1501
+ normalize_key("Return") # → "Return"
1502
+ ```
1503
+
1504
+ ---
1505
+
1506
+ ## Constants
1507
+
1508
+ ### Mouse Buttons
1509
+
1510
+ **Module**: `open_alo_core.types`
1511
+
1512
+ ```python
1513
+ BUTTON_LEFT = 1 # Left mouse button
1514
+ BUTTON_MIDDLE = 2 # Middle mouse button
1515
+ BUTTON_RIGHT = 3 # Right mouse button
1516
+ ```
1517
+
1518
+ **Example:**
1519
+ ```python
1520
+ from open_alo_core import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT
1521
+
1522
+ ctrl.click(Point(500, 500), button=BUTTON_LEFT) # Left click
1523
+ ctrl.click(Point(500, 500), button=BUTTON_MIDDLE) # Middle click
1524
+ ctrl.click(Point(500, 500), button=BUTTON_RIGHT) # Right click
1525
+ ```
1526
+
1527
+ ---
1528
+
1529
+ ## Complete API Index
1530
+
1531
+ ### Classes
1532
+ - `UnifiedRemoteDesktop` ⭐ **RECOMMENDED** - Single permission for input + capture
1533
+ - `WaylandInput` (Legacy) - Mouse and keyboard control
1534
+ - `WaylandCapture` (Legacy) - Screen capture
1535
+ - `WindowManager` - Window management
1536
+ - `Point` - 2D coordinates
1537
+ - `Size` - Dimensions
1538
+ - `Rect` - Rectangle with position and size
1539
+ - `WindowInfo` - Window information
1540
+ - `WindowType` - Window type enumeration
1541
+ - `FrameType` - Frame type enumeration
1542
+ - `CaptureResult` - Capture result container
1543
+
1544
+ ### Exceptions
1545
+ - `CoreError` - Base exception
1546
+ - `PermissionDenied` - Permission denied
1547
+ - `CaptureError` - Capture failed
1548
+ - `InputError` - Input failed
1549
+ - `SessionError` - Session failed
1550
+ - `BackendNotAvailable` - Backend unavailable
1551
+
1552
+ ### Functions
1553
+ - `detect_session_type()` - Detect Wayland/X11
1554
+ - `is_wayland()` - Check if Wayland
1555
+ - `is_portal_available()` - Check portal availability
1556
+ - `is_pipewire_available()` - Check PipeWire availability
1557
+ - `normalize_key()` - Normalize key name
1558
+ - `list_windows()` - List windows (convenience)
1559
+ - `find_window()` - Find window (convenience)
1560
+ - `activate_window()` - Activate window (convenience)
1561
+ - `get_focused_window()` - Get focused window (convenience)
1562
+
1563
+ ### Constants
1564
+ - `BUTTON_LEFT` - Left mouse button (1)
1565
+ - `BUTTON_MIDDLE` - Middle mouse button (2)
1566
+ - `BUTTON_RIGHT` - Right mouse button (3)
1567
+
1568
+ ### UnifiedRemoteDesktop Methods (Recommended) ⭐
1569
+ - `initialize(persist_mode, enable_capture)` → bool - Initialize with one dialog
1570
+ - `capture_screenshot()` → bytes - Take PNG screenshot
1571
+ - `get_frame()` → bytes - Get real-time frame
1572
+ - `get_screen_size()` → (int, int) - Get resolution
1573
+ - `click(point, button)` - Click at coordinates
1574
+ - `move_mouse(point)` - Move cursor
1575
+ - `type_text(text, interval)` - Type text
1576
+ - `press_key(key)` - Press single key
1577
+ - `key_combo(keys)` - Press key combination
1578
+ - `close()` - Release resources
1579
+
1580
+ ### WaylandInput Methods (Legacy)
1581
+ - `initialize(persist_mode)` - Initialize session
1582
+ - `click(point, button)` - Click at coordinates
1583
+ - `move_mouse(point)` - Move cursor
1584
+ - `type_text(text, interval)` - Type text
1585
+ - `press_key(key)` - Press single key
1586
+ - `key_combo(keys)` - Press key combination
1587
+ - `close()` - Release resources
1588
+
1589
+ ### WaylandCapture Methods (Legacy)
1590
+ - `capture_screen()` - Capture screen
1591
+ - `close()` - Release resources
1592
+
1593
+ ### WindowManager Methods
1594
+
1595
+ **Listing & Search:**
1596
+ - `list_windows(current_workspace_only)` - List windows
1597
+ - `find_window(query, match_title)` - Find window
1598
+ - `find_all_windows(query, match_title)` - Find all matching
1599
+ - `get_focused_window()` - Get focused window
1600
+ - `get_details(window_id)` - Get detailed info
1601
+ - `get_title(window_id)` - Get window title
1602
+
1603
+ **State Management:**
1604
+ - `activate(window_id)` - Activate window
1605
+ - `maximize(window_id)` - Maximize
1606
+ - `unmaximize(window_id)` - Unmaximize
1607
+ - `minimize(window_id)` - Minimize
1608
+ - `unminimize(window_id)` - Unminimize
1609
+ - `close(window_id)` - Close window
1610
+
1611
+ **Positioning:**
1612
+ - `move(window_id, x, y)` - Move window
1613
+ - `resize(window_id, width, height)` - Resize window
1614
+ - `move_resize(window_id, x, y, width, height)` - Move and resize
1615
+ - `get_frame_rect(window_id)` - Get frame rectangle
1616
+ - `get_frame_bounds(window_id)` - Get frame bounds
1617
+
1618
+ **Workspace:**
1619
+ - `move_to_workspace(window_id, workspace_num)` - Move to workspace
1620
+
1621
+ ---
1622
+
1623
+ ## Advanced Usage Examples
1624
+
1625
+ ### Complete Workflow
1626
+
1627
+ ```python
1628
+ from open_alo_core import (
1629
+ WaylandInput, WaylandCapture, WindowManager,
1630
+ Point, activate_window
1631
+ )
1632
+
1633
+ # 1. Activate target application
1634
+ activate_window("Text Editor")
1635
+
1636
+ # 2. Control input
1637
+ with WaylandInput() as ctrl:
1638
+ ctrl.initialize(persist_mode=2)
1639
+
1640
+ # Click and type
1641
+ ctrl.click(Point(500, 500))
1642
+ ctrl.type_text("Hello World!")
1643
+
1644
+ # Save with Ctrl+S
1645
+ ctrl.key_combo(["Control", "s"])
1646
+
1647
+ # 3. Capture screen
1648
+ with WaylandCapture() as capture:
1649
+ result = capture.capture_screen()
1650
+ Path("/tmp/screenshot.png").write_bytes(result.data)
1651
+
1652
+ print("Workflow complete!")
1653
+ ```
1654
+
1655
+ ### Window Tiling
1656
+
1657
+ ```python
1658
+ from open_alo_core import WindowManager
1659
+
1660
+ wm = WindowManager()
1661
+
1662
+ # Get windows
1663
+ editor = wm.find_window("Text Editor")
1664
+ browser = wm.find_window("Brave")
1665
+
1666
+ if editor and browser:
1667
+ # Tile left half (1920x1080 screen)
1668
+ wm.move_resize(editor.id, 0, 0, 960, 1080)
1669
+
1670
+ # Tile right half
1671
+ wm.move_resize(browser.id, 960, 0, 960, 1080)
1672
+ ```
1673
+
1674
+ ### Automated Testing
1675
+
1676
+ ```python
1677
+ from open_alo_core import WaylandInput, WaylandCapture, Point
1678
+ import time
1679
+
1680
+ with WaylandInput() as ctrl:
1681
+ ctrl.initialize(persist_mode=2)
1682
+
1683
+ # Test workflow
1684
+ steps = [
1685
+ (Point(100, 100), "Click button"),
1686
+ (Point(200, 200), "Fill form"),
1687
+ ]
1688
+
1689
+ for point, description in steps:
1690
+ print(f"Step: {description}")
1691
+ ctrl.click(point)
1692
+ time.sleep(1)
1693
+
1694
+ # Capture result
1695
+ with WaylandCapture() as cap:
1696
+ result = cap.capture_screen()
1697
+ Path(f"/tmp/{description}.png").write_bytes(result.data)
1698
+ ```
1699
+
1700
+ ---
1701
+
1702
+ ## Version History
1703
+
1704
+ ### 0.1.0 (Current)
1705
+ - Initial release
1706
+ - Wayland MVP
1707
+ - Input control via RemoteDesktop Portal
1708
+ - Screen capture via ScreenCast Portal
1709
+ - Window management via GNOME Shell D-Bus
1710
+ - Persistent permission tokens
1711
+ - Type-safe API with NamedTuples and dataclasses
1712
+
1713
+ ---
1714
+
1715
+ ## License
1716
+
1717
+ MIT License - See LICENSE file for details
1718
+
1719
+ ---
1720
+
1721
+ ## Contributing
1722
+
1723
+ Contributions welcome! Please ensure:
1724
+ - Type hints for all public APIs
1725
+ - Docstrings with examples
1726
+ - Error handling with appropriate exceptions
1727
+ - Tests for new functionality
1728
+
1729
+ ---
1730
+
1731
+ ## Support
1732
+
1733
+ - **Issues**: GitHub Issues
1734
+ - **Documentation**: This file
1735
+ - **Examples**: `examples/` directory