plover-touch-tablets 0.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.
- plover_touch_tablets-0.0.0/PKG-INFO +53 -0
- plover_touch_tablets-0.0.0/README.md +32 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/client_config.py +79 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/config.py +16 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/debug.py +7 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/encoding.py +5 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/extended_engine.py +29 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/extension.py +171 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/get_logger.py +14 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/lookup.py +147 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/signal.py +7 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets/tool.py +134 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets.egg-info/PKG-INFO +53 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets.egg-info/SOURCES.txt +19 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets.egg-info/dependency_links.txt +1 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets.egg-info/entry_points.txt +5 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets.egg-info/requires.txt +16 -0
- plover_touch_tablets-0.0.0/plover_touch_tablets.egg-info/top_level.txt +1 -0
- plover_touch_tablets-0.0.0/pyproject.toml +104 -0
- plover_touch_tablets-0.0.0/setup.cfg +4 -0
- plover_touch_tablets-0.0.0/tests/test_me.py +3 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plover-touch-tablets
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Touch Tablets Plover Plugin
|
|
5
|
+
Keywords: plover_plugin
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: plover>=5.0.0
|
|
8
|
+
Requires-Dist: PySide6
|
|
9
|
+
Requires-Dist: websocket-client
|
|
10
|
+
Requires-Dist: jsonpickle
|
|
11
|
+
Requires-Dist: nacl_middleware
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest; extra == "test"
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: plover-touch-tablets[test]; extra == "dev"
|
|
16
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
17
|
+
Requires-Dist: build; extra == "dev"
|
|
18
|
+
Requires-Dist: bump-my-version; extra == "dev"
|
|
19
|
+
Requires-Dist: twine; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# Plover Touch Tablets Plugin
|
|
23
|
+
|
|
24
|
+
This Plover plugin enables seamless integration between Plover and touch-based steno keyboard applications running on tablets. It establishes a secure WebSocket connection, allowing your tablet to function as a steno machine for Plover.
|
|
25
|
+
|
|
26
|
+
## Compatibility
|
|
27
|
+
|
|
28
|
+
This plugin is designed to be compatible with the **[Touch Steno Keyboard](https://github.com/CosmicDNA/touch-steno-keyboard)** application and the **[Plover Websocket Relay](https://github.com/CosmicDNA/plover-websocket-relay)**.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Easy Pairing**: Generates a QR code within Plover for quick connection setup.
|
|
33
|
+
- **Secure Connection**: Uses encryption to ensure secure communication between the tablet and Plover.
|
|
34
|
+
- **Bi-directional Communication**: Supports sending steno strokes to Plover and receiving dictionary lookups/translations back on the tablet.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
1. Open Plover.
|
|
39
|
+
2. Install `plover-touch-tablets` plugin via either plover_console CLI or plugin manager.
|
|
40
|
+
3. Restart Plover.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
1. Open Plover.
|
|
45
|
+
2. Go to **Tools** > **Tablet QR**.
|
|
46
|
+
3. A window will appear displaying a QR code.
|
|
47
|
+
4. Open the Touch Steno Keyboard application on your tablet.
|
|
48
|
+
5. Use the application to scan the QR code displayed on your computer screen.
|
|
49
|
+
6. Once connected, strokes entered on the tablet will be processed by Plover.
|
|
50
|
+
|
|
51
|
+
## Powered by
|
|
52
|
+

|
|
53
|
+

|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Plover Touch Tablets Plugin
|
|
2
|
+
|
|
3
|
+
This Plover plugin enables seamless integration between Plover and touch-based steno keyboard applications running on tablets. It establishes a secure WebSocket connection, allowing your tablet to function as a steno machine for Plover.
|
|
4
|
+
|
|
5
|
+
## Compatibility
|
|
6
|
+
|
|
7
|
+
This plugin is designed to be compatible with the **[Touch Steno Keyboard](https://github.com/CosmicDNA/touch-steno-keyboard)** application and the **[Plover Websocket Relay](https://github.com/CosmicDNA/plover-websocket-relay)**.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Easy Pairing**: Generates a QR code within Plover for quick connection setup.
|
|
12
|
+
- **Secure Connection**: Uses encryption to ensure secure communication between the tablet and Plover.
|
|
13
|
+
- **Bi-directional Communication**: Supports sending steno strokes to Plover and receiving dictionary lookups/translations back on the tablet.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
1. Open Plover.
|
|
18
|
+
2. Install `plover-touch-tablets` plugin via either plover_console CLI or plugin manager.
|
|
19
|
+
3. Restart Plover.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
1. Open Plover.
|
|
24
|
+
2. Go to **Tools** > **Tablet QR**.
|
|
25
|
+
3. A window will appear displaying a QR code.
|
|
26
|
+
4. Open the Touch Steno Keyboard application on your tablet.
|
|
27
|
+
5. Use the application to scan the QR code displayed on your computer screen.
|
|
28
|
+
6. Once connected, strokes entered on the tablet will be processed by Plover.
|
|
29
|
+
|
|
30
|
+
## Powered by
|
|
31
|
+

|
|
32
|
+

|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Server configuration."""
|
|
2
|
+
|
|
3
|
+
from json import dump, load
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from nacl.encoding import HexEncoder
|
|
7
|
+
from nacl.public import PrivateKey
|
|
8
|
+
from nacl_middleware import Nacl
|
|
9
|
+
from plover.oslayer.config import CONFIG_DIR
|
|
10
|
+
|
|
11
|
+
from plover_touch_tablets.get_logger import get_logger
|
|
12
|
+
|
|
13
|
+
log = get_logger("ClientConfig")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClientConfig:
|
|
17
|
+
"""Loads server configuration.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
host: The host address for the server to run on.
|
|
21
|
+
port: The port for the server to run on.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
public_key: str
|
|
26
|
+
public_key: str
|
|
27
|
+
|
|
28
|
+
def __init__(self, client_config_file: str) -> None:
|
|
29
|
+
"""Initialize the server configuration object.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
client_config_file: The file path of the configuration file to load.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
IOError: Errored when loading the server configuration file.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
# Try to load from the other plugin's config file first (read-only)
|
|
39
|
+
other_config_path = Path(CONFIG_DIR) / client_config_file
|
|
40
|
+
data = {}
|
|
41
|
+
keys_found = False
|
|
42
|
+
|
|
43
|
+
if other_config_path.exists():
|
|
44
|
+
try:
|
|
45
|
+
with other_config_path.open(encoding="utf-8") as config_file:
|
|
46
|
+
other_data = load(config_file)
|
|
47
|
+
if "private_key" in other_data and "public_key" in other_data:
|
|
48
|
+
data = other_data
|
|
49
|
+
keys_found = True
|
|
50
|
+
log.debug(f"Loaded keys from {other_config_path}")
|
|
51
|
+
except Exception:
|
|
52
|
+
log.exception(f"Error reading {other_config_path}:")
|
|
53
|
+
|
|
54
|
+
# If not found, use our own config file
|
|
55
|
+
if not keys_found:
|
|
56
|
+
my_config_path = Path(CONFIG_DIR) / "plover_touch_tablets.json"
|
|
57
|
+
if my_config_path.exists():
|
|
58
|
+
try:
|
|
59
|
+
with my_config_path.open(encoding="utf-8") as config_file:
|
|
60
|
+
data = load(config_file)
|
|
61
|
+
if "private_key" in data and "public_key" in data:
|
|
62
|
+
keys_found = True
|
|
63
|
+
log.debug(f"Loaded keys from {my_config_path}")
|
|
64
|
+
except Exception:
|
|
65
|
+
log.exception(f"Error reading {my_config_path}:")
|
|
66
|
+
|
|
67
|
+
if not keys_found:
|
|
68
|
+
log.info("No existing keys found. Generating new keys...")
|
|
69
|
+
nacl_helper = Nacl()
|
|
70
|
+
data["private_key"] = nacl_helper.decoded_private_key()
|
|
71
|
+
data["public_key"] = nacl_helper.decoded_public_key()
|
|
72
|
+
|
|
73
|
+
log.info(f"Saving new keys to {my_config_path}")
|
|
74
|
+
with my_config_path.open("w", encoding="utf-8") as config_file:
|
|
75
|
+
dump(data, config_file, indent=2)
|
|
76
|
+
|
|
77
|
+
# Assign values to class attributes
|
|
78
|
+
self.private_key = PrivateKey(data["private_key"], HexEncoder)
|
|
79
|
+
self.public_key = data["public_key"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# from plover_touch_tablets.debug import is_debug_mode
|
|
2
|
+
|
|
3
|
+
# if is_debug_mode():
|
|
4
|
+
# PROTOCOL = "http:"
|
|
5
|
+
# BASE_WORKER_URL = "localhost:8787"
|
|
6
|
+
|
|
7
|
+
WORKER_PROTOCOL = "https:"
|
|
8
|
+
BASE_WORKER_FQDN = "relay.stenography.cosmicdna.co.uk"
|
|
9
|
+
APP_URL = "https://touch.stenography.cosmicdna.co.uk"
|
|
10
|
+
|
|
11
|
+
SESSION_SLUG = "session"
|
|
12
|
+
INITIATE_SLUG = "initiate"
|
|
13
|
+
CONNECT_SLUG = "connect"
|
|
14
|
+
JOIN_SLUG = "join"
|
|
15
|
+
TOKEN_PARAM = "token" # nosec B105
|
|
16
|
+
RELAY_PARAM = "relay"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from plover.engine import StenoEngine
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from plover_touch_tablets.extension import Extension
|
|
7
|
+
|
|
8
|
+
from plover_touch_tablets.signal import Signal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExtendedStenoEngine:
|
|
12
|
+
my_minimal_extension: "Extension"
|
|
13
|
+
signals: list[Signal]
|
|
14
|
+
_engine: StenoEngine
|
|
15
|
+
|
|
16
|
+
def __init__(self, engine: StenoEngine):
|
|
17
|
+
self._engine = engine
|
|
18
|
+
|
|
19
|
+
def __getattr__(self, name):
|
|
20
|
+
"""Wrapper around StenoEngine."""
|
|
21
|
+
return getattr(self._engine, name)
|
|
22
|
+
|
|
23
|
+
def disconnect_hooks(self, host: object):
|
|
24
|
+
for signal in self.signals:
|
|
25
|
+
self._engine.hook_disconnect(signal.hook, getattr(host, signal.callback))
|
|
26
|
+
|
|
27
|
+
def connect_hooks(self, host: object):
|
|
28
|
+
for signal in self.signals:
|
|
29
|
+
self._engine.hook_connect(signal.hook, getattr(host, signal.callback))
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from importlib.metadata import metadata
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from jsonpickle import encode
|
|
7
|
+
from nacl_middleware import MailBox
|
|
8
|
+
from plover.engine import StenoEngine
|
|
9
|
+
from plover.gui_qt.paper_tape import TapeModel
|
|
10
|
+
from plover.steno import Stroke
|
|
11
|
+
from websocket import WebSocketApp
|
|
12
|
+
|
|
13
|
+
from plover_touch_tablets.client_config import ClientConfig
|
|
14
|
+
from plover_touch_tablets.config import BASE_WORKER_FQDN, WORKER_PROTOCOL
|
|
15
|
+
from plover_touch_tablets.extended_engine import ExtendedStenoEngine
|
|
16
|
+
from plover_touch_tablets.get_logger import get_logger
|
|
17
|
+
from plover_touch_tablets.lookup import lookup
|
|
18
|
+
from plover_touch_tablets.signal import Signal
|
|
19
|
+
|
|
20
|
+
log = get_logger("Extension")
|
|
21
|
+
|
|
22
|
+
SERVER_CONFIG_FILE = "plover_websocket_server_config.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Extension:
|
|
26
|
+
engine: ExtendedStenoEngine
|
|
27
|
+
_tape_model: TapeModel
|
|
28
|
+
|
|
29
|
+
def __init__(self, engine: StenoEngine):
|
|
30
|
+
self.engine = ExtendedStenoEngine(engine)
|
|
31
|
+
engine.my_minimal_extension = self
|
|
32
|
+
|
|
33
|
+
self.engine.signals = [Signal("stroked"), Signal("translated")]
|
|
34
|
+
self._config = ClientConfig(SERVER_CONFIG_FILE) # reload the configuration when the server is restarted
|
|
35
|
+
self.mail_boxes: dict[int, MailBox] = {}
|
|
36
|
+
|
|
37
|
+
self._tape_model = TapeModel()
|
|
38
|
+
self._tape_model.reset()
|
|
39
|
+
|
|
40
|
+
def on_stroked(self, stroke: Stroke):
|
|
41
|
+
# Minimal example: just log strokes
|
|
42
|
+
log.info(f"Stroke: {stroke}")
|
|
43
|
+
|
|
44
|
+
def on_translated(self, old, new):
|
|
45
|
+
if new:
|
|
46
|
+
log.info(f"Translated: {new}")
|
|
47
|
+
|
|
48
|
+
def start(self):
|
|
49
|
+
log.info("Extension initialised")
|
|
50
|
+
|
|
51
|
+
# Example: Connect to stroke signals
|
|
52
|
+
self.engine.connect_hooks(self)
|
|
53
|
+
|
|
54
|
+
def stop(self):
|
|
55
|
+
self.engine.disconnect_hooks(self)
|
|
56
|
+
|
|
57
|
+
def _handle_tablet_connected(
|
|
58
|
+
self,
|
|
59
|
+
ws: WebSocketApp,
|
|
60
|
+
tablet_id: int,
|
|
61
|
+
public_key: str,
|
|
62
|
+
on_tablet_connected: Callable[[str], None] | None,
|
|
63
|
+
new_tablet_token: str | None,
|
|
64
|
+
):
|
|
65
|
+
log.debug(f"Private key: {self._config.private_key} and public key: {public_key}")
|
|
66
|
+
if public_key:
|
|
67
|
+
self.mail_boxes[tablet_id] = MailBox(self._config.private_key, public_key)
|
|
68
|
+
ws.send(
|
|
69
|
+
json.dumps(
|
|
70
|
+
{
|
|
71
|
+
"to": {"type": "tablet", "id": tablet_id},
|
|
72
|
+
"payload": {
|
|
73
|
+
"message": "Here is my the public key for you to privately communicate with me...",
|
|
74
|
+
"public_key": self._config.public_key,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
if on_tablet_connected and new_tablet_token:
|
|
80
|
+
on_tablet_connected(new_tablet_token)
|
|
81
|
+
|
|
82
|
+
def _handle_stroke(self, ws: WebSocketApp, tablet_id: int, tablet_mail_box: MailBox, steno_keys: list):
|
|
83
|
+
try:
|
|
84
|
+
stroke = Stroke(steno_keys)
|
|
85
|
+
stroke_json = encode(stroke, unpicklable=False)
|
|
86
|
+
paper = self._tape_model._paper_format(stroke)
|
|
87
|
+
|
|
88
|
+
data = {
|
|
89
|
+
"keys": stroke.steno_keys,
|
|
90
|
+
"stroked": stroke_json,
|
|
91
|
+
"rtfcre": stroke.rtfcre,
|
|
92
|
+
"paper": paper,
|
|
93
|
+
}
|
|
94
|
+
message = {"on_stroked": data}
|
|
95
|
+
|
|
96
|
+
ws.send(
|
|
97
|
+
json.dumps(
|
|
98
|
+
{
|
|
99
|
+
"to": {"type": "tablet", "id": tablet_id},
|
|
100
|
+
"payload": tablet_mail_box.box(message),
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.engine._engine._machine_stroke_callback(steno_keys)
|
|
106
|
+
except Exception:
|
|
107
|
+
log.exception("Failed to process stroke")
|
|
108
|
+
|
|
109
|
+
def _handle_lookup(self, ws: WebSocketApp, tablet_id: int, tablet_mail_box: MailBox, text_to_lookup: str):
|
|
110
|
+
try:
|
|
111
|
+
steno_options_per_word = lookup(self.engine._engine, text_to_lookup)
|
|
112
|
+
ws.send(
|
|
113
|
+
json.dumps(
|
|
114
|
+
{
|
|
115
|
+
"to": {"type": "tablet", "id": tablet_id},
|
|
116
|
+
"payload": tablet_mail_box.box({"lookup": steno_options_per_word}),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
except Exception:
|
|
121
|
+
log.exception("Failed to process lookup request")
|
|
122
|
+
|
|
123
|
+
def connect_websocket(self, connection_string: str, on_tablet_connected: Callable[[str], None] | None = None):
|
|
124
|
+
def on_message(ws: WebSocketApp, message: Any):
|
|
125
|
+
if isinstance(message, str):
|
|
126
|
+
message: dict = json.loads(message)
|
|
127
|
+
log.debug(f"Received: {message}")
|
|
128
|
+
msg_type = message.get("type")
|
|
129
|
+
if msg_type == "tablet_connected":
|
|
130
|
+
tablet_id = message.get("id")
|
|
131
|
+
public_key = message.get("publicKey")
|
|
132
|
+
new_tablet_token = message.get("newTabletToken")
|
|
133
|
+
self._handle_tablet_connected(ws, tablet_id, public_key, on_tablet_connected, new_tablet_token)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
from_data: dict = message.get("from")
|
|
137
|
+
if from_data and from_data.get("type") == "tablet":
|
|
138
|
+
payload = message.get("payload")
|
|
139
|
+
tablet_id = from_data.get("id")
|
|
140
|
+
tablet_mail_box = self.mail_boxes.get(tablet_id)
|
|
141
|
+
decrypted_payload = tablet_mail_box.unbox(payload)
|
|
142
|
+
|
|
143
|
+
if "stroke" in decrypted_payload:
|
|
144
|
+
steno_keys = decrypted_payload["stroke"]
|
|
145
|
+
if isinstance(steno_keys, list):
|
|
146
|
+
self._handle_stroke(ws, tablet_id, tablet_mail_box, steno_keys)
|
|
147
|
+
|
|
148
|
+
if "lookup" in decrypted_payload:
|
|
149
|
+
text_to_lookup = decrypted_payload["lookup"]
|
|
150
|
+
log.debug(f"Lookup request for: {text_to_lookup}")
|
|
151
|
+
if isinstance(text_to_lookup, str):
|
|
152
|
+
self._handle_lookup(ws, tablet_id, tablet_mail_box, text_to_lookup)
|
|
153
|
+
|
|
154
|
+
def on_error(ws, error: Exception):
|
|
155
|
+
log.exception(f"Error: {error}")
|
|
156
|
+
|
|
157
|
+
def on_close(ws, close_status_code, close_msg):
|
|
158
|
+
log.info("Closed")
|
|
159
|
+
|
|
160
|
+
def on_open(ws):
|
|
161
|
+
log.info("Opened")
|
|
162
|
+
|
|
163
|
+
meta = metadata("plover-touch-tablets")
|
|
164
|
+
header = {
|
|
165
|
+
"User-Agent": f"{meta['Name']}/{meta['Version']}",
|
|
166
|
+
"Origin": f"{WORKER_PROTOCOL}//{BASE_WORKER_FQDN}",
|
|
167
|
+
"X-Public-Key": self._config.public_key,
|
|
168
|
+
}
|
|
169
|
+
log.info(header)
|
|
170
|
+
ws = WebSocketApp(connection_string, on_open=on_open, on_message=on_message, on_error=on_error, on_close=on_close, header=header)
|
|
171
|
+
ws.run_forever(reconnect=5)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_logger(name: str, level=logging.DEBUG):
|
|
6
|
+
log = logging.getLogger(f"plover.{name.lower()}")
|
|
7
|
+
if not log.handlers:
|
|
8
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
9
|
+
handler.setFormatter(logging.Formatter(f"%(asctime)s [%(threadName)s] %(levelname)s: [{name}] %(filename)s:%(lineno)d %(message)s"))
|
|
10
|
+
log.addHandler(handler)
|
|
11
|
+
log.setLevel(level)
|
|
12
|
+
log.propagate = False
|
|
13
|
+
|
|
14
|
+
return log
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Find the steno strokes for the given text
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from plover import log
|
|
5
|
+
from plover.engine import StenoEngine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PloverLookupError(Exception):
|
|
9
|
+
def __init__(self, msg, *args):
|
|
10
|
+
self.msg = msg
|
|
11
|
+
super().__init__(*args)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def lookup(engine: StenoEngine, text_to_lookup: str) -> list: # noqa: PLR0915
|
|
15
|
+
"""Recursively looks up a phrase by finding the longest possible dictionary match.
|
|
16
|
+
|
|
17
|
+
Starts from the beginning of the string and then solving for the remainder.
|
|
18
|
+
|
|
19
|
+
A lookup can fail (return an empty list) if any part of the tokenized input
|
|
20
|
+
string cannot be found in Plover's dictionaries. The lookup is performed
|
|
21
|
+
recursively, and if any segment of the phrase has no corresponding steno
|
|
22
|
+
strokes, the entire lookup for that path will fail, and if no alternative
|
|
23
|
+
paths are found, the overall result will be empty.
|
|
24
|
+
"""
|
|
25
|
+
memo = {}
|
|
26
|
+
log.debug(f"Starting lookup for: '{text_to_lookup}'")
|
|
27
|
+
|
|
28
|
+
def get_steno_for_phrase(phrase: str) -> list | None:
|
|
29
|
+
"""Finds steno for a phrase.
|
|
30
|
+
|
|
31
|
+
Handling capitalization by falling back
|
|
32
|
+
to lowercase and prepending the capitalization stroke.
|
|
33
|
+
"""
|
|
34
|
+
# 1. Try the phrase as-is (respecting capitalization)
|
|
35
|
+
log.debug(f" - get_steno_for_phrase('{phrase}')")
|
|
36
|
+
|
|
37
|
+
steno_capitalized: set = engine.reverse_lookup(phrase)
|
|
38
|
+
|
|
39
|
+
# If the phrase is a single non-word character (like '!'),
|
|
40
|
+
# also try looking it up as a Plover command (e.g., '{!}').
|
|
41
|
+
if len(phrase) == 1 and not phrase.isalnum():
|
|
42
|
+
# Handle characters that are special within Plover's command syntax
|
|
43
|
+
command_phrase = f"{{{phrase}}}"
|
|
44
|
+
log.debug(f" - Trying command lookup for '{command_phrase}'")
|
|
45
|
+
steno_from_command = engine.reverse_lookup(command_phrase)
|
|
46
|
+
if steno_from_command:
|
|
47
|
+
steno_capitalized.update(steno_from_command)
|
|
48
|
+
|
|
49
|
+
# 2. Try the lowercase version and prepend cap stroke if needed
|
|
50
|
+
steno_lowercase_modified = set()
|
|
51
|
+
if phrase.lower() != phrase:
|
|
52
|
+
steno_lowercase_raw = engine.reverse_lookup(phrase.lower())
|
|
53
|
+
if steno_lowercase_raw:
|
|
54
|
+
cap_next_results = ("KPA",)
|
|
55
|
+
steno_lowercase_modified = {cap_next_results + s_lower for s_lower in steno_lowercase_raw}
|
|
56
|
+
|
|
57
|
+
# Prioritize direct capitalized results
|
|
58
|
+
combined = steno_capitalized.union(steno_lowercase_modified)
|
|
59
|
+
numeric_phrase = re.sub(r"[$,€£]", "", phrase.replace(",", ""))
|
|
60
|
+
if numeric_phrase.isdigit():
|
|
61
|
+
digit_steno_list = []
|
|
62
|
+
all_digits_found = True
|
|
63
|
+
for digit in numeric_phrase:
|
|
64
|
+
digit_steno = engine.reverse_lookup(digit)
|
|
65
|
+
if not digit_steno:
|
|
66
|
+
all_digits_found = False
|
|
67
|
+
break
|
|
68
|
+
digit_steno_list.append(min(digit_steno, key=len)) # Choose shortest steno for the digit
|
|
69
|
+
if all_digits_found:
|
|
70
|
+
combined_digit_steno = tuple(s for steno_tuple in digit_steno_list for s in steno_tuple)
|
|
71
|
+
combined.add(combined_digit_steno)
|
|
72
|
+
|
|
73
|
+
# If after all attempts, we have no results, return None.
|
|
74
|
+
if not combined:
|
|
75
|
+
# Only issue a warning for single words that are not found, as this is the root cause of failure.
|
|
76
|
+
is_single_word = " " not in phrase
|
|
77
|
+
if is_single_word:
|
|
78
|
+
log.warning(f"Failed to find steno for word: '{phrase}'")
|
|
79
|
+
else:
|
|
80
|
+
log.debug(f" - FAILED to find steno for phrase: '{phrase}'")
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
# Sort results: 1. Direct cap match, 2. Stroke count, 3. Key count
|
|
84
|
+
return sorted(combined, key=lambda s: (s not in steno_capitalized, len(s), sum(len(p) for p in s)))
|
|
85
|
+
|
|
86
|
+
def solve(words_tuple: tuple) -> list[list[tuple]]:
|
|
87
|
+
if not words_tuple:
|
|
88
|
+
return [[]] # Base case: one valid solution, which is empty.
|
|
89
|
+
if words_tuple in memo:
|
|
90
|
+
return memo[words_tuple]
|
|
91
|
+
log.debug(f"--> solve({words_tuple})")
|
|
92
|
+
|
|
93
|
+
def get_steno_options(i):
|
|
94
|
+
return get_steno_for_phrase(" ".join(words_tuple[:i]))
|
|
95
|
+
|
|
96
|
+
max_lookup_length = min(len(words_tuple), engine._dictionaries.longest_key)
|
|
97
|
+
|
|
98
|
+
def process_i(i, best_steno_for_prefix):
|
|
99
|
+
# Recursively find all solutions for the rest of the phrase
|
|
100
|
+
prefix_phrase = " ".join(words_tuple[:i])
|
|
101
|
+
suffix_tuple = words_tuple[i:]
|
|
102
|
+
suffix_solutions = solve(suffix_tuple)
|
|
103
|
+
|
|
104
|
+
# Combine the prefix's steno with each suffix solution
|
|
105
|
+
return [[{"text": prefix_phrase, "steno": best_steno_for_prefix}] + suffix_solution for suffix_solution in suffix_solutions]
|
|
106
|
+
|
|
107
|
+
all_solutions = [
|
|
108
|
+
solution
|
|
109
|
+
for i in range(max_lookup_length, 0, -1)
|
|
110
|
+
if (steno_options := get_steno_options(i))
|
|
111
|
+
for solution in process_i(i, steno_options[0])
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
if not all_solutions:
|
|
115
|
+
# This is the point of failure. It means for the current `words_tuple`,
|
|
116
|
+
# no prefix could be found in the dictionary that also had a valid suffix solution.
|
|
117
|
+
log.debug(f" <-- solve({words_tuple}) -> FAILED: No steno found for any prefix.")
|
|
118
|
+
memo[words_tuple] = all_solutions
|
|
119
|
+
return all_solutions
|
|
120
|
+
|
|
121
|
+
# Tokenize the input string, separating words from punctuation.
|
|
122
|
+
# This finds sequences of word characters (including those with internal apostrophes)
|
|
123
|
+
# currency symbols attached to numbers, numbers with commas, or single non-word/non-space characters.
|
|
124
|
+
token_regex = r"[$€£]?\d+(?:,\d+)*|\w+(?:['’]\w+)*|[^\w\s]" # nosec B105
|
|
125
|
+
words = re.findall(token_regex, text_to_lookup)
|
|
126
|
+
|
|
127
|
+
all_possible_sequences = solve(tuple(words))
|
|
128
|
+
|
|
129
|
+
log.debug(f"All possible sequences: {all_possible_sequences}")
|
|
130
|
+
|
|
131
|
+
if not all_possible_sequences:
|
|
132
|
+
log.debug(f"Lookup failed for '{text_to_lookup}'. No valid steno sequence found.")
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
# Sort the collected sequences by overall efficiency
|
|
136
|
+
# 1. Total number of strokes in the sequence
|
|
137
|
+
# 2. Total number of keys pressed in the sequence
|
|
138
|
+
sorted_sequences = sorted(
|
|
139
|
+
all_possible_sequences,
|
|
140
|
+
key=lambda seq: (
|
|
141
|
+
sum(len(item["steno"]) for item in seq),
|
|
142
|
+
sum(sum(len(p) for p in item["steno"]) for item in seq),
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
log.debug(f"Lookup finished. Returning {sorted_sequences}")
|
|
147
|
+
return sorted_sequences
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import qrcode
|
|
7
|
+
import requests
|
|
8
|
+
from plover.engine import StenoEngine
|
|
9
|
+
from plover.gui_qt.tool import Tool
|
|
10
|
+
from plover.oslayer.config import ASSETS_DIR
|
|
11
|
+
from PySide6.QtCore import QObject, Qt, Signal
|
|
12
|
+
from PySide6.QtGui import QImage, QPixmap
|
|
13
|
+
from PySide6.QtWidgets import QLabel, QVBoxLayout
|
|
14
|
+
|
|
15
|
+
from plover_touch_tablets.config import (
|
|
16
|
+
APP_URL,
|
|
17
|
+
BASE_WORKER_FQDN,
|
|
18
|
+
CONNECT_SLUG,
|
|
19
|
+
INITIATE_SLUG,
|
|
20
|
+
JOIN_SLUG,
|
|
21
|
+
RELAY_PARAM,
|
|
22
|
+
SESSION_SLUG,
|
|
23
|
+
TOKEN_PARAM,
|
|
24
|
+
WORKER_PROTOCOL,
|
|
25
|
+
)
|
|
26
|
+
from plover_touch_tablets.encoding import encode_raw_url
|
|
27
|
+
from plover_touch_tablets.extended_engine import ExtendedStenoEngine
|
|
28
|
+
from plover_touch_tablets.get_logger import get_logger
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from plover_touch_tablets.extension import Extension
|
|
32
|
+
|
|
33
|
+
log = get_logger("Tool")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Main(Tool):
|
|
37
|
+
TITLE = "Tablet QR"
|
|
38
|
+
ICON = str(Path(ASSETS_DIR) / "plover.png")
|
|
39
|
+
ROLE = "connection_settings"
|
|
40
|
+
|
|
41
|
+
_engine: ExtendedStenoEngine
|
|
42
|
+
qr: "QRCode"
|
|
43
|
+
extension: "Extension"
|
|
44
|
+
tablet_connected = Signal(str)
|
|
45
|
+
session_id: str
|
|
46
|
+
|
|
47
|
+
def __init__(self, engine: StenoEngine):
|
|
48
|
+
super().__init__(engine)
|
|
49
|
+
self.tablet_connected.connect(self.on_tablet_connected)
|
|
50
|
+
self._engine = ExtendedStenoEngine(engine)
|
|
51
|
+
log.info("Tool initialised")
|
|
52
|
+
|
|
53
|
+
self.qr = QRCode(self)
|
|
54
|
+
|
|
55
|
+
if not hasattr(engine, "my_minimal_extension"):
|
|
56
|
+
log.warning("Extension not found. Is the plugin enabled?")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
self.extension = self._engine.my_minimal_extension
|
|
60
|
+
log.info("Tool successfully connected to Extension!")
|
|
61
|
+
|
|
62
|
+
self.thread_0 = threading.Thread(target=process_data, args=(self,), daemon=True)
|
|
63
|
+
self.thread_0.start()
|
|
64
|
+
|
|
65
|
+
def closeEvent(self, event): # noqa: N802
|
|
66
|
+
super().closeEvent(event)
|
|
67
|
+
|
|
68
|
+
def on_tablet_connected(self, new_token: str):
|
|
69
|
+
ws_protocol = WORKER_PROTOCOL.replace("http", "ws")
|
|
70
|
+
tablet_connection_string = f"{ws_protocol}//{BASE_WORKER_FQDN}/{SESSION_SLUG}/{self.session_id}/{JOIN_SLUG}?{TOKEN_PARAM}={new_token}"
|
|
71
|
+
log.info(f"Tablet connection string is {tablet_connection_string}")
|
|
72
|
+
final_qr_url = f"{APP_URL}/?{RELAY_PARAM}={encode_raw_url(tablet_connection_string)}"
|
|
73
|
+
log.info(final_qr_url)
|
|
74
|
+
self.qr.qr_ready.emit(final_qr_url)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def process_data(main_tool: Main):
|
|
78
|
+
try:
|
|
79
|
+
res = requests.post(f"{WORKER_PROTOCOL}//{BASE_WORKER_FQDN}/{SESSION_SLUG}/{INITIATE_SLUG}", timeout=10)
|
|
80
|
+
response: dict = res.json()
|
|
81
|
+
except Exception:
|
|
82
|
+
log.exception("Request failed")
|
|
83
|
+
else:
|
|
84
|
+
# protocol = response["protocol"]
|
|
85
|
+
sessionId = response["sessionId"]
|
|
86
|
+
pcConnectionToken = response["pcConnectionToken"]
|
|
87
|
+
tabletConnectionToken = response["tabletConnectionToken"]
|
|
88
|
+
connection_infos = [
|
|
89
|
+
(pcConnectionToken, CONNECT_SLUG, WORKER_PROTOCOL, BASE_WORKER_FQDN),
|
|
90
|
+
(tabletConnectionToken, JOIN_SLUG, WORKER_PROTOCOL, BASE_WORKER_FQDN),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
connection_strings = [
|
|
94
|
+
f"{connection_info[2].replace('http', 'ws')}//{connection_info[3]}/{SESSION_SLUG}/{sessionId}/{connection_info[1]}?{TOKEN_PARAM}={
|
|
95
|
+
connection_info[0]
|
|
96
|
+
}"
|
|
97
|
+
for connection_info in connection_infos
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
log.info(f"Pc connection string is {connection_strings[0]}")
|
|
101
|
+
|
|
102
|
+
tablet_connection_string = connection_strings[1]
|
|
103
|
+
log.info(f"Tablet connection string is {tablet_connection_string}")
|
|
104
|
+
|
|
105
|
+
final_qr_url = f"{APP_URL}/?{RELAY_PARAM}={encode_raw_url(tablet_connection_string)}"
|
|
106
|
+
|
|
107
|
+
log.info(final_qr_url)
|
|
108
|
+
|
|
109
|
+
if main_tool:
|
|
110
|
+
main_tool.session_id = sessionId
|
|
111
|
+
main_tool.qr.qr_ready.emit(final_qr_url)
|
|
112
|
+
main_tool.extension.connect_websocket(connection_strings[0], on_tablet_connected=main_tool.tablet_connected.emit)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class QRCode(QObject):
|
|
116
|
+
qr_label: QLabel
|
|
117
|
+
qr_ready = Signal(str)
|
|
118
|
+
|
|
119
|
+
def __init__(self, tool: Main) -> None:
|
|
120
|
+
super().__init__()
|
|
121
|
+
self.tool = tool
|
|
122
|
+
self.qr_label = QLabel("Generating QR Code...")
|
|
123
|
+
self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
124
|
+
layout = QVBoxLayout()
|
|
125
|
+
layout.addWidget(self.qr_label)
|
|
126
|
+
tool.setLayout(layout)
|
|
127
|
+
self.qr_ready.connect(self.update_qr_code)
|
|
128
|
+
|
|
129
|
+
def update_qr_code(self, connection_string):
|
|
130
|
+
img = qrcode.make(connection_string)
|
|
131
|
+
buffer = BytesIO()
|
|
132
|
+
img.save(buffer, format="PNG")
|
|
133
|
+
qimage = QImage.fromData(buffer.getvalue())
|
|
134
|
+
self.qr_label.setPixmap(QPixmap.fromImage(qimage))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plover-touch-tablets
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Touch Tablets Plover Plugin
|
|
5
|
+
Keywords: plover_plugin
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: plover>=5.0.0
|
|
8
|
+
Requires-Dist: PySide6
|
|
9
|
+
Requires-Dist: websocket-client
|
|
10
|
+
Requires-Dist: jsonpickle
|
|
11
|
+
Requires-Dist: nacl_middleware
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest; extra == "test"
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: plover-touch-tablets[test]; extra == "dev"
|
|
16
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
17
|
+
Requires-Dist: build; extra == "dev"
|
|
18
|
+
Requires-Dist: bump-my-version; extra == "dev"
|
|
19
|
+
Requires-Dist: twine; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# Plover Touch Tablets Plugin
|
|
23
|
+
|
|
24
|
+
This Plover plugin enables seamless integration between Plover and touch-based steno keyboard applications running on tablets. It establishes a secure WebSocket connection, allowing your tablet to function as a steno machine for Plover.
|
|
25
|
+
|
|
26
|
+
## Compatibility
|
|
27
|
+
|
|
28
|
+
This plugin is designed to be compatible with the **[Touch Steno Keyboard](https://github.com/CosmicDNA/touch-steno-keyboard)** application and the **[Plover Websocket Relay](https://github.com/CosmicDNA/plover-websocket-relay)**.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Easy Pairing**: Generates a QR code within Plover for quick connection setup.
|
|
33
|
+
- **Secure Connection**: Uses encryption to ensure secure communication between the tablet and Plover.
|
|
34
|
+
- **Bi-directional Communication**: Supports sending steno strokes to Plover and receiving dictionary lookups/translations back on the tablet.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
1. Open Plover.
|
|
39
|
+
2. Install `plover-touch-tablets` plugin via either plover_console CLI or plugin manager.
|
|
40
|
+
3. Restart Plover.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
1. Open Plover.
|
|
45
|
+
2. Go to **Tools** > **Tablet QR**.
|
|
46
|
+
3. A window will appear displaying a QR code.
|
|
47
|
+
4. Open the Touch Steno Keyboard application on your tablet.
|
|
48
|
+
5. Use the application to scan the QR code displayed on your computer screen.
|
|
49
|
+
6. Once connected, strokes entered on the tablet will be processed by Plover.
|
|
50
|
+
|
|
51
|
+
## Powered by
|
|
52
|
+

|
|
53
|
+

|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
plover_touch_tablets/client_config.py
|
|
4
|
+
plover_touch_tablets/config.py
|
|
5
|
+
plover_touch_tablets/debug.py
|
|
6
|
+
plover_touch_tablets/encoding.py
|
|
7
|
+
plover_touch_tablets/extended_engine.py
|
|
8
|
+
plover_touch_tablets/extension.py
|
|
9
|
+
plover_touch_tablets/get_logger.py
|
|
10
|
+
plover_touch_tablets/lookup.py
|
|
11
|
+
plover_touch_tablets/signal.py
|
|
12
|
+
plover_touch_tablets/tool.py
|
|
13
|
+
plover_touch_tablets.egg-info/PKG-INFO
|
|
14
|
+
plover_touch_tablets.egg-info/SOURCES.txt
|
|
15
|
+
plover_touch_tablets.egg-info/dependency_links.txt
|
|
16
|
+
plover_touch_tablets.egg-info/entry_points.txt
|
|
17
|
+
plover_touch_tablets.egg-info/requires.txt
|
|
18
|
+
plover_touch_tablets.egg-info/top_level.txt
|
|
19
|
+
tests/test_me.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
plover_touch_tablets
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "plover-touch-tablets"
|
|
7
|
+
version = "0.0.0"
|
|
8
|
+
description = "Touch Tablets Plover Plugin"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
keywords = ["plover_plugin"]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"plover>=5.0.0",
|
|
13
|
+
"PySide6",
|
|
14
|
+
"websocket-client",
|
|
15
|
+
"jsonpickle",
|
|
16
|
+
"nacl_middleware"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
test = ["pytest"]
|
|
21
|
+
|
|
22
|
+
dev = [
|
|
23
|
+
"plover-touch-tablets[test]", # Includes all dependencies from the 'test' group,
|
|
24
|
+
"pre-commit", # For managing pre-commit hooks
|
|
25
|
+
"build",
|
|
26
|
+
"bump-my-version",
|
|
27
|
+
"twine",
|
|
28
|
+
"ruff",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.entry-points."plover.gui.qt.tool"]
|
|
32
|
+
tablet_qr = "plover_touch_tablets.tool:Main"
|
|
33
|
+
|
|
34
|
+
[project.entry-points."plover.extension"]
|
|
35
|
+
plover_touch_tablets = "plover_touch_tablets.extension:Extension"
|
|
36
|
+
|
|
37
|
+
[tool.bumpversion]
|
|
38
|
+
current_version = "0.0.0"
|
|
39
|
+
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
40
|
+
serialize = ["{major}.{minor}.{patch}"]
|
|
41
|
+
search = "{current_version}"
|
|
42
|
+
replace = "{new_version}"
|
|
43
|
+
regex = false
|
|
44
|
+
ignore_missing_version = false
|
|
45
|
+
ignore_missing_files = false
|
|
46
|
+
tag = true
|
|
47
|
+
sign_tags = true
|
|
48
|
+
tag_name = "v{new_version}"
|
|
49
|
+
tag_message = "Bump version: {current_version} → {new_version}"
|
|
50
|
+
allow_dirty = false
|
|
51
|
+
commit = true
|
|
52
|
+
message = "Bump version: {current_version} → {new_version}"
|
|
53
|
+
moveable_tags = []
|
|
54
|
+
commit_args = ""
|
|
55
|
+
setup_hooks = []
|
|
56
|
+
pre_commit_hooks = []
|
|
57
|
+
post_commit_hooks = []
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 149
|
|
61
|
+
target-version = "py311"
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
# Enable Pyflakes (F), pycodestyle (E, W), isort (I)
|
|
65
|
+
# Enable many common Pylint (PL), flake8-bugbear (B), flake8-comprehensions (C4), etc. rules
|
|
66
|
+
select = [
|
|
67
|
+
"F",
|
|
68
|
+
"E",
|
|
69
|
+
"W",
|
|
70
|
+
"I",
|
|
71
|
+
"N",
|
|
72
|
+
"D", # Core flake8, isort, pep8-naming, pydocstyle
|
|
73
|
+
"UP", # pyupgrade
|
|
74
|
+
"B", # flake8-bugbear
|
|
75
|
+
"A", # flake8-builtins
|
|
76
|
+
"C4", # flake8-comprehensions
|
|
77
|
+
"T20", # flake8-print (T201 for print, T203 for pprint)
|
|
78
|
+
"SIM", # flake8-simplify
|
|
79
|
+
"PTH", # flake8-use-pathlib
|
|
80
|
+
"PL", # Pylint
|
|
81
|
+
"TRY", # tryceratops
|
|
82
|
+
]
|
|
83
|
+
ignore = [
|
|
84
|
+
# Ignore common missing docstring errors for now
|
|
85
|
+
"D100", # Missing docstring in public module
|
|
86
|
+
"D101", # Missing docstring in public class
|
|
87
|
+
"D102", # Missing docstring in public method
|
|
88
|
+
"D103", # Missing docstring in public function
|
|
89
|
+
"D104", # Missing docstring in public package
|
|
90
|
+
"D107", # Missing docstring in `__init__`
|
|
91
|
+
"D203", # 1 blank line required before class docstring
|
|
92
|
+
"D213", # Multi-line docstring closing quotes should be on a separate line
|
|
93
|
+
"D401", # First line of docstring should be in imperative mood
|
|
94
|
+
"N803", # Function name should be lowercase
|
|
95
|
+
"N806", # Variable in function should be lowercase
|
|
96
|
+
]
|
|
97
|
+
# mccabe (cyclomatic complexity)
|
|
98
|
+
# mccabe.max-complexity = 10 # Default is 10, adjust as needed
|
|
99
|
+
|
|
100
|
+
[tool.ruff.format]
|
|
101
|
+
quote-style = "double" # Black default
|
|
102
|
+
indent-style = "space"
|
|
103
|
+
skip-magic-trailing-comma = false # Black default
|
|
104
|
+
line-ending = "auto"
|