remoboard 1.0.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.
- remoboard-1.0.0/.gitignore +26 -0
- remoboard-1.0.0/LICENSE +21 -0
- remoboard-1.0.0/PKG-INFO +93 -0
- remoboard-1.0.0/README.md +79 -0
- remoboard-1.0.0/examples/basic.py +35 -0
- remoboard-1.0.0/pyproject.toml +21 -0
- remoboard-1.0.0/remoboard/__init__.py +44 -0
- remoboard-1.0.0/remoboard/client.py +284 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# The Xcode project is generated from project.yml via XcodeGen and committed for
|
|
2
|
+
# convenience (open without running xcodegen). Regenerate with `xcodegen generate`.
|
|
3
|
+
# Per-user data inside it is still ignored below (xcuserdata / *.xcuserstate).
|
|
4
|
+
|
|
5
|
+
# Build output
|
|
6
|
+
DerivedData/
|
|
7
|
+
build/
|
|
8
|
+
*.xcuserstate
|
|
9
|
+
xcuserdata/
|
|
10
|
+
|
|
11
|
+
# Web toolchain
|
|
12
|
+
web/node_modules/
|
|
13
|
+
web/dist/
|
|
14
|
+
|
|
15
|
+
# Developer packages
|
|
16
|
+
packages/*/node_modules/
|
|
17
|
+
packages/node/package-lock.json
|
|
18
|
+
packages/mcp/package-lock.json
|
|
19
|
+
packages/python/dist/
|
|
20
|
+
packages/python/build/
|
|
21
|
+
packages/python/*.egg-info/
|
|
22
|
+
__pycache__/
|
|
23
|
+
*.pyc
|
|
24
|
+
|
|
25
|
+
# macOS
|
|
26
|
+
.DS_Store
|
remoboard-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Remoboard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
remoboard-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: remoboard
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Type into a phone running the Remoboard keyboard from Python β official client for the Remoboard WebSocket protocol.
|
|
5
|
+
Project-URL: Homepage, https://github.com/everettjf/Remoboard
|
|
6
|
+
Project-URL: Repository, https://github.com/everettjf/Remoboard
|
|
7
|
+
Author: everettjf
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: automation,input,ios,remoboard,remote-keyboard,websocket
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: websockets>=12.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# remoboard (Python)
|
|
16
|
+
|
|
17
|
+
Official Python client for [Remoboard](https://github.com/everettjf/Remoboard) β type
|
|
18
|
+
into a phone running the Remoboard keyboard, from code.
|
|
19
|
+
|
|
20
|
+
It connects to the WebSocket server the Remoboard keyboard extension hosts on the phone
|
|
21
|
+
and speaks the versioned JSON protocol (v1).
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install remoboard
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Python 3.9+ (depends on `websockets`).
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
On the phone: switch to the Remoboard keyboard. It shows a URL like `http://192.168.1.20:7777`
|
|
34
|
+
and a **PIN**.
|
|
35
|
+
|
|
36
|
+
### Async
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
from remoboard import RemoboardClient
|
|
41
|
+
|
|
42
|
+
async def main():
|
|
43
|
+
async with RemoboardClient(host="192.168.1.20", pin="482103") as rb:
|
|
44
|
+
await rb.type("Hello δΈη π")
|
|
45
|
+
await rb.enter()
|
|
46
|
+
await rb.type("second line")
|
|
47
|
+
print(await rb.get_clipboard())
|
|
48
|
+
|
|
49
|
+
asyncio.run(main())
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Sync (no asyncio)
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from remoboard import RemoboardSync
|
|
56
|
+
|
|
57
|
+
with RemoboardSync(host="192.168.1.20", pin="482103") as rb:
|
|
58
|
+
rb.type("Hello δΈη")
|
|
59
|
+
rb.enter()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
### `RemoboardClient(host, pin, port=7777, pair_timeout=8.0, on_context=..., on_quickwords=..., on_clipboard=..., on_info=...)`
|
|
65
|
+
|
|
66
|
+
Async client; also an async context manager (`async with`).
|
|
67
|
+
|
|
68
|
+
- `await connect()` β open + pair. Raises `PairingError` on a wrong PIN.
|
|
69
|
+
- `await type(text)` β insert text at the cursor (any Unicode).
|
|
70
|
+
- `await enter()` / `await backspace()`
|
|
71
|
+
- `await move("left"|"right"|"up"|"down")`
|
|
72
|
+
- `await set_clipboard(text)` / `await get_clipboard() -> str`
|
|
73
|
+
- `await set_quick_words(list)` / `await handoff(text)` / `await ping()`
|
|
74
|
+
- `await close()`
|
|
75
|
+
|
|
76
|
+
The optional `on_*` callbacks fire from the receive loop:
|
|
77
|
+
`on_context(before, after)`, `on_quickwords(list)`, `on_clipboard(text)`, `on_info(text)`.
|
|
78
|
+
|
|
79
|
+
### `RemoboardSync(host, pin, port=7777, **kwargs)`
|
|
80
|
+
|
|
81
|
+
Blocking wrapper backed by a private event loop and a context manager (`with`). Same
|
|
82
|
+
methods without `await` (`get_clipboard()` returns the string directly).
|
|
83
|
+
|
|
84
|
+
## Notes
|
|
85
|
+
|
|
86
|
+
- The phone and your machine must be on the same network and able to reach each other.
|
|
87
|
+
- The server drops a connection after 5 wrong PINs.
|
|
88
|
+
- The protocol is also implemented by the [Node client](../node) and the
|
|
89
|
+
[MCP server](../mcp).
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# remoboard (Python)
|
|
2
|
+
|
|
3
|
+
Official Python client for [Remoboard](https://github.com/everettjf/Remoboard) β type
|
|
4
|
+
into a phone running the Remoboard keyboard, from code.
|
|
5
|
+
|
|
6
|
+
It connects to the WebSocket server the Remoboard keyboard extension hosts on the phone
|
|
7
|
+
and speaks the versioned JSON protocol (v1).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install remoboard
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.9+ (depends on `websockets`).
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
On the phone: switch to the Remoboard keyboard. It shows a URL like `http://192.168.1.20:7777`
|
|
20
|
+
and a **PIN**.
|
|
21
|
+
|
|
22
|
+
### Async
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import asyncio
|
|
26
|
+
from remoboard import RemoboardClient
|
|
27
|
+
|
|
28
|
+
async def main():
|
|
29
|
+
async with RemoboardClient(host="192.168.1.20", pin="482103") as rb:
|
|
30
|
+
await rb.type("Hello δΈη π")
|
|
31
|
+
await rb.enter()
|
|
32
|
+
await rb.type("second line")
|
|
33
|
+
print(await rb.get_clipboard())
|
|
34
|
+
|
|
35
|
+
asyncio.run(main())
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Sync (no asyncio)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from remoboard import RemoboardSync
|
|
42
|
+
|
|
43
|
+
with RemoboardSync(host="192.168.1.20", pin="482103") as rb:
|
|
44
|
+
rb.type("Hello δΈη")
|
|
45
|
+
rb.enter()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### `RemoboardClient(host, pin, port=7777, pair_timeout=8.0, on_context=..., on_quickwords=..., on_clipboard=..., on_info=...)`
|
|
51
|
+
|
|
52
|
+
Async client; also an async context manager (`async with`).
|
|
53
|
+
|
|
54
|
+
- `await connect()` β open + pair. Raises `PairingError` on a wrong PIN.
|
|
55
|
+
- `await type(text)` β insert text at the cursor (any Unicode).
|
|
56
|
+
- `await enter()` / `await backspace()`
|
|
57
|
+
- `await move("left"|"right"|"up"|"down")`
|
|
58
|
+
- `await set_clipboard(text)` / `await get_clipboard() -> str`
|
|
59
|
+
- `await set_quick_words(list)` / `await handoff(text)` / `await ping()`
|
|
60
|
+
- `await close()`
|
|
61
|
+
|
|
62
|
+
The optional `on_*` callbacks fire from the receive loop:
|
|
63
|
+
`on_context(before, after)`, `on_quickwords(list)`, `on_clipboard(text)`, `on_info(text)`.
|
|
64
|
+
|
|
65
|
+
### `RemoboardSync(host, pin, port=7777, **kwargs)`
|
|
66
|
+
|
|
67
|
+
Blocking wrapper backed by a private event loop and a context manager (`with`). Same
|
|
68
|
+
methods without `await` (`get_clipboard()` returns the string directly).
|
|
69
|
+
|
|
70
|
+
## Notes
|
|
71
|
+
|
|
72
|
+
- The phone and your machine must be on the same network and able to reach each other.
|
|
73
|
+
- The server drops a connection after 5 wrong PINs.
|
|
74
|
+
- The protocol is also implemented by the [Node client](../node) and the
|
|
75
|
+
[MCP server](../mcp).
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Basic Remoboard Python example.
|
|
2
|
+
|
|
3
|
+
REMOBOARD_HOST=192.168.1.20 REMOBOARD_PIN=482103 python examples/basic.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from remoboard import RemoboardClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def main():
|
|
13
|
+
host = os.environ.get("REMOBOARD_HOST", "127.0.0.1")
|
|
14
|
+
pin = os.environ.get("REMOBOARD_PIN", "000000")
|
|
15
|
+
port = int(os.environ.get("REMOBOARD_PORT", "7777"))
|
|
16
|
+
|
|
17
|
+
async with RemoboardClient(
|
|
18
|
+
host=host,
|
|
19
|
+
port=port,
|
|
20
|
+
pin=pin,
|
|
21
|
+
on_context=lambda b, a: print("phone field:", repr(b + "|" + a)),
|
|
22
|
+
) as rb:
|
|
23
|
+
print("paired β")
|
|
24
|
+
await rb.type("Hello from Python δΈη π")
|
|
25
|
+
await rb.enter()
|
|
26
|
+
await rb.type("second line")
|
|
27
|
+
|
|
28
|
+
await rb.set_clipboard("copied by remoboard")
|
|
29
|
+
print("phone clipboard is now:", await rb.get_clipboard())
|
|
30
|
+
await asyncio.sleep(0.3)
|
|
31
|
+
print("done")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "remoboard"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Type into a phone running the Remoboard keyboard from Python β official client for the Remoboard WebSocket protocol."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "everettjf" }]
|
|
13
|
+
keywords = ["remoboard", "remote-keyboard", "websocket", "ios", "automation", "input"]
|
|
14
|
+
dependencies = ["websockets>=12.0"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/everettjf/Remoboard"
|
|
18
|
+
Repository = "https://github.com/everettjf/Remoboard"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["remoboard"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Remoboard Python client.
|
|
2
|
+
|
|
3
|
+
Drive remote text input on a phone running the Remoboard keyboard, over the versioned
|
|
4
|
+
JSON protocol (v1) carried on a WebSocket.
|
|
5
|
+
|
|
6
|
+
Async::
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from remoboard import RemoboardClient
|
|
10
|
+
|
|
11
|
+
async def main():
|
|
12
|
+
async with RemoboardClient(host="192.168.1.20", pin="482103") as rb:
|
|
13
|
+
await rb.type("Hello δΈη")
|
|
14
|
+
await rb.enter()
|
|
15
|
+
|
|
16
|
+
asyncio.run(main())
|
|
17
|
+
|
|
18
|
+
Sync::
|
|
19
|
+
|
|
20
|
+
from remoboard import RemoboardSync
|
|
21
|
+
|
|
22
|
+
with RemoboardSync(host="192.168.1.20", pin="482103") as rb:
|
|
23
|
+
rb.type("Hello δΈη")
|
|
24
|
+
rb.enter()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .client import (
|
|
28
|
+
RemoboardClient,
|
|
29
|
+
RemoboardSync,
|
|
30
|
+
PairingError,
|
|
31
|
+
RemoboardError,
|
|
32
|
+
PROTOCOL_VERSION,
|
|
33
|
+
DEFAULT_PORT,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"RemoboardClient",
|
|
38
|
+
"RemoboardSync",
|
|
39
|
+
"PairingError",
|
|
40
|
+
"RemoboardError",
|
|
41
|
+
"PROTOCOL_VERSION",
|
|
42
|
+
"DEFAULT_PORT",
|
|
43
|
+
]
|
|
44
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Remoboard WebSocket client (async + sync)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Awaitable, Callable, Optional
|
|
9
|
+
|
|
10
|
+
import websockets
|
|
11
|
+
|
|
12
|
+
PROTOCOL_VERSION = 1
|
|
13
|
+
DEFAULT_PORT = 7777
|
|
14
|
+
|
|
15
|
+
_DIRECTIONS = ("left", "right", "up", "down")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RemoboardError(Exception):
|
|
19
|
+
"""Base error for the Remoboard client."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PairingError(RemoboardError):
|
|
23
|
+
"""Raised when the phone rejects the PIN."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, reason: Optional[str] = None):
|
|
26
|
+
super().__init__(f"Pairing rejected ({reason or 'pin'})")
|
|
27
|
+
self.reason = reason
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RemoboardClient:
|
|
31
|
+
"""Async client. Connect, pair with the PIN, then send input.
|
|
32
|
+
|
|
33
|
+
Optional callbacks (called from the receive loop):
|
|
34
|
+
on_context(before, after), on_quickwords(list), on_clipboard(text), on_info(text)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
host: str,
|
|
40
|
+
pin: str,
|
|
41
|
+
port: int = DEFAULT_PORT,
|
|
42
|
+
pair_timeout: float = 8.0,
|
|
43
|
+
on_context: Optional[Callable[[str, str], None]] = None,
|
|
44
|
+
on_quickwords: Optional[Callable[[list], None]] = None,
|
|
45
|
+
on_clipboard: Optional[Callable[[str], None]] = None,
|
|
46
|
+
on_info: Optional[Callable[[str], None]] = None,
|
|
47
|
+
):
|
|
48
|
+
if not host:
|
|
49
|
+
raise ValueError("host is required")
|
|
50
|
+
if not pin:
|
|
51
|
+
raise ValueError("pin is required")
|
|
52
|
+
self.host = host
|
|
53
|
+
self.pin = str(pin)
|
|
54
|
+
self.port = port
|
|
55
|
+
self.pair_timeout = pair_timeout
|
|
56
|
+
self.on_context = on_context
|
|
57
|
+
self.on_quickwords = on_quickwords
|
|
58
|
+
self.on_clipboard = on_clipboard
|
|
59
|
+
self.on_info = on_info
|
|
60
|
+
|
|
61
|
+
self._ws = None
|
|
62
|
+
self._seq = 0
|
|
63
|
+
self._paired = False
|
|
64
|
+
self._recv_task: Optional[asyncio.Task] = None
|
|
65
|
+
self._paired_event = asyncio.Event()
|
|
66
|
+
self._deny_reason: Optional[str] = None
|
|
67
|
+
self._clip_waiters: "list[asyncio.Future]" = []
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def url(self) -> str:
|
|
71
|
+
return f"ws://{self.host}:{self.port}/ws"
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def paired(self) -> bool:
|
|
75
|
+
return self._paired
|
|
76
|
+
|
|
77
|
+
# ---- lifecycle ----
|
|
78
|
+
|
|
79
|
+
async def connect(self) -> "RemoboardClient":
|
|
80
|
+
"""Open the socket and wait until the phone confirms pairing."""
|
|
81
|
+
self._ws = await websockets.connect(self.url, open_timeout=self.pair_timeout)
|
|
82
|
+
self._recv_task = asyncio.ensure_future(self._recv_loop())
|
|
83
|
+
await self._send({"t": "hello", "pin": self.pin})
|
|
84
|
+
try:
|
|
85
|
+
await asyncio.wait_for(self._paired_event.wait(), timeout=self.pair_timeout)
|
|
86
|
+
except asyncio.TimeoutError:
|
|
87
|
+
await self.close()
|
|
88
|
+
if self._deny_reason is not None:
|
|
89
|
+
raise PairingError(self._deny_reason)
|
|
90
|
+
raise RemoboardError(f"Timed out waiting to pair with {self.url}")
|
|
91
|
+
if not self._paired:
|
|
92
|
+
await self.close()
|
|
93
|
+
raise PairingError(self._deny_reason)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
async def close(self) -> None:
|
|
97
|
+
if self._recv_task:
|
|
98
|
+
self._recv_task.cancel()
|
|
99
|
+
self._recv_task = None
|
|
100
|
+
if self._ws:
|
|
101
|
+
await self._ws.close()
|
|
102
|
+
self._ws = None
|
|
103
|
+
self._paired = False
|
|
104
|
+
|
|
105
|
+
async def __aenter__(self) -> "RemoboardClient":
|
|
106
|
+
return await self.connect()
|
|
107
|
+
|
|
108
|
+
async def __aexit__(self, *exc) -> None:
|
|
109
|
+
await self.close()
|
|
110
|
+
|
|
111
|
+
# ---- receive ----
|
|
112
|
+
|
|
113
|
+
async def _recv_loop(self) -> None:
|
|
114
|
+
try:
|
|
115
|
+
async for raw in self._ws:
|
|
116
|
+
try:
|
|
117
|
+
msg = json.loads(raw)
|
|
118
|
+
except (ValueError, TypeError):
|
|
119
|
+
continue
|
|
120
|
+
self._handle(msg)
|
|
121
|
+
except (websockets.ConnectionClosed, asyncio.CancelledError):
|
|
122
|
+
pass
|
|
123
|
+
finally:
|
|
124
|
+
for fut in self._clip_waiters:
|
|
125
|
+
if not fut.done():
|
|
126
|
+
fut.set_exception(RemoboardError("connection closed"))
|
|
127
|
+
self._clip_waiters.clear()
|
|
128
|
+
|
|
129
|
+
def _handle(self, msg: dict) -> None:
|
|
130
|
+
t = msg.get("t")
|
|
131
|
+
if t == "paired":
|
|
132
|
+
self._paired = True
|
|
133
|
+
self._paired_event.set()
|
|
134
|
+
elif t == "deny":
|
|
135
|
+
self._deny_reason = msg.get("reason")
|
|
136
|
+
self._paired_event.set()
|
|
137
|
+
elif t == "context":
|
|
138
|
+
if self.on_context:
|
|
139
|
+
self.on_context(msg.get("before", ""), msg.get("after", ""))
|
|
140
|
+
elif t == "quickwords":
|
|
141
|
+
if self.on_quickwords:
|
|
142
|
+
self.on_quickwords(msg.get("items", []))
|
|
143
|
+
elif t == "clip":
|
|
144
|
+
text = msg.get("text", "")
|
|
145
|
+
if self._clip_waiters:
|
|
146
|
+
fut = self._clip_waiters.pop(0)
|
|
147
|
+
if not fut.done():
|
|
148
|
+
fut.set_result(text)
|
|
149
|
+
if self.on_clipboard:
|
|
150
|
+
self.on_clipboard(text)
|
|
151
|
+
elif t == "info":
|
|
152
|
+
if self.on_info:
|
|
153
|
+
self.on_info(msg.get("message", ""))
|
|
154
|
+
|
|
155
|
+
# ---- send ----
|
|
156
|
+
|
|
157
|
+
async def _send(self, obj: dict) -> None:
|
|
158
|
+
if self._ws is None:
|
|
159
|
+
raise RemoboardError("not connected")
|
|
160
|
+
await self._ws.send(json.dumps({"v": PROTOCOL_VERSION, **obj}))
|
|
161
|
+
|
|
162
|
+
def _require_paired(self) -> None:
|
|
163
|
+
if not self._paired:
|
|
164
|
+
raise RemoboardError("not paired β await connect() first")
|
|
165
|
+
|
|
166
|
+
async def type(self, text: str) -> None:
|
|
167
|
+
"""Insert text at the phone's cursor."""
|
|
168
|
+
self._require_paired()
|
|
169
|
+
if text:
|
|
170
|
+
await self._send({"t": "input", "text": str(text), "seq": self._next_seq()})
|
|
171
|
+
|
|
172
|
+
async def enter(self) -> None:
|
|
173
|
+
"""Insert a newline / submit."""
|
|
174
|
+
self._require_paired()
|
|
175
|
+
await self._send({"t": "input", "text": "\n", "seq": self._next_seq()})
|
|
176
|
+
|
|
177
|
+
async def backspace(self) -> None:
|
|
178
|
+
"""Delete one character backwards."""
|
|
179
|
+
self._require_paired()
|
|
180
|
+
await self._send({"t": "delete", "seq": self._next_seq()})
|
|
181
|
+
|
|
182
|
+
async def move(self, direction: str) -> None:
|
|
183
|
+
"""Move the cursor: 'left' | 'right' | 'up' | 'down'."""
|
|
184
|
+
self._require_paired()
|
|
185
|
+
if direction not in _DIRECTIONS:
|
|
186
|
+
raise ValueError(f"bad direction: {direction}")
|
|
187
|
+
await self._send({"t": "move", "dir": direction, "seq": self._next_seq()})
|
|
188
|
+
|
|
189
|
+
async def set_clipboard(self, text: str) -> None:
|
|
190
|
+
"""Write text into the phone's system clipboard."""
|
|
191
|
+
self._require_paired()
|
|
192
|
+
await self._send({"t": "clip-set", "text": str(text)})
|
|
193
|
+
|
|
194
|
+
async def get_clipboard(self, timeout: float = 5.0) -> str:
|
|
195
|
+
"""Read the phone's clipboard."""
|
|
196
|
+
self._require_paired()
|
|
197
|
+
loop = asyncio.get_event_loop()
|
|
198
|
+
fut: asyncio.Future = loop.create_future()
|
|
199
|
+
self._clip_waiters.append(fut)
|
|
200
|
+
await self._send({"t": "clip-get"})
|
|
201
|
+
try:
|
|
202
|
+
return await asyncio.wait_for(fut, timeout=timeout)
|
|
203
|
+
except asyncio.TimeoutError:
|
|
204
|
+
if fut in self._clip_waiters:
|
|
205
|
+
self._clip_waiters.remove(fut)
|
|
206
|
+
raise RemoboardError("get_clipboard timed out")
|
|
207
|
+
|
|
208
|
+
async def set_quick_words(self, words) -> None:
|
|
209
|
+
"""Replace the user's quick words on the phone."""
|
|
210
|
+
self._require_paired()
|
|
211
|
+
await self._send({"t": "words-set", "items": [str(w) for w in words]})
|
|
212
|
+
|
|
213
|
+
async def handoff(self, text: str) -> None:
|
|
214
|
+
"""Hand text to the phone's host app."""
|
|
215
|
+
self._require_paired()
|
|
216
|
+
await self._send({"t": "handoff", "text": str(text)})
|
|
217
|
+
|
|
218
|
+
async def ping(self) -> None:
|
|
219
|
+
await self._send({"t": "ping"})
|
|
220
|
+
|
|
221
|
+
def _next_seq(self) -> int:
|
|
222
|
+
s = self._seq
|
|
223
|
+
self._seq += 1
|
|
224
|
+
return s
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class RemoboardSync:
|
|
228
|
+
"""Blocking wrapper around :class:`RemoboardClient`, backed by a private event loop.
|
|
229
|
+
|
|
230
|
+
Useful for scripts that don't want to deal with asyncio::
|
|
231
|
+
|
|
232
|
+
with RemoboardSync(host="...", pin="...") as rb:
|
|
233
|
+
rb.type("hi")
|
|
234
|
+
rb.enter()
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def __init__(self, host: str, pin: str, port: int = DEFAULT_PORT, **kwargs):
|
|
238
|
+
self._loop = asyncio.new_event_loop()
|
|
239
|
+
self._thread = threading.Thread(target=self._loop.run_forever, daemon=True)
|
|
240
|
+
self._thread.start()
|
|
241
|
+
self._client = RemoboardClient(host=host, pin=pin, port=port, **kwargs)
|
|
242
|
+
|
|
243
|
+
def _run(self, coro: Awaitable):
|
|
244
|
+
return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
|
|
245
|
+
|
|
246
|
+
def connect(self) -> "RemoboardSync":
|
|
247
|
+
self._run(self._client.connect())
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def type(self, text: str) -> None:
|
|
251
|
+
self._run(self._client.type(text))
|
|
252
|
+
|
|
253
|
+
def enter(self) -> None:
|
|
254
|
+
self._run(self._client.enter())
|
|
255
|
+
|
|
256
|
+
def backspace(self) -> None:
|
|
257
|
+
self._run(self._client.backspace())
|
|
258
|
+
|
|
259
|
+
def move(self, direction: str) -> None:
|
|
260
|
+
self._run(self._client.move(direction))
|
|
261
|
+
|
|
262
|
+
def set_clipboard(self, text: str) -> None:
|
|
263
|
+
self._run(self._client.set_clipboard(text))
|
|
264
|
+
|
|
265
|
+
def get_clipboard(self, timeout: float = 5.0) -> str:
|
|
266
|
+
return self._run(self._client.get_clipboard(timeout))
|
|
267
|
+
|
|
268
|
+
def set_quick_words(self, words) -> None:
|
|
269
|
+
self._run(self._client.set_quick_words(words))
|
|
270
|
+
|
|
271
|
+
def handoff(self, text: str) -> None:
|
|
272
|
+
self._run(self._client.handoff(text))
|
|
273
|
+
|
|
274
|
+
def close(self) -> None:
|
|
275
|
+
try:
|
|
276
|
+
self._run(self._client.close())
|
|
277
|
+
finally:
|
|
278
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
279
|
+
|
|
280
|
+
def __enter__(self) -> "RemoboardSync":
|
|
281
|
+
return self.connect()
|
|
282
|
+
|
|
283
|
+
def __exit__(self, *exc) -> None:
|
|
284
|
+
self.close()
|