diff --git a/ceki_sdk/__init__.py b/ceki_sdk/__init__.py index 4a5fe32..b5b3b01 100644 --- a/ceki_sdk/__init__.py +++ b/ceki_sdk/__init__.py @@ -21,7 +21,7 @@ from ._profile import BrowserProfile from .humanize import HumanProfile -__version__ = "2.23.0" +__version__ = "2.35.0" __all__ = [ "connect", "ConnectOptions", diff --git a/ceki_sdk/_browser.py b/ceki_sdk/_browser.py index 8645ece..41f7f63 100644 --- a/ceki_sdk/_browser.py +++ b/ceki_sdk/_browser.py @@ -486,6 +486,121 @@ async def upload( return parsed + async def _dispatch_hotkey(self, key: str, code: str) -> None: + """Dispatch a Ctrl+ hotkey as ``keyDown``+``keyUp`` via CDP. + + ``modifiers=2`` is Chromium's bitmask for Control. We fire both + ``keyDown`` and ``keyUp`` because the browser's clipboard shortcuts + only trigger on a full press cycle. Used by :meth:`copy` (Ctrl+C) + and :meth:`paste` (Ctrl+C on the seed textarea, then Ctrl+V on the + target). + """ + vk = ord(key.upper()) + params = { + "modifiers": 2, + "key": key, + "code": code, + "windowsVirtualKeyCode": vk, + "nativeVirtualKeyCode": vk, + } + await self.send({ + "method": "Input.dispatchKeyEvent", + "params": {"type": "keyDown", **params}, + }) + await self.send({ + "method": "Input.dispatchKeyEvent", + "params": {"type": "keyUp", **params}, + }) + + async def copy(self) -> str: + """Copy the current window selection into the OS clipboard, return it. + + Reads the current selection text via ``Runtime.evaluate`` + (``window.getSelection().toString()``) so the caller still gets it as a + return value, then dispatches a synthetic ``Ctrl+C`` via + ``Input.dispatchKeyEvent`` — that is the step that actually flips the OS + clipboard. Verified against real headed Chromium in contract task 4098. + + The reason we read the selection before Ctrl+C rather than reading it + back from the clipboard: the main-mode CDP allowlist forbids + ``navigator.clipboard``, and ``document.execCommand('paste')`` is dead + in modern Chromium, so there's no read-back path from JS. Reading the + selection directly is cheap and gives an exact return value. + + Returns: + The selection text (``""`` when nothing is selected). The OS + clipboard is flipped as a side effect regardless of the return. + """ + result = await self.send({ + "method": "Runtime.evaluate", + "params": { + "expression": "window.getSelection().toString()", + "returnByValue": True, + }, + }) + selection = (result.get("result") or {}).get("value") or "" + await self._dispatch_hotkey("c", "KeyC") + return selection + + async def paste(self, selector: str, text: str) -> None: + """Put ``text`` into the OS clipboard, focus ``selector``, Ctrl+V it in. + + Real system-clipboard paste: a temporary offscreen ``