codeaway 0.1.0__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.
codeaway/desktop.py ADDED
@@ -0,0 +1,644 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ import math
4
+ from numbers import Real
5
+ from typing import Any, Protocol
6
+ from uuid import uuid4
7
+
8
+ from PIL import Image, ImageGrab
9
+
10
+
11
+ def _pin_thread_v2_dpi(user32: Any | None = None) -> bool:
12
+ """Make Win32 rectangles and screen captures use physical pixels."""
13
+ import ctypes
14
+ import sys
15
+
16
+ if user32 is None:
17
+ if not sys.platform.startswith("win"):
18
+ return False
19
+ user32 = ctypes.windll.user32
20
+ try:
21
+ setter = user32.SetThreadDpiAwarenessContext
22
+ setter.argtypes = (ctypes.c_void_p,)
23
+ setter.restype = ctypes.c_void_p
24
+ return bool(setter(ctypes.c_void_p(-4))) # PER_MONITOR_AWARE_V2
25
+ except Exception:
26
+ return False
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class PixelPoint:
31
+ x: int
32
+ y: int
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class PixelRegion:
37
+ x: int
38
+ y: int
39
+ width: int
40
+ height: int
41
+
42
+ @property
43
+ def center(self) -> PixelPoint:
44
+ return PixelPoint(self.x + self.width // 2, self.y + self.height // 2)
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class FractionalRegion:
49
+ x: float
50
+ y: float
51
+ width: float
52
+ height: float
53
+
54
+ def __post_init__(self) -> None:
55
+ values = (self.x, self.y, self.width, self.height)
56
+ if any(isinstance(value, bool) or not isinstance(value, Real) for value in values):
57
+ raise TypeError("fractional region values must be numbers")
58
+ if any(not math.isfinite(value) for value in values):
59
+ raise ValueError("fractional region values must be finite")
60
+ if not 0 <= self.x <= 1 or not 0 <= self.y <= 1:
61
+ raise ValueError("fractional region origin must be between 0 and 1")
62
+ if self.width <= 0 or self.height <= 0:
63
+ raise ValueError("fractional region dimensions must be positive")
64
+ if self.width > 1 or self.height > 1:
65
+ raise ValueError("fractional region dimensions must be at most 1")
66
+ if self.x + self.width > 1 or self.y + self.height > 1:
67
+ raise ValueError("fractional region must fit inside its parent")
68
+
69
+ def resolve(self, parent: PixelRegion) -> PixelRegion:
70
+ return PixelRegion(
71
+ parent.x + round(parent.width * self.x),
72
+ parent.y + round(parent.height * self.y),
73
+ round(parent.width * self.width),
74
+ round(parent.height * self.height),
75
+ )
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class DesktopWindow:
80
+ id: str
81
+ native_handle: int
82
+ title: str
83
+ process_path: str
84
+ region: PixelRegion
85
+
86
+
87
+ class AccessibilityAction(str, Enum):
88
+ INVOKE = "invoke"
89
+ EXPAND = "expand"
90
+ COLLAPSE = "collapse"
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class AccessibilityNode:
95
+ id: str
96
+ role: str
97
+ name: str
98
+ class_name: str
99
+ region: PixelRegion
100
+ depth: int = 0
101
+ expanded: bool | None = None
102
+ actions: frozenset[AccessibilityAction] = frozenset()
103
+
104
+
105
+ class InputUnavailable(RuntimeError):
106
+ """Global input was aborted because its exact target was not safe."""
107
+
108
+
109
+ class AccessibilityUnavailable(RuntimeError):
110
+ """The desktop accessibility provider could not produce a control tree."""
111
+
112
+
113
+ class DesktopBackend(Protocol):
114
+ id: str
115
+
116
+ def list_windows(self) -> list[DesktopWindow]: ...
117
+
118
+ def activate(self, window: DesktopWindow) -> bool: ...
119
+
120
+ def is_foreground(self, window: DesktopWindow) -> bool: ...
121
+
122
+ def capture(self, region: PixelRegion) -> Image.Image: ...
123
+
124
+ def accessibility_tree(self, window: DesktopWindow) -> list[AccessibilityNode]: ...
125
+
126
+ def accessibility_action(
127
+ self, node: AccessibilityNode, action: AccessibilityAction
128
+ ) -> None: ...
129
+
130
+ def click(self, window: DesktopWindow, point: PixelPoint) -> None: ...
131
+
132
+ def scroll(
133
+ self, window: DesktopWindow, point: PixelPoint, amount: int
134
+ ) -> None: ...
135
+
136
+ def paste_and_submit(
137
+ self, window: DesktopWindow, point: PixelPoint, text: str
138
+ ) -> None: ...
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class _NativeWindow:
143
+ native_handle: int
144
+ title: str
145
+ process_path: str
146
+ region: PixelRegion
147
+ visible: bool
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class _NativeControl:
152
+ id: str
153
+ role: str
154
+ name: str
155
+ class_name: str
156
+ region: PixelRegion
157
+ depth: int
158
+ expanded: bool | None
159
+ actions: frozenset[AccessibilityAction]
160
+
161
+
162
+ class WindowsDesktop:
163
+ """Windows desktop mechanics behind an injectable native boundary."""
164
+
165
+ id = "windows"
166
+
167
+ def __init__(self, native: "_WindowsNative | None" = None) -> None:
168
+ self._native = native or _WindowsNative()
169
+ self._tree_version = 0
170
+ self._node_controls: dict[str, str] = {}
171
+
172
+ def list_windows(self) -> list[DesktopWindow]:
173
+ _pin_thread_v2_dpi()
174
+ return [
175
+ DesktopWindow(
176
+ id=f"window-{uuid4().hex}",
177
+ native_handle=window.native_handle,
178
+ title=window.title,
179
+ process_path=window.process_path,
180
+ region=window.region,
181
+ )
182
+ for window in self._native.list_windows()
183
+ if window.visible and window.region.width > 0 and window.region.height > 0
184
+ ]
185
+
186
+ def activate(self, window: DesktopWindow) -> bool:
187
+ if not _pin_thread_v2_dpi():
188
+ return False
189
+ return self._native.activate(window.native_handle)
190
+
191
+ def is_foreground(self, window: DesktopWindow) -> bool:
192
+ return self._native.is_foreground(window.native_handle)
193
+
194
+ def capture(self, region: PixelRegion) -> Image.Image:
195
+ _pin_thread_v2_dpi()
196
+ return self._native.capture(
197
+ (region.x, region.y, region.x + region.width, region.y + region.height)
198
+ )
199
+
200
+ def accessibility_tree(self, window: DesktopWindow) -> list[AccessibilityNode]:
201
+ try:
202
+ controls = self._native.accessibility_tree(window.native_handle)
203
+ self._tree_version += 1
204
+ self._node_controls = {}
205
+ nodes: list[AccessibilityNode] = []
206
+ for index, control in enumerate(controls):
207
+ node_id = f"tree-{self._tree_version}-{index}"
208
+ self._node_controls[node_id] = control.id
209
+ nodes.append(
210
+ AccessibilityNode(
211
+ id=node_id,
212
+ role=control.role,
213
+ name=control.name,
214
+ class_name=control.class_name,
215
+ region=control.region,
216
+ depth=control.depth,
217
+ expanded=control.expanded,
218
+ actions=control.actions,
219
+ )
220
+ )
221
+ return nodes
222
+ except AccessibilityUnavailable:
223
+ raise
224
+ except Exception as error:
225
+ raise AccessibilityUnavailable(
226
+ "desktop accessibility is unavailable"
227
+ ) from error
228
+
229
+ def accessibility_action(
230
+ self, node: AccessibilityNode, action: AccessibilityAction
231
+ ) -> None:
232
+ if action not in node.actions:
233
+ raise ValueError(f"accessibility action {action.value!r} is unavailable")
234
+ control_id = self._node_controls.get(node.id)
235
+ if control_id is None:
236
+ raise ValueError("accessibility node is not from the latest tree read")
237
+ self._native.accessibility_action(control_id, action)
238
+
239
+ def _require_foreground(self, window: DesktopWindow) -> None:
240
+ if not self._native.is_foreground(window.native_handle):
241
+ raise InputUnavailable("the exact target window is not foreground")
242
+
243
+ def click(self, window: DesktopWindow, point: PixelPoint) -> None:
244
+ if not _pin_thread_v2_dpi():
245
+ raise InputUnavailable("physical DPI coordinate context is unavailable")
246
+ self._require_foreground(window)
247
+ if not self._native.click(window.native_handle, point.x, point.y):
248
+ raise InputUnavailable(
249
+ "cursor placement or foreground validation failed"
250
+ )
251
+
252
+ def scroll(
253
+ self, window: DesktopWindow, point: PixelPoint, amount: int
254
+ ) -> None:
255
+ if not _pin_thread_v2_dpi():
256
+ raise InputUnavailable("physical DPI coordinate context is unavailable")
257
+ self._require_foreground(window)
258
+ if not self._native.scroll(
259
+ window.native_handle, point.x, point.y, amount * 80
260
+ ):
261
+ raise InputUnavailable(
262
+ "cursor placement or foreground validation failed"
263
+ )
264
+
265
+ def paste_and_submit(
266
+ self, window: DesktopWindow, point: PixelPoint, text: str
267
+ ) -> None:
268
+ self.click(window, point)
269
+ self._native.set_clipboard_text(text)
270
+ self._require_foreground(window)
271
+ if not self._native.send_paste_and_submit(window.native_handle):
272
+ raise InputUnavailable("input injection failed")
273
+
274
+
275
+ class _WindowsNative:
276
+ """The only class that holds Win32 or UI Automation objects."""
277
+
278
+ def __init__(self) -> None:
279
+ self._controls: dict[str, Any] = {}
280
+
281
+ def list_windows(self) -> list[_NativeWindow]:
282
+ import ctypes
283
+ from ctypes import wintypes
284
+
285
+ user32 = ctypes.windll.user32
286
+ windows: list[_NativeWindow] = []
287
+
288
+ @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
289
+ def visit(native_handle: int, _: int) -> bool:
290
+ if not user32.IsWindowVisible(native_handle):
291
+ return True
292
+ rect = wintypes.RECT()
293
+ if not user32.GetWindowRect(native_handle, ctypes.byref(rect)):
294
+ return True
295
+ width = rect.right - rect.left
296
+ height = rect.bottom - rect.top
297
+ if width <= 0 or height <= 0:
298
+ return True
299
+ title_length = user32.GetWindowTextLengthW(native_handle)
300
+ title_buffer = ctypes.create_unicode_buffer(title_length + 1)
301
+ user32.GetWindowTextW(native_handle, title_buffer, len(title_buffer))
302
+ process_id = wintypes.DWORD()
303
+ user32.GetWindowThreadProcessId(native_handle, ctypes.byref(process_id))
304
+ windows.append(
305
+ _NativeWindow(
306
+ int(native_handle),
307
+ title_buffer.value,
308
+ self._process_path(process_id.value),
309
+ PixelRegion(rect.left, rect.top, width, height),
310
+ True,
311
+ )
312
+ )
313
+ return True
314
+
315
+ user32.EnumWindows(visit, 0)
316
+ return windows
317
+
318
+ def _process_path(self, process_id: int) -> str:
319
+ import ctypes
320
+ from ctypes import wintypes
321
+
322
+ kernel32 = ctypes.windll.kernel32
323
+ kernel32.OpenProcess.restype = ctypes.c_void_p
324
+ process_handle = kernel32.OpenProcess(0x1000, False, process_id)
325
+ if not process_handle:
326
+ return ""
327
+ try:
328
+ path = ctypes.create_unicode_buffer(32768)
329
+ path_size = wintypes.DWORD(len(path))
330
+ if not kernel32.QueryFullProcessImageNameW(
331
+ process_handle, 0, path, ctypes.byref(path_size)
332
+ ):
333
+ return ""
334
+ return path.value
335
+ finally:
336
+ kernel32.CloseHandle(process_handle)
337
+
338
+ def activate(self, native_handle: int) -> bool:
339
+ import ctypes
340
+ from ctypes import wintypes
341
+
342
+ user32 = ctypes.windll.user32
343
+ kernel32 = ctypes.windll.kernel32
344
+ user32.GetForegroundWindow.restype = ctypes.c_void_p
345
+ if not user32.IsWindow(native_handle):
346
+ return False
347
+ foreground_handle = user32.GetForegroundWindow()
348
+ foreground_thread = wintypes.DWORD()
349
+ if foreground_handle:
350
+ foreground_thread_id = user32.GetWindowThreadProcessId(
351
+ foreground_handle, ctypes.byref(foreground_thread)
352
+ )
353
+ else:
354
+ foreground_thread_id = 0
355
+ current_thread_id = kernel32.GetCurrentThreadId()
356
+ attached = bool(
357
+ foreground_thread_id
358
+ and foreground_thread_id != current_thread_id
359
+ and user32.AttachThreadInput(foreground_thread_id, current_thread_id, True)
360
+ )
361
+ try:
362
+ user32.SetForegroundWindow(native_handle)
363
+ return user32.GetForegroundWindow() == native_handle
364
+ finally:
365
+ if attached:
366
+ user32.AttachThreadInput(foreground_thread_id, current_thread_id, False)
367
+
368
+ def is_foreground(self, native_handle: int) -> bool:
369
+ import ctypes
370
+
371
+ user32 = ctypes.windll.user32
372
+ user32.GetForegroundWindow.restype = ctypes.c_void_p
373
+ return user32.GetForegroundWindow() == native_handle
374
+
375
+ def capture(self, bounding_box: tuple[int, int, int, int]) -> Image.Image:
376
+ return ImageGrab.grab(bbox=bounding_box, all_screens=True)
377
+
378
+ def accessibility_tree(self, native_handle: int) -> list[_NativeControl]:
379
+ import uiautomation as auto
380
+
381
+ self._controls = {}
382
+ controls: list[_NativeControl] = []
383
+
384
+ def walk(control: Any, depth: int) -> None:
385
+ if depth > 40:
386
+ return
387
+ if not self._is_offscreen(control):
388
+ region = self._control_region(control)
389
+ if region is not None:
390
+ control_id = f"control-{len(controls)}"
391
+ actions, expanded = self._capabilities(control, auto)
392
+ self._controls[control_id] = control
393
+ controls.append(
394
+ _NativeControl(
395
+ control_id,
396
+ str(getattr(control, "ControlTypeName", "")),
397
+ str(getattr(control, "Name", "")),
398
+ str(getattr(control, "ClassName", "")),
399
+ region,
400
+ depth,
401
+ expanded,
402
+ actions,
403
+ )
404
+ )
405
+ for child in self._children(control):
406
+ walk(child, depth + 1)
407
+
408
+ walk(auto.ControlFromHandle(native_handle), 0)
409
+ return controls
410
+
411
+ @staticmethod
412
+ def _children(control: Any) -> list[Any]:
413
+ try:
414
+ return list(control.GetChildren())
415
+ except Exception:
416
+ return []
417
+
418
+ @staticmethod
419
+ def _is_offscreen(control: Any) -> bool:
420
+ try:
421
+ return bool(control.IsOffscreen)
422
+ except Exception:
423
+ return True
424
+
425
+ @staticmethod
426
+ def _control_region(control: Any) -> PixelRegion | None:
427
+ try:
428
+ rectangle = control.BoundingRectangle
429
+ width = int(rectangle.right) - int(rectangle.left)
430
+ height = int(rectangle.bottom) - int(rectangle.top)
431
+ if width <= 0 or height <= 0:
432
+ return None
433
+ return PixelRegion(int(rectangle.left), int(rectangle.top), width, height)
434
+ except Exception:
435
+ return None
436
+
437
+ def _capabilities(
438
+ self, control: Any, auto: Any
439
+ ) -> tuple[frozenset[AccessibilityAction], bool | None]:
440
+ actions: set[AccessibilityAction] = set()
441
+ if self._pattern(control, auto, "Invoke") is not None:
442
+ actions.add(AccessibilityAction.INVOKE)
443
+ expand_collapse = self._pattern(control, auto, "ExpandCollapse")
444
+ if expand_collapse is None:
445
+ return frozenset(actions), None
446
+ actions.update({AccessibilityAction.EXPAND, AccessibilityAction.COLLAPSE})
447
+ try:
448
+ state = expand_collapse.ExpandCollapseState
449
+ except Exception:
450
+ return frozenset(actions), None
451
+ state_name = str(state).casefold()
452
+ if "expanded" in state_name or state == 1:
453
+ return frozenset(actions), True
454
+ if "collapsed" in state_name or state == 0:
455
+ return frozenset(actions), False
456
+ return frozenset(actions), None
457
+
458
+ @staticmethod
459
+ def _pattern(control: Any, auto: Any, name: str) -> Any | None:
460
+ getter = getattr(control, f"Get{name}Pattern", None)
461
+ if getter is not None:
462
+ try:
463
+ return getter()
464
+ except Exception:
465
+ pass
466
+ pattern_id = getattr(getattr(auto, "PatternId", object()), f"{name}Pattern", None)
467
+ get_pattern = getattr(control, "GetPattern", None)
468
+ if pattern_id is not None and get_pattern is not None:
469
+ try:
470
+ return get_pattern(pattern_id)
471
+ except Exception:
472
+ pass
473
+ return None
474
+
475
+ def accessibility_action(self, control_id: str, action: AccessibilityAction) -> None:
476
+ import uiautomation as auto
477
+
478
+ control = self._controls.get(control_id)
479
+ if control is None:
480
+ raise ValueError("accessibility control is unavailable")
481
+ pattern_name = (
482
+ "Invoke"
483
+ if action is AccessibilityAction.INVOKE
484
+ else "ExpandCollapse"
485
+ )
486
+ pattern = self._pattern(control, auto, pattern_name)
487
+ if pattern is None:
488
+ raise ValueError(f"accessibility action {action.value!r} is unavailable")
489
+ method_name = {
490
+ AccessibilityAction.INVOKE: "Invoke",
491
+ AccessibilityAction.EXPAND: "Expand",
492
+ AccessibilityAction.COLLAPSE: "Collapse",
493
+ }[action]
494
+ getattr(pattern, method_name)()
495
+
496
+ def click(self, native_handle: int, x: int, y: int) -> bool:
497
+ import ctypes
498
+
499
+ user32 = ctypes.windll.user32
500
+ if not user32.SetCursorPos(x, y) or not self.is_foreground(native_handle):
501
+ return False
502
+ user32.mouse_event(0x0002, 0, 0, 0, 0) # MOUSEEVENTF_LEFTDOWN
503
+ user32.mouse_event(0x0004, 0, 0, 0, 0) # MOUSEEVENTF_LEFTUP
504
+ return True
505
+
506
+ def scroll(self, native_handle: int, x: int, y: int, wheel_data: int) -> bool:
507
+ import ctypes
508
+
509
+ user32 = ctypes.windll.user32
510
+ if not user32.SetCursorPos(x, y) or not self.is_foreground(native_handle):
511
+ return False
512
+ user32.mouse_event(0x0800, 0, 0, wheel_data, 0) # MOUSEEVENTF_WHEEL
513
+ return True
514
+
515
+ def set_clipboard_text(self, text: str) -> None:
516
+ self._set_clipboard_text(text)
517
+
518
+ def send_paste_and_submit(self, native_handle: int) -> bool:
519
+ import ctypes
520
+ from ctypes import wintypes
521
+
522
+ class MouseInput(ctypes.Structure):
523
+ _fields_ = [
524
+ ("dx", wintypes.LONG),
525
+ ("dy", wintypes.LONG),
526
+ ("mouseData", wintypes.DWORD),
527
+ ("dwFlags", wintypes.DWORD),
528
+ ("time", wintypes.DWORD),
529
+ ("dwExtraInfo", ctypes.c_size_t),
530
+ ]
531
+
532
+ class KeyboardInput(ctypes.Structure):
533
+ _fields_ = [
534
+ ("wVk", wintypes.WORD),
535
+ ("wScan", wintypes.WORD),
536
+ ("dwFlags", wintypes.DWORD),
537
+ ("time", wintypes.DWORD),
538
+ ("dwExtraInfo", ctypes.c_size_t),
539
+ ]
540
+
541
+ class InputUnion(ctypes.Union):
542
+ _fields_ = [("mi", MouseInput), ("ki", KeyboardInput)]
543
+
544
+ class Input(ctypes.Structure):
545
+ _anonymous_ = ("input",)
546
+ _fields_ = [("type", wintypes.DWORD), ("input", InputUnion)]
547
+
548
+ def keyboard_input(virtual_key: int, flags: int = 0) -> Input:
549
+ event = Input()
550
+ event.type = 1 # INPUT_KEYBOARD
551
+ event.ki = KeyboardInput(virtual_key, 0, flags, 0, 0)
552
+ return event
553
+
554
+ if not self.is_foreground(native_handle):
555
+ return False
556
+ user32 = ctypes.windll.user32
557
+ user32.SendInput.argtypes = (wintypes.UINT, ctypes.POINTER(Input), ctypes.c_int)
558
+ user32.SendInput.restype = wintypes.UINT
559
+
560
+ def send_inputs(*events: Input) -> int:
561
+ inputs = (Input * len(events))(*events)
562
+ return user32.SendInput(len(inputs), inputs, ctypes.sizeof(Input))
563
+
564
+ def drain_key_releases(*releases: Input) -> bool:
565
+ all_released = True
566
+ for release in releases:
567
+ all_released = send_inputs(release) == 1 and all_released
568
+ return all_released
569
+
570
+ paste = (
571
+ keyboard_input(0x11), # VK_CONTROL
572
+ keyboard_input(0x56), # VK_V
573
+ keyboard_input(0x56, 0x0002), # KEYEVENTF_KEYUP
574
+ keyboard_input(0x11, 0x0002), # KEYEVENTF_KEYUP
575
+ )
576
+ paste_sent = send_inputs(*paste)
577
+ if paste_sent != len(paste):
578
+ key_releases = {
579
+ 1: (keyboard_input(0x11, 0x0002),),
580
+ 2: (keyboard_input(0x56, 0x0002), keyboard_input(0x11, 0x0002)),
581
+ 3: (keyboard_input(0x11, 0x0002),),
582
+ }.get(paste_sent, ())
583
+ if key_releases:
584
+ drain_key_releases(*key_releases)
585
+ return False
586
+ if not self.is_foreground(native_handle):
587
+ return False
588
+ enter = (
589
+ keyboard_input(0x0D), # VK_RETURN
590
+ keyboard_input(0x0D, 0x0002), # KEYEVENTF_KEYUP
591
+ )
592
+ enter_sent = send_inputs(*enter)
593
+ if enter_sent != len(enter):
594
+ if enter_sent == 1:
595
+ drain_key_releases(keyboard_input(0x0D, 0x0002))
596
+ return False
597
+ return True
598
+
599
+ @staticmethod
600
+ def _set_clipboard_text(text: str) -> None:
601
+ import ctypes
602
+ from ctypes import wintypes
603
+
604
+ user32 = ctypes.windll.user32
605
+ kernel32 = ctypes.windll.kernel32
606
+ hglobal = ctypes.c_void_p
607
+ user32.OpenClipboard.argtypes = (wintypes.HWND,)
608
+ user32.OpenClipboard.restype = wintypes.BOOL
609
+ user32.EmptyClipboard.argtypes = ()
610
+ user32.EmptyClipboard.restype = wintypes.BOOL
611
+ user32.SetClipboardData.argtypes = (wintypes.UINT, hglobal)
612
+ user32.SetClipboardData.restype = hglobal
613
+ user32.CloseClipboard.argtypes = ()
614
+ user32.CloseClipboard.restype = wintypes.BOOL
615
+ kernel32.GlobalAlloc.argtypes = (wintypes.UINT, ctypes.c_size_t)
616
+ kernel32.GlobalAlloc.restype = hglobal
617
+ kernel32.GlobalLock.argtypes = (hglobal,)
618
+ kernel32.GlobalLock.restype = ctypes.c_void_p
619
+ kernel32.GlobalUnlock.argtypes = (hglobal,)
620
+ kernel32.GlobalUnlock.restype = wintypes.BOOL
621
+ kernel32.GlobalFree.argtypes = (hglobal,)
622
+ kernel32.GlobalFree.restype = hglobal
623
+ if not user32.OpenClipboard(None):
624
+ raise OSError("could not open the clipboard")
625
+ try:
626
+ if not user32.EmptyClipboard():
627
+ raise OSError("could not clear the clipboard")
628
+ value = ctypes.create_unicode_buffer(text)
629
+ memory = kernel32.GlobalAlloc(0x0002, ctypes.sizeof(value))
630
+ if not memory:
631
+ raise OSError("could not allocate clipboard memory")
632
+ pointer = kernel32.GlobalLock(memory)
633
+ if not pointer:
634
+ kernel32.GlobalFree(memory)
635
+ raise OSError("could not lock clipboard memory")
636
+ try:
637
+ ctypes.memmove(pointer, value, ctypes.sizeof(value))
638
+ finally:
639
+ kernel32.GlobalUnlock(memory)
640
+ if not user32.SetClipboardData(13, memory): # CF_UNICODETEXT
641
+ kernel32.GlobalFree(memory)
642
+ raise OSError("could not set clipboard text")
643
+ finally:
644
+ user32.CloseClipboard()